@curdx/flow 2.3.10 → 3.0.0

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 (51) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +2 -20
  3. package/CHANGELOG.md +55 -2
  4. package/README.md +69 -19
  5. package/agents/flow-adversary.md +1 -0
  6. package/agents/flow-architect.md +1 -0
  7. package/agents/flow-brownfield-analyst.md +1 -0
  8. package/agents/flow-edge-hunter.md +1 -0
  9. package/agents/flow-planner.md +1 -0
  10. package/agents/flow-researcher.md +1 -0
  11. package/agents/flow-reviewer.md +1 -0
  12. package/agents/flow-ui-researcher.md +1 -0
  13. package/agents/flow-verifier.md +1 -0
  14. package/bin/curdx-flow-state +104 -0
  15. package/cli/README.md +6 -1
  16. package/cli/install-companions.js +1 -1
  17. package/cli/install-context7-config.js +5 -3
  18. package/cli/install-required-plugins.js +2 -2
  19. package/cli/uninstall-actions.js +37 -0
  20. package/cli/upgrade-workflow.js +1 -1
  21. package/cli/upgrade.js +42 -14
  22. package/hooks/hooks.json +72 -0
  23. package/hooks/scripts/common.sh +191 -0
  24. package/hooks/scripts/config-change-guard.sh +94 -0
  25. package/hooks/scripts/flow-context-watch.sh +94 -0
  26. package/hooks/scripts/quick-mode-guard.sh +4 -3
  27. package/hooks/scripts/session-start.sh +14 -10
  28. package/hooks/scripts/session-title.sh +87 -0
  29. package/hooks/scripts/stop-watcher.sh +4 -3
  30. package/hooks/scripts/subagent-artifact-guard.sh +7 -74
  31. package/hooks/scripts/subagent-statusline.sh +8 -2
  32. package/hooks/scripts/task-lifecycle-guard.sh +106 -0
  33. package/hooks/scripts/teammate-idle-guard.sh +83 -0
  34. package/knowledge/claude-code-runtime-contracts.md +21 -0
  35. package/monitors/scripts/flow-state-monitor.sh +8 -5
  36. package/output-styles/curdx-fast-mode.md +42 -0
  37. package/output-styles/curdx-spec-mode.md +46 -0
  38. package/package.json +5 -3
  39. package/schemas/agent-frontmatter.schema.json +4 -1
  40. package/schemas/spec-state.schema.json +18 -0
  41. package/settings.json +2 -1
  42. package/skills/implement/SKILL.md +8 -0
  43. package/skills/implement/references/linear-execution.md +11 -0
  44. package/skills/implement/references/native-task-sync.md +107 -0
  45. package/skills/implement/references/progress-contract.md +4 -0
  46. package/skills/implement/references/state-init.md +3 -0
  47. package/skills/implement/references/stop-hook-execution.md +19 -5
  48. package/skills/implement/references/subagent-execution.md +16 -2
  49. package/skills/implement/references/wave-execution.md +18 -0
  50. package/skills/status/references/gather-contract.md +3 -0
  51. package/skills/status/references/output-contract.md +1 -0
package/cli/upgrade.js CHANGED
@@ -2,7 +2,8 @@
2
2
  * upgrade command — update curdx-flow + recommended plugins to latest.
3
3
  */
4
4
 
