@curdx/flow 2.3.11 → 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 (44) 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/hooks/hooks.json +72 -0
  16. package/hooks/scripts/common.sh +191 -0
  17. package/hooks/scripts/config-change-guard.sh +94 -0
  18. package/hooks/scripts/flow-context-watch.sh +94 -0
  19. package/hooks/scripts/quick-mode-guard.sh +4 -3
  20. package/hooks/scripts/session-start.sh +14 -10
  21. package/hooks/scripts/session-title.sh +87 -0
  22. package/hooks/scripts/stop-watcher.sh +4 -3
  23. package/hooks/scripts/subagent-artifact-guard.sh +7 -74
  24. package/hooks/scripts/subagent-statusline.sh +8 -2
  25. package/hooks/scripts/task-lifecycle-guard.sh +106 -0
  26. package/hooks/scripts/teammate-idle-guard.sh +83 -0
  27. package/knowledge/claude-code-runtime-contracts.md +21 -0
  28. package/monitors/scripts/flow-state-monitor.sh +8 -5
  29. package/output-styles/curdx-fast-mode.md +42 -0
  30. package/output-styles/curdx-spec-mode.md +46 -0
  31. package/package.json +5 -3
  32. package/schemas/agent-frontmatter.schema.json +4 -1
  33. package/schemas/spec-state.schema.json +18 -0
  34. package/settings.json +2 -1
  35. package/skills/implement/SKILL.md +8 -0
  36. package/skills/implement/references/linear-execution.md +11 -0
  37. package/skills/implement/references/native-task-sync.md +107 -0
  38. package/skills/implement/references/progress-contract.md +4 -0
  39. package/skills/implement/references/state-init.md +3 -0
  40. package/skills/implement/references/stop-hook-execution.md +19 -5
  41. package/skills/implement/references/subagent-execution.md +16 -2
  42. package/skills/implement/references/wave-execution.md +18 -0
  43. package/skills/status/references/gather-contract.md +3 -0
  44. package/skills/status/references/output-contract.md +1 -0
@@ -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
@@ -0,0 +1,87 @@
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
+ DATA_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.claude/plugins/data/curdx-flow}"
10
+ FLOW_ROOT="$(resolve_flow_root 2>/dev/null || true)"
11
+
12
+ [ -n "$FLOW_ROOT" ] || exit 0
13
+
14
+ ACTIVE="$(cat "$FLOW_ROOT/.flow/.active-spec" 2>/dev/null || true)"
15
+ [ -n "$ACTIVE" ] || exit 0
16
+
17
+ if ! has_python3; then
18
+ emit_userprompt_submit_title "curdx-flow/${ACTIVE}"
19
+ exit 0
20
+ fi
21
+
22
+ export CURDX_FLOW_SESSION_TITLE_INPUT="$INPUT"
23
+ export CURDX_FLOW_SESSION_TITLE_ACTIVE="$ACTIVE"
24
+ export CURDX_FLOW_ROOT="$FLOW_ROOT"
25
+
26
+ TITLE_INFO="$(python3 <<'PY' 2>/dev/null
27
+ import json
28
+ import os
29
+ from pathlib import Path
30
+
31
+ active = os.environ["CURDX_FLOW_SESSION_TITLE_ACTIVE"]
32
+ data = json.loads(os.environ.get("CURDX_FLOW_SESSION_TITLE_INPUT", "{}"))
33
+ session_id = data.get("session_id", "")
34
+
35
+ phase = ""
36
+ task_index = 0
37
+ total_tasks = 0
38
+
39
+ state_path = Path(os.environ["CURDX_FLOW_ROOT"]) / ".flow" / "specs" / active / ".state.json"
40
+ if state_path.exists():
41
+ try:
42
+ state = json.loads(state_path.read_text(encoding="utf-8"))
43
+ phase = state.get("phase") or ""
44
+ execute_state = state.get("execute_state") or {}
45
+ task_index = int(execute_state.get("task_index") or 0)
46
+ total_tasks = int(execute_state.get("total_tasks") or 0)
47
+ except Exception:
48
+ phase = ""
49
+ task_index = 0
50
+ total_tasks = 0
51
+
52
+ label = phase
53
+ if phase == "execute" and total_tasks > 0:
54
+ label = f"{phase} {task_index}/{total_tasks}"
55
+
56
+ title = f"curdx-flow/{active}"
57
+ if label:
58
+ title = f"{title} [{label}]"
59
+
60
+ print(session_id)
61
+ print(title)
62
+ PY
63
+ )"
64
+
65
+ SESSION_ID="$(printf '%s\n' "$TITLE_INFO" | sed -n '1p')"
66
+ TITLE="$(printf '%s\n' "$TITLE_INFO" | sed -n '2p')"
67
+ [ -n "$TITLE" ] || exit 0
68
+
69
+ if [ -n "$SESSION_ID" ]; then
70
+ MARKER_DIR="$DATA_DIR/session-titles"
71
+ MARKER_FILE="$MARKER_DIR/$SESSION_ID"
72
+ LAST_TITLE=""
73
+
74
+ if [ -f "$MARKER_FILE" ]; then
75
+ LAST_TITLE="$(cat "$MARKER_FILE" 2>/dev/null || true)"
76
+ fi
77
+
78
+ if [ "$LAST_TITLE" = "$TITLE" ]; then
79
+ exit 0
80
+ fi
81
+
82
+ mkdir -p "$MARKER_DIR" 2>/dev/null || true
83
+ printf '%s\n' "$TITLE" > "$MARKER_FILE" 2>/dev/null || true
84
+ fi
85
+
86
+ emit_userprompt_submit_title "$TITLE"
87
+ exit 0
@@ -38,12 +38,13 @@ block_continue() {
38
38
  }
