km-spec-driven-development 0.1.0 → 0.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/bin/km-spec CHANGED
@@ -11,7 +11,8 @@ done
11
11
  SCRIPT_DIR="$(cd -P "$(dirname "$_SOURCE")" && pwd)"
12
12
  TEMPLATES_DIR="${SCRIPT_DIR}/../templates"
13
13
 
14
- VERSION="0.1.0"
14
+ VERSION="0.2.0"
15
+ TEMPLATE_VERSION="1.0"
15
16
 
16
17
  # ─── Logo & Help ──────────────────────────────────────────────────────────────
17
18
 
@@ -31,7 +32,7 @@ EOF
31
32
 
32
33
  print_help() {
33
34
  print_logo
34
- cat <<'EOF'
35
+ cat <<EOF
35
36
 
36
37
  Usage:
37
38
  km-spec <command> [options]
@@ -47,11 +48,36 @@ Commands:
47
48
  testing Update Testing in CONVENTIONS.md
48
49
  integrations Update Integrations in CONVENTIONS.md
49
50
  concerns Update Concerns in CONVENTIONS.md
50
- feature <name> Create a feature spec
51
- quick <name> Create a quick task
51
+ feature <name> [options] Create a feature spec (non-interactive by default)
52
+ quick <name> [options] Create a quick task (non-interactive by default)
52
53
  status Show SDD status
53
54
  doctor Validate SDD structure
54
55
  help Show this help
56
+ version Show version
57
+
58
+ Feature options:
59
+ --description <text> Confirmed overview text for the feature
60
+ --tier standard|complex Planning depth (default: standard)
61
+ --interactive Prompt for fields (legacy interview mode)
62
+ --force Overwrite files in the target feature directory
63
+
64
+ Quick options:
65
+ --description <text> Confirmed objective/description text
66
+ --interactive Prompt for fields (legacy interview mode)
67
+ --force Overwrite files in the target quick-task directory
68
+
69
+ Examples:
70
+ km-spec init
71
+ km-spec feature "User Authentication"
72
+ km-spec feature "User Authentication" --description "Login and session management"
73
+ km-spec feature "SSO Federation" --tier complex
74
+ km-spec feature "User Authentication" --interactive
75
+ km-spec quick "Fix login validation"
76
+ km-spec quick "Fix login validation" --description "Reject expired sessions"
77
+ km-spec doctor
78
+ km-spec status
79
+
80
+ km-spec v${VERSION}
55
81
  EOF
56
82
  }
57
83
 
@@ -97,45 +123,121 @@ confirm() {
97
123
  [[ "$reply" =~ ^[Yy]$ ]]
98
124
  }
99
125
 
