@rpamis/comet 0.3.5 → 0.3.7

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 (54) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +180 -56
  3. package/assets/manifest.json +11 -1
  4. package/assets/skills/comet/SKILL.md +59 -24
  5. package/assets/skills/comet/reference/dirty-worktree.md +1 -0
  6. package/assets/skills/comet/rules/comet-phase-guard.md +90 -0
  7. package/assets/skills/comet/scripts/comet-archive.sh +75 -57
  8. package/assets/skills/comet/scripts/comet-env.sh +57 -2
  9. package/assets/skills/comet/scripts/comet-guard.sh +187 -29
  10. package/assets/skills/comet/scripts/comet-handoff.sh +137 -8
  11. package/assets/skills/comet/scripts/comet-hook-guard.sh +260 -0
  12. package/assets/skills/comet/scripts/comet-state.sh +312 -25
  13. package/assets/skills/comet/scripts/comet-yaml-validate.sh +26 -1
  14. package/assets/skills/comet-archive/SKILL.md +45 -12
  15. package/assets/skills/comet-build/SKILL.md +159 -33
  16. package/assets/skills/comet-design/SKILL.md +129 -23
  17. package/assets/skills/comet-hotfix/SKILL.md +57 -15
  18. package/assets/skills/comet-open/SKILL.md +90 -17
  19. package/assets/skills/comet-tweak/SKILL.md +40 -15
  20. package/assets/skills/comet-verify/SKILL.md +64 -25
  21. package/assets/skills-zh/comet/SKILL.md +63 -25
  22. package/assets/skills-zh/comet/reference/dirty-worktree.md +2 -1
  23. package/assets/skills-zh/comet-archive/SKILL.md +46 -13
  24. package/assets/skills-zh/comet-build/SKILL.md +161 -35
  25. package/assets/skills-zh/comet-design/SKILL.md +132 -25
  26. package/assets/skills-zh/comet-hotfix/SKILL.md +53 -15
  27. package/assets/skills-zh/comet-open/SKILL.md +90 -17
  28. package/assets/skills-zh/comet-tweak/SKILL.md +36 -15
  29. package/assets/skills-zh/comet-verify/SKILL.md +64 -27
  30. package/bin/comet.js +3 -3
  31. package/dist/commands/doctor.d.ts.map +1 -1
  32. package/dist/commands/doctor.js +22 -0
  33. package/dist/commands/doctor.js.map +1 -1
  34. package/dist/commands/init.d.ts +5 -1
  35. package/dist/commands/init.d.ts.map +1 -1
  36. package/dist/commands/init.js +64 -9
  37. package/dist/commands/init.js.map +1 -1
  38. package/dist/commands/update.d.ts.map +1 -1
  39. package/dist/commands/update.js +60 -1
  40. package/dist/commands/update.js.map +1 -1
  41. package/dist/core/codegraph.d.ts +9 -0
  42. package/dist/core/codegraph.d.ts.map +1 -0
  43. package/dist/core/codegraph.js +96 -0
  44. package/dist/core/codegraph.js.map +1 -0
  45. package/dist/core/platforms.d.ts +10 -0
  46. package/dist/core/platforms.d.ts.map +1 -1
  47. package/dist/core/platforms.js +163 -15
  48. package/dist/core/platforms.js.map +1 -1
  49. package/dist/core/skills.d.ts +34 -1
  50. package/dist/core/skills.d.ts.map +1 -1
  51. package/dist/core/skills.js +360 -1
  52. package/dist/core/skills.js.map +1 -1
  53. package/package.json +3 -1
  54. package/scripts/postinstall.js +44 -44
@@ -7,6 +7,8 @@
7
7
 
8
8
  set -euo pipefail
9
9
 
10
+ COMET_BASH="${COMET_BASH:-${BASH:-bash}}"
11
+
10
12
  red() { echo -e "\033[31m$1\033[0m" >&2; }
11
13
  green() { echo -e "\033[32m$1\033[0m" >&2; }