39
39
 
40
40
  # ---------- 1. Must be a flow project ----------
41
- [ ! -d ".flow" ] && allow_stop
41
+ FLOW_ROOT="$(resolve_flow_root 2>/dev/null || true)"
42
+ [ -n "$FLOW_ROOT" ] || allow_stop
42
43
 
43
44
  # ---------- 2. Must have active spec ----------
44
- ACTIVE=$(cat .flow/.active-spec 2>/dev/null)
45
+ ACTIVE=$(cat "$FLOW_ROOT/.flow/.active-spec" 2>/dev/null)
45
46
  [ -z "$ACTIVE" ] && allow_stop
46
- SPEC_DIR=".flow/specs/$ACTIVE"
47
+ SPEC_DIR="$FLOW_ROOT/.flow/specs/$ACTIVE"
47
48
  [ ! -d "$SPEC_DIR" ] && allow_stop
48
49
 
49
50
  STATE_FILE="$SPEC_DIR/.state.json"
@@ -14,6 +14,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
14
  . "$SCRIPT_DIR/common.sh"
15
15
 
16
16
  INPUT="$(cat 2>/dev/null || echo "{}")"
17
+ FLOW_ROOT="$(resolve_flow_root 2>/dev/null || true)"
17
18
 
18
19
  if ! has_python3; then
19
20
  # Without JSON parsing, fail open rather than blocking subagents blindly.
@@ -43,18 +44,6 @@ looks_like_success() {
43
44
  printf '%s' "$msg" | grep -Eq '(^✓| generated$| generated\n|Wrote |Review complete|Requirements done|Research complete|UI Sketch generation complete|Report:|Next:|TASK_COMPLETE|ALL_TASKS_COMPLETE)'
44
45
  }
45
46
 
