@kafka0102/onespec 0.1.2 → 0.2.2

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 (29) hide show
  1. package/README.md +45 -48
  2. package/assets/skills/onespec/SKILL.md +22 -14
  3. package/assets/skills/onespec/references/archive.md +214 -0
  4. package/assets/skills/{onespec-design/SKILL.md → onespec/references/design.md} +55 -51
  5. package/assets/skills/onespec/references/execute.md +291 -0
  6. package/assets/skills/onespec/references/fast.md +110 -0
  7. package/assets/skills/onespec/scripts/onespec-closeout.sh +238 -77
  8. package/assets/skills/onespec/scripts/onespec-commit.sh +191 -11
  9. package/assets/skills/onespec/scripts/onespec-handoff.sh +19 -6
  10. package/assets/skills/onespec/scripts/onespec-state.sh +157 -18
  11. package/assets/skills/onespec-fast/SKILL.md +22 -0
  12. package/assets/skills/onespec-fast/agents/openai.yaml +4 -0
  13. package/assets/skills-en/onespec/SKILL.md +22 -13
  14. package/assets/skills-en/onespec/references/archive.md +213 -0
  15. package/assets/skills-en/{onespec-design/SKILL.md → onespec/references/design.md} +58 -43
  16. package/assets/skills-en/onespec/references/execute.md +291 -0
  17. package/assets/skills-en/onespec/references/fast.md +110 -0
  18. package/assets/skills-en/onespec-fast/SKILL.md +22 -0
  19. package/package.json +10 -3
  20. package/scripts/postinstall.js +3 -3
  21. package/src/cli.js +120 -110
  22. package/src/doctor.js +46 -20
  23. package/src/init.js +24 -10
  24. package/src/platforms.js +88 -8
  25. package/src/setup.js +211 -0
  26. package/assets/skills/onespec-archive/SKILL.md +0 -202
  27. package/assets/skills/onespec-execute/SKILL.md +0 -219
  28. package/assets/skills-en/onespec-archive/SKILL.md +0 -199
  29. package/assets/skills-en/onespec-execute/SKILL.md +0 -219
@@ -26,7 +26,9 @@ change_dir() {
26
26
 
27
27
  state_file() {
28
28
  local change="$1"
29
- printf '%s/.onespec.yaml\n' "$(change_dir "$change")"
29
+ local script_root
30
+ script_root="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd -P)"
31
+ "${BASH:-bash}" "$script_root/onespec-state.sh" path "$change"
30
32
  }
31
33
 
32
34
  field_value() {
@@ -62,6 +64,18 @@ current_workspace_path() {
62
64
  pwd -P
63
65
  }
64
66
 
67
+ state_workspace_path() {
68
+ local change="$1"
69
+ local state
70
+ state="$(state_file "$change")"
71
+ [ -f "$state" ] || die "state not found: $state"
72
+ cd "$(dirname "$state")/../../.." && pwd -P
73
+ }
74
+
75
+ script_dir() {
76
+ cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd -P
77
+ }
78
+
65
79
  canonicalize_path() {
66
80
  local input="$1"
67
81
  if [ -z "$input" ] || [ "$input" = "unknown" ] || [ ! -d "$input" ]; then
@@ -87,22 +101,20 @@ selected_actions_csv() {
87
101
  printf '%s\n' "$joined"
88
102
  }
89
103
 
90
- state_destination_in_origin() {
91
- local change="$1"
92
- local origin_path
93
- origin_path="$(canonicalize_path "$(get_state_value "$change" origin_workspace_path)")"
94
- [ -n "$origin_path" ] || die "origin workspace path is empty"
95
- [ "$origin_path" != "unknown" ] || die "origin workspace path is unknown"
96
- printf '%s/openspec/changes/%s/.onespec.yaml\n' "$origin_path" "$change"
104
+ origin_workspace_path_for_change() {
105
+ canonicalize_path "$(get_state_value "$1" origin_workspace_path)"
97
106
  }
98
107
 
99
108
  normalize_action() {
100
109
  case "$1" in
101
- delete-worktree|drop-worktree|cleanup-worktree)
102
- echo "delete-worktree"
110
+ archive-then-merge-worktree|merge-after-archive|archive-then-merge|merge-worktree|merge|accept-worktree|accept)
111
+ echo "archive-then-merge-worktree"
103
112
  ;;
104
- archive|run-archive)
105
- echo "archive"
113
+ archive-only|archive|run-archive)
114
+ echo "archive-only"
115
+ ;;
116
+ discard-worktree|discard|drop-code|delete-worktree|drop-worktree|cleanup-worktree)
117
+ echo "discard-worktree"
106
118
  ;;