12
14
  warn() { echo -e "\033[33m$1\033[0m" >&2; }
@@ -32,18 +34,26 @@ validate_change_name() {
32
34
  fi
33
35
  }
34
36
 
35
- validate_change_name "$1"
37
+ if [ "${COMET_GUARD_SOURCE_ONLY:-0}" = "1" ]; then
38
+ CHANGE="${CHANGE:-}"
39
+ PHASE="${PHASE:-}"
40
+ APPLY="${APPLY:-0}"
41
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd -P)"
42
+ CHANGE_DIR="${CHANGE_DIR:-}"
43
+ else
44
+ validate_change_name "$1"
36
45
 
37
- CHANGE="$1"
38
- PHASE="$2"
39
- APPLY=0
40
- SCRIPT_DIR="$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")" 2>/dev/null || dirname "$0")"
41
- if [[ "${3:-}" == "--apply" ]]; then
42
- APPLY=1
43
- fi
44
- CHANGE_DIR="openspec/changes/$CHANGE"
45
- if [ "$PHASE" = "archive" ] && [ ! -d "$CHANGE_DIR" ] && [ -d "openspec/changes/archive/$CHANGE" ]; then
46
- CHANGE_DIR="openspec/changes/archive/$CHANGE"
46
+ CHANGE="$1"
47
+ PHASE="$2"
48
+ APPLY=0
49
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
50
+ if [[ "${3:-}" == "--apply" ]]; then
51
+ APPLY=1
52
+ fi
53
+ CHANGE_DIR="openspec/changes/$CHANGE"
54
+ if [ "$PHASE" = "archive" ] && [ ! -d "$CHANGE_DIR" ] && [ -d "openspec/changes/archive/$CHANGE" ]; then
55
+ CHANGE_DIR="openspec/changes/archive/$CHANGE"
56
+ fi
47
57
  fi
48
58
 
49
59
  BLOCK=0
@@ -92,6 +102,27 @@ tasks_has_any() {
92
102
  [ -f "$tasks" ] && grep -q '\- \[' "$tasks"
93
103
  }
94
104
 