46
- active_spec_path() {
47
- local file_name="$1"
48
-
49
- [ ! -d ".flow" ] && return 1
50
-
51
- local active
52
- active="$(cat .flow/.active-spec 2>/dev/null)"
53
- [ -z "$active" ] && return 1
54
-
55
- printf '.flow/specs/%s/%s\n' "$active" "$file_name"
56
- }
57
-
58
47
  completed_task_id() {
59
48
  local msg="${1:-}"
60
49
  printf '%s' "$msg" | sed -nE 's/.*TASK_COMPLETE:[[:space:]]*([0-9]+(\.([0-9]+|VF|X(\+[0-9]+)?))*).*/\1/p' | head -1
@@ -64,24 +53,10 @@ artifact_target=""
64
53
  minimum_size=200
65
54
 
66
55
  case "$AGENT_TYPE" in
67
- flow-researcher)
68
- artifact_target="$(active_spec_path research.md)" || exit 0
69
- minimum_size=400
70
- ;;
71
- flow-product-designer)
72
- artifact_target="$(active_spec_path requirements.md)" || exit 0
73
- minimum_size=400
74
- ;;
75
- flow-architect)
76
- artifact_target="$(active_spec_path design.md)" || exit 0
77
- minimum_size=400
78
- ;;
79
- flow-planner)
80
- artifact_target="$(active_spec_path tasks.md)" || exit 0
81
- minimum_size=400
82
- ;;
83
56
  flow-executor)
84
- artifact_target="$(active_spec_path tasks.md)" || exit 0
57
+ curdx_resolve_artifact_contract "$AGENT_TYPE" "$LAST_MESSAGE" "$FLOW_ROOT" || exit 0
58
+ artifact_target="$CURDX_ARTIFACT_TARGET"
59
+ minimum_size="$CURDX_ARTIFACT_MIN_SIZE"
85
60
  if printf '%s' "$LAST_MESSAGE" | grep -q 'ALL_TASKS_COMPLETE'; then
86
61
  exit 0
87
62
  fi
@@ -93,52 +68,10 @@ case "$AGENT_TYPE" in
93
68
  emit_subagentstop_block "[CurDX-Flow subagent-artifact-guard] flow-executor emitted TASK_COMPLETE: ${task_id}, but ${artifact_target} does not mark that task as [x]. Update tasks.md and the spec progress/state before stopping."
94
69
  exit 0
95
70
  ;;
96
- flow-debugger)
97
- artifact_target="$(active_spec_path debug-report.md)" || exit 0
98
- minimum_size=250
99
- ;;
100
- flow-triage-analyst)
101
- artifact_target="$(active_spec_path triage-report.md)" || exit 0
102
- minimum_size=250
103
- ;;
104
- flow-ux-designer)
105
- artifact_target="$(active_spec_path ui-sketch.md)" || exit 0
106
- minimum_size=250
107
- ;;
108
- flow-reviewer)
109
- artifact_target="$(active_spec_path review-report.md)" || exit 0
110
- minimum_size=300
111
- ;;
112
- flow-verifier)
113
- artifact_target="$(active_spec_path verification-report.md)" || exit 0
114
- minimum_size=300
115
- ;;
116
- flow-security-auditor)
117
- artifact_target="$(active_spec_path security-audit.md)" || exit 0
118
- minimum_size=250
119
- ;;
120
- flow-qa-engineer)
121
- artifact_target="$(active_spec_path qa-report.md)" || exit 0
122
- minimum_size=250
123
- ;;
124
- flow-edge-hunter)
125
- artifact_target="$(active_spec_path edge-cases.md)" || exit 0
126
- minimum_size=250
127
- ;;
128
- flow-adversary)
129
- artifact_target="$(active_spec_path adversarial-review.md)" || exit 0
130
- minimum_size=250
131
- ;;
132
- flow-ui-researcher)
133
- artifact_target="$(active_spec_path ui-research.md)" || exit 0
134
- minimum_size=250
135
- ;;
136
- flow-brownfield-analyst)
137
- artifact_target=".flow/codebase-index.md"
138
- minimum_size=250
139
- ;;
140
71
  *)
141
- exit 0
72
+ curdx_resolve_artifact_contract "$AGENT_TYPE" "$LAST_MESSAGE" "$FLOW_ROOT" || exit 0
73
+ artifact_target="$CURDX_ARTIFACT_TARGET"
74
+ minimum_size="$CURDX_ARTIFACT_MIN_SIZE"
142
75
  ;;
143
76
  esac
144
77