107
119
  "")
108
120
  echo ""
@@ -121,8 +133,9 @@ temporary_worktree_status() {
121
133
  origin_path="$(get_state_value "$change" origin_workspace_path)"
122
134
  origin_mode="$(get_state_value "$change" origin_workspace_mode)"
123
135
  origin_path="$(canonicalize_path "$origin_path")"
124
- current_path="$(current_workspace_path)"
125
- current_head="$(current_branch)"
136
+ current_path="$(state_workspace_path "$change")"
137
+ current_head="$(git -C "$current_path" branch --show-current 2>/dev/null || true)"
138
+ current_head="${current_head:-detached}"
126
139
  temporary="false"
127
140
  reason="none"
128
141
 
@@ -163,10 +176,10 @@ recommended_combination() {
163
176
  reason="review-only"
164
177
 
165
178
  if [ "$temporary" = "true" ]; then
166
- recommendation="delete-worktree,archive"
167
- reason="temporary-worktree-can-be-cleaned-before-or-with-archive"
179
+ recommendation="archive-then-merge-worktree"
180
+ reason="temporary-worktree-targets-base-branch"
168
181
  else
169
- recommendation="archive"
182
+ recommendation="archive-only"
170
183
  reason="already-on-target-path"
171
184
  fi
172
185
 
@@ -186,6 +199,8 @@ cmd_inspect() {
186
199
  cleanup_local_branch_after_merge: true
187
200
  cleanup_local_worktree_after_merge: true
188
201
  cleanup_remote_branch_after_merge: false
202
+ cleanup_local_branch_after_discard: true
203
+ cleanup_local_worktree_after_discard: true
189
204
  cleanup_local_branch_after_preserve: false
190
205
  cleanup_local_worktree_after_preserve: false
191
206
  EOF
@@ -208,8 +223,9 @@ cmd_validate_actions() {
208
223
  local -a selected=()
209
224
  local action normalized
210
225
  local already_selected
211
- local has_delete_worktree="false"
212
- local has_archive="false"
226
+ local has_archive_then_merge="false"
227
+ local has_archive_only="false"
228
+ local has_discard_worktree="false"
213
229
  local current_head origin_branch temporary valid message
214
230
 
215
231
  for action in "$@"; do
@@ -229,8 +245,9 @@ cmd_validate_actions() {
229
245
 
230
246
  for action in "${selected[@]}"; do
231
247
  case "$action" in
232
- delete-worktree) has_delete_worktree="true" ;;
233
- archive) has_archive="true" ;;
248
+ archive-then-merge-worktree) has_archive_then_merge="true" ;;
249
+ archive-only) has_archive_only="true" ;;
250
+ discard-worktree) has_discard_worktree="true" ;;
234
251
  esac
235
252
  done
236
253
 