5
- import { log } from "./utils.js";
5
+ import { log, readConfig } from "./utils.js";
6
+ import { reconcileLegacyContext7InstallState } from "./install-context7-config.js";
6
7
  import {
7
8
  PLUGINS_TO_UPDATE,
8
9
  MARKETPLACES_TO_REFRESH,
@@ -21,43 +22,70 @@ import {
21
22
  UPGRADE_STEP_COUNT,
22
23
  } from "./upgrade-workflow.js";
23
24
 
24
- export async function upgrade(args = []) {
25
- log.title("⬆️ CurdX-Flow upgrade");
25
+ export async function upgrade(
26
+ args = [],
27
+ {
28
+ updatePluginImpl = updatePlugin,
29
+ updatePluginMarketplaceImpl = updatePluginMarketplace,
30
+ ensureClaudeCliAvailableForUpgradeImpl = ensureClaudeCliAvailableForUpgrade,
31
+ getInstalledPluginNamesImpl = getInstalledPluginNames,
32
+ readConfigImpl = readConfig,
33
+ reconcileLegacyContext7InstallStateImpl = reconcileLegacyContext7InstallState,
34
+ logImpl = log,
35
+ } = {}
36
+ ) {
37
+ logImpl.title("⬆️ CurdX-Flow upgrade");
26
38
 
27
- ensureClaudeCliAvailableForUpgrade();
39
+ ensureClaudeCliAvailableForUpgradeImpl({ logImpl });
28
40
 
29
41
  // Refresh marketplaces first (derived from cli/registry.js)
30
42
  await refreshMarketplaces(MARKETPLACES_TO_REFRESH, {
31
- updatePluginMarketplaceImpl: updatePluginMarketplace,
43
+ updatePluginMarketplaceImpl,
44
+ logImpl,
32
45
  });
33
46
 
34
47
  // Update each plugin
35
- log.blank();
36
- log.step(2, UPGRADE_STEP_COUNT, "Updating installed plugins...");
37
- const installedNames = getInstalledPluginNames();
48
+ logImpl.blank();
49
+ logImpl.step(2, UPGRADE_STEP_COUNT, "Updating installed plugins...");
50
+ const installedNames = getInstalledPluginNamesImpl();
38
51
 
39
52
  for (const spec of PLUGINS_TO_UPDATE) {
40
53
  const pluginName = getPluginNameFromSpec(spec);
41
54
  if (!installedNames.has(pluginName)) {
42
- log.info(` ${pluginName.padEnd(22)} not installed, skipping`);
55
+ logImpl.info(` ${pluginName.padEnd(22)} not installed, skipping`);
43
56
  continue;
44
57
  }
45
58
 
46
- const result = await updatePlugin(spec);
59
+ const result = await updatePluginImpl(spec);
47
60
  const status = classifyPluginUpdateResult(result);
48
61
 
49
62
  if (status.status === "updated") {
50
- log.ok(` ${pluginName.padEnd(22)} ${status.message}`);
63
+ logImpl.ok(` ${pluginName.padEnd(22)} ${status.message}`);
51
64
  continue;
52
65
  }
53
66
 
54
67
  if (status.status === "unchanged") {
55
- log.info(` ${pluginName.padEnd(22)} ${status.message}`);
68
+ logImpl.info(` ${pluginName.padEnd(22)} ${status.message}`);
56
69
  continue;
57
70
  }
58
71
 
59
- log.warn(` ${pluginName.padEnd(22)} ${status.message}`);
72
+ logImpl.warn(` ${pluginName.padEnd(22)} ${status.message}`);
60
73
  }
61
74
 
62
- printUpgradeSummary();
75
+ logImpl.blank();
76
+ logImpl.step(3, UPGRADE_STEP_COUNT, "Reconciling legacy Context7 state...");
77
+ if (installedNames.has("context7-plugin")) {
78
+ const config = readConfigImpl();
79
+ const language = config?.language === "zh" ? "zh" : "en";
80
+ await reconcileLegacyContext7InstallStateImpl(
81
+ { name: "context7-plugin" },
82
+ language,
83
+ config,
84
+ { logImpl }
85
+ );
86
+ } else {
87
+ logImpl.info(" context7-plugin not installed, skipping");
88
+ }
89
+
90
+ printUpgradeSummary({ logImpl });
63
91
  }
package/hooks/hooks.json CHANGED
@@ -21,6 +21,37 @@
21
21
  ]
22
22
  }
23
23
  ],
24
+ "UserPromptSubmit": [
25
+ {
26
+ "hooks": [
27
+ {
28
+ "type": "command",
29
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/session-title.sh",
30
+ "statusMessage": "Refreshing CurDX-Flow session title"
31
+ }
32
+ ]
33
+ }
34
+ ],
35
+ "CwdChanged": [
36
+ {
37
+ "hooks": [
38
+ {
39
+ "type": "command",
40
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/flow-context-watch.sh"
41
+ }
42
+ ]
43
+ }
44
+ ],
45
+ "FileChanged": [
46
+ {
47
+ "hooks": [
48
+ {
49
+ "type": "command",
50
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/flow-context-watch.sh"
51
+ }
52
+ ]
53
+ }
54
+ ],
24
55
  "Stop": [
25
56
  {
26
57
  "hooks": [
@@ -43,6 +74,47 @@
43
74
  ]
44
75
  }
45
76
  ],
