specline 1.1.2 → 1.2.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.
- package/README.md +95 -16
- package/package.json +1 -1
- package/templates/.cursor/agents/specline-spec-reviewer.md +11 -1
- package/templates/.cursor/commands/specline-quickfix.md +24 -0
- package/templates/.cursor/hooks/specline-agent-guard.sh +28 -20
- package/templates/.cursor/hooks/specline-phase-guard.sh +21 -22
- package/templates/.cursor/hooks/specline-pipeline-gate.sh +163 -14
- package/templates/.cursor/hooks/specline-reminder.sh +27 -19
- package/templates/.cursor/hooks/specline-session-start.sh +205 -80
- package/templates/.cursor/skills/specline-explore/SKILL.md +17 -0
- package/templates/.cursor/skills/specline-pipeline/SKILL.md +256 -22
- package/templates/.cursor/skills/specline-propose/SKILL.md +4 -1
- package/templates/.cursor/skills/specline-quickfix/SKILL.md +239 -0
|
@@ -13,6 +13,7 @@ set -euo pipefail
|
|
|
13
13
|
input=$(cat)
|
|
14
14
|
tool_name=$(echo "$input" | jq -r '.tool_name // empty')
|
|
15
15
|
tool_input=$(echo "$input" | jq -r '.tool_input // "{}"')
|
|
16
|
+
SESSION_ID=$(echo "$input" | jq -r '.session_id // empty')
|
|
16
17
|
|
|
17
18
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
18
19
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
@@ -20,28 +21,35 @@ CHANGES_DIR="$PROJECT_ROOT/specline/changes"
|
|
|
20
21
|
|
|
21
22
|
# ===== 辅助函数 =====
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
resolve_pipeline_for_session() {
|
|
25
|
+
local session_id="$1"
|
|
26
|
+
local bindings_file="$PROJECT_ROOT/specline/.pipeline-sessions.json"
|
|
27
|
+
|
|
28
|
+
if [ ! -f "$bindings_file" ]; then
|
|
29
|
+
return 1
|
|
27
30
|
fi
|
|
28
|
-
for f in "$CHANGES_DIR"/*/.pipeline-state.json; do
|
|
29
|
-
[ -f "$f" ] || continue
|
|
30
|
-
if echo "$f" | grep -q "/archive/"; then continue; fi
|
|
31
|
-
local ph
|
|
32
|
-
ph=$(jq -r '.current_phase // ""' "$f" 2>/dev/null)
|
|
33
|
-
if [ "$ph" != "archive" ] && [ "$ph" != "" ]; then
|
|
34
|
-
echo "$f"
|
|
35
|
-
return
|
|
36
|
-
fi
|
|
37
|
-
done
|
|
38
|
-
echo ""
|
|
39
|
-
}
|
|
40
31
|
|
|
41
|
-
|
|
32
|
+
local change_name
|
|
33
|
+
change_name=$(jq -r --arg sid "$session_id" '.[$sid].change // empty' "$bindings_file" 2>/dev/null)
|
|
34
|
+
|
|
35
|
+
if [ -z "$change_name" ]; then
|
|
36
|
+
return 1
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
local state_file="$CHANGES_DIR/$change_name/.pipeline-state.json"
|
|
40
|
+
|
|
41
|
+
if [ ! -f "$state_file" ]; then
|
|
42
|
+
# 脏数据:绑定存在但 pipeline 已不存在,清理绑定
|
|
43
|
+
jq --arg sid "$session_id" 'del(.[$sid])' "$bindings_file" > "$bindings_file.tmp" && mv "$bindings_file.tmp" "$bindings_file"
|
|
44
|
+
return 1
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
STATE_FILE="$state_file"
|
|
48
|
+
return 0
|
|
49
|
+
}
|
|
42
50
|
|
|
43
|
-
|
|
44
|
-
if
|
|
51
|
+
STATE_FILE=""
|
|
52
|
+
if ! resolve_pipeline_for_session "$SESSION_ID"; then
|
|
45
53
|
echo '{}'
|
|
46
54
|
exit 0
|
|
47
55
|
fi
|
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# specline-session-start.sh — sessionStart Hook
|
|
3
|
-
#
|
|
2
|
+
# specline-session-start.sh — sessionStart Hook (Pipeline Session Binding)
|
|
3
|
+
#
|
|
4
|
+
# 新会话启动时:
|
|
5
|
+
# 1. 清理过期绑定(7 天未更新)
|
|
6
|
+
# 2. 检查当前 session 是否已有绑定
|
|
7
|
+
# 3. 有绑定且 pipeline 仍活跃 → 使用已有绑定,注入上下文
|
|
8
|
+
# 4. 有绑定但 pipeline 已失效 → 清理脏数据,重新扫描
|
|
9
|
+
# 5. 无绑定 → 透明放行(echo '{}')——不自动绑定,避免跨窗口污染
|
|
4
10
|
#
|
|
5
11
|
# Input (stdin JSON):
|
|
6
|
-
# { "session_id": "...", "is_background_agent": bool,
|
|
12
|
+
# { "session_id": "...", "is_background_agent": bool, ... }
|
|
7
13
|
#
|
|
8
14
|
# Output (stdout JSON):
|
|
9
15
|
# { "additional_context": "<pipeline 上下文>" } 或 {}(无活跃 pipeline)
|
|
10
16
|
|
|
11
17
|
set -euo pipefail
|
|
12
18
|
|
|
19
|
+
# ============================================================================
|
|
20
|
+
# Input
|
|
21
|
+
# ============================================================================
|
|
22
|
+
|
|
13
23
|
input=$(cat)
|
|
14
24
|
|
|
15
25
|
# 跳过 background agent(子 Agent 不需要 pipeline 上下文)
|
|
@@ -19,116 +29,231 @@ if [ "$is_bg" = "true" ]; then
|
|
|
19
29
|
exit 0
|
|
20
30
|
fi
|
|
21
31
|
|
|
32
|
+
session_id=$(echo "$input" | jq -r '.session_id // empty')
|
|
33
|
+
if [ -z "$session_id" ]; then
|
|
34
|
+
echo '{}'
|
|
35
|
+
exit 0
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# ============================================================================
|
|
39
|
+
# Paths
|
|
40
|
+
# ============================================================================
|
|
41
|
+
|
|
22
42
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
23
43
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
44
|
+
BINDINGS_FILE="$PROJECT_ROOT/specline/.pipeline-sessions.json"
|
|
24
45
|
CHANGES_DIR="$PROJECT_ROOT/specline/changes"
|
|
25
46
|
|
|
26
|
-
#
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
47
|
+
# ============================================================================
|
|
48
|
+
# Helper Functions
|
|
49
|
+
# ============================================================================
|
|
50
|
+
|
|
51
|
+
# init_bindings_file — ensure bindings file exists with a valid JSON object
|
|
52
|
+
init_bindings_file() {
|
|
53
|
+
if [ ! -f "$BINDINGS_FILE" ]; then
|
|
54
|
+
mkdir -p "$(dirname "$BINDINGS_FILE")"
|
|
55
|
+
echo '{}' > "$BINDINGS_FILE"
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
# Validate it's readable JSON; reset if corrupted
|
|
59
|
+
if ! jq empty "$BINDINGS_FILE" 2>/dev/null; then
|
|
60
|
+
echo '{}' > "$BINDINGS_FILE"
|
|
61
|
+
fi
|
|
62
|
+
}
|
|
31
63
|
|
|
32
|
-
#
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
first_phase=""
|
|
64
|
+
# clean_expired_bindings — remove bindings where bound_at > 7 days ago
|
|
65
|
+
# Uses ISO-8601 lexical comparison: works on both macOS and Linux
|
|
66
|
+
clean_expired_bindings() {
|
|
67
|
+
[ -f "$BINDINGS_FILE" ] || return 0
|
|
37
68
|
|
|
38
|
-
|
|
39
|
-
|
|
69
|
+
local cutoff_date
|
|
70
|
+
# macOS date, fallback to Linux date
|
|
71
|
+
cutoff_date=$(date -u -v-7d +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || \
|
|
72
|
+
date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null)
|
|
40
73
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if echo "$parent_dir" | grep -q "/archive/"; then
|
|
44
|
-
continue
|
|
74
|
+
if [ -z "$cutoff_date" ]; then
|
|
75
|
+
return 0 # Cannot determine cutoff, skip cleanup
|
|
45
76
|
fi
|
|
46
77
|
|
|
47
|
-
|
|
78
|
+
local tmp_file="${BINDINGS_FILE}.tmp"
|
|
79
|
+
# Keep only entries whose bound_at is >= cutoff (i.e. within the last 7 days)
|
|
80
|
+
jq --arg cutoff "$cutoff_date" \
|
|
81
|
+
'with_entries(select(.value.bound_at >= $cutoff))' \
|
|
82
|
+
"$BINDINGS_FILE" > "$tmp_file" 2>/dev/null && \
|
|
83
|
+
mv "$tmp_file" "$BINDINGS_FILE" 2>/dev/null || true
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# is_pipeline_active — check if a pipeline's state file exists and is not archived
|
|
87
|
+
# Returns 0 (active) or 1 (inactive)
|
|
88
|
+
is_pipeline_active() {
|
|
89
|
+
local change_name="$1"
|
|
90
|
+
local state_file="$CHANGES_DIR/$change_name/.pipeline-state.json"
|
|
91
|
+
|
|
92
|
+
[ -f "$state_file" ] || return 1
|
|
93
|
+
|
|
94
|
+
local phase
|
|
48
95
|
phase=$(jq -r '.current_phase // "unknown"' "$state_file" 2>/dev/null)
|
|
49
|
-
|
|
96
|
+
[ "$phase" != "archive" ] || return 1
|
|
50
97
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
continue
|
|
54
|
-
fi
|
|
98
|
+
return 0
|
|
99
|
+
}
|
|
55
100
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
101
|
+
# scan_active_pipelines — scan changes/ (excluding archive/) for active pipelines
|
|
102
|
+
# Output: one line per pipeline: change_name|phase|state_file_path
|
|
103
|
+
scan_active_pipelines() {
|
|
104
|
+
if [ ! -d "$CHANGES_DIR" ]; then
|
|
105
|
+
return 0
|
|
60
106
|
fi
|
|
61
|
-
pipeline_count=$((pipeline_count + 1))
|
|
62
107
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
total=$(jq -r '[.phases.coding.tasks[]?] | length' "$state_file" 2>/dev/null || echo "0")
|
|
108
|
+
for state_file in "$CHANGES_DIR"/*/.pipeline-state.json; do
|
|
109
|
+
[ -f "$state_file" ] || continue
|
|
66
110
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
active_pipelines="${active_pipelines}\n"
|
|
72
|
-
done
|
|
111
|
+
# Exclude archive/
|
|
112
|
+
case "$state_file" in
|
|
113
|
+
*/archive/*) continue ;;
|
|
114
|
+
esac
|
|
73
115
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
116
|
+
local change_name phase
|
|
117
|
+
change_name=$(jq -r '.change_name // ""' "$state_file" 2>/dev/null)
|
|
118
|
+
phase=$(jq -r '.current_phase // ""' "$state_file" 2>/dev/null)
|
|
119
|
+
|
|
120
|
+
[ -n "$change_name" ] || continue
|
|
121
|
+
[ "$phase" != "archive" ] || continue
|
|
79
122
|
|
|
80
|
-
|
|
123
|
+
printf '%s|%s|%s\n' "$change_name" "$phase" "$state_file"
|
|
124
|
+
done
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# phase_constraints — output constraint text for a given phase (to stdout)
|
|
81
128
|
phase_constraints() {
|
|
82
|
-
|
|
129
|
+
local phase="$1"
|
|
130
|
+
case "$phase" in
|
|
83
131
|
spec)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
132
|
+
printf '%s\n' "- 只能通过 specline-spec-creator / specline-spec-reviewer 子 Agent 工作"
|
|
133
|
+
printf '%s\n' "- 禁止编辑任何应用代码文件(.ts/.tsx/.py/.go 等)"
|
|
134
|
+
printf '%s\n' "- 规划文件生成后需运行 Spec Gate"
|
|
87
135
|
;;
|
|
88
136
|
coding)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
137
|
+
printf '%s\n' "- 编码必须通过子 Agent:specline-frontend-dev / specline-backend-dev / specline-config-dev"
|
|
138
|
+
printf '%s\n' "- 禁止直接编辑应用代码文件"
|
|
139
|
+
printf '%s\n' "- 每批次任务完成后运行 Build Gate"
|
|
140
|
+
printf '%s\n' "- 每个 Task 完成后更新 tasks.md 的 checkbox"
|
|
93
141
|
;;
|
|
94
142
|
code_review)
|
|
95
|
-
|
|
96
|
-
|
|
143
|
+
printf '%s\n' "- 只能运行 specline-code-reviewer + Lint Gate"
|
|
144
|
+
printf '%s\n' "- 如需修复代码,通过子 Agent 完成"
|
|
97
145
|
;;
|
|
98
146
|
test)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
147
|
+
printf '%s\n' "- 运行测试 Gate 链:unit → integration → e2e"
|
|
148
|
+
printf '%s\n' "- 测试失败时通过 specline-test-runner 分析原因"
|
|
149
|
+
printf '%s\n' "- 代码修复通过子 Agent,测试修复通过 specline-test-writer"
|
|
102
150
|
;;
|
|
103
151
|
*)
|
|
104
|
-
|
|
152
|
+
printf '%s\n' "- 遵循 specline-pipeline SKILL 的当前阶段约束"
|
|
105
153
|
;;
|
|
106
154
|
esac
|
|
107
155
|
}
|
|
108
156
|
|
|
109
|
-
#
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
else
|
|
120
|
-
context_text+="**活跃变更** (${pipeline_count} 个):\n${active_pipelines}\n"
|
|
121
|
-
fi
|
|
122
|
-
context_text+="**阶段约束**:\n${constraints}\n\n"
|
|
123
|
-
context_text+="**重要**: 你是 Specline Pipeline 编排者。上述约束具有最高优先级,必须在每个操作前检查是否符合当前阶段要求。"
|
|
157
|
+
# build_context — generate a JSON-escaped additional_context string for a bound pipeline
|
|
158
|
+
build_context() {
|
|
159
|
+
local change_name="$1"
|
|
160
|
+
local phase="$2"
|
|
161
|
+
local state_file="$3"
|
|
162
|
+
|
|
163
|
+
{
|
|
164
|
+
printf '🚨 **Specline Pipeline 运行中**\n\n'
|
|
165
|
+
printf '**当前变更**: %s\n' "$change_name"
|
|
166
|
+
printf '**当前阶段**: %s\n' "$phase"
|
|
124
167
|
|
|
125
|
-
#
|
|
126
|
-
|
|
168
|
+
# Task progress for coding phase
|
|
169
|
+
if [ "$phase" = "coding" ] && [ -f "$state_file" ]; then
|
|
170
|
+
local completed total
|
|
171
|
+
completed=$(jq -r '[.phases.coding.tasks[]? | select(.status == "completed")] | length' "$state_file" 2>/dev/null || printf '0')
|
|
172
|
+
total=$(jq -r '[.phases.coding.tasks[]?] | length' "$state_file" 2>/dev/null || printf '0')
|
|
173
|
+
if [ "$total" != "0" ]; then
|
|
174
|
+
printf '**任务进度**: %s/%s 完成\n' "$completed" "$total"
|
|
175
|
+
fi
|
|
176
|
+
fi
|
|
127
177
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
178
|
+
printf '\n**阶段约束**:\n'
|
|
179
|
+
phase_constraints "$phase"
|
|
180
|
+
printf '\n'
|
|
181
|
+
printf '**重要**: 你是 Specline Pipeline 编排者。上述约束具有最高优先级,必须在每个操作前检查是否符合当前阶段要求。'
|
|
182
|
+
} | jq -Rs '.'
|
|
131
183
|
}
|
|
132
|
-
EOF
|
|
133
184
|
|
|
185
|
+
# phase_constraint_table — static phase constraint reference table (markdown)
|
|
186
|
+
phase_constraint_table() {
|
|
187
|
+
cat << 'TABLEEOF'
|
|
188
|
+
| 阶段 | 约束 |
|
|
189
|
+
|------|------|
|
|
190
|
+
| spec | 只能通过 specline-spec-creator/specline-spec-reviewer 子 Agent 工作,禁止编辑代码 |
|
|
191
|
+
| coding | 必须通过子 Agent 编码,批次完成后运行 Build Gate,更新 tasks.md |
|
|
192
|
+
| code_review | 只能运行 specline-code-reviewer + Lint Gate |
|
|
193
|
+
| test | 运行测试 Gate 链:unit → integration → e2e |
|
|
194
|
+
TABLEEOF
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
# write_binding — write a session_id → change_name binding to .pipeline-sessions.json
|
|
198
|
+
write_binding() {
|
|
199
|
+
local sid="$1"
|
|
200
|
+
local change="$2"
|
|
201
|
+
|
|
202
|
+
local now_iso
|
|
203
|
+
now_iso=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
204
|
+
|
|
205
|
+
local tmp_file="${BINDINGS_FILE}.tmp"
|
|
206
|
+
jq --arg sid "$sid" --arg change "$change" --arg now "$now_iso" \
|
|
207
|
+
'.[$sid] = {"change": $change, "bound_at": $now}' \
|
|
208
|
+
"$BINDINGS_FILE" > "$tmp_file" 2>/dev/null && \
|
|
209
|
+
mv "$tmp_file" "$BINDINGS_FILE" 2>/dev/null || true
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
# delete_binding — remove a session_id entry from .pipeline-sessions.json
|
|
213
|
+
delete_binding() {
|
|
214
|
+
local session_id="$1"
|
|
215
|
+
|
|
216
|
+
[ -f "$BINDINGS_FILE" ] || return 0
|
|
217
|
+
|
|
218
|
+
local tmp_file="${BINDINGS_FILE}.tmp"
|
|
219
|
+
jq --arg sid "$session_id" 'del(.[$sid])' "$BINDINGS_FILE" > "$tmp_file" 2>/dev/null && \
|
|
220
|
+
mv "$tmp_file" "$BINDINGS_FILE" 2>/dev/null || true
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
# ============================================================================
|
|
224
|
+
# Main Logic
|
|
225
|
+
# ============================================================================
|
|
226
|
+
|
|
227
|
+
init_bindings_file
|
|
228
|
+
|
|
229
|
+
# 1. Clean expired bindings (bound_at > 7 days ago)
|
|
230
|
+
clean_expired_bindings
|
|
231
|
+
|
|
232
|
+
# 2. Check existing binding for this session
|
|
233
|
+
existing_change=""
|
|
234
|
+
if [ -f "$BINDINGS_FILE" ]; then
|
|
235
|
+
existing_change=$(jq -r --arg sid "$session_id" '.[$sid].change // empty' "$BINDINGS_FILE" 2>/dev/null)
|
|
236
|
+
fi
|
|
237
|
+
|
|
238
|
+
if [ -n "$existing_change" ]; then
|
|
239
|
+
if is_pipeline_active "$existing_change"; then
|
|
240
|
+
# Binding is still valid — use it
|
|
241
|
+
state_file="$CHANGES_DIR/$existing_change/.pipeline-state.json"
|
|
242
|
+
phase=$(jq -r '.current_phase // "unknown"' "$state_file" 2>/dev/null)
|
|
243
|
+
|
|
244
|
+
ctx_json=$(build_context "$existing_change" "$phase" "$state_file")
|
|
245
|
+
|
|
246
|
+
printf '{\n "additional_context": %s\n}\n' "$ctx_json"
|
|
247
|
+
exit 0
|
|
248
|
+
else
|
|
249
|
+
# Dirty data: pipeline archived or deleted — clean up and rescan
|
|
250
|
+
delete_binding "$session_id"
|
|
251
|
+
fi
|
|
252
|
+
fi
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# 3. No (valid) binding → transparent pass-through
|
|
256
|
+
# 不再自动绑定或注入任何 pipeline 上下文,避免跨窗口污染。
|
|
257
|
+
# 用户需通过 /specline-pipeline --change <name> 显式绑定。
|
|
258
|
+
echo '{}'
|
|
134
259
|
exit 0
|
|
@@ -9,6 +9,23 @@ metadata:
|
|
|
9
9
|
generatedBy: "1.3.1"
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
+
## ⚠️ Mode Check — Read Before Anything Else
|
|
13
|
+
|
|
14
|
+
探索模式 MUST run in **Ask Mode** for hard read-only enforcement by Cursor.
|
|
15
|
+
|
|
16
|
+
### Your very first action
|
|
17
|
+
|
|
18
|
+
1. Check which Cursor mode you are currently in (agent / ask / plan).
|
|
19
|
+
2. **If you are NOT in Ask Mode** — Output ONLY the following message and STOP. Do not explore, do not read files, do not do anything else:
|
|
20
|
+
|
|
21
|
+
> 探索模式需要在只读(Ask)模式下运行。请点击输入框左侧的模式选择器,切换到 **Ask 模式**,然后重新输入 `/specline-explore`。
|
|
22
|
+
|
|
23
|
+
3. **If you are in Ask Mode** — You are in a safe, read-only environment. The system guarantees you cannot edit files, create files, rewrite files, delete files, or run commands. Proceed with exploration freely.
|
|
24
|
+
|
|
25
|
+
### Why this matters
|
|
26
|
+
|
|
27
|
+
Ask Mode is Cursor's only truly read-only mode. In Agent/Plan mode, only Prompt-level instructions prevent code changes — and those can be ignored. Ask Mode enforces read-only at the system level, making it impossible to accidentally modify files during exploration.
|
|
28
|
+
|
|
12
29
|
> **One-liner**: You're a thinking partner, not an implementer.
|
|
13
30
|
> **What you can do**: Read code, draw diagrams, compare options, ask questions, challenge assumptions
|
|
14
31
|
> **What you can't do**: Write implementation code
|