@@ -240,22 +257,27 @@ cmd_validate_actions() {
240
257
  origin_branch="$(get_state_value "$change" origin_branch)"
241
258
  temporary="$(temporary_worktree_status "$change" | awk -F ': ' '$1 == "temporary_worktree" { print $2 }')"
242
259
 
243
- if [ "$has_delete_worktree" = "true" ] && [ "$temporary" != "true" ]; then
260
+ if [ "${#selected[@]}" -gt 1 ]; then
261
+ valid="false"
262
+ message="当前收尾菜单一次只允许选择一个动作。"
263
+ elif [ "$has_archive_then_merge" = "true" ] && [ "$temporary" != "true" ]; then
244
264
  valid="false"
245
- message="不能删除 worktree:当前不在临时 worktree。"
246
- elif [ "$has_archive" = "true" ] && [ "$has_delete_worktree" = "true" ]; then
247
- message="允许先删除临时 worktree,再继续归档。"
248
- elif [ "$has_delete_worktree" = "true" ] && [ "$has_archive" != "true" ]; then
249
- message="允许仅删除临时 worktree;之后仍可单独执行归档。"
250
- elif [ "$has_archive" = "true" ] && [ "$has_delete_worktree" != "true" ]; then
251
- if [ "$temporary" = "true" ] || { [ "$origin_branch" != "unknown" ] && [ "$current_head" != "$origin_branch" ]; }; then
252
- valid="false"
253
- message="不能单独执行归档:当前代码尚未确认位于目标分支。"
265
+ message="不能先归档再合并:当前不在临时 worktree。"
266
+ elif [ "$has_discard_worktree" = "true" ] && [ "$temporary" != "true" ]; then
267
+ valid="false"
268
+ message="不能废弃 worktree:当前不在临时 worktree。"
269
+ elif [ "$has_archive_then_merge" = "true" ]; then
270
+ message="允许先归档当前 change,再把临时 worktree 合并到 ${origin_branch} 并删除 worktree。"
271
+ elif [ "$has_discard_worktree" = "true" ]; then
272
+ message="允许删除临时 worktree 并废弃对应本地分支代码;废弃后不归档。"
273
+ elif [ "$has_archive_only" = "true" ]; then
274
+ if [ "$temporary" != "true" ]; then
275
+ message="允许直接归档当前 change;当前不在临时 worktree,当前分支 ${current_head} 已是目标分支,无需额外合并分支,也不会删除工作区。"
254
276
  else
255
- message="允许单独执行归档:当前已在目标分支路径上。"
277
+ message="允许直接归档当前 change,不合并到 base 分支,也不自动删除当前 worktree。"
256
278
  fi
257
279
  elif [ "${#selected[@]}" -eq 0 ]; then
258
- message="本次不删除 worktree 或归档;之后仍可再次进入收尾。"
280
+ message="本次保持当前评审阶段不变;之后仍可再次进入收尾。"
259
281
  fi
260
282
 
261
283
  cat <<EOF
@@ -269,21 +291,33 @@ EOF
269
291
  }
270
292
 
271
293
  run_archive_command() {
272
- local change="$1"
294
+ local workspace="$1"
295
+ local change="$2"
273
296
  local archive_bin="${ONESPEC_ARCHIVE_BIN:-openspec}"
274
- "$archive_bin" archive "$change" --yes
297
+ (
298
+ cd "$workspace"
299
+ "$archive_bin" archive "$change" --yes
300
+ )
275
301
  }
276
302
 