77
+ "TaskCreated": [
78
+ {
79
+ "hooks": [
80
+ {
81
+ "type": "command",
82
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/task-lifecycle-guard.sh"
83
+ }
84
+ ]
85
+ }
86
+ ],
87
+ "TaskCompleted": [
88
+ {
89
+ "hooks": [
90
+ {
91
+ "type": "command",
92
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/task-lifecycle-guard.sh"
93
+ }
94
+ ]
95
+ }
96
+ ],
97
+ "TeammateIdle": [
98
+ {
99
+ "hooks": [
100
+ {
101
+ "type": "command",
102
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/teammate-idle-guard.sh"
103
+ }
104
+ ]
105
+ }
106
+ ],
107
+ "ConfigChange": [
108
+ {
109
+ "matcher": "project_settings|local_settings",
110
+ "hooks": [
111
+ {
112
+ "type": "command",
113
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/config-change-guard.sh"
114
+ }
115
+ ]
116
+ }
117
+ ],
46
118
  "PreToolUse": [
47
119
  {
48
120
  "matcher": "AskUserQuestion",
@@ -4,6 +4,186 @@ has_python3() {
4
4
  command -v python3 >/dev/null 2>&1
5
5
  }
6
6
 
7
+ resolve_flow_root() {
8
+ local candidate="${1:-${PWD:-}}"
9
+
10
+ if [ -n "${CLAUDE_PROJECT_DIR:-}" ] && [ -d "${CLAUDE_PROJECT_DIR}/.flow" ]; then
11
+ printf '%s\n' "${CLAUDE_PROJECT_DIR%/}"
12
+ return 0
13
+ fi
14
+
15
+ [ -n "$candidate" ] || candidate="$(pwd 2>/dev/null || printf '.')"
16
+
17
+ while [ -n "$candidate" ]; do
18
+ if [ -d "$candidate/.flow" ]; then
19
+ printf '%s\n' "$candidate"
20
+ return 0
21
+ fi
22
+
23
+ [ "$candidate" = "/" ] && break
24
+ candidate="$(dirname "$candidate")"
25
+ done
26
+
27
+ return 1
28
+ }
29
+
30
+ curdx_active_spec_name() {
31
+ local flow_root="${1:-${FLOW_ROOT:-}}"
32
+ [ -n "$flow_root" ] || return 1
33
+ [ -f "$flow_root/.flow/.active-spec" ] || return 1
34
+ cat "$flow_root/.flow/.active-spec" 2>/dev/null
35
+ }
36
+
37
+ curdx_active_spec_path() {
38
+ local flow_root="${1:-${FLOW_ROOT:-}}"
39
+ local file_name="${2:-}"
40
+ local active
41
+
42
+ [ -n "$flow_root" ] || return 1
43
+ [ -n "$file_name" ] || return 1
44
+
45
+ active="$(curdx_active_spec_name "$flow_root" 2>/dev/null || true)"
46
+ [ -n "$active" ] || return 1
47
+
48
+ printf '%s/.flow/specs/%s/%s\n' "$flow_root" "$active" "$file_name"
49
+ }
50
+
51
+ curdx_latest_epic_artifact_path() {
52
+ local flow_root="${1:-${FLOW_ROOT:-}}"
53
+
54
+ [ -n "$flow_root" ] || return 1
55
+ [ -d "$flow_root/.flow/_epics" ] || return 1
56
+ has_python3 || return 1
57
+
58
+ export CURDX_EPIC_DIR="$flow_root/.flow/_epics"
59
+ python3 <<'PY' 2>/dev/null
60
+ import os
61
+ from pathlib import Path
62
+
63
+ base = Path(os.environ["CURDX_EPIC_DIR"])
64
+ candidates = [path for path in base.glob("*/epic.md") if path.is_file()]
65
+ if not candidates:
66
+ raise SystemExit(1)
67
+
68
+ latest = max(candidates, key=lambda path: path.stat().st_mtime)
69
+ print(str(latest))
70
+ PY
71
+ }
72
+
73
+ curdx_epic_artifact_path_from_message() {
74
+ local flow_root="${1:-${FLOW_ROOT:-}}"
75
+ local msg="${2:-}"
76
+
77
+ [ -n "$flow_root" ] || return 1
78
+ has_python3 || return 1
79
+
80
+ export CURDX_EPIC_MSG="$msg"
81
+ export CURDX_FLOW_ROOT="$flow_root"
82
+ python3 <<'PY' 2>/dev/null
83
+ import os
84
+ import re
85
+
86
+ msg = os.environ.get("CURDX_EPIC_MSG", "")
87
+ match = re.search(r'(\.flow/_epics/[^/\s]+/epic\.md)\b', msg)
88
+ if not match:
89
+ raise SystemExit(1)
90
+
91
+ rel = match.group(1)
92
+ if rel.startswith(".flow/"):
93
+ rel = rel[len(".flow/"):]
94
+
95
+ print(os.path.join(os.environ["CURDX_FLOW_ROOT"], ".flow", rel))
96
+ PY
97
+ }
98
+
99
+ curdx_resolve_artifact_contract() {
100
+ local agent_type="${1:-}"
101
+ local last_message="${2:-}"
102
+ local flow_root="${3:-${FLOW_ROOT:-}}"
103
+
104
+ CURDX_ARTIFACT_TARGET=""
105
+ CURDX_ARTIFACT_MIN_SIZE=0
106
+
107
+ [ -n "$agent_type" ] || return 1
108
+
109
+ case "$agent_type" in
110
+ flow-researcher)
111
+ CURDX_ARTIFACT_TARGET="$(curdx_active_spec_path "$flow_root" research.md)" || return 1
112
+ CURDX_ARTIFACT_MIN_SIZE=400
113
+ ;;
114
+ flow-product-designer)
115
+ CURDX_ARTIFACT_TARGET="$(curdx_active_spec_path "$flow_root" requirements.md)" || return 1
116
+ CURDX_ARTIFACT_MIN_SIZE=400
117
+ ;;
118
+ flow-architect)
119
+ CURDX_ARTIFACT_TARGET="$(curdx_active_spec_path "$flow_root" design.md)" || return 1
120
+ CURDX_ARTIFACT_MIN_SIZE=400
121
+ ;;
122
+ flow-planner)
123
+ CURDX_ARTIFACT_TARGET="$(curdx_active_spec_path "$flow_root" tasks.md)" || return 1
124
+ CURDX_ARTIFACT_MIN_SIZE=400
125
+ ;;
126
+ flow-executor)
127
+ CURDX_ARTIFACT_TARGET="$(curdx_active_spec_path "$flow_root" tasks.md)" || return 1
128
+ CURDX_ARTIFACT_MIN_SIZE=400
129
+ ;;
130
+ flow-debugger)
131
+ CURDX_ARTIFACT_TARGET="$(curdx_active_spec_path "$flow_root" debug-report.md)" || return 1
132
+ CURDX_ARTIFACT_MIN_SIZE=250
133
+ ;;
134
+ flow-reviewer)
135
+ CURDX_ARTIFACT_TARGET="$(curdx_active_spec_path "$flow_root" review-report.md)" || return 1
136
+ CURDX_ARTIFACT_MIN_SIZE=300
137
+ ;;
138
+ flow-verifier)
139
+ CURDX_ARTIFACT_TARGET="$(curdx_active_spec_path "$flow_root" verification-report.md)" || return 1
140
+ CURDX_ARTIFACT_MIN_SIZE=300
141
+ ;;
142
+ flow-security-auditor)
143
+ CURDX_ARTIFACT_TARGET="$(curdx_active_spec_path "$flow_root" security-audit.md)" || return 1
144
+ CURDX_ARTIFACT_MIN_SIZE=250
145
+ ;;
146
+ flow-qa-engineer)
147
+ CURDX_ARTIFACT_TARGET="$(curdx_active_spec_path "$flow_root" qa-report.md)" || return 1
148
+ CURDX_ARTIFACT_MIN_SIZE=250
149
+ ;;
150
+ flow-edge-hunter)
151
+ CURDX_ARTIFACT_TARGET="$(curdx_active_spec_path "$flow_root" edge-cases.md)" || return 1
152
+ CURDX_ARTIFACT_MIN_SIZE=250
153
+ ;;
154
+ flow-adversary)
155
+ CURDX_ARTIFACT_TARGET="$(curdx_active_spec_path "$flow_root" adversarial-review.md)" || return 1
156
+ CURDX_ARTIFACT_MIN_SIZE=250
157
+ ;;
158
+ flow-ui-researcher)
159
+ CURDX_ARTIFACT_TARGET="$(curdx_active_spec_path "$flow_root" ui-research.md)" || return 1
160
+ CURDX_ARTIFACT_MIN_SIZE=250
161
+ ;;
162
+ flow-ux-designer)
163
+ CURDX_ARTIFACT_TARGET="$(curdx_active_spec_path "$flow_root" ui-sketch/index.html)" || return 1
164
+ CURDX_ARTIFACT_MIN_SIZE=400
165
+ ;;
166
+ flow-triage-analyst)
167
+ CURDX_ARTIFACT_TARGET="$(curdx_epic_artifact_path_from_message "$flow_root" "$last_message" 2>/dev/null || true)"
168
+ if [ -z "$CURDX_ARTIFACT_TARGET" ]; then
169
+ CURDX_ARTIFACT_TARGET="$(curdx_latest_epic_artifact_path "$flow_root" 2>/dev/null || true)"
170
+ fi
171
+ [ -n "$CURDX_ARTIFACT_TARGET" ] || return 1
172
+ CURDX_ARTIFACT_MIN_SIZE=400
173
+ ;;
174
+ flow-brownfield-analyst)
175
+ [ -n "$flow_root" ] || return 1
176
+ CURDX_ARTIFACT_TARGET="$flow_root/.flow/codebase-index.md"
177
+ CURDX_ARTIFACT_MIN_SIZE=250
178
+ ;;
179
+ *)
180
+ return 1
181
+ ;;
182
+ esac
183
+
184
+ return 0
185
+ }
186
+
7
187
  env_flag_enabled() {
8
188
  case "$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')" in
9
189
  1|true|yes|on) return 0 ;;