100
- # update_section FILE HEADING NEW_CONTENT
101
- # Replaces the body of a ## section with NEW_CONTENT.
102
- # Appends the section if the heading is not found.
103
- # Preserves all other sections.
104
- update_section() {
105
- local file="$1"
106
- local heading="$2"
107
- local content="$3"
108
- local tmp_content tmp_result
109
- tmp_content="$(mktemp)"
110
- tmp_result="$(mktemp)"
126
+ # Multi-line safe: use awk for placeholder replacement without eval.
127
+ # render_template SRC DEST KEY=VALUE KEY=VALUE ...
128
+ # Keys are placeholder names without braces (e.g. FEATURE_NAME).
129
+ render_template() {
130
+ local src="$1"
131
+ local dest="$2"
132
+ shift 2
111
133
 
112
- printf '%s\n' "$content" > "$tmp_content"
134
+ if [[ ! -f "$src" ]]; then
135
+ printf 'Error: template not found: %s\n' "$src" >&2
136
+ exit 1
137
+ fi
113
138
 
114
- awk -v heading="$heading" -v cf="$tmp_content" '
115
- BEGIN {
116
- in_section=0; inserted=0; n=0
117
- while ((getline line < cf) > 0) lines[++n] = line
118
- close(cf)
119
- }
120
- $0 == heading {
121
- in_section=1; inserted=1
122
- print $0
123
- for (i=1; i<=n; i++) print lines[i]
124
- next
125
- }
126
- in_section && /^## / { in_section=0 }
127
- !in_section { print }
128
- END {
129
- if (!inserted) {
130
- print ""
131
- print heading
132
- for (i=1; i<=n; i++) print lines[i]
139
+ local tmp
140
+ tmp="$(mktemp)"
141
+ cp "$src" "$tmp"
142
+
143
+ local pair key val
144
+ for pair in "$@"; do
145
+ key="${pair%%=*}"
146
+ val="${pair#*=}"
147
+ # Use awk with ENVIRON-style via -v and index replace for all occurrences.
148
+ # Pass value via file to preserve newlines safely.
149
+ local valfile
150
+ valfile="$(mktemp)"
151
+ printf '%s' "$val" > "$valfile"
152
+ local out
153
+ out="$(mktemp)"
154
+ awk -v key="{{${key}}}" -v vf="$valfile" '
155
+ BEGIN {
156
+ repl = ""
157
+ while ((getline line < vf) > 0) {
158
+ if (repl != "") repl = repl "\n"
159
+ repl = repl line
160
+ }
161
+ # trailing content without final newline still ok
162
+ close(vf)
133
163
  }
134
- }
135
- ' "$file" > "$tmp_result"
164
+ {
165
+ n = index($0, key)
166
+ if (n == 0) { print; next }
167
+ # replace all occurrences on the line
168
+ line = $0
169
+ out = ""
170
+ while ((n = index(line, key)) > 0) {
171
+ out = out substr(line, 1, n - 1) repl
172
+ line = substr(line, n + length(key))
173
+ }
174
+ print out line
175
+ }
176
+ ' "$tmp" > "$out"
177
+ mv "$out" "$tmp"
178
+ rm -f "$valfile"
179
+ done
136
180
 
137
- cp "$tmp_result" "$file"
138
- rm -f "$tmp_result" "$tmp_content"
181
+ mkdir -p "$(dirname "$dest")"
182
+ cp "$tmp" "$dest"
183
+ rm -f "$tmp"
184
+ }
185
+
186
+ next_feature_id() {
187
+ local n=0
188
+ local d
189
+ if [[ -d ".specs/features" ]]; then
190
+ for d in .specs/features/*/; do
191
+ [[ -d "$d" ]] || continue
192
+ n=$((n + 1))
193
+ done
194
+ fi
195
+ # When creating a new dir, this count includes existing only (new not yet created).
196
+ printf 'FEAT-%03d' $((n + 1))
197
+ }
198
+
199
+ complex_sections_text() {
200
+ cat <<'COMPLEX'
201
+ ## Component Boundaries
202
+
203
+ | Boundary | Owns | Must not own | Confidence |
204
+ |----------|------|--------------|------------|
205
+ | [NEEDS CLARIFICATION] | | | [NEEDS CLARIFICATION] |
206
+
207
+ ## Data Model
208
+
209
+ | Entity / table | Fields (summary) | Notes | Confidence |
210
+ |----------------|------------------|-------|------------|
211
+ | [NEEDS CLARIFICATION] | | | [NEEDS CLARIFICATION] |
212
+
213
+ ## API or Interface Contracts
214
+
215
+ ### Endpoint / interface: [NEEDS CLARIFICATION]
216
+
217
+ - **Request:** [NEEDS CLARIFICATION]
218
+ - **Response:** [NEEDS CLARIFICATION]
219
+ - **Errors:** [NEEDS CLARIFICATION]
220
+
221
+ ## Security
222
+
223
+ - [NEEDS CLARIFICATION]: Authn/authz, input validation, secrets, threat notes.
224
+
225
+ ## Performance
226
+
227
+ - [NEEDS CLARIFICATION]: Latency, throughput, N+1, caching, limits.
228
+
229
+ ## Migration or Rollout
230
+
231
+ - [NEEDS CLARIFICATION]: Schema migrations, feature flags, phased rollout.
232
+
233
+ ## Observability
234
+
235
+ - [NEEDS CLARIFICATION]: Logs, metrics, traces, alerts.
236
+
237
+ ## Rollback Strategy
238
+
239
+ - [NEEDS CLARIFICATION]: How to revert safely if the release fails.
240
+ COMPLEX
139
241
  }
140
242
 
141
243
  # ─── cmd_help ─────────────────────────────────────────────────────────────────
@@ -156,6 +258,7 @@ cmd_init() {
156
258
 
157
259
  local files=(
158
260
  "AGENTS.md"
261
+ "HARNESS.md"
159
262
  "SDD.md"
160
263
  ".specs/project/PROJECT.md"
161
264
  ".specs/project/ROADMAP.md"
@@ -192,220 +295,398 @@ cmd_init() {
192
295
 
193
296
  printf '\n[KM Spec] Done. Created: %d Skipped: %d\n' "$created" "$skipped"
194
297
  printf '\nNext steps:\n'
195
- printf ' km-spec project fill in project goals and constraints\n'
196
- printf ' km-spec stack document the technical stack\n'
298
+ printf ' km-spec feature "Your Feature" scaffold a guided feature spec\n'
299
+ printf ' km-spec project fill in project goals (optional interview)\n'
300
+ printf ' Read HARNESS.md and AGENTS.md before agent work\n'
197
301
  }
198
302
 
199
303
  # ─── cmd_feature ──────────────────────────────────────────────────────────────
200
304
 
201
305
  cmd_feature() {
202
306
  require_init
307
+ require_templates
203
308
 
204
309
  local name="${1:-}"
205
- local force=false
206
-
207
- if [[ -z "$name" ]]; then
208
- printf 'Usage: km-spec feature "Feature Name"\n' >&2
310
+ if [[ -z "$name" || "$name" == --* ]]; then
311
+ printf 'Usage: km-spec feature "Feature Name" [--description TEXT] [--tier standard|complex] [--interactive] [--force]\n' >&2
209
312
  exit 1
210
313
  fi
211
-
212
314
  shift || true
213
- [[ "${1:-}" == "--force" ]] && force=true
315
+
316
+ local force=false interactive=false description="" tier="standard"
317
+
318
+ while [[ $# -gt 0 ]]; do
319
+ case "$1" in
320
+ --force) force=true; shift ;;
321
+ --interactive) interactive=true; shift ;;
322
+ --description)
323
+ if [[ -z "${2:-}" || "${2:-}" == --* ]]; then
324
+ printf 'Error: --description requires a value.\n' >&2
325
+ exit 1
326
+ fi
327
+ description="$2"
328
+ shift 2
329
+ ;;
330
+ --description=*)
331
+ description="${1#--description=}"
332
+ shift
333
+ ;;
334
+ --tier)
335
+ if [[ -z "${2:-}" || "${2:-}" == --* ]]; then
336
+ printf 'Error: --tier requires a value (standard|complex).\n' >&2
337
+ exit 1
338
+ fi
339
+ tier="$2"
340
+ shift 2
341
+ ;;
342
+ --tier=*)
343
+ tier="${1#--tier=}"
344
+ shift
345
+ ;;
346
+ *)
347
+ printf 'Error: unknown option for feature: %s\n' "$1" >&2
348
+ exit 1
349
+ ;;
350
+ esac
351
+ done
352
+
353
+ case "$tier" in
354
+ standard|complex) ;;
355
+ *)
356
+ printf 'Error: unknown tier "%s". Supported tiers: standard, complex.\n' "$tier" >&2
357
+ exit 1
358
+ ;;
359
+ esac
214
360
 
215
361
  local slug
216
362
  slug="$(slugify "$name")"
217
- local dir=".specs/features/$slug"
218
-
219
- if [[ -d "$dir" ]] && [[ "$force" != true ]]; then
220
- printf 'Feature already exists: %s\nUse --force to overwrite.\n' "$dir"
363
+ if [[ -z "$slug" ]]; then
364
+ printf 'Error: could not derive a safe slug from feature name.\n' >&2
221
365
  exit 1
222
366
  fi
223
367
 
224
- mkdir -p "$dir"
225
-
226
- printf '\nCreating feature: %s\n\n' "$name"
227
-
228
- local goal req1 req2 req3 ac1 ac2 ac3 oos
229
- goal="" req1="" req2="" req3="" ac1="" ac2="" ac3="" oos=""
230
- read -r -p "Feature goal: " goal || true
231
- read -r -p "Requirement 1: " req1 || true
232
- read -r -p "Requirement 2: " req2 || true
233
- read -r -p "Requirement 3: " req3 || true
234
- read -r -p "Acceptance criterion 1: " ac1 || true
235
- read -r -p "Acceptance criterion 2: " ac2 || true
236
- read -r -p "Acceptance criterion 3: " ac3 || true
237
- read -r -p "Out of scope: " oos || true
238
-
239
- cat > "$dir/spec.md" <<SPEC
240
- # Feature: $name
241
-
242
- ## Goal
243
-
244
- $goal
245
-
246
- ## Requirements
247
-
248
- - REQ-001: $req1
249
- - REQ-002: $req2
250
- - REQ-003: $req3
251
-
252
- ## Acceptance Criteria
253
-
254
- - [ ] AC-001: $ac1
255
- - [ ] AC-002: $ac2
256
- - [ ] AC-003: $ac3
257
-
258
- ## Out of Scope
259
-
260
- - $oos
261
- SPEC
262
-
263
- cat > "$dir/tasks.md" <<TASKS
264
- # Tasks: $name
265
-
266
- ## TASK-001 — Define implementation plan
267
-
268
- Status: TODO
269
-
270
- Related requirements:
271
-
272
- - REQ-001
273
- - REQ-002
274
- - REQ-003
275
-
276
- Objective:
277
-
278
- Create an implementation plan for this feature based on \`spec.md\`.
279
-
280
- Implementation steps:
281
-
282
- 1. Read \`.specs/project/STATE.md\`.
283
- 2. Read \`.specs/project/PROJECT.md\`.
284
- 3. Read \`.specs/codebase/CONVENTIONS.md\`.
285
- 4. Inspect the existing code before changing anything.
286
- 5. Break the feature into atomic implementation tasks.
287
-
288
- Acceptance criteria:
289
-
290
- - [ ] Implementation tasks are clear and atomic.
291
- - [ ] Each task references related requirements.
292
- - [ ] Validation requirements are defined.
293
-
294
- Validation:
295
-
296
- - [ ] Build passes, if applicable.
297
- - [ ] Lint passes, if available.
298
- - [ ] Typecheck passes, if available.
299
- - [ ] Tests pass, if available.
300
- - [ ] Acceptance criteria verified.
368
+ local dir=".specs/features/$slug"
301
369
 
302
- Notes:
303
- TASKS
370
+ # Path safety: slug is already alnum+hyphen only
371
+ if [[ "$slug" == *"/"* || "$slug" == *".."* ]]; then
372
+ printf 'Error: invalid feature slug.\n' >&2
373
+ exit 1
374
+ fi
304
375
 
305
- cat > "$dir/validation.md" <<VALIDATION
306
- # Validation: $name
376
+ if [[ -d "$dir" || -e "$dir" ]] && [[ "$force" != true ]]; then
377
+ printf 'Feature already exists: %s\nUse --force to overwrite generated files in this directory only.\n' "$dir" >&2
378
+ exit 1
379
+ fi
307
380
 
308
- ## Validation Summary
381
+ if [[ "$force" == true ]] && [[ -d "$dir" ]]; then
382
+ printf '[KM Spec] --force: will replace generated files in %s:\n' "$dir"
383
+ printf ' - %s/spec.md\n' "$dir"
384
+ printf ' - %s/context.md\n' "$dir"
385
+ printf ' - %s/plan.md\n' "$dir"
386
+ printf ' - %s/tasks.md\n' "$dir"
387
+ printf ' - %s/validation.md\n' "$dir"
388
+ fi
309
389
 
310
- Status: Pending
390
+ mkdir -p "$dir"
311
391
 
312
- ## Evidence
392
+ local feature_id created_date
393
+ created_date="$(date '+%Y-%m-%d')"
394
+ feature_id="$(next_feature_id)"
395
+ # If force-overwrite and existing FEAT id present, reuse it
396
+ if [[ -f "$dir/spec.md" ]]; then
397
+ local existing_id
398
+ existing_id="$(grep -E '\*\*Feature ID\*\*' "$dir/spec.md" 2>/dev/null | head -1 | sed -E 's/.*\|\s*(FEAT-[A-Za-z0-9-]+)\s*\|.*/\1/' || true)"
399
+ if [[ "$existing_id" == FEAT-* ]]; then
400
+ feature_id="$existing_id"
401
+ fi
402
+ fi
313
403
 
314
- | Gate | Result | Command / Notes |
315
- |------|--------|-----------------|
316
- | Build | Pending | |
317
- | Lint | Pending | |
318
- | Typecheck | Pending | |
319
- | Tests | Pending | |
320
- | Manual Review | Pending | |
404
+ local overview_block confirmed_block inferred_block
405
+ if [[ -n "$description" ]]; then
406
+ overview_block="[CONFIRMED]: ${description}"
407
+ confirmed_block="- [CONFIRMED]: Feature name is **${name}**.
408
+ - [CONFIRMED]: Overview description provided at scaffold time (see Overview)."
409
+ else
410
+ overview_block="[NEEDS CLARIFICATION]: Write 1–3 sentences describing what this feature does and why it exists. Do not invent product claims."
411
+ confirmed_block="- [CONFIRMED]: Feature name is **${name}** (scaffold title only).
412
+ - [NEEDS CLARIFICATION]: No overview description was provided at scaffold time."
413
+ fi
321
414
 
322
- ## Issues Found
415
+ inferred_block="- None yet. Do **not** invent product requirements from the feature name.
416
+ - Optional later: list non-binding ideas here only with explicit \`[INFERRED]\` labels, then promote with evidence."
417
+
418
+ # Interactive mode fills optional fields into overview/confirmed without inventing REQs
419
+ if [[ "$interactive" == true ]]; then
420
+ printf '\nInteractive feature setup: %s\n' "$name"
421
+ printf '(Leave blank to keep template placeholders. Requirements are never auto-invented.)\n\n'
422
+ local goal oos
423
+ goal="" oos=""
424
+ read -r -p "Feature goal / overview: " goal || true
425
+ read -r -p "Out of scope (optional): " oos || true
426
+ if [[ -n "$goal" ]]; then
427
+ overview_block="[CONFIRMED]: ${goal}"
428
+ confirmed_block="- [CONFIRMED]: Feature name is **${name}**.
429
+ - [CONFIRMED]: Overview provided interactively (see Overview)."
430
+ description="$goal"
431
+ fi
432
+ if [[ -n "$oos" ]]; then
433
+ # Append note via environment for post-pass; store for sed after render
434
+ :
435
+ fi
436
+ fi
323
437
 
324
- None.
438
+ local complex_sections=""
439
+ if [[ "$tier" == "complex" ]]; then
440
+ complex_sections="$(complex_sections_text)"
441
+ else
442
+ complex_sections="<!-- Complex-tier sections omitted (tier=standard). Re-scaffold with --tier complex or add sections manually if complexity grows. -->"
443
+ fi
325
444
 
326
- ## Final Result
445
+ local desc_context=""
446
+ if [[ -n "$description" ]]; then
447
+ desc_context="Scaffold description ([CONFIRMED] at creation): ${description}"
448
+ else
449
+ desc_context="[NEEDS CLARIFICATION]: No scaffold description provided."
450
+ fi
327
451
 
328
- Pending.
329
- VALIDATION
452
+ printf '\nCreating feature: %s (tier=%s)\n\n' "$name" "$tier"
453
+
454
+ local tdir="$TEMPLATES_DIR/features"
455
+ render_template "$tdir/spec.md" "$dir/spec.md" \
456
+ "FEATURE_NAME=${name}" \
457
+ "FEATURE_SLUG=${slug}" \
458
+ "FEATURE_ID=${feature_id}" \
459
+ "DESCRIPTION=${description}" \
460
+ "CREATED_DATE=${created_date}" \
461
+ "TIER=${tier}" \
462
+ "TEMPLATE_VERSION=${TEMPLATE_VERSION}" \
463
+ "OVERVIEW_BLOCK=${overview_block}" \
464
+ "CONFIRMED_BLOCK=${confirmed_block}" \
465
+ "INFERRED_BLOCK=${inferred_block}"
466
+
467
+ render_template "$tdir/context.md" "$dir/context.md" \
468
+ "FEATURE_NAME=${name}" \
469
+ "FEATURE_SLUG=${slug}" \
470
+ "FEATURE_ID=${feature_id}" \
471
+ "DESCRIPTION=${description}" \
472
+ "CREATED_DATE=${created_date}" \
473
+ "TIER=${tier}" \
474
+ "TEMPLATE_VERSION=${TEMPLATE_VERSION}" \
475
+ "DESCRIPTION_CONTEXT=${desc_context}"
476
+
477
+ render_template "$tdir/plan.md" "$dir/plan.md" \
478
+ "FEATURE_NAME=${name}" \
479
+ "FEATURE_SLUG=${slug}" \
480
+ "FEATURE_ID=${feature_id}" \
481
+ "DESCRIPTION=${description}" \
482
+ "CREATED_DATE=${created_date}" \
483
+ "TIER=${tier}" \
484
+ "TEMPLATE_VERSION=${TEMPLATE_VERSION}" \
485
+ "COMPLEX_SECTIONS=${complex_sections}"
486
+
487
+ render_template "$tdir/tasks.md" "$dir/tasks.md" \
488
+ "FEATURE_NAME=${name}" \
489
+ "FEATURE_SLUG=${slug}" \
490
+ "FEATURE_ID=${feature_id}" \
491
+ "DESCRIPTION=${description}" \
492
+ "CREATED_DATE=${created_date}" \
493
+ "TIER=${tier}" \
494
+ "TEMPLATE_VERSION=${TEMPLATE_VERSION}"
495
+
496
+ render_template "$tdir/validation.md" "$dir/validation.md" \
497
+ "FEATURE_NAME=${name}" \
498
+ "FEATURE_SLUG=${slug}" \
499
+ "FEATURE_ID=${feature_id}" \
500
+ "DESCRIPTION=${description}" \
501
+ "CREATED_DATE=${created_date}" \
502
+ "TIER=${tier}" \
503
+ "TEMPLATE_VERSION=${TEMPLATE_VERSION}"
504
+
505
+ # Interactive optional out-of-scope injection
506
+ if [[ "$interactive" == true ]]; then
507
+ local oos_val="${oos:-}"
508
+ if [[ -n "${oos_val:-}" ]]; then
509
+ # Replace first NEEDS CLARIFICATION out-of-scope bullet with confirmed text
510
+ local tmp
511
+ tmp="$(mktemp)"
512
+ awk -v oos="$oos_val" '
513
+ BEGIN { done=0 }
514
+ !done && /\[NEEDS CLARIFICATION\]: List what this feature/ {
515
+ print "- [CONFIRMED]: " oos
516
+ done=1
517
+ next
518
+ }
519
+ { print }
520
+ ' "$dir/spec.md" > "$tmp"
521
+ mv "$tmp" "$dir/spec.md"
522
+ fi
523
+ fi
330
524
 
331
- printf '\n'
332
525
  printf '[KM Spec] Created: %s/spec.md\n' "$dir"
526
+ printf '[KM Spec] Created: %s/context.md\n' "$dir"
527
+ printf '[KM Spec] Created: %s/plan.md\n' "$dir"
333
528
  printf '[KM Spec] Created: %s/tasks.md\n' "$dir"
334
529
  printf '[KM Spec] Created: %s/validation.md\n' "$dir"
335
- printf '\nNext step: open %s/spec.md and review the spec.\n' "$dir"
336
- printf 'Then ask your AI agent to implement TASK-001.\n'
530
+ printf '\nNext steps:\n'
531
+ printf ' 1. Fill spec.md (requirements stay unresolved until confirmed).\n'
532
+ printf ' 2. Ask your agent to follow AGENTS.md + HARNESS.md starting at TASK-001.\n'
337
533
  }
338
534
 
339
535
  # ─── cmd_quick ────────────────────────────────────────────────────────────────
340
536
 
341
537
  cmd_quick() {
342
538
  require_init
539
+ require_templates
343
540
 
344
541
  local name="${1:-}"
345
- local force=false
346
-
347
- if [[ -z "$name" ]]; then
348
- printf 'Usage: km-spec quick "Task Name"\n' >&2
542
+ if [[ -z "$name" || "$name" == --* ]]; then
543
+ printf 'Usage: km-spec quick "Task Name" [--description TEXT] [--interactive] [--force]\n' >&2
349
544
  exit 1
350
545
  fi
351
-
352
546
  shift || true
353
- [[ "${1:-}" == "--force" ]] && force=true
547
+
548
+ local force=false interactive=false description=""
549
+
550
+ while [[ $# -gt 0 ]]; do
551
+ case "$1" in
552
+ --force) force=true; shift ;;
553
+ --interactive) interactive=true; shift ;;
554
+ --description)
555
+ if [[ -z "${2:-}" || "${2:-}" == --* ]]; then
556
+ printf 'Error: --description requires a value.\n' >&2
557
+ exit 1
558
+ fi
559
+ description="$2"
560
+ shift 2
561
+ ;;
562
+ --description=*)
563
+ description="${1#--description=}"
564
+ shift
565
+ ;;
566
+ *)
567
+ printf 'Error: unknown option for quick: %s\n' "$1" >&2
568
+ exit 1
569
+ ;;
570
+ esac
571
+ done
354
572
 
355
573
  local slug
356
574
  slug="$(slugify "$name")"
357
- local file=".specs/quick/${slug}.md"
358
-
359
- if [[ -f "$file" ]] && [[ "$force" != true ]]; then
360
- printf 'Quick task already exists: %s\nUse --force to overwrite.\n' "$file"
575
+ if [[ -z "$slug" ]]; then
576
+ printf 'Error: could not derive a safe slug from task name.\n' >&2
577
+ exit 1
578
+ fi
579
+ if [[ "$slug" == *"/"* || "$slug" == *".."* ]]; then
580
+ printf 'Error: invalid quick-task slug.\n' >&2
361
581
  exit 1
362
582
  fi
363
583
 
364
- mkdir -p ".specs/quick"
365
-
366
- printf '\nCreating quick task: %s\n\n' "$name"
367
-
368
- local objective files_affected ac validation_steps
369
- objective="" files_affected="" ac="" validation_steps=""
370
- read -r -p "Objective: " objective || true
371
- read -r -p "Likely affected files: " files_affected || true
372
- read -r -p "Acceptance criteria: " ac || true
373
- read -r -p "Validation steps: " validation_steps || true
374
-
375
- cat > "$file" <<QUICK
376
- # Quick Task: $name
377
-
378
- ## Objective
379
-
380
- $objective
381
-
382
- ## Scope
383
-
384
- Files likely affected:
584
+ local dir=".specs/quick/$slug"
585
+ local legacy_file=".specs/quick/${slug}.md"
385
586
 
386
- - $files_affected
587
+ if [[ -e "$dir" || -f "$legacy_file" ]] && [[ "$force" != true ]]; then
588
+ if [[ -f "$legacy_file" ]]; then
589
+ printf 'Quick task already exists (legacy file): %s\nUse --force to replace with directory layout (legacy file left in place unless removed manually).\n' "$legacy_file" >&2
590
+ else
591
+ printf 'Quick task already exists: %s\nUse --force to overwrite generated files in this directory only.\n' "$dir" >&2
592
+ fi
593
+ exit 1
594
+ fi
387
595
 
388
- ## Acceptance Criteria
596
+ if [[ "$force" == true ]]; then
597
+ printf '[KM Spec] --force: will replace generated files in %s:\n' "$dir"
598
+ printf ' - %s/TASK.md\n' "$dir"
599
+ printf ' - %s/SUMMARY.md\n' "$dir"
600
+ fi
389
601
 
390
- - [ ] $ac
602
+ mkdir -p "$dir"
391
603
 
392
- ## Validation
604
+ local created_date
605
+ created_date="$(date '+%Y-%m-%d')"
393
606
 
394
- - [ ] Build passes, if applicable.
395
- - [ ] Lint passes, if available.
396
- - [ ] Typecheck passes, if available.
397
- - [ ] Tests pass, if available.
398
- - [ ] Manual check completed, if needed.
607
+ local quick_objective quick_description
608
+ if [[ -n "$description" ]]; then
609
+ quick_objective="[CONFIRMED]: ${description}"
610
+ quick_description="[CONFIRMED]: ${description}"
611
+ else
612
+ quick_objective="[NEEDS CLARIFICATION]: State the objective in one sentence. Do not invent scope."
613
+ quick_description="[NEEDS CLARIFICATION]: Add brief context for the change. Inspect the repository before listing files."
614
+ fi
399
615
 
400
- Notes: $validation_steps
616
+ if [[ "$interactive" == true ]]; then
617
+ printf '\nInteractive quick task setup: %s\n\n' "$name"
618
+ local objective files_affected ac validation_steps
619
+ objective="" files_affected="" ac="" validation_steps=""
620
+ read -r -p "Objective: " objective || true
621
+ read -r -p "Likely affected files: " files_affected || true
622
+ read -r -p "Acceptance criteria: " ac || true
623
+ read -r -p "Validation steps: " validation_steps || true
624
+ if [[ -n "$objective" ]]; then
625
+ quick_objective="[CONFIRMED]: ${objective}"
626
+ quick_description="[CONFIRMED]: ${objective}"
627
+ fi
628
+ fi
401
629
 
402
- ## Summary
630
+ printf '\nCreating quick task: %s\n\n' "$name"
403
631
 
404
- [Describe what was changed after implementation.]
405
- QUICK
632
+ local tdir="$TEMPLATES_DIR/quick"
633
+ render_template "$tdir/TASK.md" "$dir/TASK.md" \
634
+ "FEATURE_NAME=${name}" \
635
+ "FEATURE_SLUG=${slug}" \
636
+ "FEATURE_ID=" \
637
+ "DESCRIPTION=${description}" \
638
+ "CREATED_DATE=${created_date}" \
639
+ "TIER=quick" \
640
+ "TEMPLATE_VERSION=${TEMPLATE_VERSION}" \
641
+ "QUICK_OBJECTIVE=${quick_objective}" \
642
+ "QUICK_DESCRIPTION=${quick_description}"
643
+
644
+ render_template "$tdir/SUMMARY.md" "$dir/SUMMARY.md" \
645
+ "FEATURE_NAME=${name}" \
646
+ "FEATURE_SLUG=${slug}" \
647
+ "FEATURE_ID=" \
648
+ "DESCRIPTION=${description}" \
649
+ "CREATED_DATE=${created_date}" \
650
+ "TIER=quick" \
651
+ "TEMPLATE_VERSION=${TEMPLATE_VERSION}"
652
+
653
+ # Interactive: inject optional fields into TASK.md if provided
654
+ if [[ "$interactive" == true ]]; then
655
+ if [[ -n "${files_affected:-}" ]]; then
656
+ local tmp
657
+ tmp="$(mktemp)"
658
+ awk -v f="$files_affected" '
659
+ /\[NEEDS CLARIFICATION\]: `path\/to\/file`/ && !done {
660
+ print "- [CONFIRMED]: " f
661
+ done=1
662
+ next
663
+ }
664
+ { print }
665
+ ' "$dir/TASK.md" > "$tmp"
666
+ mv "$tmp" "$dir/TASK.md"
667
+ fi
668
+ if [[ -n "${ac:-}" ]]; then
669
+ local tmp
670
+ tmp="$(mktemp)"
671
+ awk -v a="$ac" '
672
+ /\[NEEDS CLARIFICATION\]: Observable outcome/ && !done {
673
+ print "- [ ] [CONFIRMED]: " a
674
+ done=1
675
+ next
676
+ }
677
+ { print }
678
+ ' "$dir/TASK.md" > "$tmp"
679
+ mv "$tmp" "$dir/TASK.md"
680
+ fi
681
+ if [[ -n "${validation_steps:-}" ]]; then
682
+ printf '\n## Interactive validation notes\n\n%s\n' "$validation_steps" >> "$dir/TASK.md"
683
+ fi
684
+ fi
406
685
 
407
- printf '\n[KM Spec] Created: %s\n' "$file"
408
- printf '\nNext step: ask your AI agent to complete this task, then update the Summary section.\n'
686
+ printf '[KM Spec] Created: %s/TASK.md\n' "$dir"
687
+ printf '[KM Spec] Created: %s/SUMMARY.md\n' "$dir"
688
+ printf '\nNext step: ask your agent to complete TASK.md following AGENTS.md and HARNESS.md,\n'
689
+ printf 'then fill SUMMARY.md and update STATE.md.\n'
409
690
  }
410
691
 
411
692
  # ─── cmd_project ──────────────────────────────────────────────────────────────
@@ -478,7 +759,7 @@ $nonneg_md
478
759
  PROJECT
479
760
 
480
761
  printf '\n[KM Spec] Updated: %s\n' "$file"
481
- printf '\nNext step: km-spec stack\n'
762
+ printf '\nNext step: km-spec feature "Your first feature"\n'
482
763
  }
483
764
 
484
765
  # ─── cmd_roadmap ──────────────────────────────────────────────────────────────
@@ -574,8 +855,17 @@ cmd_state() {
574
855
  next_md="$(printf '%s' "$next_steps" | tr ',' '\n' | awk 'NR>0{print NR". "$0}' | sed 's/^[0-9]*\.[[:space:]]*/& /')"
575
856
 
576
857
  local decisions_text blockers_text
577
- decisions_text="$([ -n "$decision" ] && printf '- %s' "$decision" || printf '- None.')"
578
- blockers_text="$([ -n "$blocker" ] && printf '- %s' "$blocker" || printf '- None.')"
858
+ # Use printf -- so values starting with '-' are not treated as options
859
+ if [[ -n "$decision" ]]; then
860
+ decisions_text="$(printf -- '- %s' "$decision")"
861
+ else
862
+ decisions_text="- None."
863
+ fi
864
+ if [[ -n "$blocker" ]]; then
865
+ blockers_text="$(printf -- '- %s' "$blocker")"
866
+ else
867
+ blockers_text="- None."
868
+ fi
579
869
 
580
870
  cat >> "$file" <<SESSION
581
871
 
@@ -795,13 +1085,51 @@ cmd_concerns() {
795
1085
  printf '\nRun km-spec status to review the full setup.\n'
796
1086
  }
797
1087
 
1088
+ # update_section FILE HEADING NEW_CONTENT
1089
+ update_section() {
1090
+ local file="$1"
1091
+ local heading="$2"
1092
+ local content="$3"
1093
+ local tmp_content tmp_result
1094
+ tmp_content="$(mktemp)"
1095
+ tmp_result="$(mktemp)"
1096
+
1097
+ printf '%s\n' "$content" > "$tmp_content"
1098
+
1099
+ awk -v heading="$heading" -v cf="$tmp_content" '
1100
+ BEGIN {
1101
+ in_section=0; inserted=0; n=0
1102
+ while ((getline line < cf) > 0) lines[++n] = line
1103
+ close(cf)
1104
+ }
1105
+ $0 == heading {
1106
+ in_section=1; inserted=1
1107
+ print $0
1108
+ for (i=1; i<=n; i++) print lines[i]
1109
+ next
1110
+ }
1111
+ in_section && /^## / { in_section=0 }
1112
+ !in_section { print }
1113
+ END {
1114
+ if (!inserted) {
1115
+ print ""
1116
+ print heading
1117
+ for (i=1; i<=n; i++) print lines[i]
1118
+ }
1119
+ }
1120
+ ' "$file" > "$tmp_result"
1121
+
1122
+ cp "$tmp_result" "$file"
1123
+ rm -f "$tmp_result" "$tmp_content"
1124
+ }
1125
+
798
1126
  # ─── cmd_status ───────────────────────────────────────────────────────────────
799
1127
 
800
1128
  cmd_status() {
801
1129
  printf 'KM Spec Status\n\n'
802
1130
 
803
1131
  printf 'Base files:\n'
804
- local base_files=("AGENTS.md" "SDD.md" ".specs/project/PROJECT.md"
1132
+ local base_files=("AGENTS.md" "HARNESS.md" "SDD.md" ".specs/project/PROJECT.md"
805
1133
  ".specs/project/ROADMAP.md" ".specs/project/STATE.md"
806
1134
  ".specs/codebase/CONVENTIONS.md")
807
1135
 
@@ -816,10 +1144,34 @@ cmd_status() {
816
1144
  printf '\nFeatures:\n'
817
1145
  local feat_found=false
818
1146
  if [[ -d ".specs/features" ]]; then
1147
+ local d
819
1148
  for d in .specs/features/*/; do
820
1149
  [[ -d "$d" ]] || continue
821
- printf ' %s\n' "$(basename "$d")"
822
1150
  feat_found=true
1151
+ local feat layout tier
1152
+ feat="$(basename "$d")"
1153
+ layout="legacy"
1154
+ if [[ -f "$d/spec.md" && -f "$d/context.md" && -f "$d/plan.md" && -f "$d/tasks.md" && -f "$d/validation.md" ]]; then
1155
+ layout="standard"
1156
+ if grep -qE '\|\s*\*\*Tier\*\*\s*\|\s*complex\s*\|' "$d/spec.md" 2>/dev/null \
1157
+ || grep -qE '\*\*Tier\*\*.*complex' "$d/plan.md" 2>/dev/null \
1158
+ || grep -q '## Component Boundaries' "$d/plan.md" 2>/dev/null; then
1159
+ if grep -qiE 'Tier[[:space:]]*\|\s*complex|tier.*complex|\*\*Tier\*\*.*complex' "$d/spec.md" "$d/plan.md" 2>/dev/null; then
1160
+ layout="complex"
1161
+ fi
1162
+ fi
1163
+ # Prefer explicit tier field
1164
+ if grep -qE '\| \*\*Tier\*\* \| complex \|' "$d/spec.md" 2>/dev/null; then
1165
+ layout="complex"
1166
+ elif grep -qE '\| \*\*Tier\*\* \| standard \|' "$d/spec.md" 2>/dev/null; then
1167
+ layout="standard"
1168
+ fi
1169
+ elif [[ -f "$d/spec.md" && -f "$d/tasks.md" && -f "$d/validation.md" ]]; then
1170
+ layout="legacy"
1171
+ else
1172
+ layout="incomplete"
1173
+ fi
1174
+ printf ' %s [%s]\n' "$feat" "$layout"
823
1175
  done
824
1176
  fi
825
1177
  [[ "$feat_found" == true ]] || printf ' (none)\n'
@@ -827,10 +1179,16 @@ cmd_status() {
827
1179
  printf '\nQuick tasks:\n'
828
1180
  local quick_found=false
829
1181
  if [[ -d ".specs/quick" ]]; then
1182
+ local d f
1183
+ for d in .specs/quick/*/; do
1184
+ [[ -d "$d" ]] || continue
1185
+ quick_found=true
1186
+ printf ' %s/ [directory]\n' "$(basename "$d")"
1187
+ done
830
1188
  for f in .specs/quick/*.md; do
831
1189
  [[ -f "$f" ]] || continue
832
- printf ' %s\n' "$(basename "$f" .md)"
833
1190
  quick_found=true
1191
+ printf ' %s [legacy-file]\n' "$(basename "$f")"
834
1192
  done
835
1193
  fi
836
1194
  [[ "$quick_found" == true ]] || printf ' (none)\n'
@@ -876,6 +1234,14 @@ cmd_doctor() {
876
1234
  else err "$f — missing"; errors=$((errors + 1)); fi
877
1235
  done
878
1236
 
1237
+ printf '\nRecommended files:\n'
1238
+ if [[ -f "HARNESS.md" ]]; then
1239
+ ok "HARNESS.md"
1240
+ else
1241
+ warn "HARNESS.md — missing (recommended for agent lifecycle; run km-spec init or copy from package templates)"
1242
+ warnings=$((warnings + 1))
1243
+ fi
1244
+
879
1245
  printf '\nRequired directories:\n'
880
1246
  local required_dirs=(".specs/project" ".specs/codebase" ".specs/features" ".specs/quick")
881
1247
  for d in "${required_dirs[@]}"; do
@@ -927,20 +1293,50 @@ cmd_doctor() {
927
1293
  warn "AGENTS.md — missing one or more required file references"
928
1294
  warnings=$((warnings + 1))
929
1295
  fi
1296
+ if ! grep -q "HARNESS.md" "AGENTS.md" 2>/dev/null; then
1297
+ warn "AGENTS.md — does not reference HARNESS.md (recommended)"
1298
+ warnings=$((warnings + 1))
1299
+ fi
930
1300
  fi
931
1301
 
932
1302
  printf '\nFeature integrity:\n'
933
1303
  local feat_checked=false
934
1304
  if [[ -d ".specs/features" ]]; then
1305
+ local d
935
1306
  for d in .specs/features/*/; do
936
1307
  [[ -d "$d" ]] || continue
937
1308
  feat_checked=true
938
1309
  local feat
939
1310
  feat="$(basename "$d")"
940
- for req_file in spec.md tasks.md validation.md; do
1311
+
1312
+ # Core files: spec + tasks required; validation expected
1313
+ for req_file in spec.md tasks.md; do
941
1314
  if [[ -f "$d$req_file" ]]; then ok "$feat/$req_file"
942
- else warn "$feat/$req_file — missing"; warnings=$((warnings + 1)); fi
1315
+ else err "$feat/$req_file — missing"; errors=$((errors + 1)); fi
943
1316
  done
1317
+
1318
+ if [[ -f "$d/validation.md" ]]; then ok "$feat/validation.md"
1319
+ else warn "$feat/validation.md — missing"; warnings=$((warnings + 1)); fi
1320
+
1321
+ # New layout files: warn if missing (legacy compatible)
1322
+ if [[ -f "$d/context.md" ]]; then ok "$feat/context.md"
1323
+ else warn "$feat/context.md — missing (legacy layout; recommended for 0.2+)"; warnings=$((warnings + 1)); fi
1324
+
1325
+ if [[ -f "$d/plan.md" ]]; then ok "$feat/plan.md"
1326
+ else warn "$feat/plan.md — missing (legacy layout; recommended for 0.2+; replaces design.md role)"; warnings=$((warnings + 1)); fi
1327
+
1328
+ # Lightweight content warnings
1329
+ if [[ -f "$d/spec.md" ]]; then
1330
+ if grep -qE '\{\{[A-Z0-9_]+\}\}' "$d/spec.md" 2>/dev/null; then
1331
+ warn "$feat/spec.md — unresolved template placeholders {{…}}"
1332
+ warnings=$((warnings + 1))
1333
+ fi
1334
+ # Empty requirement table rows that never left NEEDS CLARIFICATION only — informational
1335
+ if ! grep -qE 'REQ-[0-9]+' "$d/spec.md" 2>/dev/null; then
1336
+ warn "$feat/spec.md — no REQ-XXX identifiers found"
1337
+ warnings=$((warnings + 1))
1338
+ fi
1339
+ fi
944
1340
  done
945
1341
  fi
946
1342
  [[ "$feat_checked" == true ]] || info "No features found."
@@ -948,10 +1344,22 @@ cmd_doctor() {
948
1344
  printf '\nQuick tasks:\n'
949
1345
  local quick_checked=false
950
1346
  if [[ -d ".specs/quick" ]]; then
1347
+ local d f
1348
+ for d in .specs/quick/*/; do
1349
+ [[ -d "$d" ]] || continue
1350
+ quick_checked=true
1351
+ local q
1352
+ q="$(basename "$d")"
1353
+ if [[ -f "$d/TASK.md" ]]; then ok "quick/$q/TASK.md"
1354
+ else warn "quick/$q/TASK.md — missing"; warnings=$((warnings + 1)); fi
1355
+ if [[ -f "$d/SUMMARY.md" ]]; then ok "quick/$q/SUMMARY.md"
1356
+ else warn "quick/$q/SUMMARY.md — missing"; warnings=$((warnings + 1)); fi
1357
+ done
951
1358
  for f in .specs/quick/*.md; do
952
1359
  [[ -f "$f" ]] || continue
953
1360
  quick_checked=true
954
- ok "$(basename "$f")"
1361
+ warn "$(basename "$f") — legacy single-file quick task (prefer quick/<slug>/TASK.md + SUMMARY.md)"
1362
+ warnings=$((warnings + 1))
955
1363
  done
956
1364
  fi
957
1365
  [[ "$quick_checked" == true ]] || info "No quick tasks found."
@@ -963,6 +1371,9 @@ cmd_doctor() {
963
1371
  [[ $errors -gt 0 ]] && printf ' \033[31m%d error(s) found.\033[0m\n' "$errors"
964
1372
  [[ $warnings -gt 0 ]] && printf ' \033[33m%d warning(s) found.\033[0m\n' "$warnings"
965
1373
  fi
1374
+
1375
+ # Exit non-zero only on errors (warnings are informational)
1376
+ [[ $errors -eq 0 ]]
966
1377
  }
967
1378
 
968
1379
  # ─── Main ─────────────────────────────────────────────────────────────────────