duaer-spec 0.1.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/.cursor/rules/agents-workflow.mdc +50 -0
- package/.cursor/rules/ai-ui-copy.mdc +12 -0
- package/.cursor/rules/duaer-spec.mdc +33 -0
- package/.cursor/skills/duaer-analyze/SKILL.md +259 -0
- package/.cursor/skills/duaer-checklist/SKILL.md +383 -0
- package/.cursor/skills/duaer-clarify/SKILL.md +291 -0
- package/.cursor/skills/duaer-constitution/SKILL.md +177 -0
- package/.cursor/skills/duaer-converge/SKILL.md +277 -0
- package/.cursor/skills/duaer-git-commit/SKILL.md +68 -0
- package/.cursor/skills/duaer-git-feature/SKILL.md +94 -0
- package/.cursor/skills/duaer-git-initialize/SKILL.md +54 -0
- package/.cursor/skills/duaer-git-remote/SKILL.md +50 -0
- package/.cursor/skills/duaer-git-validate/SKILL.md +54 -0
- package/.cursor/skills/duaer-implement/SKILL.md +226 -0
- package/.cursor/skills/duaer-plan/SKILL.md +166 -0
- package/.cursor/skills/duaer-specify/SKILL.md +345 -0
- package/.cursor/skills/duaer-tasks/SKILL.md +214 -0
- package/.cursor/skills/duaer-taskstoissues/SKILL.md +109 -0
- package/.duaer/extensions/.registry +23 -0
- package/.duaer/extensions/git/README.md +119 -0
- package/.duaer/extensions/git/commands/duaer.git.commit.md +63 -0
- package/.duaer/extensions/git/commands/duaer.git.feature.md +82 -0
- package/.duaer/extensions/git/commands/duaer.git.initialize.md +49 -0
- package/.duaer/extensions/git/commands/duaer.git.remote.md +45 -0
- package/.duaer/extensions/git/commands/duaer.git.validate.md +49 -0
- package/.duaer/extensions/git/config-template.yml +79 -0
- package/.duaer/extensions/git/extension.yml +142 -0
- package/.duaer/extensions/git/git-config.yml +79 -0
- package/.duaer/extensions/git/scripts/bash/auto-commit.sh +211 -0
- package/.duaer/extensions/git/scripts/bash/create-new-feature-branch.sh +626 -0
- package/.duaer/extensions/git/scripts/bash/git-common.sh +56 -0
- package/.duaer/extensions/git/scripts/bash/initialize-repo.sh +54 -0
- package/.duaer/extensions/git/scripts/powershell/auto-commit.ps1 +230 -0
- package/.duaer/extensions/git/scripts/powershell/create-new-feature-branch.ps1 +592 -0
- package/.duaer/extensions/git/scripts/powershell/git-common.ps1 +52 -0
- package/.duaer/extensions/git/scripts/powershell/initialize-repo.ps1 +69 -0
- package/.duaer/extensions/git/scripts/python/auto_commit.py +195 -0
- package/.duaer/extensions/git/scripts/python/create_new_feature_branch.py +634 -0
- package/.duaer/extensions/git/scripts/python/git_common.py +81 -0
- package/.duaer/extensions/git/scripts/python/initialize_repo.py +89 -0
- package/.duaer/extensions.yml +167 -0
- package/.duaer/init-options.json +9 -0
- package/.duaer/integration.json +15 -0
- package/.duaer/integrations/cursor-agent.manifest.json +17 -0
- package/.duaer/integrations/duaer.manifest.json +19 -0
- package/.duaer/memory/.constitution-template.json +4 -0
- package/.duaer/memory/constitution.md +37 -0
- package/.duaer/memory/project-context.md +24 -0
- package/.duaer/memory/testing.md +14 -0
- package/.duaer/scripts/bash/check-prerequisites.sh +243 -0
- package/.duaer/scripts/bash/common.sh +926 -0
- package/.duaer/scripts/bash/create-new-feature.sh +407 -0
- package/.duaer/scripts/bash/resolve-template.sh +57 -0
- package/.duaer/scripts/bash/setup-plan.sh +85 -0
- package/.duaer/scripts/bash/setup-tasks.sh +94 -0
- package/.duaer/templates/checklist-template.md +45 -0
- package/.duaer/templates/constitution-template.md +50 -0
- package/.duaer/templates/plan-template.md +113 -0
- package/.duaer/templates/spec-template.md +131 -0
- package/.duaer/templates/tasks-template.md +252 -0
- package/.duaer/workflows/duaer/workflow.yml +78 -0
- package/.duaer/workflows/workflow-registry.json +13 -0
- package/ADOPT.md +75 -0
- package/AGENTS.md +290 -0
- package/CHANGELOG.md +28 -0
- package/DUADER.md +57 -0
- package/LICENSE +21 -0
- package/README.md +74 -0
- package/bin/duaer.mjs +298 -0
- package/docs/agent/README.md +12 -0
- package/docs/agent/change-checklist.md +130 -0
- package/docs/agent/e2e-test-plan.md +33 -0
- package/docs/agent/workflow.md +127 -0
- package/docs/baseline.md +10 -0
- package/package.json +49 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
JSON_MODE=false
|
|
6
|
+
DRY_RUN=false
|
|
7
|
+
ALLOW_EXISTING=false
|
|
8
|
+
SHORT_NAME=""
|
|
9
|
+
BRANCH_NUMBER=""
|
|
10
|
+
USE_TIMESTAMP=false
|
|
11
|
+
NUMBER_EXPLICIT=false
|
|
12
|
+
ARGS=()
|
|
13
|
+
i=1
|
|
14
|
+
while [ $i -le $# ]; do
|
|
15
|
+
arg="${!i}"
|
|
16
|
+
case "$arg" in
|
|
17
|
+
--json)
|
|
18
|
+
JSON_MODE=true
|
|
19
|
+
;;
|
|
20
|
+
--dry-run)
|
|
21
|
+
DRY_RUN=true
|
|
22
|
+
;;
|
|
23
|
+
--allow-existing-branch)
|
|
24
|
+
ALLOW_EXISTING=true
|
|
25
|
+
;;
|
|
26
|
+
--short-name)
|
|
27
|
+
if [ $((i + 1)) -gt $# ]; then
|
|
28
|
+
echo 'Error: --short-name requires a value' >&2
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
31
|
+
i=$((i + 1))
|
|
32
|
+
next_arg="${!i}"
|
|
33
|
+
# Check if the next argument is another option (starts with --)
|
|
34
|
+
if [[ "$next_arg" == --* ]]; then
|
|
35
|
+
echo 'Error: --short-name requires a value' >&2
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
SHORT_NAME="$next_arg"
|
|
39
|
+
;;
|
|
40
|
+
--number)
|
|
41
|
+
if [ $((i + 1)) -gt $# ]; then
|
|
42
|
+
echo 'Error: --number requires a value' >&2
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
i=$((i + 1))
|
|
46
|
+
next_arg="${!i}"
|
|
47
|
+
if [[ "$next_arg" == --* ]]; then
|
|
48
|
+
echo 'Error: --number requires a value' >&2
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
BRANCH_NUMBER="$next_arg"
|
|
52
|
+
if [ -n "$BRANCH_NUMBER" ]; then
|
|
53
|
+
NUMBER_EXPLICIT=true
|
|
54
|
+
fi
|
|
55
|
+
;;
|
|
56
|
+
--timestamp)
|
|
57
|
+
USE_TIMESTAMP=true
|
|
58
|
+
;;
|
|
59
|
+
--help|-h)
|
|
60
|
+
echo "Usage: $0 [--json] [--dry-run] [--allow-existing-branch] [--short-name <name>] [--number N] [--timestamp] <feature_description>"
|
|
61
|
+
echo ""
|
|
62
|
+
echo "Options:"
|
|
63
|
+
echo " --json Output in JSON format"
|
|
64
|
+
echo " --dry-run Compute feature name and paths without creating directories or files"
|
|
65
|
+
echo " --allow-existing-branch Reuse an existing feature directory if it already exists"
|
|
66
|
+
echo " --short-name <name> Provide a custom short name (2-4 words) for the feature"
|
|
67
|
+
echo " --number N Prefer a feature number (auto-corrected if its specs prefix exists)"
|
|
68
|
+
echo " --timestamp Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering"
|
|
69
|
+
echo " --help, -h Show this help message"
|
|
70
|
+
echo ""
|
|
71
|
+
echo "Examples:"
|
|
72
|
+
echo " $0 'Add user authentication system' --short-name 'user-auth'"
|
|
73
|
+
echo " $0 'Implement OAuth2 integration for API' --number 5"
|
|
74
|
+
echo " $0 --timestamp --short-name 'user-auth' 'Add user authentication'"
|
|
75
|
+
exit 0
|
|
76
|
+
;;
|
|
77
|
+
*)
|
|
78
|
+
ARGS+=("$arg")
|
|
79
|
+
;;
|
|
80
|
+
esac
|
|
81
|
+
i=$((i + 1))
|
|
82
|
+
done
|
|
83
|
+
|
|
84
|
+
FEATURE_DESCRIPTION="${ARGS[*]}"
|
|
85
|
+
if [ -z "$FEATURE_DESCRIPTION" ]; then
|
|
86
|
+
echo "Usage: $0 [--json] [--dry-run] [--allow-existing-branch] [--short-name <name>] [--number N] [--timestamp] <feature_description>" >&2
|
|
87
|
+
exit 1
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
# Trim whitespace and validate description is not empty (e.g., user passed only whitespace)
|
|
91
|
+
FEATURE_DESCRIPTION=$(echo "$FEATURE_DESCRIPTION" | sed -E 's/^[[:space:]]+|[[:space:]]+$//g')
|
|
92
|
+
if [ -z "$FEATURE_DESCRIPTION" ]; then
|
|
93
|
+
echo "Error: Feature description cannot be empty or contain only whitespace" >&2
|
|
94
|
+
exit 1
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
MAX_FEATURE_NUMBER=9223372036854775807
|
|
98
|
+
MAX_BRANCH_LENGTH=244
|
|
99
|
+
|
|
100
|
+
is_feature_number_in_range() {
|
|
101
|
+
local value="$1"
|
|
102
|
+
local normalized="${value#"${value%%[!0]*}"}"
|
|
103
|
+
[ -n "$normalized" ] || normalized=0
|
|
104
|
+
[ ${#normalized} -lt ${#MAX_FEATURE_NUMBER} ] && return 0
|
|
105
|
+
[ ${#normalized} -gt ${#MAX_FEATURE_NUMBER} ] && return 1
|
|
106
|
+
# Equal-length digit strings must be compared without arithmetic overflow.
|
|
107
|
+
# shellcheck disable=SC2071
|
|
108
|
+
[[ "$normalized" < "$MAX_FEATURE_NUMBER" || "$normalized" == "$MAX_FEATURE_NUMBER" ]]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# Function to get highest number from specs directory
|
|
112
|
+
get_highest_from_specs() {
|
|
113
|
+
local specs_dir="$1"
|
|
114
|
+
local highest=0
|
|
115
|
+
|
|
116
|
+
if [ -d "$specs_dir" ]; then
|
|
117
|
+
for dir in "$specs_dir"/*; do
|
|
118
|
+
[ -d "$dir" ] || continue
|
|
119
|
+
dirname=$(basename "$dir")
|
|
120
|
+
# Match sequential prefixes (>=3 digits), but skip timestamp dirs.
|
|
121
|
+
if echo "$dirname" | grep -Eq '^[0-9]{3,}-' && ! echo "$dirname" | grep -Eq '^[0-9]{8}-[0-9]{6}-'; then
|
|
122
|
+
number=$(echo "$dirname" | grep -Eo '^[0-9]+')
|
|
123
|
+
if is_feature_number_in_range "$number"; then
|
|
124
|
+
number=$((10#$number))
|
|
125
|
+
if [ "$number" -gt "$highest" ]; then
|
|
126
|
+
highest=$number
|
|
127
|
+
fi
|
|
128
|
+
fi
|
|
129
|
+
fi
|
|
130
|
+
done
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
echo "$highest"
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# Return success when a spec directory owns the given numeric prefix.
|
|
137
|
+
spec_prefix_exists() {
|
|
138
|
+
local specs_dir="$1"
|
|
139
|
+
local feature_num="$2"
|
|
140
|
+
|
|
141
|
+
for spec_path in "$specs_dir/${feature_num}-"*; do
|
|
142
|
+
[ -d "$spec_path" ] && return 0
|
|
143
|
+
done
|
|
144
|
+
return 1
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# Function to clean and format a branch name
|
|
148
|
+
clean_branch_name() {
|
|
149
|
+
local name="$1"
|
|
150
|
+
echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//'
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
# Fit a feature prefix and suffix within GitHub's branch-name limit.
|
|
154
|
+
fit_branch_name() {
|
|
155
|
+
local feature_num="$1"
|
|
156
|
+
local branch_suffix="$2"
|
|
157
|
+
local branch_name="${feature_num}-${branch_suffix}"
|
|
158
|
+
|
|
159
|
+
if [ ${#branch_name} -gt $MAX_BRANCH_LENGTH ]; then
|
|
160
|
+
local prefix_length=$(( ${#feature_num} + 1 ))
|
|
161
|
+
local max_suffix_length=$((MAX_BRANCH_LENGTH - prefix_length))
|
|
162
|
+
local truncated_suffix
|
|
163
|
+
truncated_suffix=$(printf '%s' "$branch_suffix" | cut -c "1-$max_suffix_length" | sed 's/-$//')
|
|
164
|
+
branch_name="${feature_num}-${truncated_suffix}"
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
printf '%s' "$branch_name"
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# Quote a value for POSIX shell reuse, byte-identical to Python's shlex.quote
|
|
171
|
+
# so the persistence hints match the Python variant exactly (printf %q output
|
|
172
|
+
# differs between bash versions and from shlex.quote for spaces/metachars).
|
|
173
|
+
shell_quote() {
|
|
174
|
+
local value="$1" LC_ALL=C
|
|
175
|
+
if [[ "$value" =~ ^[A-Za-z0-9_@%+=:,./-]+$ ]]; then
|
|
176
|
+
printf '%s' "$value"
|
|
177
|
+
else
|
|
178
|
+
local q="'\"'\"'"
|
|
179
|
+
printf "'%s'" "${value//\'/$q}"
|
|
180
|
+
fi
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
# Resolve repository root using common.sh functions which prioritize .duaer
|
|
184
|
+
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
185
|
+
source "$SCRIPT_DIR/common.sh"
|
|
186
|
+
|
|
187
|
+
REPO_ROOT=$(get_repo_root) || exit 1
|
|
188
|
+
|
|
189
|
+
cd "$REPO_ROOT"
|
|
190
|
+
|
|
191
|
+
SPECS_DIR="$REPO_ROOT/specs"
|
|
192
|
+
if [ "$DRY_RUN" != true ]; then
|
|
193
|
+
mkdir -p "$SPECS_DIR"
|
|
194
|
+
fi
|
|
195
|
+
|
|
196
|
+
# Function to generate branch name with stop word filtering and length filtering
|
|
197
|
+
generate_branch_name() {
|
|
198
|
+
local description="$1"
|
|
199
|
+
|
|
200
|
+
# Common stop words to filter out
|
|
201
|
+
local stop_words="^(i|a|an|the|to|for|of|in|on|at|by|with|from|is|are|was|were|be|been|being|have|has|had|do|does|did|will|would|should|could|can|may|might|must|shall|this|that|these|those|my|your|our|their|want|need|add|get|set)$"
|
|
202
|
+
|
|
203
|
+
# Convert to lowercase and split into words
|
|
204
|
+
local clean_name=$(printf '%s' "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g')
|
|
205
|
+
|
|
206
|
+
# Filter words: remove stop words and words shorter than 3 chars (unless they're uppercase acronyms in original)
|
|
207
|
+
local meaningful_words=()
|
|
208
|
+
for word in $clean_name; do
|
|
209
|
+
# Skip empty words
|
|
210
|
+
[ -z "$word" ] && continue
|
|
211
|
+
|
|
212
|
+
# Keep words that are NOT stop words AND (length >= 3 OR are potential acronyms)
|
|
213
|
+
if ! echo "$word" | grep -qiE "$stop_words"; then
|
|
214
|
+
if [ ${#word} -ge 3 ]; then
|
|
215
|
+
meaningful_words+=("$word")
|
|
216
|
+
# Keep short words that appear as an uppercase acronym in the original.
|
|
217
|
+
# Uppercase via tr and match with grep -w (both portable) rather than
|
|
218
|
+
# bash's 4+ "^^" case expansion (breaks on macOS bash 3.2) and \b (non-POSIX).
|
|
219
|
+
elif printf '%s' "$description" | grep -qw -- "$(printf '%s' "$word" | tr '[:lower:]' '[:upper:]')"; then
|
|
220
|
+
meaningful_words+=("$word")
|
|
221
|
+
fi
|
|
222
|
+
fi
|
|
223
|
+
done
|
|
224
|
+
|
|
225
|
+
# If we have meaningful words, use first 3-4 of them
|
|
226
|
+
if [ ${#meaningful_words[@]} -gt 0 ]; then
|
|
227
|
+
local max_words=3
|
|
228
|
+
if [ ${#meaningful_words[@]} -eq 4 ]; then max_words=4; fi
|
|
229
|
+
|
|
230
|
+
local result=""
|
|
231
|
+
local count=0
|
|
232
|
+
for word in "${meaningful_words[@]}"; do
|
|
233
|
+
if [ $count -ge $max_words ]; then break; fi
|
|
234
|
+
if [ -n "$result" ]; then result="$result-"; fi
|
|
235
|
+
result="$result$word"
|
|
236
|
+
count=$((count + 1))
|
|
237
|
+
done
|
|
238
|
+
echo "$result"
|
|
239
|
+
else
|
|
240
|
+
# Fallback to original logic if no meaningful words found
|
|
241
|
+
local cleaned=$(clean_branch_name "$description")
|
|
242
|
+
echo "$cleaned" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//'
|
|
243
|
+
fi
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
# Generate branch name
|
|
247
|
+
if [ -n "$SHORT_NAME" ]; then
|
|
248
|
+
# Use provided short name, just clean it up
|
|
249
|
+
BRANCH_SUFFIX=$(clean_branch_name "$SHORT_NAME")
|
|
250
|
+
else
|
|
251
|
+
# Generate from description with smart filtering
|
|
252
|
+
BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION")
|
|
253
|
+
fi
|
|
254
|
+
|
|
255
|
+
# Warn if --number and --timestamp are both specified
|
|
256
|
+
if [ "$USE_TIMESTAMP" = true ] && [ -n "$BRANCH_NUMBER" ]; then
|
|
257
|
+
>&2 echo "[specify] Warning: --number is ignored when --timestamp is used"
|
|
258
|
+
BRANCH_NUMBER=""
|
|
259
|
+
fi
|
|
260
|
+
|
|
261
|
+
# Determine branch prefix
|
|
262
|
+
if [ "$USE_TIMESTAMP" = true ]; then
|
|
263
|
+
FEATURE_NUM=$(date +%Y%m%d-%H%M%S)
|
|
264
|
+
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
|
|
265
|
+
else
|
|
266
|
+
if [ -n "$BRANCH_NUMBER" ] && [[ ! "$BRANCH_NUMBER" =~ ^[0-9]+$ ]]; then
|
|
267
|
+
echo "Error: --number must be an unsigned integer, got '$BRANCH_NUMBER'" >&2
|
|
268
|
+
exit 1
|
|
269
|
+
fi
|
|
270
|
+
|
|
271
|
+
# Bash arithmetic is signed 64-bit; reject digit strings that would wrap.
|
|
272
|
+
if [ -n "$BRANCH_NUMBER" ] && ! is_feature_number_in_range "$BRANCH_NUMBER"; then
|
|
273
|
+
echo "Error: --number must be between 0 and $MAX_FEATURE_NUMBER, got '$BRANCH_NUMBER'" >&2
|
|
274
|
+
exit 1
|
|
275
|
+
fi
|
|
276
|
+
|
|
277
|
+
# Determine branch number from existing feature directories
|
|
278
|
+
if [ -z "$BRANCH_NUMBER" ]; then
|
|
279
|
+
HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
|
|
280
|
+
if [ "$HIGHEST" -eq "$MAX_FEATURE_NUMBER" ]; then
|
|
281
|
+
echo "Error: feature number must be between 0 and $MAX_FEATURE_NUMBER, got '9223372036854775808'" >&2
|
|
282
|
+
exit 1
|
|
283
|
+
fi
|
|
284
|
+
BRANCH_NUMBER=$((HIGHEST + 1))
|
|
285
|
+
fi
|
|
286
|
+
|
|
287
|
+
# Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal)
|
|
288
|
+
FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))")
|
|
289
|
+
|
|
290
|
+
# Treat an explicit number as a preference when its prefix is already used
|
|
291
|
+
# by a feature directory. Auto-detected numbers are already conflict-free.
|
|
292
|
+
if [ "$NUMBER_EXPLICIT" = true ]; then
|
|
293
|
+
SPEC_CONFLICT=false
|
|
294
|
+
REQUESTED_BRANCH_NAME=$(fit_branch_name "$FEATURE_NUM" "$BRANCH_SUFFIX")
|
|
295
|
+
REQUESTED_DIR="$SPECS_DIR/$REQUESTED_BRANCH_NAME"
|
|
296
|
+
if [ "$ALLOW_EXISTING" != true ] || [ ! -d "$REQUESTED_DIR" ]; then
|
|
297
|
+
spec_prefix_exists "$SPECS_DIR" "$FEATURE_NUM" && SPEC_CONFLICT=true
|
|
298
|
+
fi
|
|
299
|
+
|
|
300
|
+
if [ "$SPEC_CONFLICT" = true ]; then
|
|
301
|
+
REQUESTED_NUM="$FEATURE_NUM"
|
|
302
|
+
HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
|
|
303
|
+
BRANCH_NUMBER=$HIGHEST
|
|
304
|
+
while true; do
|
|
305
|
+
if [ "$BRANCH_NUMBER" -eq "$MAX_FEATURE_NUMBER" ]; then
|
|
306
|
+
echo "Error: feature number must be between 0 and $MAX_FEATURE_NUMBER, got '9223372036854775808'" >&2
|
|
307
|
+
exit 1
|
|
308
|
+
fi
|
|
309
|
+
BRANCH_NUMBER=$((BRANCH_NUMBER + 1))
|
|
310
|
+
FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))")
|
|
311
|
+
spec_prefix_exists "$SPECS_DIR" "$FEATURE_NUM" || break
|
|
312
|
+
done
|
|
313
|
+
>&2 echo "[specify] Warning: --number $REQUESTED_NUM conflicts with an existing spec directory; using $FEATURE_NUM instead"
|
|
314
|
+
fi
|
|
315
|
+
fi
|
|
316
|
+
|
|
317
|
+
fi
|
|
318
|
+
|
|
319
|
+
# GitHub enforces a 244-byte limit on branch names
|
|
320
|
+
# Validate and truncate if necessary
|
|
321
|
+
ORIGINAL_BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
|
|
322
|
+
BRANCH_NAME=$(fit_branch_name "$FEATURE_NUM" "$BRANCH_SUFFIX")
|
|
323
|
+
if [ "$BRANCH_NAME" != "$ORIGINAL_BRANCH_NAME" ]; then
|
|
324
|
+
>&2 echo "[specify] Warning: Branch name exceeded GitHub's 244-byte limit"
|
|
325
|
+
>&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME (${#ORIGINAL_BRANCH_NAME} bytes)"
|
|
326
|
+
>&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)"
|
|
327
|
+
fi
|
|
328
|
+
|
|
329
|
+
FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
|
|
330
|
+
SPEC_FILE="$FEATURE_DIR/spec.md"
|
|
331
|
+
|
|
332
|
+
if [ "$DRY_RUN" != true ]; then
|
|
333
|
+
if [ -d "$FEATURE_DIR" ] && [ "$ALLOW_EXISTING" != true ]; then
|
|
334
|
+
if [ "$USE_TIMESTAMP" = true ]; then
|
|
335
|
+
>&2 echo "Error: Feature directory '$FEATURE_DIR' already exists. Rerun to get a new timestamp or use a different --short-name."
|
|
336
|
+
else
|
|
337
|
+
>&2 echo "Error: Feature directory '$FEATURE_DIR' already exists. Please use a different feature name or specify a different number with --number."
|
|
338
|
+
fi
|
|
339
|
+
exit 1
|
|
340
|
+
fi
|
|
341
|
+
|
|
342
|
+
NEEDS_SPEC=false
|
|
343
|
+
SPEC_TEMPLATE_FOUND=false
|
|
344
|
+
SPEC_TEMPLATE_CONTENT=""
|
|
345
|
+
if [ ! -f "$SPEC_FILE" ]; then
|
|
346
|
+
NEEDS_SPEC=true
|
|
347
|
+
if SPEC_TEMPLATE_CONTENT=$(resolve_template_content "spec-template" "$REPO_ROOT"; status=$?; printf x; exit "$status"); then
|
|
348
|
+
SPEC_TEMPLATE_CONTENT="${SPEC_TEMPLATE_CONTENT%x}"
|
|
349
|
+
SPEC_TEMPLATE_FOUND=true
|
|
350
|
+
else
|
|
351
|
+
resolve_status=$?
|
|
352
|
+
if [ "$resolve_status" -ne 1 ]; then
|
|
353
|
+
exit "$resolve_status"
|
|
354
|
+
fi
|
|
355
|
+
fi
|
|
356
|
+
fi
|
|
357
|
+
|
|
358
|
+
mkdir -p "$FEATURE_DIR"
|
|
359
|
+
|
|
360
|
+
if [ "$NEEDS_SPEC" = true ]; then
|
|
361
|
+
if [ "$SPEC_TEMPLATE_FOUND" = true ]; then
|
|
362
|
+
printf '%s' "$SPEC_TEMPLATE_CONTENT" > "$SPEC_FILE"
|
|
363
|
+
else
|
|
364
|
+
echo "Warning: Spec template not found; created empty spec file" >&2
|
|
365
|
+
touch "$SPEC_FILE"
|
|
366
|
+
fi
|
|
367
|
+
fi
|
|
368
|
+
|
|
369
|
+
# Persist to .duaer/feature.json so downstream commands can find the feature
|
|
370
|
+
_persist_feature_json "$REPO_ROOT" "$FEATURE_DIR"
|
|
371
|
+
|
|
372
|
+
# Inform the user how to set feature state in their own shell
|
|
373
|
+
printf '# To persist: export SPECIFY_FEATURE=%s\n' "$(shell_quote "$BRANCH_NAME")" >&2
|
|
374
|
+
printf '# export SPECIFY_FEATURE_DIRECTORY=%s\n' "$(shell_quote "$FEATURE_DIR")" >&2
|
|
375
|
+
fi
|
|
376
|
+
|
|
377
|
+
if $JSON_MODE; then
|
|
378
|
+
if command -v jq >/dev/null 2>&1; then
|
|
379
|
+
if [ "$DRY_RUN" = true ]; then
|
|
380
|
+
jq -cn \
|
|
381
|
+
--arg branch_name "$BRANCH_NAME" \
|
|
382
|
+
--arg spec_file "$SPEC_FILE" \
|
|
383
|
+
--arg feature_num "$FEATURE_NUM" \
|
|
384
|
+
'{BRANCH_NAME:$branch_name,SPEC_FILE:$spec_file,FEATURE_NUM:$feature_num,DRY_RUN:true}'
|
|
385
|
+
else
|
|
386
|
+
jq -cn \
|
|
387
|
+
--arg branch_name "$BRANCH_NAME" \
|
|
388
|
+
--arg spec_file "$SPEC_FILE" \
|
|
389
|
+
--arg feature_num "$FEATURE_NUM" \
|
|
390
|
+
'{BRANCH_NAME:$branch_name,SPEC_FILE:$spec_file,FEATURE_NUM:$feature_num}'
|
|
391
|
+
fi
|
|
392
|
+
else
|
|
393
|
+
if [ "$DRY_RUN" = true ]; then
|
|
394
|
+
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s","DRY_RUN":true}\n' "$(json_escape "$BRANCH_NAME")" "$(json_escape "$SPEC_FILE")" "$(json_escape "$FEATURE_NUM")"
|
|
395
|
+
else
|
|
396
|
+
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$(json_escape "$BRANCH_NAME")" "$(json_escape "$SPEC_FILE")" "$(json_escape "$FEATURE_NUM")"
|
|
397
|
+
fi
|
|
398
|
+
fi
|
|
399
|
+
else
|
|
400
|
+
echo "BRANCH_NAME: $BRANCH_NAME"
|
|
401
|
+
echo "SPEC_FILE: $SPEC_FILE"
|
|
402
|
+
echo "FEATURE_NUM: $FEATURE_NUM"
|
|
403
|
+
if [ "$DRY_RUN" != true ]; then
|
|
404
|
+
printf '# To persist in your shell: export SPECIFY_FEATURE=%s\n' "$(shell_quote "$BRANCH_NAME")"
|
|
405
|
+
printf '# export SPECIFY_FEATURE_DIRECTORY=%s\n' "$(shell_quote "$FEATURE_DIR")"
|
|
406
|
+
fi
|
|
407
|
+
fi
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
SCRIPT_DIR="$(CDPATH="" cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
+
source "$SCRIPT_DIR/common.sh"
|
|
7
|
+
|
|
8
|
+
JSON_MODE=false
|
|
9
|
+
TEMPLATE_NAME=""
|
|
10
|
+
|
|
11
|
+
for arg in "$@"; do
|
|
12
|
+
case "$arg" in
|
|
13
|
+
--json) JSON_MODE=true ;;
|
|
14
|
+
--help|-h)
|
|
15
|
+
echo "Usage: $0 <template-name> [--json]"
|
|
16
|
+
exit 0
|
|
17
|
+
;;
|
|
18
|
+
-*)
|
|
19
|
+
echo "ERROR: Unknown option '$arg'" >&2
|
|
20
|
+
exit 1
|
|
21
|
+
;;
|
|
22
|
+
*)
|
|
23
|
+
if [[ -n "$TEMPLATE_NAME" ]]; then
|
|
24
|
+
echo "ERROR: Unexpected argument '$arg'" >&2
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
TEMPLATE_NAME="$arg"
|
|
28
|
+
;;
|
|
29
|
+
esac
|
|
30
|
+
done
|
|
31
|
+
|
|
32
|
+
if [[ -z "$TEMPLATE_NAME" ]]; then
|
|
33
|
+
echo "ERROR: Template name is required" >&2
|
|
34
|
+
exit 1
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
REPO_ROOT=$(get_repo_root)
|
|
38
|
+
if TEMPLATE_CONTENT=$(resolve_template_content "$TEMPLATE_NAME" "$REPO_ROOT"; status=$?; printf x; exit "$status"); then
|
|
39
|
+
TEMPLATE_CONTENT="${TEMPLATE_CONTENT%x}"
|
|
40
|
+
else
|
|
41
|
+
echo "ERROR: Could not resolve required $TEMPLATE_NAME from the template override stack for $REPO_ROOT" >&2
|
|
42
|
+
exit 1
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
if $JSON_MODE; then
|
|
46
|
+
if has_jq; then
|
|
47
|
+
jq -cn \
|
|
48
|
+
--arg template_name "$TEMPLATE_NAME" \
|
|
49
|
+
--arg template_content "$TEMPLATE_CONTENT" \
|
|
50
|
+
'{TEMPLATE_NAME:$template_name,TEMPLATE_CONTENT:$template_content}'
|
|
51
|
+
else
|
|
52
|
+
printf '{"TEMPLATE_NAME":"%s","TEMPLATE_CONTENT":"%s"}\n' \
|
|
53
|
+
"$(json_escape "$TEMPLATE_NAME")" "$(json_escape "$TEMPLATE_CONTENT")"
|
|
54
|
+
fi
|
|
55
|
+
else
|
|
56
|
+
printf '%s' "$TEMPLATE_CONTENT"
|
|
57
|
+
fi
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
# Parse command line arguments
|
|
6
|
+
JSON_MODE=false
|
|
7
|
+
|
|
8
|
+
for arg in "$@"; do
|
|
9
|
+
case "$arg" in
|
|
10
|
+
--json)
|
|
11
|
+
JSON_MODE=true
|
|
12
|
+
;;
|
|
13
|
+
--help|-h)
|
|
14
|
+
echo "Usage: $0 [--json]"
|
|
15
|
+
echo " --json Output results in JSON format"
|
|
16
|
+
echo " --help Show this help message"
|
|
17
|
+
exit 0
|
|
18
|
+
;;
|
|
19
|
+
*)
|
|
20
|
+
echo "ERROR: Unknown option '$arg'" >&2
|
|
21
|
+
exit 1
|
|
22
|
+
;;
|
|
23
|
+
esac
|
|
24
|
+
done
|
|
25
|
+
|
|
26
|
+
# Get script directory and load common functions
|
|
27
|
+
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
28
|
+
source "$SCRIPT_DIR/common.sh"
|
|
29
|
+
|
|
30
|
+
# Get all paths and variables from common functions
|
|
31
|
+
_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
|
|
32
|
+
eval "$_paths_output"
|
|
33
|
+
unset _paths_output
|
|
34
|
+
|
|
35
|
+
# Ensure the feature directory exists
|
|
36
|
+
mkdir -p "$FEATURE_DIR"
|
|
37
|
+
|
|
38
|
+
# Copy plan template if plan doesn't already exist
|
|
39
|
+
if [[ -f "$IMPL_PLAN" ]]; then
|
|
40
|
+
if $JSON_MODE; then
|
|
41
|
+
echo "Plan already exists at $IMPL_PLAN, skipping template copy" >&2
|
|
42
|
+
else
|
|
43
|
+
echo "Plan already exists at $IMPL_PLAN, skipping template copy"
|
|
44
|
+
fi
|
|
45
|
+
else
|
|
46
|
+
if resolve_template_content "plan-template" "$REPO_ROOT" > "$IMPL_PLAN"; then
|
|
47
|
+
if $JSON_MODE; then
|
|
48
|
+
echo "Copied plan template to $IMPL_PLAN" >&2
|
|
49
|
+
else
|
|
50
|
+
echo "Copied plan template to $IMPL_PLAN"
|
|
51
|
+
fi
|
|
52
|
+
else
|
|
53
|
+
resolve_status=$?
|
|
54
|
+
rm -f "$IMPL_PLAN"
|
|
55
|
+
if [ "$resolve_status" -ne 1 ]; then
|
|
56
|
+
exit "$resolve_status"
|
|
57
|
+
fi
|
|
58
|
+
if $JSON_MODE; then
|
|
59
|
+
echo "Warning: Plan template not found" >&2
|
|
60
|
+
else
|
|
61
|
+
echo "Warning: Plan template not found"
|
|
62
|
+
fi
|
|
63
|
+
touch "$IMPL_PLAN"
|
|
64
|
+
fi
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
# Output results
|
|
68
|
+
if $JSON_MODE; then
|
|
69
|
+
if has_jq; then
|
|
70
|
+
jq -cn \
|
|
71
|
+
--arg feature_spec "$FEATURE_SPEC" \
|
|
72
|
+
--arg impl_plan "$IMPL_PLAN" \
|
|
73
|
+
--arg specs_dir "$FEATURE_DIR" \
|
|
74
|
+
--arg branch "$CURRENT_BRANCH" \
|
|
75
|
+
'{FEATURE_SPEC:$feature_spec,IMPL_PLAN:$impl_plan,SPECS_DIR:$specs_dir,BRANCH:$branch}'
|
|
76
|
+
else
|
|
77
|
+
printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s"}\n' \
|
|
78
|
+
"$(json_escape "$FEATURE_SPEC")" "$(json_escape "$IMPL_PLAN")" "$(json_escape "$FEATURE_DIR")" "$(json_escape "$CURRENT_BRANCH")"
|
|
79
|
+
fi
|
|
80
|
+
else
|
|
81
|
+
echo "FEATURE_SPEC: $FEATURE_SPEC"
|
|
82
|
+
echo "IMPL_PLAN: $IMPL_PLAN"
|
|
83
|
+
echo "SPECS_DIR: $FEATURE_DIR"
|
|
84
|
+
echo "BRANCH: $CURRENT_BRANCH"
|
|
85
|
+
fi
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
# Parse command line arguments
|
|
6
|
+
JSON_MODE=false
|
|
7
|
+
|
|
8
|
+
for arg in "$@"; do
|
|
9
|
+
case "$arg" in
|
|
10
|
+
--json) JSON_MODE=true ;;
|
|
11
|
+
--help|-h)
|
|
12
|
+
echo "Usage: $0 [--json]"
|
|
13
|
+
echo " --json Output results in JSON format"
|
|
14
|
+
echo " --help Show this help message"
|
|
15
|
+
exit 0
|
|
16
|
+
;;
|
|
17
|
+
*) echo "ERROR: Unknown option '$arg'" >&2; exit 1 ;;
|
|
18
|
+
esac
|
|
19
|
+
done
|
|
20
|
+
|
|
21
|
+
# Source common functions
|
|
22
|
+
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
23
|
+
source "$SCRIPT_DIR/common.sh"
|
|
24
|
+
|
|
25
|
+
# Get feature paths
|
|
26
|
+
_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
|
|
27
|
+
eval "$_paths_output"
|
|
28
|
+
unset _paths_output
|
|
29
|
+
|
|
30
|
+
# Validate required files
|
|
31
|
+
if [[ ! -f "$IMPL_PLAN" ]]; then
|
|
32
|
+
echo "ERROR: plan.md not found in $FEATURE_DIR" >&2
|
|
33
|
+
echo "Run /duaer-plan first to create the implementation plan." >&2
|
|
34
|
+
exit 1
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
if [[ ! -f "$FEATURE_SPEC" ]]; then
|
|
38
|
+
echo "ERROR: spec.md not found in $FEATURE_DIR" >&2
|
|
39
|
+
echo "Run /duaer-specify first to create the feature structure." >&2
|
|
40
|
+
exit 1
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# Build available docs list
|
|
44
|
+
docs=()
|
|
45
|
+
[[ -f "$RESEARCH" ]] && docs+=("research.md")
|
|
46
|
+
[[ -f "$DATA_MODEL" ]] && docs+=("data-model.md")
|
|
47
|
+
if [[ -d "$CONTRACTS_DIR" ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]; then
|
|
48
|
+
docs+=("contracts/")
|
|
49
|
+
fi
|
|
50
|
+
[[ -f "$QUICKSTART" ]] && docs+=("quickstart.md")
|
|
51
|
+
|
|
52
|
+
# Resolve tasks template through override stack
|
|
53
|
+
TASKS_TEMPLATE=$(resolve_template "tasks-template" "$REPO_ROOT") || true
|
|
54
|
+
if TASKS_TEMPLATE_CONTENT=$(resolve_template_content "tasks-template" "$REPO_ROOT"; status=$?; printf x; exit "$status"); then
|
|
55
|
+
TASKS_TEMPLATE_CONTENT="${TASKS_TEMPLATE_CONTENT%x}"
|
|
56
|
+
else
|
|
57
|
+
echo "ERROR: Could not resolve required tasks-template from the template override stack for $REPO_ROOT" >&2
|
|
58
|
+
echo "Template 'tasks-template' was not found in any supported location (overrides, presets, extensions, or shared core). Add an override at .duaer/templates/overrides/tasks-template.md, or run 'specify init' / reinstall shared infra to restore the core .duaer/templates/tasks-template.md template." >&2
|
|
59
|
+
exit 1
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# Output results
|
|
63
|
+
if $JSON_MODE; then
|
|
64
|
+
if has_jq; then
|
|
65
|
+
if [[ ${#docs[@]} -eq 0 ]]; then
|
|
66
|
+
json_docs="[]"
|
|
67
|
+
else
|
|
68
|
+
json_docs=$(printf '%s\n' "${docs[@]}" | jq -R . | jq -s .)
|
|
69
|
+
fi
|
|
70
|
+
jq -cn \
|
|
71
|
+
--arg feature_dir "$FEATURE_DIR" \
|
|
72
|
+
--argjson docs "$json_docs" \
|
|
73
|
+
--arg tasks_template "${TASKS_TEMPLATE:-}" \
|
|
74
|
+
--arg tasks_template_content "$TASKS_TEMPLATE_CONTENT" \
|
|
75
|
+
'{FEATURE_DIR:$feature_dir,AVAILABLE_DOCS:$docs,TASKS_TEMPLATE:$tasks_template,TASKS_TEMPLATE_CONTENT:$tasks_template_content}'
|
|
76
|
+
else
|
|
77
|
+
if [[ ${#docs[@]} -eq 0 ]]; then
|
|
78
|
+
json_docs="[]"
|
|
79
|
+
else
|
|
80
|
+
json_docs=$(for d in "${docs[@]}"; do printf '"%s",' "$(json_escape "$d")"; done)
|
|
81
|
+
json_docs="[${json_docs%,}]"
|
|
82
|
+
fi
|
|
83
|
+
printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s,"TASKS_TEMPLATE":"%s","TASKS_TEMPLATE_CONTENT":"%s"}\n' \
|
|
84
|
+
"$(json_escape "$FEATURE_DIR")" "$json_docs" "$(json_escape "${TASKS_TEMPLATE:-}")" "$(json_escape "$TASKS_TEMPLATE_CONTENT")"
|
|
85
|
+
fi
|
|
86
|
+
else
|
|
87
|
+
echo "FEATURE_DIR: $FEATURE_DIR"
|
|
88
|
+
echo "TASKS_TEMPLATE: ${TASKS_TEMPLATE:-not found}"
|
|
89
|
+
echo "AVAILABLE_DOCS:"
|
|
90
|
+
check_file "$RESEARCH" "research.md"
|
|
91
|
+
check_file "$DATA_MODEL" "data-model.md"
|
|
92
|
+
check_dir "$CONTRACTS_DIR" "contracts/"
|
|
93
|
+
check_file "$QUICKSTART" "quickstart.md"
|
|
94
|
+
fi
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# [CHECKLIST TYPE] Checklist: [FEATURE NAME]
|
|
2
|
+
|
|
3
|
+
**Purpose**: [Brief description of what this checklist covers]
|
|
4
|
+
**Created**: [DATE]
|
|
5
|
+
**Feature**: [Link to spec.md or relevant documentation]
|
|
6
|
+
|
|
7
|
+
**Note**: This custom checklist is generated by the `/duaer-checklist` command based on feature context and requirements.
|
|
8
|
+
**Review Ownership**: This checklist is a reviewer-owned requirements-quality review artifact. Mark an item `[x]` only when the reviewer determines the requirements-quality criterion is satisfied.
|
|
9
|
+
**Marker Semantics**: `[x]` means the criterion has been reviewed and satisfied for requirements quality. It does not mean implementation work is complete.
|
|
10
|
+
|
|
11
|
+
<!--
|
|
12
|
+
============================================================================
|
|
13
|
+
IMPORTANT: The checklist items below are SAMPLE ITEMS for illustration only.
|
|
14
|
+
|
|
15
|
+
The /duaer-checklist command MUST replace these with actual items based on:
|
|
16
|
+
- User's specific checklist request
|
|
17
|
+
- Feature requirements from spec.md
|
|
18
|
+
- Technical context from plan.md
|
|
19
|
+
- Implementation details from tasks.md
|
|
20
|
+
|
|
21
|
+
DO NOT keep these sample items in the generated checklist file.
|
|
22
|
+
============================================================================
|
|
23
|
+
-->
|
|
24
|
+
|
|
25
|
+
## [Category 1]
|
|
26
|
+
|
|
27
|
+
- [ ] CHK001 First checklist item with clear action
|
|
28
|
+
- [ ] CHK002 Second checklist item
|
|
29
|
+
- [ ] CHK003 Third checklist item
|
|
30
|
+
|
|
31
|
+
## [Category 2]
|
|
32
|
+
|
|
33
|
+
- [ ] CHK004 Another category item
|
|
34
|
+
- [ ] CHK005 Item with specific criteria
|
|
35
|
+
- [ ] CHK006 Final item in this category
|
|
36
|
+
|
|
37
|
+
## Notes
|
|
38
|
+
|
|
39
|
+
- Mark items `[x]` only after review confirms the requirement-quality criterion is satisfied
|
|
40
|
+
- Leave items unchecked when they still require clarification, correction, or reviewer evaluation
|
|
41
|
+
- `/duaer-implement` reads checklist checkbox state as a gate and must not modify markers
|
|
42
|
+
- `checklists/requirements.md` has a separate built-in lifecycle maintained by `/duaer-specify` and `/duaer-clarify`
|
|
43
|
+
- Add comments or findings inline
|
|
44
|
+
- Link to relevant resources or documentation
|
|
45
|
+
- Items are numbered sequentially for easy reference
|