@@ -30,6 +210,12 @@ emit_session_start_context() {
30
210
  "$(json_escape "$context")"
31
211
  }
32
212
 
213
+ emit_userprompt_submit_title() {
214
+ local title="${1:-}"
215
+ printf '{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","sessionTitle":%s}}\n' \
216
+ "$(json_escape "$title")"
217
+ }
218
+
33
219
  emit_pretooluse_deny() {
34
220
  local reason="${1:-}"
35
221
  printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":%s}}\n' \
@@ -44,3 +230,8 @@ emit_stop_block() {
44
230
  emit_subagentstop_block() {
45
231
  emit_stop_block "${1:-}"
46
232
  }
233
+
234
+ emit_configchange_block() {
235
+ local reason="${1:-}"
236
+ printf '{"decision":"block","reason":%s}\n' "$(json_escape "$reason")"
237
+ }
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env bash
2
+ # CurDX-Flow ConfigChange Hook
3
+ # Blocks mid-execute settings changes that would disable CurDX's runtime spine.
4
+
5
+ set -u
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ . "$SCRIPT_DIR/common.sh"
9
+
10
+ INPUT="$(cat 2>/dev/null || echo "{}")"
11
+ FLOW_ROOT="$(resolve_flow_root 2>/dev/null || true)"
12
+
13
+ [ -n "$FLOW_ROOT" ] || exit 0
14
+ has_python3 || exit 0
15
+
16
+ export CURDX_CONFIG_CHANGE_INPUT="$INPUT"
17
+
18
+ SOURCE="$(python3 -c 'import json, os
19
+ try:
20
+ data = json.loads(os.environ["CURDX_CONFIG_CHANGE_INPUT"])
21
+ print(data.get("source", ""))
22
+ except Exception:
23
+ print("")
24
+ ' 2>/dev/null)"
25
+
26
+ case "$SOURCE" in
27
+ project_settings|local_settings) ;;
28
+ *) exit 0 ;;
29
+ esac
30
+
31
+ FILE_PATH="$(python3 -c 'import json, os
32
+ try:
33
+ data = json.loads(os.environ["CURDX_CONFIG_CHANGE_INPUT"])
34
+ print(data.get("file_path", ""))
35
+ except Exception:
36
+ print("")
37
+ ' 2>/dev/null)"
38
+
39
+ [ -n "$FILE_PATH" ] || exit 0
40
+ [ -f "$FILE_PATH" ] || exit 0
41
+ [ -f "$FLOW_ROOT/.flow/.active-spec" ] || exit 0
42
+
43
+ ACTIVE="$(cat "$FLOW_ROOT/.flow/.active-spec" 2>/dev/null)"
44
+ [ -n "$ACTIVE" ] || exit 0
45
+
46
+ STATE_FILE="$FLOW_ROOT/.flow/specs/$ACTIVE/.state.json"
47
+ [ -f "$STATE_FILE" ] || exit 0
48
+
49
+ export STATE_FILE
50
+ PHASE="$(python3 -c 'import json, os
51
+ try:
52
+ state = json.load(open(os.environ["STATE_FILE"]))
53
+ print(state.get("phase", ""))
54
+ except Exception:
55
+ print("")
56
+ ' 2>/dev/null)"
57
+
58
+ [ "$PHASE" = "execute" ] || exit 0
59
+
60
+ export CURDX_CONFIG_CHANGE_FILE="$FILE_PATH"
61
+ BLOCK_REASONS="$(python3 <<'PY' 2>/dev/null
62
+ import json
63
+ import os
64
+
65
+ file_path = os.environ["CURDX_CONFIG_CHANGE_FILE"]
66
+ try:
67
+ parsed = json.load(open(file_path))
68
+ except Exception:
69
+ raise SystemExit(0)
70
+
71
+ reasons = []
72
+
73
+ if parsed.get("disableAllHooks") is True:
74
+ reasons.append("disableAllHooks would disable CurDX-Flow stop/recovery hooks and status lines")
75
+
76
+ agent = parsed.get("agent")
77
+ if isinstance(agent, str):
78
+ agent = agent.strip()
79
+ if agent and agent != "flow-orchestrator":
80
+ reasons.append(f'agent would reroute the main thread through subagent "{agent}"')
81
+
82
+ enabled_plugins = parsed.get("enabledPlugins")
83
+ if isinstance(enabled_plugins, dict) and enabled_plugins.get("curdx-flow@curdx-flow-marketplace") is False:
84
+ reasons.append("enabledPlugins would disable curdx-flow@curdx-flow-marketplace for this project")
85
+
86
+ if reasons:
87
+ print(" | ".join(reasons))
88
+ PY
89
+ )"
90
+
91
+ [ -n "$BLOCK_REASONS" ] || exit 0
92
+
93
+ emit_configchange_block "[CurDX-Flow config-change-guard] Blocking ${SOURCE} update while spec '${ACTIVE}' is in execute: ${BLOCK_REASONS}. Finish or cancel the active implementation first, then reapply the settings change after execute ends."
94
+ exit 0
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -u
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ . "$SCRIPT_DIR/common.sh"
7
+
8
+ INPUT="$(cat 2>/dev/null || echo "{}")"
9
+
10
+ if ! has_python3; then
11
+ exit 0
12
+ fi
13
+
14
+ export CURDX_FLOW_CONTEXT_WATCH_INPUT="$INPUT"
15
+
16
+ read_json_field() {
17
+ local field="$1"
18
+ python3 - "$field" <<'PY' 2>/dev/null
19
+ import json
20
+ import os
21
+ import sys
22
+
23
+ field = sys.argv[1]
24
+ try:
25
+ data = json.loads(os.environ.get("CURDX_FLOW_CONTEXT_WATCH_INPUT", "{}"))
26
+ except Exception:
27
+ data = {}
28
+
29
+ value = data.get(field, "")
30
+ if value is None:
31
+ value = ""
32
+ print(value)
33
+ PY
34
+ }
35
+
36
+ HOOK_EVENT_NAME="$(read_json_field hook_event_name)"
37
+ NEW_CWD="$(read_json_field new_cwd)"
38
+ CURRENT_CWD="$(read_json_field cwd)"
39
+
40
+ START_DIR="$CURRENT_CWD"
41
+ if [ "$HOOK_EVENT_NAME" = "CwdChanged" ] && [ -n "$NEW_CWD" ]; then
42
+ START_DIR="$NEW_CWD"
43
+ fi
44
+
45
+ FLOW_ROOT="$(resolve_flow_root "$START_DIR" 2>/dev/null || true)"
46
+ ACTIVE_SPEC=""
47
+ SPEC_DIR=""
48
+
49
+ if [ -n "$FLOW_ROOT" ] && [ -f "$FLOW_ROOT/.flow/.active-spec" ]; then
50
+ ACTIVE_SPEC="$(cat "$FLOW_ROOT/.flow/.active-spec" 2>/dev/null || true)"
51
+ fi
52
+
53
+ if [ -n "$FLOW_ROOT" ] && [ -n "$ACTIVE_SPEC" ]; then
54
+ SPEC_DIR="$FLOW_ROOT/.flow/specs/$ACTIVE_SPEC"
55
+ fi
56
+
57
+ if [ -n "${CLAUDE_ENV_FILE:-}" ]; then
58
+ {
59
+ printf 'export CURDX_FLOW_PROJECT_ROOT=%s\n' "$(json_escape "$FLOW_ROOT")"
60
+ printf 'export CURDX_FLOW_ACTIVE_SPEC=%s\n' "$(json_escape "$ACTIVE_SPEC")"
61
+ if [ -n "$SPEC_DIR" ]; then
62
+ printf 'export CURDX_FLOW_SPEC_DIR=%s\n' "$(json_escape "$SPEC_DIR")"
63
+ else
64
+ printf 'export CURDX_FLOW_SPEC_DIR=%s\n' "$(json_escape "")"
65
+ fi
66
+ } >> "$CLAUDE_ENV_FILE" 2>/dev/null || true
67
+ fi
68
+
69
+ if [ -z "$FLOW_ROOT" ]; then
70
+ printf '{"watchPaths":[]}\n'
71
+ exit 0
72
+ fi
73
+
74
+ export CURDX_FLOW_CONTEXT_WATCH_ROOT="$FLOW_ROOT"
75
+ export CURDX_FLOW_CONTEXT_WATCH_ACTIVE="$ACTIVE_SPEC"
76
+
77
+ python3 <<'PY' 2>/dev/null
78
+ import json
79
+ import os
80
+ from pathlib import Path
81
+
82
+ root = Path(os.environ["CURDX_FLOW_CONTEXT_WATCH_ROOT"])
83
+ active = os.environ.get("CURDX_FLOW_CONTEXT_WATCH_ACTIVE", "").strip()
84
+
85
+ watch_paths = [str(root / ".flow" / ".active-spec")]
86
+ if active:
87
+ spec_dir = root / ".flow" / "specs" / active
88
+ watch_paths.append(str(spec_dir / ".state.json"))
89
+ watch_paths.append(str(spec_dir / "tasks.md"))
90
+
91
+ print(json.dumps({"watchPaths": watch_paths}, ensure_ascii=True))
92
+ PY
93
+
94
+ exit 0
@@ -35,12 +35,13 @@ if [ "$TOOL_NAME" != "AskUserQuestion" ]; then
35
35
  fi
