@kafka0102/onespec 0.1.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.
package/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # OneSpec
2
+
3
+ > 中文说明见 [README-zh.md](README-zh.md)
4
+
5
+ OneSpec is a Codex skill package for running an OpenSpec + Superpowers workflow.
6
+
7
+ ## Install
8
+
9
+ Use one of these installation methods.
10
+
11
+ ### Via `npm install -g`
12
+
13
+ Install the CLI globally:
14
+
15
+ ```bash
16
+ npm install -g @kafka0102/onespec
17
+ ```
18
+
19
+ Then install the bundled skills into Codex:
20
+
21
+ ```bash
22
+ onespec init --scope global --yes
23
+ ```
24
+
25
+ Or install into the current project only:
26
+
27
+ ```bash
28
+ onespec init . --scope project --yes
29
+ ```
30
+
31
+ ### Via `npx`
32
+
33
+ If you publish the package to npm:
34
+
35
+ ```bash
36
+ npx @kafka0102/onespec init
37
+ ```
38
+
39
+ ### Via Repository Source
40
+
41
+ Install the skill bundle directly from this repository:
42
+
43
+ ```bash
44
+ npx skills add https://github.com/kafka0102/onespec/tree/main/assets/skills-en -a codex -y
45
+ ```
46
+
47
+ For the Chinese bundle:
48
+
49
+ ```bash
50
+ npx skills add https://github.com/kafka0102/onespec/tree/main/assets/skills -a codex -y
51
+ ```
52
+
53
+ The `skills` CLI does not parse `owner/repo/tree/<ref>/...` shorthand correctly. Use the full GitHub URL above, or the shorthand form with an explicit ref such as `kafka0102/onespec/assets/skills#main`.
54
+
55
+ ## Use
56
+
57
+ `npm install -g @kafka0102/onespec` only installs the `onespec` CLI.
58
+ `onespec init` is the step that copies the bundled skills into Codex.
59
+
60
+ After `onespec init`, restart Codex and invoke the OneSpec skills in your task.
61
+ During active work, OneSpec keeps runtime state in `openspec/changes/<change-id>/.onespec.yaml`; archive is the point where that file is cleaned up.
62
+
63
+ ## Release
64
+
65
+ To publish from GitHub Actions:
66
+
67
+ 1. Recommended: configure npm Trusted Publishing for `@kafka0102/onespec` and this GitHub repository.
68
+ 2. Fallback: if you do not use Trusted Publishing, add repository secret `NPM_TOKEN` with an npm granular access token that has publish permission for `@kafka0102/onespec` and `bypass 2FA` enabled.
69
+ 3. Update `package.json` version.
70
+ 4. Create and push a matching tag such as `v0.1.2`.
71
+
72
+ ```bash
73
+ git tag v0.1.2
74
+ git push origin main --tags
75
+ ```
76
+
77
+ The workflow at `.github/workflows/publish.yml` runs on `v*` tags, checks that the tag matches `package.json`, runs `npm test`, previews the published package, and then publishes to npm.
78
+
79
+ It now supports both modes:
80
+
81
+ 1. Trusted Publishing if no `NPM_TOKEN` secret is configured.
82
+ 2. Token-based publishing if `NPM_TOKEN` is present.
83
+
84
+ If npm returns `E403` with a message about 2FA or granular access tokens, the token is not valid for publishing under npm's current policy. Replace it with a granular token that can bypass 2FA, or switch the package to Trusted Publishing.
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: onespec
3
+ description: 当用户需要用 OpenSpec 与 Superpowers 管理完整 AI Coding 变更生命周期,或不确定应进入设计、执行还是归档阶段时使用。
4
+ ---
5
+
6
+ # OneSpec 工作流
7
+
8
+ OneSpec 是组合路由 Skill。它只负责恢复状态、判断阶段并切换到 `onespec-design`、`onespec-execute` 或 `onespec-archive`;具体阶段规则以对应子 Skill 为准。
9
+
10
+ 开始时说明:
11
+
12
+ > 我正在使用 `onespec` 工作流。
13
+
14
+ ## 恢复优先
15
+
16
+ 每次进入先检查状态,不依赖聊天历史:
17
+
18
+ ```bash
19
+ ONESPEC_ENV="${ONESPEC_ENV:-$(find . "$HOME"/.codex "$HOME"/.agents "$HOME"/.config -path '*/onespec/scripts/onespec-env.sh' -type f -print -quit 2>/dev/null)}"
20
+ . "$ONESPEC_ENV"
21
+ "$ONESPEC_BASH" "$ONESPEC_STATE" list
22
+ ```
23
+
24
+ 如果发现相关 change,运行:
25
+
26
+ ```bash
27
+ "$ONESPEC_BASH" "$ONESPEC_STATE" recover <change-id>
28
+ ```
29
+
30
+ 运行时状态文件是 `openspec/changes/<change-id>/.onespec.yaml`。handoff 摘要、哈希和 touched files 都写回这里;archive 前保留,archive 后删除。
31
+ `recover` 的输出是执行合同,不是参考信息。至少先读取 `phase`、`next_skill`、`next_gate` 与 `allowed_actions`,再决定是否继续。
32
+
33
+ ## 阶段路由
34
+
35
+ 先判断当前请求属于哪一类:
36
+
37
+ - `propose`:定义新 change、梳理范围、生成 `proposal.md`、`design.md`、`tasks.md` 与 spec delta。使用 `onespec-design`。
38
+ - `apply`:实现已批准 change、继续已有 change、生成或恢复 Superpowers plan、回填 OpenSpec 状态。使用 `onespec-execute`。
39
+ - `review-closeout`:用户评审、处理反馈、删除 worktree 或执行归档。使用 `onespec-archive`。
40
+
41
+ 如果用户意图不清,只问一个简短问题,不要同时问多个。
42
+
43
+ 默认意图映射:
44
+
45
+ - 用户说“新需求”、“设计一下”、“写 proposal / spec”、“定义 change”时,进入 `onespec-design`。
46
+ - 用户说“开始实现”、“执行这个 change”、“apply 这个 proposal / change”、“继续做这个 change”、“开始 coding / 开发”、“make plan”时,进入 `onespec-execute`。如果 proposal 尚未批准,`onespec-execute` 必须停止并转回 `onespec-design` 的批准 gate。
47
+ - 用户说“review”、“收尾”、“归档”、“archive”、“删除 worktree”时,进入 `onespec-archive`。
48
+
49
+ ## 共同约束
50
+
51
+ - OpenSpec 负责“要做什么”、formal artifacts、approval gate、spec delta 与归档语义。
52
+ - Superpowers 负责高歧义需求澄清、实现计划、TDD、分任务 review 与工程执行质量。
53
+ - 不要询问变更名称。根据任务自动生成简短短横线命名的 `change-id`,如冲突则追加数字。
54
+ - 读取最少必要上下文:`openspec/config.yaml`、`openspec/project.md`、相关 `openspec/specs/**`、项目入口文档、当前分支和工作区状态。
55
+ - 只问会改变 proposal、执行路径、分支处理或归档结果的问题。
56
+ - 当阶段规则冲突时,以当前阶段子 Skill 的停止条件为准。
57
+ - 每个阶段子 Skill 定义了强制暂停 gate(如 `onespec-execute` 的"实现完成 Gate"、`onespec-design` 的"批准 Gate")。路由进入下一阶段前,必须确认上一阶段的 gate 已完成。如果 gate 未完成就试图进入下一阶段,必须拒绝并指出缺失步骤。
58
+ - 如果 `recover` 已经给出 `next_skill`,默认先按它恢复;只有在用户当前请求明确改变阶段,且上一阶段的 gate 已完成时,才允许覆盖该恢复结果。
@@ -0,0 +1,404 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ die() {
5
+ echo "ERROR: $*" >&2
6
+ exit 1
7
+ }
8
+
9
+ valid_change() {
10
+ local change="$1"
11
+ [[ -n "$change" ]] || die "change name is required"
12
+ [[ "$change" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || die "invalid change name: $change"
13
+ [[ "$change" != *".."* ]] || die "change name must not contain '..'"
14
+ }
15
+
16
+ change_dir() {
17
+ local change="$1"
18
+ if [ -d "openspec/changes/$change" ]; then
19
+ printf 'openspec/changes/%s\n' "$change"
20
+ elif [ -d "openspec/changes/archive/$change" ]; then
21
+ printf 'openspec/changes/archive/%s\n' "$change"
22
+ else
23
+ printf 'openspec/changes/%s\n' "$change"
24
+ fi
25
+ }
26
+
27
+ state_file() {
28
+ local change="$1"
29
+ printf '%s/.onespec.yaml\n' "$(change_dir "$change")"
30
+ }
31
+
32
+ field_value() {
33
+ local file="$1"
34
+ local key="$2"
35
+ awk -F ': *' -v key="$key" '$1 == key { sub(/^[^:]+: */, ""); print; found=1; exit } END { if (!found) exit 0 }' "$file" 2>/dev/null
36
+ }
37
+
38
+ get_state_value() {
39
+ local change="$1"
40
+ local key="$2"
41
+ local file
42
+ file="$(state_file "$change")"
43
+ [ -f "$file" ] || die "state not found: $file"
44
+ field_value "$file" "$key"
45
+ }
46
+
47
+ ensure_git_repo() {
48
+ git rev-parse --show-toplevel >/dev/null 2>&1 || die "current directory is not inside a git repository"
49
+ }
50
+
51
+ git_common_dir() {
52
+ git rev-parse --git-common-dir
53
+ }
54
+
55
+ current_branch() {
56
+ local branch
57
+ branch="$(git branch --show-current 2>/dev/null || true)"
58
+ printf '%s\n' "${branch:-detached}"
59
+ }
60
+
61
+ current_workspace_path() {
62
+ pwd -P
63
+ }
64
+
65
+ canonicalize_path() {
66
+ local input="$1"
67
+ if [ -z "$input" ] || [ "$input" = "unknown" ] || [ ! -d "$input" ]; then
68
+ printf '%s\n' "$input"
69
+ return 0
70
+ fi
71
+ (
72
+ cd "$input"
73
+ pwd -P
74
+ )
75
+ }
76
+
77
+ selected_actions_csv() {
78
+ local joined=""
79
+ local item
80
+ for item in "$@"; do
81
+ if [ -n "$joined" ]; then
82
+ joined="${joined},${item}"
83
+ else
84
+ joined="$item"
85
+ fi
86
+ done
87
+ printf '%s\n' "$joined"
88
+ }
89
+
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"
97
+ }
98
+
99
+ normalize_action() {
100
+ case "$1" in
101
+ delete-worktree|drop-worktree|cleanup-worktree)
102
+ echo "delete-worktree"
103
+ ;;
104
+ archive|run-archive)
105
+ echo "archive"
106
+ ;;
107
+ "")
108
+ echo ""
109
+ ;;
110
+ *)
111
+ die "unsupported closeout action: $1"
112
+ ;;
113
+ esac
114
+ }
115
+
116
+ temporary_worktree_status() {
117
+ local change="$1"
118
+ local origin_branch origin_path origin_mode current_path current_head temporary reason
119
+
120
+ origin_branch="$(get_state_value "$change" origin_branch)"
121
+ origin_path="$(get_state_value "$change" origin_workspace_path)"
122
+ origin_mode="$(get_state_value "$change" origin_workspace_mode)"
123
+ origin_path="$(canonicalize_path "$origin_path")"
124
+ current_path="$(current_workspace_path)"
125
+ current_head="$(current_branch)"
126
+ temporary="false"
127
+ reason="none"
128
+
129
+ if [ "$origin_mode" = "worktree" ]; then
130
+ temporary="true"
131
+ reason="origin-workspace-mode"
132
+ fi
133
+ if [ -n "$origin_path" ] && [ "$origin_path" != "unknown" ] && [ "$origin_path" != "$current_path" ]; then
134
+ temporary="true"
135
+ reason="workspace-path-differs"
136
+ fi
137
+ if [ -n "$origin_branch" ] && [ "$origin_branch" != "unknown" ] && [ "$origin_branch" != "$current_head" ]; then
138
+ temporary="true"
139
+ if [ "$reason" = "none" ]; then
140
+ reason="branch-differs"
141
+ fi
142
+ fi
143
+
144
+ cat <<EOF
145
+ current_branch: $current_head
146
+ current_workspace_path: $current_path
147
+ origin_branch: $origin_branch
148
+ origin_workspace_path: $origin_path
149
+ origin_workspace_mode: $origin_mode
150
+ temporary_worktree: $temporary
151
+ temporary_worktree_reason: $reason
152
+ EOF
153
+ }
154
+
155
+ recommended_combination() {
156
+ local change="$1"
157
+ local worktree_probe temporary recommendation reason
158
+
159
+ worktree_probe="$(temporary_worktree_status "$change")"
160
+ temporary="$(printf '%s\n' "$worktree_probe" | awk -F ': ' '$1 == "temporary_worktree" { print $2 }')"
161
+
162
+ recommendation="none"
163
+ reason="review-only"
164
+
165
+ if [ "$temporary" = "true" ]; then
166
+ recommendation="delete-worktree,archive"
167
+ reason="temporary-worktree-can-be-cleaned-before-or-with-archive"
168
+ else
169
+ recommendation="archive"
170
+ reason="already-on-target-path"
171
+ fi
172
+
173
+ cat <<EOF
174
+ recommended_actions: $recommendation
175
+ recommended_reason: $reason
176
+ EOF
177
+ }
178
+
179
+ cmd_inspect() {
180
+ local change="$1"
181
+ valid_change "$change"
182
+ ensure_git_repo
183
+
184
+ temporary_worktree_status "$change"
185
+ cat <<'EOF'
186
+ cleanup_local_branch_after_merge: true
187
+ cleanup_local_worktree_after_merge: true
188
+ cleanup_remote_branch_after_merge: false
189
+ cleanup_local_branch_after_preserve: false
190
+ cleanup_local_worktree_after_preserve: false
191
+ EOF
192
+ recommended_combination "$change"
193
+ }
194
+
195
+ cmd_recommend_actions() {
196
+ local change="$1"
197
+ valid_change "$change"
198
+ ensure_git_repo
199
+ cmd_inspect "$change"
200
+ }
201
+
202
+ cmd_validate_actions() {
203
+ local change="$1"
204
+ shift
205
+ valid_change "$change"
206
+ ensure_git_repo
207
+
208
+ local -a selected=()
209
+ local action normalized
210
+ local already_selected
211
+ local has_delete_worktree="false"
212
+ local has_archive="false"
213
+ local current_head origin_branch temporary valid message
214
+
215
+ for action in "$@"; do
216
+ normalized="$(normalize_action "$action")"
217
+ [ -n "$normalized" ] || continue
218
+ already_selected="false"
219
+ for action in "${selected[@]:-}"; do
220
+ if [ "$action" = "$normalized" ]; then
221
+ already_selected="true"
222
+ break
223
+ fi
224
+ done
225
+ if [ "$already_selected" != "true" ]; then
226
+ selected+=("$normalized")
227
+ fi
228
+ done
229
+
230
+ for action in "${selected[@]}"; do
231
+ case "$action" in
232
+ delete-worktree) has_delete_worktree="true" ;;
233
+ archive) has_archive="true" ;;
234
+ esac
235
+ done
236
+
237
+ valid="true"
238
+ message="可以执行所选收尾动作。"
239
+ current_head="$(current_branch)"
240
+ origin_branch="$(get_state_value "$change" origin_branch)"
241
+ temporary="$(temporary_worktree_status "$change" | awk -F ': ' '$1 == "temporary_worktree" { print $2 }')"
242
+
243
+ if [ "$has_delete_worktree" = "true" ] && [ "$temporary" != "true" ]; then
244
+ 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="不能单独执行归档:当前代码尚未确认位于目标分支。"
254
+ else
255
+ message="允许单独执行归档:当前已在目标分支路径上。"
256
+ fi
257
+ elif [ "${#selected[@]}" -eq 0 ]; then
258
+ message="本次不删除 worktree 或归档;之后仍可再次进入收尾。"
259
+ fi
260
+
261
+ cat <<EOF
262
+ selected_actions: $(selected_actions_csv "${selected[@]}")
263
+ valid: $valid
264
+ message: $message
265
+ temporary_worktree: $temporary
266
+ current_branch: $current_head
267
+ origin_branch: $origin_branch
268
+ EOF
269
+ }
270
+
271
+ run_archive_command() {
272
+ local change="$1"
273
+ local archive_bin="${ONESPEC_ARCHIVE_BIN:-openspec}"
274
+ "$archive_bin" archive "$change" --yes
275
+ }
276
+
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")"
283
+
284
+ mkdir -p "$destination_dir"
285
+ cp "$source_file" "$destination_file"
286
+ printf '%s\n' "$destination_file"
287
+ }
288
+
289
+ delete_current_worktree() {
290
+ local current_path common_dir
291
+ current_path="$(current_workspace_path)"
292
+ common_dir="$(git_common_dir)"
293
+ git --git-dir="$common_dir" worktree remove --force "$current_path"
294
+ printf '%s\n' "$current_path"
295
+ }
296
+
297
+ cmd_run_actions() {
298
+ local change="$1"
299
+ shift
300
+ valid_change "$change"
301
+ ensure_git_repo
302
+
303
+ local validation selected valid archive_selected delete_selected preserved_state removed_worktree
304
+ validation="$(cmd_validate_actions "$change" "$@")"
305
+ selected="$(printf '%s\n' "$validation" | awk -F ': ' '$1 == "selected_actions" { print $2 }')"
306
+ valid="$(printf '%s\n' "$validation" | awk -F ': ' '$1 == "valid" { print $2 }')"
307
+
308
+ [ "$valid" = "true" ] || die "$(printf '%s\n' "$validation" | awk -F ': ' '$1 == "message" { print $2 }')"
309
+ [ -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"
315
+ fi
316
+ if printf '%s\n' "$selected" | grep -Eq '(^|,)delete-worktree($|,)'; then
317
+ delete_selected="true"
318
+ fi
319
+
320
+ preserved_state=""
321
+ removed_worktree=""
322
+
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
331
+ fi
332
+
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)"
342
+ fi
343
+
344
+ cat <<EOF
345
+ selected_actions: $selected
346
+ archive_executed: $archive_selected
347
+ worktree_deleted: $delete_selected
348
+ preserved_state_file: ${preserved_state:-none}
349
+ deleted_worktree_path: ${removed_worktree:-none}
350
+ EOF
351
+ }
352
+
353
+ cmd_cleanup_runtime() {
354
+ local change="$1"
355
+ valid_change "$change"
356
+
357
+ local file
358
+ file="$(state_file "$change")"
359
+ if [ -f "$file" ]; then
360
+ rm -f "$file"
361
+ echo "$file"
362
+ fi
363
+ }
364
+
365
+ usage() {
366
+ cat <<'EOF'
367
+ 用法:
368
+ onespec-closeout.sh inspect <change>
369
+ 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]
372
+ onespec-closeout.sh cleanup-runtime <change>
373
+ EOF
374
+ }
375
+
376
+ cmd="${1:-}"
377
+ case "$cmd" in
378
+ inspect)
379
+ [ "$#" -eq 2 ] || { usage; exit 2; }
380
+ cmd_inspect "$2"
381
+ ;;
382
+ recommend-actions)
383
+ [ "$#" -eq 2 ] || { usage; exit 2; }
384
+ cmd_recommend_actions "$2"
385
+ ;;
386
+ validate-actions)
387
+ [ "$#" -ge 2 ] || { usage; exit 2; }
388
+ shift
389
+ cmd_validate_actions "$@"
390
+ ;;
391
+ run-actions)
392
+ [ "$#" -ge 3 ] || { usage; exit 2; }
393
+ shift
394
+ cmd_run_actions "$@"
395
+ ;;
396
+ cleanup-runtime)
397
+ [ "$#" -eq 2 ] || { usage; exit 2; }
398
+ cmd_cleanup_runtime "$2"
399
+ ;;
400
+ *)
401
+ usage
402
+ exit 2
403
+ ;;
404
+ esac