277
- preserve_runtime_state_in_origin() {
278
- local change="$1"
279
- local source_file destination_file destination_dir
280
- source_file="$(state_file "$change")"
281
- destination_file="$(state_destination_in_origin "$change")"
282
- destination_dir="$(dirname "$destination_file")"
303
+ run_state_set_in_workspace() {
304
+ local workspace="$1"
305
+ local change="$2"
306
+ local key="$3"
307
+ local value="$4"
308
+ (
309
+ cd "$workspace"
310
+ "${BASH:-bash}" "$(script_dir)/onespec-state.sh" set "$change" "$key" "$value"
311
+ )
312
+ }
283
313
 
284
- mkdir -p "$destination_dir"
285
- cp "$source_file" "$destination_file"
286
- printf '%s\n' "$destination_file"
314
+ run_cleanup_runtime_in_workspace() {
315
+ local workspace="$1"
316
+ local change="$2"
317
+ (
318
+ cd "$workspace"
319
+ "${BASH:-bash}" "$(script_dir)/onespec-closeout.sh" cleanup-runtime "$change" >/dev/null
320
+ )
287
321
  }
288
322
 
289
323
  delete_current_worktree() {
@@ -294,58 +328,177 @@ delete_current_worktree() {
294
328
  printf '%s\n' "$current_path"
295
329
  }
296
330
 
331
+ merge_current_worktree_to_origin() {
332
+ local change="$1"
333
+ local cached_origin_branch="${2:-}"
334
+ local cached_origin_workspace="${3:-}"
335
+ local current_path current_head origin_branch origin_workspace origin_head
336
+
337
+ current_path="$(current_workspace_path)"
338
+ current_head="$(current_branch)"
339
+ if [ -n "$cached_origin_branch" ]; then
340
+ origin_branch="$cached_origin_branch"
341
+ else
342
+ origin_branch="$(get_state_value "$change" origin_branch)"
343
+ fi
344
+ if [ -n "$cached_origin_workspace" ]; then
345
+ origin_workspace="$cached_origin_workspace"
346
+ else
347
+ origin_workspace="$(origin_workspace_path_for_change "$change")"
348
+ fi
349
+
350
+ [ -n "$origin_workspace" ] || die "origin workspace path is empty"
351
+ [ "$origin_workspace" != "unknown" ] || die "origin workspace path is unknown"
352
+ [ "$origin_workspace" != "$current_path" ] || die "current workspace is already the origin workspace"
353
+ [ "$current_head" != "detached" ] || die "cannot merge a detached worktree"
354
+
355
+ origin_head="$(git -C "$origin_workspace" branch --show-current 2>/dev/null || true)"
356
+ [ "$origin_head" = "$origin_branch" ] || die "origin workspace is on ${origin_head:-detached}, expected $origin_branch"
357
+
358
+ if [ -n "$(git -C "$origin_workspace" status --porcelain=v1 --untracked-files=no)" ]; then
359
+ die "origin workspace has tracked uncommitted changes: $origin_workspace"
360
+ fi
361
+
362
+ git -C "$origin_workspace" merge "$current_head"
363
+
364
+ cat <<EOF
365
+ merged_branch: $current_head
366
+ merged_into: $origin_branch
367
+ origin_workspace_path: $origin_workspace
368
+ EOF
369
+ }
370
+
371
+ delete_current_worktree_branch() {
372
+ local current_head common_dir current_path parent_path
373
+ current_head="$(current_branch)"
374
+ [ "$current_head" != "detached" ] || die "cannot delete a detached worktree branch"
375
+ common_dir="$(git_common_dir)"
376
+ current_path="$(current_workspace_path)"
377
+ parent_path="$(dirname "$current_path")"
378
+ delete_current_worktree >/dev/null
379
+ git -C "$parent_path" --git-dir="$common_dir" branch -D "$current_head" >/dev/null
380
+ printf '%s\n' "$current_head"
381
+ }
382
+
383
+ delete_current_worktree_and_merged_branch() {
384
+ local current_head common_dir current_path parent_path
385
+ current_head="$(current_branch)"
386
+ [ "$current_head" != "detached" ] || die "cannot delete a detached worktree branch"
387
+ common_dir="$(git_common_dir)"
388
+ current_path="$(current_workspace_path)"
389
+ parent_path="$(dirname "$current_path")"
390
+ delete_current_worktree >/dev/null
391
+ git -C "$parent_path" --git-dir="$common_dir" branch -d "$current_head" >/dev/null
392
+ printf '%s\n' "$current_path"
393
+ }
394
+
395
+ commit_field() {
396
+ local payload="$1"
397
+ local key="$2"
398
+ printf '%s\n' "$payload" | awk -F ': ' -v key="$key" '$1 == key { sub(/^[^:]+: /, ""); print; exit }'
399
+ }
400
+
401
+ run_commit_related() {
402
+ local workspace="$1"
403
+ local change="$2"
404
+ local context="$3"
405
+ (
406
+ cd "$workspace"
407
+ "${BASH:-bash}" "$(script_dir)/onespec-commit.sh" commit-related "$change" "$context"
408
+ )
409
+ }
410
+
297
411
  cmd_run_actions() {
298
412
  local change="$1"
299
413
  shift
300
414
  valid_change "$change"
301
415
  ensure_git_repo
302
416
 
303
- local validation selected valid archive_selected delete_selected preserved_state removed_worktree
417
+ local validation selected valid archive_then_merge_selected archive_only_selected removed_worktree
418
+ local discard_selected merged_branch discarded_branch merge_result
419
+ local pre_closeout_commit post_archive_commit preserved_state_commit
420
+ local origin_branch_cached origin_workspace_cached
421
+ local action_workspace archive_workspace
304
422
  validation="$(cmd_validate_actions "$change" "$@")"
305
423
  selected="$(printf '%s\n' "$validation" | awk -F ': ' '$1 == "selected_actions" { print $2 }')"
306
424
  valid="$(printf '%s\n' "$validation" | awk -F ': ' '$1 == "valid" { print $2 }')"
307
425
 
308
426
  [ "$valid" = "true" ] || die "$(printf '%s\n' "$validation" | awk -F ': ' '$1 == "message" { print $2 }')"
309
427
  [ -n "$selected" ] || die "run-actions requires at least one closeout action"
310
-
311
- archive_selected="false"
312
- delete_selected="false"
313
- if printf '%s\n' "$selected" | grep -Eq '(^|,)archive($|,)'; then
314
- archive_selected="true"
428
+ action_workspace="$(current_workspace_path)"
429
+ archive_workspace="$action_workspace"
430
+ origin_branch_cached="$(get_state_value "$change" origin_branch)"
431
+ origin_workspace_cached="$(origin_workspace_path_for_change "$change")"
432
+
433
+ archive_then_merge_selected="false"
434
+ archive_only_selected="false"
435
+ discard_selected="false"
436
+ if printf '%s\n' "$selected" | grep -Eq '(^|,)archive-then-merge-worktree($|,)'; then
437
+ archive_then_merge_selected="true"
438
+ fi
439
+ if printf '%s\n' "$selected" | grep -Eq '(^|,)archive-only($|,)'; then
440
+ archive_only_selected="true"
315
441
  fi
316
- if printf '%s\n' "$selected" | grep -Eq '(^|,)delete-worktree($|,)'; then
317
- delete_selected="true"
442
+ if printf '%s\n' "$selected" | grep -Eq '(^|,)discard-worktree($|,)'; then
443
+ discard_selected="true"
318
444
  fi
319
445
 
320
- preserved_state=""
321
446
  removed_worktree=""
447
+ merged_branch="none"
448
+ discarded_branch="none"
449
+ merge_result=""
450
+ if [ "$discard_selected" = "true" ]; then
451
+ pre_closeout_commit='commit_created: false
452
+ commit_context: closeout
453
+ commit_sha: none
454
+ commit_message: none'
455
+ else
456
+ pre_closeout_commit="$(run_commit_related "$action_workspace" "$change" closeout)"
457
+ fi
458
+ post_archive_commit='commit_created: false
459
+ commit_context: archive
460
+ commit_sha: none
461
+ commit_message: none'
462
+ preserved_state_commit='commit_created: false
463
+ commit_context: preserve-state
464
+ commit_sha: none
465
+ commit_message: none'
466
+
467
+ if [ "$discard_selected" = "true" ]; then
468
+ discarded_branch="$(current_branch)"
469
+ removed_worktree="$(current_workspace_path)"
470
+ delete_current_worktree_branch >/dev/null
471
+ fi
322
472
 
323
- # Safe order: archive first, then delete the temporary worktree.
324
- if [ "$archive_selected" = "true" ]; then
325
- run_archive_command "$change"
326
- local script_dir
327
- script_dir="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd -P)"
328
- "${BASH:-bash}" "$script_dir/onespec-state.sh" set "$change" phase archived
329
- "${BASH:-bash}" "$script_dir/onespec-state.sh" set "$change" archive archived
330
- "${BASH:-bash}" "$script_dir/onespec-closeout.sh" cleanup-runtime "$change" >/dev/null
473
+ if [ "$archive_then_merge_selected" = "true" ] || [ "$archive_only_selected" = "true" ]; then
474
+ run_archive_command "$archive_workspace" "$change"
475
+ run_state_set_in_workspace "$archive_workspace" "$change" phase archived
476
+ run_state_set_in_workspace "$archive_workspace" "$change" archive archived
477
+ run_cleanup_runtime_in_workspace "$archive_workspace" "$change"
478
+ post_archive_commit="$(run_commit_related "$archive_workspace" "$change" archive)"
331
479
  fi
332
480
 
333
- if [ "$delete_selected" = "true" ]; then
334
- if [ "$archive_selected" != "true" ]; then
335
- local script_dir
336
- script_dir="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd -P)"
337
- "${BASH:-bash}" "$script_dir/onespec-state.sh" set "$change" phase done
338
- "${BASH:-bash}" "$script_dir/onespec-state.sh" set "$change" archive skipped
339
- preserved_state="$(preserve_runtime_state_in_origin "$change")"
340
- fi
341
- removed_worktree="$(delete_current_worktree)"
481
+ if [ "$archive_then_merge_selected" = "true" ]; then
482
+ merge_result="$(merge_current_worktree_to_origin "$change" "$origin_branch_cached" "$origin_workspace_cached")"
483
+ merged_branch="$(commit_field "$merge_result" merged_branch)"
484
+ removed_worktree="$(delete_current_worktree_and_merged_branch)"
342
485
  fi
343
486
 
344
487
  cat <<EOF
345
488
  selected_actions: $selected
346
- archive_executed: $archive_selected
347
- worktree_deleted: $delete_selected
348
- preserved_state_file: ${preserved_state:-none}
489
+ worktree_merged: $archive_then_merge_selected
490
+ merged_branch: $merged_branch
491
+ worktree_discarded: $discard_selected
492
+ discarded_branch: $discarded_branch
493
+ archive_executed: $(if [ "$archive_then_merge_selected" = "true" ] || [ "$archive_only_selected" = "true" ]; then echo "true"; else echo "false"; fi)
494
+ worktree_deleted: $(if [ "$archive_then_merge_selected" = "true" ] || [ "$discard_selected" = "true" ]; then echo "true"; else echo "false"; fi)
495
+ pre_closeout_commit_created: $(commit_field "$pre_closeout_commit" commit_created)
496
+ pre_closeout_commit_sha: $(commit_field "$pre_closeout_commit" commit_sha)
497
+ post_archive_commit_created: $(commit_field "$post_archive_commit" commit_created)
498
+ post_archive_commit_sha: $(commit_field "$post_archive_commit" commit_sha)
499
+ preserved_state_commit_created: $(commit_field "$preserved_state_commit" commit_created)
500
+ preserved_state_commit_sha: $(commit_field "$preserved_state_commit" commit_sha)
501
+ preserved_state_file: none
349
502
  deleted_worktree_path: ${removed_worktree:-none}
350
503
  EOF
351
504
  }
@@ -354,11 +507,19 @@ cmd_cleanup_runtime() {
354
507
  local change="$1"
355
508
  valid_change "$change"
356
509
 
357
- local file
510
+ local file display
358
511
  file="$(state_file "$change")"
359
512
  if [ -f "$file" ]; then
360
513
  rm -f "$file"
361
- echo "$file"
514
+ case "$file" in
515
+ */openspec/*)
516
+ display="openspec/${file##*/openspec/}"
517
+ ;;
518
+ *)
519
+ display="$file"
520
+ ;;
521
+ esac
522
+ echo "$display"
362
523
  fi
363
524
  }
364
525
 
@@ -367,8 +528,8 @@ usage() {
367
528
  用法:
368
529
  onespec-closeout.sh inspect <change>
369
530
  onespec-closeout.sh recommend-actions <change>
370
- onespec-closeout.sh validate-actions <change> [delete-worktree] [archive]
371
- onespec-closeout.sh run-actions <change> [delete-worktree] [archive]
531
+ onespec-closeout.sh validate-actions <change> [archive-then-merge-worktree] [archive-only] [discard-worktree]
532
+ onespec-closeout.sh run-actions <change> [archive-then-merge-worktree] [archive-only] [discard-worktree]
372
533
  onespec-closeout.sh cleanup-runtime <change>
373
534
  EOF
374
535
  }