36
36
 
37
37
  # Check if we're in a flow project with quick mode enabled.
38
- [ ! -d ".flow" ] && exit 0
38
+ FLOW_ROOT="$(resolve_flow_root 2>/dev/null || true)"
39
+ [ -n "$FLOW_ROOT" ] || exit 0
39
40
 
40
- ACTIVE=$(cat .flow/.active-spec 2>/dev/null)
41
+ ACTIVE=$(cat "$FLOW_ROOT/.flow/.active-spec" 2>/dev/null)
41
42
  [ -z "$ACTIVE" ] && exit 0
42
43
 
43
- STATE_FILE=".flow/specs/$ACTIVE/.state.json"
44
+ STATE_FILE="$FLOW_ROOT/.flow/specs/$ACTIVE/.state.json"
44
45
  [ ! -f "$STATE_FILE" ] && exit 0
45
46
 
46
47
  # Read quickMode. Pass STATE_FILE via env (NOT shell interpolation
@@ -19,6 +19,7 @@ DATA_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.claude/plugins/data/curdx-flow}"
19
19
  MARKER="$DATA_DIR/.deps-checked"
20
20
  TODAY="$(date +%Y-%m-%d)"
21
21
  ADDITIONAL_CONTEXT=""
22
+ FLOW_ROOT="$(resolve_flow_root 2>/dev/null || true)"
22
23
 