105
+ plan_tasks_all_done() {
106
+ local plan
107
+ plan=$(yaml_field_value "plan" 2>/dev/null || true)
108
+
109
+ if [ -z "$plan" ] || [ "$plan" = "null" ]; then
110
+ return 0
111
+ fi
112
+ if [ ! -f "$plan" ]; then
113
+ echo "plan file is missing at $plan" >&2
114
+ echo "Next: restore the Superpowers plan file or update .comet.yaml plan before leaving build." >&2
115
+ return 1
116
+ fi
117
+ if grep -q '^[[:space:]]*- \[ \]' "$plan"; then
118
+ echo "Unfinished Superpowers plan tasks:" >&2
119
+ grep -n '^[[:space:]]*- \[ \]' "$plan" >&2 || true
120
+ echo "Next: check off corresponding completed plan tasks, then commit the plan update." >&2
121
+ return 1
122
+ fi
123
+ return 0
124
+ }
125
+
95
126
  yaml_field_value() {
96
127
  local field="$1"
97
128
  local yaml="$CHANGE_DIR/.comet.yaml"
@@ -180,8 +211,19 @@ is_windows_bash() {
180
211
 
181
212
  run_command_string() {
182
213
  local command="$1"
214
+ if [ -z "$command" ]; then
215
+ red "ERROR: build/verify command is empty" >&2
216
+ return 1
217
+ fi
218
+ # Basic command injection guard: reject dangerous shell metacharacters
219
+ # Quotes are allowed to support paths with spaces (e.g. Windows)
220
+ if [[ "$command" =~ [\;\|\&\$\`] ]]; then
221
+ red "ERROR: build/verify command contains shell metacharacters: $command" >&2
222
+ red "Allowed: alphanumeric, spaces, hyphens, underscores, dots, colons, forward slashes, quotes" >&2
223
+ return 1
224
+ fi
183
225
  echo "+ $command" >&2
184
- bash -lc "$command"
226
+ "$COMET_BASH" -lc "$command"
185
227
  }
186
228
 
187
229
  hash_stream() {
@@ -217,12 +259,14 @@ handoff_source_files() {
217
259
  }
218
260
 
219
261
  compute_handoff_hash() {
220
- handoff_source_files | while IFS= read -r file; do
262
+ local hash_input
263
+ hash_input=$(handoff_source_files | while IFS= read -r file; do
221
264
  if [ -f "$file" ]; then
222
265
  printf 'path:%s\n' "$file"
223
266
  printf 'sha256:%s\n' "$(hash_file "$file")"
224
267
  fi
225
- done | hash_stream
268
+ done)
269
+ printf '%s' "$hash_input" | hash_stream
226
270
  }
227
271
 
228
272
  preflight() {
@@ -240,8 +284,8 @@ preflight() {
240
284
  local validate_script
241
285
  validate_script="$SCRIPT_DIR/comet-yaml-validate.sh"
242
286
  if [ -f "$validate_script" ]; then
243
- if ! bash "$validate_script" "$CHANGE" 2>/dev/null; then
244
- bash "$validate_script" "$CHANGE"
287
+ if ! "$COMET_BASH" "$validate_script" "$CHANGE" 2>/dev/null; then
288
+ "$COMET_BASH" "$validate_script" "$CHANGE" || true
245
289
  red "FATAL: .comet.yaml schema validation failed"
246
290
  exit 1
247
291
  fi
@@ -300,7 +344,7 @@ isolation_selected() {
300
344
  *)
301
345
  echo "isolation must be branch or worktree, got '${isolation:-null}'" >&2
302
346
  echo "Next: ask the user to choose branch or worktree, create the chosen isolation, then run:" >&2
303
- echo " bash \"\$COMET_STATE\" set $CHANGE isolation <branch|worktree>" >&2
347
+ echo " \"\$COMET_BASH\" \"\$COMET_STATE\" set $CHANGE isolation <branch|worktree>" >&2
304
348
  return 1
305
349
  ;;
306
350
  esac
@@ -313,8 +357,8 @@ build_mode_selected() {
313
357
  subagent-driven-development|executing-plans|direct) return 0 ;;
314
358
  *)
315
359
  echo "build_mode must be selected before leaving build, got '${build_mode:-null}'" >&2
316
- echo "Next: ask the user to choose an implementation mode, then run:" >&2
317
- echo " bash \"\$COMET_STATE\" set $CHANGE build_mode <subagent-driven-development|executing-plans>" >&2
360
+ echo "Next: ask the user to choose an execution mode, then run:" >&2
361
+ echo " \"\$COMET_BASH\" \"\$COMET_STATE\" set $CHANGE build_mode <subagent-driven-development|executing-plans>" >&2
318
362
  return 1
319
363
  ;;
320
364
  esac
@@ -336,7 +380,48 @@ build_mode_allowed_for_workflow() {
336
380
  return 0
337
381
  fi
338
382
  echo "build_mode=direct is only allowed for hotfix/tweak unless direct_override: true is recorded" >&2
339
- echo "Next: switch build_mode to executing-plans or subagent-driven-development, or stop and ask the user for an explicit direct override." >&2
383
+ echo "Next: choose executing-plans or subagent-driven-development, or stop and ask the user for an explicit direct override." >&2
384
+ return 1
385
+ ;;
386
+ esac
387
+ }
388
+
389
+ subagent_dispatch_confirmed() {
390
+ local build_mode subagent_dispatch
391
+ build_mode=$(yaml_field_value "build_mode" 2>/dev/null || true)
392
+ subagent_dispatch=$(yaml_field_value "subagent_dispatch" 2>/dev/null || true)
393
+
394
+ if [ "$build_mode" != "subagent-driven-development" ]; then
395
+ return 0
396
+ fi
397
+
398
+ if [ "$subagent_dispatch" = "confirmed" ]; then
399
+ return 0
400
+ fi
401
+
402
+ echo "subagent_dispatch must be confirmed before using build_mode=subagent-driven-development" >&2
403
+ echo "Next: confirm the current platform has a real background subagent/Task/multi-agent dispatcher, then run:" >&2
404
+ echo " \"\$COMET_BASH\" \"\$COMET_STATE\" set $CHANGE subagent_dispatch confirmed" >&2
405
+ echo "Or ask the user to switch to executing-plans and run:" >&2
406
+ echo " \"\$COMET_BASH\" \"\$COMET_STATE\" set $CHANGE build_mode executing-plans" >&2
407
+ return 1
408
+ }
409
+
410
+ tdd_mode_selected() {
411
+ local workflow tdd_mode
412
+ workflow=$(yaml_field_value "workflow" 2>/dev/null || true)
413
+ tdd_mode=$(yaml_field_value "tdd_mode" 2>/dev/null || true)
414
+
415
+ case "$workflow" in
416
+ hotfix|tweak) return 0 ;;
417
+ esac
418
+
419
+ case "$tdd_mode" in
420
+ tdd|direct) return 0 ;;
421
+ *)
422
+ echo "tdd_mode must be tdd or direct for full workflow, got '${tdd_mode:-null}'" >&2
423
+ echo "Next: ask the user to choose TDD enforcement level, then run:" >&2
424
+ echo " \"\$COMET_BASH\" \"\$COMET_STATE\" set $CHANGE tdd_mode <tdd|direct>" >&2
340
425
  return 1
341
426
  ;;
342
427
  esac
@@ -367,7 +452,7 @@ design_handoff_context_valid() {
367
452
 
368
453
  if [ -z "$context" ] || [ "$context" = "null" ]; then
369
454
  echo "handoff_context is missing from .comet.yaml" >&2
370
- echo "Next: run bash \"\$COMET_HANDOFF\" $CHANGE design --write before invoking Superpowers." >&2
455
+ echo "Next: run \"\$COMET_BASH\" \"\$COMET_HANDOFF\" $CHANGE design --write before invoking Superpowers." >&2
371
456
  return 1
372
457
  fi
373
458
  if [ ! -s "$context" ]; then
@@ -414,7 +499,7 @@ design_handoff_markdown_traceable() {
414
499
  echo "handoff markdown is missing Generated-by marker" >&2
415
500
  missing=1
416
501
  }
417
- grep -Eq '^- Mode: (compact|full)$' "$markdown" || {
502
+ grep -Eq '^- Mode: (compact|full|beta)$' "$markdown" || {
418
503
  echo "handoff markdown is missing Mode marker" >&2
419
504
  missing=1
420
505
  }
@@ -433,6 +518,47 @@ design_handoff_markdown_traceable() {
433
518
  [ "$missing" -eq 0 ]
434
519
  }
435
520
 
521
+ context_compression_mode() {
522
+ local mode
523
+ mode=$(yaml_field_value "context_compression" 2>/dev/null || true)
524
+ printf '%s\n' "${mode:-off}"
525
+ }
526
+
527
+ beta_spec_json_structurally_valid() {
528
+ local context missing=0
529
+ if [ "$(context_compression_mode)" != "beta" ]; then
530
+ return 0
531
+ fi
532
+
533
+ context=$(yaml_field_value "handoff_context" 2>/dev/null || true)
534
+ if [ -z "$context" ] || [ "$context" = "null" ]; then
535
+ echo "handoff_context is missing from .comet.yaml" >&2
536
+ return 1
537
+ fi
538
+ if [ ! -s "$context" ]; then
539
+ echo "spec-context.json is missing or empty: $context" >&2
540
+ return 1
541
+ fi
542
+
543
+ # Validate required JSON fields
544
+ grep -q '"change"' "$context" || { echo "spec-context.json missing 'change' field" >&2; return 1; }
545
+ grep -q '"phase"' "$context" || { echo "spec-context.json missing 'phase' field" >&2; return 1; }
546
+ grep -q '"mode": "beta"' "$context" || { echo "spec-context.json mode is not beta" >&2; return 1; }
547
+ grep -q '"files"' "$context" || { echo "spec-context.json missing 'files' field" >&2; return 1; }
548
+ grep -q '"context_hash"' "$context" || { echo "spec-context.json missing 'context_hash' field" >&2; return 1; }
549
+
550
+ # Verify all source files are referenced in the JSON
551
+ handoff_source_files | while IFS= read -r file; do
552
+ [ -f "$file" ] || continue
553
+ if ! grep -qF "$file" "$context"; then
554
+ echo "spec-context.json missing source file reference: $file" >&2
555
+ exit 2
556
+ fi
557
+ done || missing=1
558
+
559
+ [ "$missing" -eq 0 ]
560
+ }
561
+
436
562
  design_doc_frontmatter_has() {
437
563
  local design_doc="$1"
438
564
  local field="$2"
@@ -492,32 +618,55 @@ guard_open() {
492
618
  guard_design() {
493
619
  echo "=== Guard: design → build ===" >&2
494
620
 
495
- local design_doc
621
+ local design_doc workflow
496
622
  design_doc=$(yaml_field_value "design_doc" 2>/dev/null || true)
623
+ workflow=$(yaml_field_value "workflow" 2>/dev/null || true)
497
624
 
498
625
  check "proposal.md exists" file_nonempty "$CHANGE_DIR/proposal.md"
499
626
  check "design.md exists" file_nonempty "$CHANGE_DIR/design.md"
500
627
  check "tasks.md exists" file_nonempty "$CHANGE_DIR/tasks.md"
501
628
  check "design handoff context exists" design_handoff_context_valid
502
629
  check "design handoff markdown is traceable" design_handoff_markdown_traceable
630
+ if [ "$(context_compression_mode)" = "beta" ]; then
631
+ check "beta spec-context.json is structurally valid" beta_spec_json_structurally_valid
632
+ fi
633
+
634
+ if [ "$workflow" = "full" ]; then
635
+ # Full workflow: design_doc is REQUIRED
636
+ check "design_doc is recorded for full workflow" design_doc_recorded
637
+ fi
503
638
 
504
639
  if [ -n "$design_doc" ] && [ "$design_doc" != "null" ]; then
505
640
  check "Design Doc ($design_doc) exists" file_nonempty "$design_doc"
506
641
  check "Design Doc frontmatter links current change" design_doc_links_current_change
507
642
  check "Design Doc declares technical design role" design_doc_declares_technical_role
508
643
  check "Design Doc declares OpenSpec as canonical spec" design_doc_declares_canonical_spec
509
- else
510
- warn " [WARN] No design_doc recorded in .comet.yaml"
644
+ elif [ "$workflow" != "full" ]; then
645
+ warn " [WARN] No design_doc recorded in .comet.yaml (optional for hotfix/tweak)"
511
646
  fi
512
647
  }
513
648
 
649
+ design_doc_recorded() {
650
+ local design_doc
651
+ design_doc=$(yaml_field_value "design_doc" 2>/dev/null || true)
652
+ if [ -n "$design_doc" ] && [ "$design_doc" != "null" ] && [ -f "$design_doc" ]; then
653
+ return 0
654
+ fi
655
+ echo "design_doc must point to an existing Superpowers Design Doc for full workflow before leaving design." >&2
656
+ echo "Next: create the Design Doc and run: \"\$COMET_BASH\" \"\$COMET_STATE\" set $CHANGE design_doc <path>" >&2
657
+ return 1
658
+ }
659
+
514
660
  guard_build() {
515
661
  echo "=== Guard: build → verify ===" >&2
516
662
 
517
663
  check "isolation selected" isolation_selected
518
664
  check "build_mode selected" build_mode_selected
519
665
  check "build_mode allowed for workflow" build_mode_allowed_for_workflow
666
+ check "subagent dispatch confirmed" subagent_dispatch_confirmed
667
+ check "tdd_mode selected" tdd_mode_selected
520
668
  check "tasks.md all tasks checked" tasks_all_done
669
+ check "Superpowers plan all tasks checked" plan_tasks_all_done
521
670
  check "proposal.md exists" file_nonempty "$CHANGE_DIR/proposal.md"
522
671
  check "Build passes" build_passes
523
672
  }
@@ -536,6 +685,7 @@ guard_archive() {
536
685
 
537
686
  check "archived is true" archived_is_true
538
687
  check "proposal.md exists" file_nonempty "$CHANGE_DIR/proposal.md"
688
+ check "design.md exists" file_nonempty "$CHANGE_DIR/design.md"
539
689
  check "tasks.md all tasks checked" tasks_all_done
540
690
  }
541
691
 
@@ -545,10 +695,10 @@ apply_state_update() {
545
695
 
546
696
  if [ -f "$state_sh" ]; then
547
697
  case "$p" in
548
- open) bash "$state_sh" transition "$CHANGE" open-complete ;;
549
- design) bash "$state_sh" transition "$CHANGE" design-complete ;;
550
- build) bash "$state_sh" transition "$CHANGE" build-complete ;;
551
- verify) bash "$state_sh" transition "$CHANGE" verify-pass ;;
698
+ open) "$COMET_BASH" "$state_sh" transition "$CHANGE" open-complete ;;
699
+ design) "$COMET_BASH" "$state_sh" transition "$CHANGE" design-complete ;;
700
+ build) "$COMET_BASH" "$state_sh" transition "$CHANGE" build-complete ;;
701
+ verify) "$COMET_BASH" "$state_sh" transition "$CHANGE" verify-pass ;;
552
702
  esac
553
703
  else
554
704
  red "FATAL: comet-state.sh not found; cannot apply state transition"
@@ -558,6 +708,14 @@ apply_state_update() {
558
708
 
559
709
  # --- Main ---
560
710
 
711
+ if [ "${COMET_GUARD_SOURCE_ONLY:-0}" = "1" ]; then
712
+ return 0 2>/dev/null
713
+ # shellcheck disable=SC2317 # unreachable if sourced; fallback for direct execution
714
+ red "ERROR: COMET_GUARD_SOURCE_ONLY=1 is only for sourcing, not direct execution" >&2
715
+ # shellcheck disable=SC2317
716
+ exit 1
717
+ fi
718
+
561
719
  case "$PHASE" in
562
720
  open) preflight ; guard_open ;;
563
721
  design) preflight ; guard_design ;;
@@ -1,11 +1,15 @@
1
1
  #!/bin/bash
2
2
  # Comet Handoff — creates machine-owned context packages between phases
3
3
  # Usage: comet-handoff.sh <change-name> design --write [--full]
4
+ # comet-handoff.sh <change-name> --hash-only
4
5
 
5
6
  set -euo pipefail
6
7
 
8
+ COMET_BASH="${COMET_BASH:-${BASH:-bash}}"
9
+
7
10
  red() { echo -e "\033[31m$1\033[0m" >&2; }
8
11
  green() { echo -e "\033[32m$1\033[0m" >&2; }
12
+ warn() { echo -e "\033[33m$1\033[0m" >&2; }
9
13
 
10
14
  validate_change_name() {
11
15
  local name="$1"
@@ -101,12 +105,14 @@ source_files() {
101
105
  }
102
106
 
103
107
  compute_context_hash() {
104
- source_files | while IFS= read -r file; do
108
+ local hash_input
109
+ hash_input=$(source_files | while IFS= read -r file; do
105
110
  if [ -f "$file" ]; then
106
111
  printf 'path:%s\n' "$file"
107
112
  printf 'sha256:%s\n' "$(hash_file "$file")"
108
113
  fi
109
- done | hash_stream
114
+ done)
115
+ printf '%s' "$hash_input" | hash_stream
110
116
  }
111
117
 
112
118
  json_escape() {
@@ -194,6 +200,86 @@ write_json_context() {
194
200
  } > "$output"
195
201
  }
196
202
 
203
+ write_spec_projection_for_file() {
204
+ local file="$1"
205
+ echo "## $file"
206
+ echo ""
207
+ echo "- Source: $file"
208
+ echo "- Lines: 1-$(file_line_count "$file")"
209
+ echo "- SHA256: $(hash_file "$file")"
210
+ echo ""
211
+ echo '```md'
212
+ cat "$file"
213
+ echo '```'
214
+ echo ""
215
+ }
216
+
217
+ write_spec_markdown_context() {
218
+ local output="$1"
219
+ {
220
+ echo "# Comet Spec Context"
221
+ echo ""
222
+ echo "- Change: $CHANGE"
223
+ echo "- Phase: design"
224
+ echo "- Mode: beta"
225
+ echo "- Context hash: $CONTEXT_HASH"
226
+ echo ""
227
+ echo "Generated-by: comet-handoff.sh"
228
+ echo ""
229
+ echo "OpenSpec remains the canonical capability spec. This beta context pack verbatim-projects spec files and references supporting artifacts by hash, not an agent-authored summary."
230
+ echo ""
231
+ echo "## Source References"
232
+ echo ""
233
+ source_files | while IFS= read -r file; do
234
+ [ -f "$file" ] || continue
235
+ echo "- Source: $file"
236
+ echo "- SHA256: $(hash_file "$file")"
237
+ done
238
+ echo ""
239
+ echo "## Acceptance Projection"
240
+ echo ""
241
+ if [ -d "$CHANGE_DIR/specs" ]; then
242
+ find "$CHANGE_DIR/specs" -path '*/spec.md' -type f 2>/dev/null | sort | while IFS= read -r file; do
243
+ write_spec_projection_for_file "$file"
244
+ done
245
+ else
246
+ echo "No delta spec files found."
247
+ echo ""
248
+ fi
249
+ echo "Full source files remain canonical. If a required heading or scenario is missing here, regenerate the handoff or read the source spec directly. Supporting files (proposal, design, tasks) are referenced by hash only."
250
+ } > "$output"
251
+ }
252
+
253
+ write_spec_json_context() {
254
+ local output="$1"
255
+ {
256
+ echo "{"
257
+ echo " \"change\": \"$(json_escape "$CHANGE")\","
258
+ echo " \"phase\": \"design\","
259
+ echo " \"mode\": \"beta\","
260
+ echo " \"canonical_spec\": \"openspec\","
261
+ echo " \"generated_by\": \"comet-handoff.sh\","
262
+ echo " \"context_hash\": \"$CONTEXT_HASH\","
263
+ echo " \"files\": ["
264
+ local first_file=1
265
+ while IFS= read -r file; do
266
+ [ -f "$file" ] || continue
267
+ local role="supporting"
268
+ case "$file" in
269
+ */specs/*/spec.md) role="spec" ;;
270
+ esac
271
+ if [ "$first_file" -eq 0 ]; then
272
+ echo ","
273
+ fi
274
+ first_file=0
275
+ printf ' { "path": "%s", "sha256": "%s", "role": "%s" }' "$(json_escape "$file")" "$(hash_file "$file")" "$role"
276
+ done < <(source_files)
277
+ echo ""
278
+ echo " ]"
279
+ echo "}"
280
+ } > "$output"
281
+ }
282
+
197
283
  CHANGE="${1:-}"
198
284
  PHASE="${2:-}"
199
285
  MODE="${3:-}"
@@ -201,6 +287,25 @@ FULL_FLAG="${4:-}"
201
287
 
202
288
  validate_change_name "$CHANGE"
203
289
 
290
+ # --hash-only: compute and output context hash without generating handoff files
291
+ if [ "${PHASE:-}" = "--hash-only" ]; then
292
+ CHANGE_DIR="openspec/changes/$CHANGE"
293
+ if [ ! -d "$CHANGE_DIR" ]; then
294
+ red "ERROR: change directory not found: $CHANGE_DIR"
295
+ exit 1
296
+ fi
297
+ for required in proposal.md design.md tasks.md; do
298
+ if [ ! -s "$CHANGE_DIR/$required" ]; then
299
+ red "ERROR: required file missing or empty: $CHANGE_DIR/$required"
300
+ exit 1
301
+ fi
302
+ done
303
+ CONTEXT_HASH="$(compute_context_hash)"
304
+ printf '%s
305
+ ' "$CONTEXT_HASH"
306
+ exit 0
307
+ fi
308
+
204
309
  if [ "$PHASE" != "design" ] || [ "$MODE" != "--write" ]; then
205
310
  red "Usage: comet-handoff.sh <change-name> design --write [--full]"
206
311
  exit 1
@@ -240,17 +345,41 @@ for required in proposal.md design.md tasks.md; do
240
345
  done
241
346
 
242
347
  HANDOFF_DIR="$CHANGE_DIR/.comet/handoff"
243
- CONTEXT_JSON="$HANDOFF_DIR/design-context.json"
244
- CONTEXT_MD="$HANDOFF_DIR/design-context.md"
348
+ CONTEXT_COMPRESSION="$(yaml_field_value context_compression 2>/dev/null || true)"
349
+ CONTEXT_COMPRESSION="${CONTEXT_COMPRESSION:-off}"
350
+ case "$CONTEXT_COMPRESSION" in
351
+ off)
352
+ CONTEXT_JSON="$HANDOFF_DIR/design-context.json"
353
+ CONTEXT_MD="$HANDOFF_DIR/design-context.md"
354
+ ;;
355
+ beta)
356
+ if [ "$HANDOFF_MODE" = "full" ]; then
357
+ warn "[HANDOFF] --full is ignored in beta mode; spec files are projected verbatim"
358
+ fi
359
+ HANDOFF_MODE="beta"
360
+ CONTEXT_JSON="$HANDOFF_DIR/spec-context.json"
361
+ CONTEXT_MD="$HANDOFF_DIR/spec-context.md"
362
+ ;;
363
+ *)
364
+ red "ERROR: invalid context_compression: $CONTEXT_COMPRESSION"
365
+ red "Valid values: off, beta"
366
+ exit 1
367
+ ;;
368
+ esac
245
369
  mkdir -p "$HANDOFF_DIR"
246
370
 
247
371
  CONTEXT_HASH="$(compute_context_hash)"
248
- write_markdown_context "$CONTEXT_MD"
249
- write_json_context "$CONTEXT_JSON"
372
+ if [ "$CONTEXT_COMPRESSION" = "beta" ]; then
373
+ write_spec_markdown_context "$CONTEXT_MD"
374
+ write_spec_json_context "$CONTEXT_JSON"
375
+ else
376
+ write_markdown_context "$CONTEXT_MD"
377
+ write_json_context "$CONTEXT_JSON"
378
+ fi
250
379
 
251
380
  if [ -x "$STATE_SH" ] || [ -f "$STATE_SH" ]; then
252
- bash "$STATE_SH" set "$CHANGE" handoff_context "$CONTEXT_JSON" >/dev/null
253
- bash "$STATE_SH" set "$CHANGE" handoff_hash "$CONTEXT_HASH" >/dev/null
381
+ "$COMET_BASH" "$STATE_SH" set "$CHANGE" handoff_context "$CONTEXT_JSON" >/dev/null
382
+ "$COMET_BASH" "$STATE_SH" set "$CHANGE" handoff_hash "$CONTEXT_HASH" >/dev/null
254
383
  else
255
384
  red "ERROR: comet-state.sh not found; cannot record handoff fields"
256
385
  exit 1