23
24
  mkdir -p "$DATA_DIR" 2>/dev/null || true
24
25
 
@@ -50,22 +51,22 @@ if env_flag_enabled "${CLAUDE_PLUGIN_OPTION_DAILY_DEPENDENCY_CHECK:-1}" && [ "$L
50
51
  fi
51
52
 
52
53
  # ---------- 2. Load .flow/ state (if project is a flow project) ----------
53
- if [ -d ".flow" ]; then
54
+ if [ -n "$FLOW_ROOT" ]; then
54
55
  ADDITIONAL_CONTEXT+="## CurDX-Flow Project Active\n\n"
55
56
  ADDITIONAL_CONTEXT+="- Plugin root: \`${CLAUDE_PLUGIN_ROOT:-unknown}\`\n"
56
57
  ADDITIONAL_CONTEXT+="- Plugin data: \`${CLAUDE_PLUGIN_DATA:-$DATA_DIR}\`\n"
57
58
  ADDITIONAL_CONTEXT+="- Best practice: write long agent artifacts to disk first; keep final assistant summaries short.\n\n"
58
59
 
59
- if [ -f ".flow/PROJECT.md" ]; then
60
- ADDITIONAL_CONTEXT+="### Project Vision\n$(head -80 .flow/PROJECT.md)\n\n"
60
+ if [ -f "$FLOW_ROOT/.flow/PROJECT.md" ]; then
61
+ ADDITIONAL_CONTEXT+="### Project Vision\n$(head -80 "$FLOW_ROOT/.flow/PROJECT.md")\n\n"
61
62
  fi
62
63
 
63
- if [ -f ".flow/.active-spec" ]; then
64
- ACTIVE="$(cat .flow/.active-spec 2>/dev/null)"
65
- if [ -n "$ACTIVE" ] && [ -d ".flow/specs/$ACTIVE" ]; then
64
+ if [ -f "$FLOW_ROOT/.flow/.active-spec" ]; then
65
+ ACTIVE="$(cat "$FLOW_ROOT/.flow/.active-spec" 2>/dev/null)"
66
+ if [ -n "$ACTIVE" ] && [ -d "$FLOW_ROOT/.flow/specs/$ACTIVE" ]; then
66
67
  ADDITIONAL_CONTEXT+="### Active Spec: \`$ACTIVE\`\n\n"
67
- if [ -f ".flow/specs/$ACTIVE/.progress.md" ]; then
68
- ADDITIONAL_CONTEXT+="$(head -40 ".flow/specs/$ACTIVE/.progress.md")\n\n"
68
+ if [ -f "$FLOW_ROOT/.flow/specs/$ACTIVE/.progress.md" ]; then
69
+ ADDITIONAL_CONTEXT+="$(head -40 "$FLOW_ROOT/.flow/specs/$ACTIVE/.progress.md")\n\n"
69
70
  fi
70
71
  fi
71
72
  fi
@@ -76,8 +77,11 @@ if [ -n "${CLAUDE_ENV_FILE:-}" ]; then
76
77
  {
77
78
  printf 'export CURDX_FLOW_PLUGIN_ROOT=%s\n' "$(json_escape "${CLAUDE_PLUGIN_ROOT:-}")"
78
79
  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)")"
80
+ if [ -n "$FLOW_ROOT" ]; then
81
+ printf 'export CURDX_FLOW_PROJECT_ROOT=%s\n' "$(json_escape "$FLOW_ROOT")"
82
+ fi
83
+ if [ -n "$FLOW_ROOT" ] && [ -f "$FLOW_ROOT/.flow/.active-spec" ]; then
84
+ printf 'export CURDX_FLOW_ACTIVE_SPEC=%s\n' "$(json_escape "$(cat "$FLOW_ROOT/.flow/.active-spec" 2>/dev/null)")"
81
85
  fi
82
86
  } >> "$CLAUDE_ENV_FILE" 2>/dev/null || true
83
87
  fi