xwang 0.0.1
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/assets/skills/xwang/SKILL.md +112 -0
- package/assets/skills/xwang/scripts/xwang-env.sh +48 -0
- package/assets/skills/xwang/scripts/xwang-guard.sh +530 -0
- package/assets/skills/xwang/scripts/xwang-state.sh +374 -0
- package/assets/skills/xwang-archive/SKILL.md +86 -0
- package/assets/skills/xwang-build/REFERENCE.md +94 -0
- package/assets/skills/xwang-build/SKILL.md +113 -0
- package/assets/skills/xwang-design/SKILL.md +104 -0
- package/assets/skills/xwang-hotfix/SKILL.md +102 -0
- package/assets/skills/xwang-open/SKILL.md +108 -0
- package/assets/skills/xwang-open/scripts/xwang-open-check.sh +197 -0
- package/assets/skills/xwang-tweak/SKILL.md +99 -0
- package/assets/skills/xwang-verify/SKILL.md +112 -0
- package/bin/xwang.js +3 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +30 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/commands/init.d.ts +9 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +238 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/core/detect.d.ts +6 -0
- package/dist/core/detect.d.ts.map +1 -0
- package/dist/core/detect.js +74 -0
- package/dist/core/detect.js.map +1 -0
- package/dist/core/mattpocock.d.ts +6 -0
- package/dist/core/mattpocock.d.ts.map +1 -0
- package/dist/core/mattpocock.js +82 -0
- package/dist/core/mattpocock.js.map +1 -0
- package/dist/core/openspec.d.ts +3 -0
- package/dist/core/openspec.d.ts.map +1 -0
- package/dist/core/openspec.js +178 -0
- package/dist/core/openspec.js.map +1 -0
- package/dist/core/package-manager.d.ts +8 -0
- package/dist/core/package-manager.d.ts.map +1 -0
- package/dist/core/package-manager.js +62 -0
- package/dist/core/package-manager.js.map +1 -0
- package/dist/core/platforms.d.ts +22 -0
- package/dist/core/platforms.d.ts.map +1 -0
- package/dist/core/platforms.js +23 -0
- package/dist/core/platforms.js.map +1 -0
- package/dist/core/superpowers.d.ts +5 -0
- package/dist/core/superpowers.d.ts.map +1 -0
- package/dist/core/superpowers.js +75 -0
- package/dist/core/superpowers.js.map +1 -0
- package/dist/core/types.d.ts +2 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/utils/file-system.d.ts +5 -0
- package/dist/utils/file-system.d.ts.map +1 -0
- package/dist/utils/file-system.js +41 -0
- package/dist/utils/file-system.js.map +1 -0
- package/package.json +50 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: xwang
|
|
3
|
+
description: xwang 项目主入口 skill,自动检测当前所处阶段并分发到 open、design、build、verify、archive 子流程;也支持快速路径 xwang-hotfix 与 xwang-tweak。当用户输入 /xwang 或提到启动 xwang 工作流时使用。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# xwang
|
|
7
|
+
|
|
8
|
+
xwang 工作流包含 5 个阶段:open → design → build → verify → archive。另有快速路径:
|
|
9
|
+
- `xwang-hotfix`:bug 修复,open → build → verify → archive,跳过 design
|
|
10
|
+
- `xwang-tweak`:轻量小改动,open → lightweight build → light verify → archive,跳过 design
|
|
11
|
+
|
|
12
|
+
## 快速开始
|
|
13
|
+
|
|
14
|
+
当用户输入 `/xwang` 或想要启动 xwang 工作流时:
|
|
15
|
+
|
|
16
|
+
1. **Preset 意图检测**:优先判断用户输入是否命中 `xwang-hotfix` 或 `xwang-tweak`;命中时直接调用对应快速路径,不再走普通阶段检测。
|
|
17
|
+
2. 执行阶段检测(检查未完成任务与现有 change 状态)。
|
|
18
|
+
3. 根据检测结果分发到对应子流程:
|
|
19
|
+
- 有未完成任务 → 进入 `xwang-open`
|
|
20
|
+
- 无未完成任务且无现有 change → **直接进入 `xwang-design`,无需用户确认**
|
|
21
|
+
- 有 design 产物但未完成 → 进入 `xwang-design` 继续
|
|
22
|
+
- design 已完成 → 进入 `xwang-build`
|
|
23
|
+
- build 已完成(`phase: verify`) → 进入 `xwang-verify`
|
|
24
|
+
- verify 已完成(`phase: archive`) → 进入 `xwang-archive` 执行归档(需用户确认)
|
|
25
|
+
4. 如果无法明确判断,默认进入 `xwang-open` 阶段。
|
|
26
|
+
|
|
27
|
+
## 阶段判定(待补充)
|
|
28
|
+
|
|
29
|
+
按以下优先级判断当前阶段:
|
|
30
|
+
|
|
31
|
+
1. **open 阶段 / Preset 入口**:项目存在未完成任务,或用户明确表示要先检查状态。`xwang-open` 自身也会先做 Preset 意图检测,命中 hotfix / tweak 时直接分发到对应快速路径。
|
|
32
|
+
2. **design 阶段**:
|
|
33
|
+
- 没有未完成任务,且存在已创建、未完成的 OpenSpec / xwang design 产物;或
|
|
34
|
+
- 没有未完成任务,也没有现有 change(`start-fresh`),直接为当前需求新建 change 并进入 design。
|
|
35
|
+
3. **build 阶段**:没有未完成任务,design 产物已就绪,且当前 phase 为 `build`,进入实施构建。
|
|
36
|
+
4. **verify 阶段**:build 完成后,phase 为 `verify`,进入验证阶段。
|
|
37
|
+
5. **archive 阶段**:verify 完成后,phase 为 `archive`,进入 `xwang-archive` 执行归档(需用户确认)。
|
|
38
|
+
|
|
39
|
+
具体检测信号和规则后续补充。
|
|
40
|
+
|
|
41
|
+
## 子流程分发
|
|
42
|
+
|
|
43
|
+
| 阶段 | 调用 |
|
|
44
|
+
|------|------|
|
|
45
|
+
| open | `xwang-open`(内部可分发到 `xwang-hotfix` / `xwang-tweak`) |
|
|
46
|
+
| design | `xwang-design` |
|
|
47
|
+
| build | `xwang-build` / `xwang-hotfix` / `xwang-tweak` |
|
|
48
|
+
| verify | `xwang-verify` |
|
|
49
|
+
| archive | `xwang-archive` |
|
|
50
|
+
|
|
51
|
+
快速路径:
|
|
52
|
+
- `xwang-hotfix`:bug 修复,跳过 design,build 使用 direct 模式
|
|
53
|
+
- `xwang-tweak`:轻量小改动,跳过 design,使用轻量 build 与轻量 verify
|
|
54
|
+
|
|
55
|
+
分发时向用户简要说明检测结论,然后调用对应 skill。
|
|
56
|
+
|
|
57
|
+
## 报告格式
|
|
58
|
+
|
|
59
|
+
返回简洁的状态块:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
xwang 阶段检测: <open | design | build | verify | archive>
|
|
63
|
+
|
|
64
|
+
判定依据:
|
|
65
|
+
- <依据项>: <简要描述>
|
|
66
|
+
|
|
67
|
+
分发动作:
|
|
68
|
+
- 调用 <xwang-open | xwang-design | xwang-build | xwang-verify | xwang-archive>
|
|
69
|
+
|
|
70
|
+
说明:
|
|
71
|
+
- 若阶段无法明确判断,默认进入 open 阶段。
|
|
72
|
+
- 若当前没有未完成任务且没有现有 change,直接进入 design 阶段,无需用户确认。
|
|
73
|
+
- 若用户输入命中 hotfix / tweak preset,直接调用对应快速路径。
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## 脚本
|
|
77
|
+
|
|
78
|
+
- `xwang-env.sh`(位于 `xwang/scripts/`):脚本定位器。source 后会动态设置 `XWANG_STATE`、`XWANG_OPEN_CHECK` 与 `XWANG_GUARD` 环境变量,避免在 skill 指令中硬编码 `.claude/skills/...` 路径。示例:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
XWANG_ENV="${XWANG_ENV:-$(find . "$HOME"/.*/skills "$HOME/.config" -path '*/xwang/scripts/xwang-env.sh' -type f -print -quit 2>/dev/null)}"
|
|
82
|
+
if [ -z "$XWANG_ENV" ]; then
|
|
83
|
+
echo "ERROR: xwang-env.sh not found. Ensure the xwang skill is installed." >&2
|
|
84
|
+
return 1
|
|
85
|
+
fi
|
|
86
|
+
. "$XWANG_ENV"
|
|
87
|
+
# 后续使用 "$XWANG_STATE" 调用状态脚本,使用 "$XWANG_OPEN_CHECK" 调用检测脚本,使用 "$XWANG_GUARD" 调用阶段守护脚本
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
- `xwang-open-check.sh`(位于 `xwang-open/scripts/`):未完成任务检测脚本。扫描活跃 xwang change、Git dirty worktree、TODO/FIXME/XXX 注释,输出 `READY` 或 `HAS-UNFINISHED-WORK` 报告。支持 `--text`(默认)和 `--json` 两种输出格式。
|
|
91
|
+
|
|
92
|
+
- `xwang-guard.sh`(位于 `xwang/scripts/`):阶段出口守护者。在修改 `.xwang.yaml` 的 `phase` 前运行,校验当前阶段所有退出条件是否满足;支持 `--apply` 自动调用 `xwang-state.sh` 推进到下一阶段。用法示例:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
"$XWANG_GUARD" <change-name> design --apply # design → build
|
|
96
|
+
"$XWANG_GUARD" <change-name> build --apply # build → verify
|
|
97
|
+
"$XWANG_GUARD" <change-name> verify --apply # verify → archive
|
|
98
|
+
"$XWANG_GUARD" <change-name> archive # 仅检查归档完整性
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
- `xwang-state.sh`(位于 `xwang/scripts/`):xwang 变更状态管理脚本。
|
|
102
|
+
- `init <change-name> <workflow>`:在 `openspec/changes/<change-name>/.xwang.yaml` 初始化状态文件。`full` 起始 phase 为 `design`;`quick`/`tweak` 跳过 design,起始 phase 为 `build`,并默认设置 `build_mode: direct`、`tdd_mode: direct`、`isolation: branch`。
|
|
103
|
+
- `get <change-name> <field>`:读取 `.xwang.yaml` 中指定字段的值。
|
|
104
|
+
- `set <change-name> <field> <value>`:更新 `.xwang.yaml` 中指定字段,并自动更新 `updated_at`。支持字段包括 `workflow`、`phase`(design / build / verify / archive)、`design_doc`、`plan`、`base_ref`、`isolation`、`build_mode`、`build_pause`、`tdd_mode`、`subagent_dispatch`、`direct_override`、`verify_mode`、`verify_result`、`verification_report`、`branch_status`、`archived`、`created_at`、`updated_at`。
|
|
105
|
+
- 后续可扩展 `transition` 等子命令。
|
|
106
|
+
|
|
107
|
+
## 说明
|
|
108
|
+
|
|
109
|
+
- 当前本 skill 仅负责阶段判定和分发,不直接执行具体任务。
|
|
110
|
+
- 阶段判定逻辑将在 xwang 流程模型完善后补充。
|
|
111
|
+
- 子流程分别由 `xwang-open`、`xwang-design`、`xwang-build`、`xwang-verify`、`xwang-archive` skill 实现。
|
|
112
|
+
- 状态管理脚本 `xwang-state.sh` 由本 skill 维护,后续按需扩展。
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# xwang-env — locator for xwang management scripts
|
|
3
|
+
# Usage: source this file to set XWANG_STATE and XWANG_OPEN_CHECK
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
# Resolve the directory of this script. Support both bash (BASH_SOURCE)
|
|
8
|
+
# and zsh/other shells where $0 points to the sourced file.
|
|
9
|
+
SCRIPT_SOURCE="${BASH_SOURCE[0]:-$0}"
|
|
10
|
+
XWANG_SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_SOURCE")" && pwd)"
|
|
11
|
+
|
|
12
|
+
export XWANG_STATE="${XWANG_SCRIPT_DIR}/xwang-state.sh"
|
|
13
|
+
|
|
14
|
+
if [ ! -f "$XWANG_STATE" ]; then
|
|
15
|
+
echo "ERROR: xwang-state.sh not found at $XWANG_STATE" >&2
|
|
16
|
+
return 1
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
# Locate xwang-open-check.sh dynamically without hardcoding a .claude path.
|
|
20
|
+
# Prefer the sibling relative location, then fall back to a platform-agnostic search.
|
|
21
|
+
if [ -z "${XWANG_OPEN_CHECK:-}" ]; then
|
|
22
|
+
XWANG_OPEN_CHECK_SIBLING="${XWANG_SCRIPT_DIR}/../xwang-open/scripts/xwang-open-check.sh"
|
|
23
|
+
if [ -f "$XWANG_OPEN_CHECK_SIBLING" ]; then
|
|
24
|
+
XWANG_OPEN_CHECK="$XWANG_OPEN_CHECK_SIBLING"
|
|
25
|
+
else
|
|
26
|
+
XWANG_OPEN_CHECK="$(find "$XWANG_SCRIPT_DIR" "$XWANG_SCRIPT_DIR/.." "$HOME" -path '*/xwang-open/scripts/xwang-open-check.sh' -type f -print -quit 2>/dev/null || true)"
|
|
27
|
+
fi
|
|
28
|
+
fi
|
|
29
|
+
export XWANG_OPEN_CHECK
|
|
30
|
+
|
|
31
|
+
# Locate xwang-guard.sh using the same approach.
|
|
32
|
+
if [ -z "${XWANG_GUARD:-}" ]; then
|
|
33
|
+
XWANG_GUARD_SIBLING="${XWANG_SCRIPT_DIR}/xwang-guard.sh"
|
|
34
|
+
if [ -f "$XWANG_GUARD_SIBLING" ]; then
|
|
35
|
+
XWANG_GUARD="$XWANG_GUARD_SIBLING"
|
|
36
|
+
else
|
|
37
|
+
XWANG_GUARD="$(find "$XWANG_SCRIPT_DIR" "$XWANG_SCRIPT_DIR/.." "$HOME" -path '*/xwang/scripts/xwang-guard.sh' -type f -print -quit 2>/dev/null || true)"
|
|
38
|
+
fi
|
|
39
|
+
fi
|
|
40
|
+
export XWANG_GUARD
|
|
41
|
+
|
|
42
|
+
if [ -z "$XWANG_OPEN_CHECK" ] || [ ! -f "$XWANG_OPEN_CHECK" ]; then
|
|
43
|
+
echo "WARNING: xwang-open-check.sh not found" >&2
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
if [ -z "$XWANG_GUARD" ] || [ ! -f "$XWANG_GUARD" ]; then
|
|
47
|
+
echo "WARNING: xwang-guard.sh not found" >&2
|
|
48
|
+
fi
|
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# xwang Phase Guard — validates exit conditions before phase transitions
|
|
3
|
+
# Usage: xwang-guard.sh <change-name> <phase> [--apply]
|
|
4
|
+
# Phases: design, build, verify, archive (open is not recorded in .xwang.yaml)
|
|
5
|
+
# Exit 0 = all checks pass, exit 1 = blocked (reasons printed to stderr)
|
|
6
|
+
# shellcheck disable=SC2329 # Functions called indirectly via check() dispatch
|
|
7
|
+
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
XWANG_BASH="${XWANG_BASH:-${BASH:-bash}}"
|
|
11
|
+
|
|
12
|
+
red() { echo -e "\033[31m$1\033[0m" >&2; }
|
|
13
|
+
green() { echo -e "\033[32m$1\033[0m" >&2; }
|
|
14
|
+
warn() { echo -e "\033[33m$1\033[0m" >&2; }
|
|
15
|
+
|
|
16
|
+
# --- Input validation ---
|
|
17
|
+
|
|
18
|
+
validate_change_name() {
|
|
19
|
+
local name="$1"
|
|
20
|
+
if [ -z "$name" ]; then
|
|
21
|
+
red "ERROR: Change name cannot be empty" >&2
|
|
22
|
+
exit 1
|
|
23
|
+
fi
|
|
24
|
+
if [[ ! "$name" =~ ^[a-zA-Z0-9_-]+$ ]]; then
|
|
25
|
+
red "ERROR: Invalid change name: '$name'" >&2
|
|
26
|
+
red "Valid characters: a-z, A-Z, 0-9, -, _" >&2
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
if [[ "$name" =~ \.\. ]]; then
|
|
30
|
+
red "ERROR: Change name cannot contain '..' (path traversal not allowed)" >&2
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if [ "${XWANG_GUARD_SOURCE_ONLY:-0}" = "1" ]; then
|
|
36
|
+
CHANGE="${CHANGE:-}"
|
|
37
|
+
PHASE="${PHASE:-}"
|
|
38
|
+
APPLY="${APPLY:-0}"
|
|
39
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd -P)"
|
|
40
|
+
CHANGE_DIR="${CHANGE_DIR:-}"
|
|
41
|
+
else
|
|
42
|
+
validate_change_name "$1"
|
|
43
|
+
|
|
44
|
+
CHANGE="$1"
|
|
45
|
+
PHASE="$2"
|
|
46
|
+
APPLY=0
|
|
47
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
|
48
|
+
if [[ "${3:-}" == "--apply" ]]; then
|
|
49
|
+
APPLY=1
|
|
50
|
+
fi
|
|
51
|
+
CHANGE_DIR="openspec/changes/$CHANGE"
|
|
52
|
+
if [ "$PHASE" = "archive" ] && [ ! -d "$CHANGE_DIR" ] && [ -d "openspec/changes/archive/$CHANGE" ]; then
|
|
53
|
+
CHANGE_DIR="openspec/changes/archive/$CHANGE"
|
|
54
|
+
fi
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
BLOCK=0
|
|
58
|
+
check() {
|
|
59
|
+
local desc="$1"
|
|
60
|
+
shift
|
|
61
|
+
local output
|
|
62
|
+
if output=$("$@" 2>&1); then
|
|
63
|
+
green " [PASS] $desc"
|
|
64
|
+
else
|
|
65
|
+
red " [FAIL] $desc"
|
|
66
|
+
if [ -n "$output" ]; then
|
|
67
|
+
while IFS= read -r line; do
|
|
68
|
+
red " $line"
|
|
69
|
+
done <<< "$output"
|
|
70
|
+
fi
|
|
71
|
+
BLOCK=1
|
|
72
|
+
fi
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# --- Helper functions ---
|
|
76
|
+
|
|
77
|
+
yaml_field_value() {
|
|
78
|
+
local field="$1"
|
|
79
|
+
local yaml="$CHANGE_DIR/.xwang.yaml"
|
|
80
|
+
if [ -f "$yaml" ]; then
|
|
81
|
+
local value
|
|
82
|
+
value=$(grep "^${field}:" "$yaml" 2>/dev/null | sed "s/^${field}: *//" || true)
|
|
83
|
+
value=$(strip_inline_comment "$value")
|
|
84
|
+
strip_wrapping_quotes "$value"
|
|
85
|
+
fi
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
strip_inline_comment() {
|
|
89
|
+
local value="$1"
|
|
90
|
+
printf '%s\n' "$value" | awk -v squote="'" '
|
|
91
|
+
{
|
|
92
|
+
out = ""
|
|
93
|
+
quote = ""
|
|
94
|
+
for (i = 1; i <= length($0); i++) {
|
|
95
|
+
c = substr($0, i, 1)
|
|
96
|
+
if (quote == "") {
|
|
97
|
+
if (c == "\"" || c == squote) {
|
|
98
|
+
quote = c
|
|
99
|
+
} else if (c == "#" && (i == 1 || substr($0, i - 1, 1) ~ /[[:space:]]/)) {
|
|
100
|
+
sub(/[[:space:]]+$/, "", out)
|
|
101
|
+
print out
|
|
102
|
+
next
|
|
103
|
+
}
|
|
104
|
+
} else if (c == quote) {
|
|
105
|
+
quote = ""
|
|
106
|
+
}
|
|
107
|
+
out = out c
|
|
108
|
+
}
|
|
109
|
+
print out
|
|
110
|
+
}
|
|
111
|
+
'
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
strip_wrapping_quotes() {
|
|
115
|
+
local value="$1"
|
|
116
|
+
case "$value" in
|
|
117
|
+
\"*\")
|
|
118
|
+
printf '%s\n' "${value:1:${#value}-2}"
|
|
119
|
+
;;
|
|
120
|
+
\'*\')
|
|
121
|
+
printf '%s\n' "${value:1:${#value}-2}"
|
|
122
|
+
;;
|
|
123
|
+
*)
|
|
124
|
+
printf '%s\n' "$value"
|
|
125
|
+
;;
|
|
126
|
+
esac
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
project_config_value() {
|
|
130
|
+
local field="$1"
|
|
131
|
+
local value
|
|
132
|
+
|
|
133
|
+
value=$(yaml_field_value "$field" 2>/dev/null || true)
|
|
134
|
+
if [ -n "$value" ] && [ "$value" != "null" ]; then
|
|
135
|
+
echo "$value"
|
|
136
|
+
return 0
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
for config in ".xwang.yaml" "xwang.yaml" ".xwang.yml" "xwang.yml"; do
|
|
140
|
+
if [ -f "$config" ]; then
|
|
141
|
+
value=$(grep "^${field}:" "$config" 2>/dev/null | sed "s/^${field}: *//" || true)
|
|
142
|
+
value=$(strip_inline_comment "$value")
|
|
143
|
+
value=$(strip_wrapping_quotes "$value")
|
|
144
|
+
if [ -n "$value" ] && [ "$value" != "null" ]; then
|
|
145
|
+
echo "$value"
|
|
146
|
+
return 0
|
|
147
|
+
fi
|
|
148
|
+
fi
|
|
149
|
+
done
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
file_nonempty() {
|
|
153
|
+
[ -f "$1" ] && [ -s "$1" ]
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
tasks_has_any() {
|
|
157
|
+
local tasks="$CHANGE_DIR/tasks.md"
|
|
158
|
+
[ -f "$tasks" ] && grep -q '\- \[' "$tasks"
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
tasks_all_done() {
|
|
162
|
+
local tasks="$CHANGE_DIR/tasks.md"
|
|
163
|
+
if [ ! -f "$tasks" ]; then
|
|
164
|
+
echo "tasks.md is missing at $tasks" >&2
|
|
165
|
+
echo "Next: restore or create tasks.md for this change before leaving build." >&2
|
|
166
|
+
return 1
|
|
167
|
+
fi
|
|
168
|
+
if ! grep -q '\- \[x\]' "$tasks"; then
|
|
169
|
+
echo "tasks.md has no completed tasks." >&2
|
|
170
|
+
echo "Next: complete implementation tasks and mark them with '- [x]'." >&2
|
|
171
|
+
return 1
|
|
172
|
+
fi
|
|
173
|
+
if grep -q '\- \[ \]' "$tasks"; then
|
|
174
|
+
echo "Unfinished tasks:" >&2
|
|
175
|
+
grep -n '\- \[ \]' "$tasks" >&2 || true
|
|
176
|
+
echo "Next: complete or explicitly remove unfinished tasks, then mark tasks.md with '- [x]'." >&2
|
|
177
|
+
return 1
|
|
178
|
+
fi
|
|
179
|
+
return 0
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
plan_tasks_all_done() {
|
|
183
|
+
local plan
|
|
184
|
+
plan=$(yaml_field_value "plan" 2>/dev/null || true)
|
|
185
|
+
|
|
186
|
+
if [ -z "$plan" ] || [ "$plan" = "null" ]; then
|
|
187
|
+
return 0
|
|
188
|
+
fi
|
|
189
|
+
if [ ! -f "$plan" ]; then
|
|
190
|
+
echo "plan file is missing at $plan" >&2
|
|
191
|
+
echo "Next: restore the plan file or update .xwang.yaml plan before leaving build." >&2
|
|
192
|
+
return 1
|
|
193
|
+
fi
|
|
194
|
+
if grep -q '^[[:space:]]*- \[ \]' "$plan"; then
|
|
195
|
+
echo "Unfinished plan tasks:" >&2
|
|
196
|
+
grep -n '^[[:space:]]*- \[ \]' "$plan" >&2 || true
|
|
197
|
+
echo "Next: check off corresponding completed plan tasks, then commit the plan update." >&2
|
|
198
|
+
return 1
|
|
199
|
+
fi
|
|
200
|
+
return 0
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
isolation_selected() {
|
|
204
|
+
local isolation
|
|
205
|
+
isolation=$(yaml_field_value "isolation" 2>/dev/null || true)
|
|
206
|
+
case "$isolation" in
|
|
207
|
+
branch|worktree) return 0 ;;
|
|
208
|
+
*)
|
|
209
|
+
echo "isolation must be branch or worktree, got '${isolation:-null}'" >&2
|
|
210
|
+
echo "Next: ask the user to choose branch or worktree, create the chosen isolation, then run:" >&2
|
|
211
|
+
echo " \"\$XWANG_BASH\" \"\$XWANG_STATE\" set $CHANGE isolation <branch|worktree>" >&2
|
|
212
|
+
return 1
|
|
213
|
+
;;
|
|
214
|
+
esac
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
build_mode_selected() {
|
|
218
|
+
local build_mode
|
|
219
|
+
build_mode=$(yaml_field_value "build_mode" 2>/dev/null || true)
|
|
220
|
+
case "$build_mode" in
|
|
221
|
+
subagent-driven-development|executing-plans|direct) return 0 ;;
|
|
222
|
+
*)
|
|
223
|
+
echo "build_mode must be selected before leaving build, got '${build_mode:-null}'" >&2
|
|
224
|
+
echo "Next: ask the user to choose an execution mode, then run:" >&2
|
|
225
|
+
echo " \"\$XWANG_BASH\" \"\$XWANG_STATE\" set $CHANGE build_mode <subagent-driven-development|executing-plans|direct>" >&2
|
|
226
|
+
return 1
|
|
227
|
+
;;
|
|
228
|
+
esac
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
build_mode_allowed_for_workflow() {
|
|
232
|
+
local workflow build_mode direct_override
|
|
233
|
+
workflow=$(yaml_field_value "workflow" 2>/dev/null || true)
|
|
234
|
+
build_mode=$(yaml_field_value "build_mode" 2>/dev/null || true)
|
|
235
|
+
direct_override=$(yaml_field_value "direct_override" 2>/dev/null || true)
|
|
236
|
+
|
|
237
|
+
if [ "$build_mode" != "direct" ]; then
|
|
238
|
+
return 0
|
|
239
|
+
fi
|
|
240
|
+
case "$workflow" in
|
|
241
|
+
quick|tweak) return 0 ;;
|
|
242
|
+
*)
|
|
243
|
+
if [ "$direct_override" = "true" ]; then
|
|
244
|
+
return 0
|
|
245
|
+
fi
|
|
246
|
+
echo "build_mode=direct is only allowed for quick/tweak unless direct_override: true is recorded" >&2
|
|
247
|
+
echo "Next: choose executing-plans or subagent-driven-development, or stop and ask the user for an explicit direct override." >&2
|
|
248
|
+
return 1
|
|
249
|
+
;;
|
|
250
|
+
esac
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
subagent_dispatch_confirmed() {
|
|
254
|
+
local build_mode subagent_dispatch
|
|
255
|
+
build_mode=$(yaml_field_value "build_mode" 2>/dev/null || true)
|
|
256
|
+
subagent_dispatch=$(yaml_field_value "subagent_dispatch" 2>/dev/null || true)
|
|
257
|
+
|
|
258
|
+
if [ "$build_mode" != "subagent-driven-development" ]; then
|
|
259
|
+
return 0
|
|
260
|
+
fi
|
|
261
|
+
|
|
262
|
+
if [ "$subagent_dispatch" = "confirmed" ]; then
|
|
263
|
+
return 0
|
|
264
|
+
fi
|
|
265
|
+
|
|
266
|
+
echo "subagent_dispatch must be confirmed before using build_mode=subagent-driven-development" >&2
|
|
267
|
+
echo "Next: confirm the current platform has a real background subagent/Task/multi-agent dispatcher, then run:" >&2
|
|
268
|
+
echo " \"\$XWANG_BASH\" \"\$XWANG_STATE\" set $CHANGE subagent_dispatch confirmed" >&2
|
|
269
|
+
echo "Or ask the user to switch to executing-plans and run:" >&2
|
|
270
|
+
echo " \"\$XWANG_BASH\" \"\$XWANG_STATE\" set $CHANGE build_mode executing-plans" >&2
|
|
271
|
+
return 1
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
tdd_mode_selected() {
|
|
275
|
+
local workflow tdd_mode
|
|
276
|
+
workflow=$(yaml_field_value "workflow" 2>/dev/null || true)
|
|
277
|
+
tdd_mode=$(yaml_field_value "tdd_mode" 2>/dev/null || true)
|
|
278
|
+
|
|
279
|
+
case "$workflow" in
|
|
280
|
+
quick|tweak) return 0 ;;
|
|
281
|
+
esac
|
|
282
|
+
|
|
283
|
+
case "$tdd_mode" in
|
|
284
|
+
tdd|direct) return 0 ;;
|
|
285
|
+
*)
|
|
286
|
+
echo "tdd_mode must be tdd or direct for full workflow, got '${tdd_mode:-null}'" >&2
|
|
287
|
+
echo "Next: ask the user to choose TDD enforcement level, then run:" >&2
|
|
288
|
+
echo " \"\$XWANG_BASH\" \"\$XWANG_STATE\" set $CHANGE tdd_mode <tdd|direct>" >&2
|
|
289
|
+
return 1
|
|
290
|
+
;;
|
|
291
|
+
esac
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
verification_report_exists() {
|
|
295
|
+
local report
|
|
296
|
+
report=$(yaml_field_value "verification_report" 2>/dev/null || true)
|
|
297
|
+
[ -n "$report" ] && [ "$report" != "null" ] && [ -f "$report" ]
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
branch_status_handled() {
|
|
301
|
+
local status
|
|
302
|
+
status=$(yaml_field_value "branch_status" 2>/dev/null || true)
|
|
303
|
+
[ "$status" = "handled" ]
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
design_doc_recorded() {
|
|
307
|
+
local design_doc
|
|
308
|
+
design_doc=$(yaml_field_value "design_doc" 2>/dev/null || true)
|
|
309
|
+
if [ -n "$design_doc" ] && [ "$design_doc" != "null" ] && [ -f "$design_doc" ]; then
|
|
310
|
+
return 0
|
|
311
|
+
fi
|
|
312
|
+
echo "design_doc must point to an existing PRD for full workflow before leaving design." >&2
|
|
313
|
+
echo "Next: create the PRD and run: \"\$XWANG_BASH\" \"\$XWANG_STATE\" set $CHANGE design_doc <path>" >&2
|
|
314
|
+
return 1
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
archived_is_true() {
|
|
318
|
+
local val
|
|
319
|
+
val=$(yaml_field_value "archived" 2>/dev/null || true)
|
|
320
|
+
[ "$val" = "true" ]
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
run_command_string() {
|
|
324
|
+
local command="$1"
|
|
325
|
+
if [ -z "$command" ]; then
|
|
326
|
+
red "ERROR: build/verify command is empty" >&2
|
|
327
|
+
return 1
|
|
328
|
+
fi
|
|
329
|
+
# Basic command injection guard: reject dangerous shell metacharacters
|
|
330
|
+
if [[ "$command" =~ [\;\|\&\$\`] ]]; then
|
|
331
|
+
red "ERROR: build/verify command contains shell metacharacters: $command" >&2
|
|
332
|
+
red "Allowed: alphanumeric, spaces, hyphens, underscores, dots, colons, forward slashes, quotes" >&2
|
|
333
|
+
return 1
|
|
334
|
+
fi
|
|
335
|
+
echo "+ $command" >&2
|
|
336
|
+
"$XWANG_BASH" -lc "$command"
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
is_windows_bash() {
|
|
340
|
+
case "$(uname -s 2>/dev/null || true)" in
|
|
341
|
+
MINGW*|MSYS*|CYGWIN*) return 0 ;;
|
|
342
|
+
*) return 1 ;;
|
|
343
|
+
esac
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
build_passes() {
|
|
347
|
+
if [ "${XWANG_SKIP_BUILD:-0}" = "1" ]; then
|
|
348
|
+
return 0
|
|
349
|
+
fi
|
|
350
|
+
local configured_build
|
|
351
|
+
configured_build=$(project_config_value "build_command" 2>/dev/null || true)
|
|
352
|
+
if [ -n "$configured_build" ]; then
|
|
353
|
+
run_command_string "$configured_build"
|
|
354
|
+
return $?
|
|
355
|
+
fi
|
|
356
|
+
if [ -f "package.json" ] && grep -q '"build"' "package.json"; then
|
|
357
|
+
npm run build
|
|
358
|
+
return $?
|
|
359
|
+
fi
|
|
360
|
+
if [ -f "pom.xml" ]; then
|
|
361
|
+
if [ -x "./mvnw" ]; then
|
|
362
|
+
./mvnw compile -q
|
|
363
|
+
elif is_windows_bash && command -v mvn.cmd >/dev/null 2>&1; then
|
|
364
|
+
mvn.cmd compile -q
|
|
365
|
+
else
|
|
366
|
+
mvn compile -q
|
|
367
|
+
fi
|
|
368
|
+
return $?
|
|
369
|
+
fi
|
|
370
|
+
if [ -f "Cargo.toml" ]; then
|
|
371
|
+
cargo build
|
|
372
|
+
return $?
|
|
373
|
+
fi
|
|
374
|
+
return 1
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
verification_command_passes() {
|
|
378
|
+
if [ "${XWANG_SKIP_BUILD:-0}" = "1" ]; then
|
|
379
|
+
return 0
|
|
380
|
+
fi
|
|
381
|
+
local configured_verify
|
|
382
|
+
configured_verify=$(project_config_value "verify_command" 2>/dev/null || true)
|
|
383
|
+
if [ -n "$configured_verify" ]; then
|
|
384
|
+
run_command_string "$configured_verify"
|
|
385
|
+
return $?
|
|
386
|
+
fi
|
|
387
|
+
build_passes
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
preflight() {
|
|
391
|
+
if [ ! -d "$CHANGE_DIR" ]; then
|
|
392
|
+
red "FATAL: change directory not found: $CHANGE_DIR"
|
|
393
|
+
exit 1
|
|
394
|
+
fi
|
|
395
|
+
if [ ! -f "$CHANGE_DIR/.xwang.yaml" ]; then
|
|
396
|
+
red "FATAL: .xwang.yaml not found in $CHANGE_DIR"
|
|
397
|
+
exit 1
|
|
398
|
+
fi
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
# --- Phase-specific guards ---
|
|
402
|
+
|
|
403
|
+
guard_design() {
|
|
404
|
+
echo "=== Guard: design → build ===" >&2
|
|
405
|
+
|
|
406
|
+
local workflow
|
|
407
|
+
workflow=$(yaml_field_value "workflow" 2>/dev/null || true)
|
|
408
|
+
|
|
409
|
+
check "proposal.md exists and non-empty" file_nonempty "$CHANGE_DIR/proposal.md"
|
|
410
|
+
check "design.md exists and non-empty" file_nonempty "$CHANGE_DIR/design.md"
|
|
411
|
+
check "tasks.md exists and non-empty" file_nonempty "$CHANGE_DIR/tasks.md"
|
|
412
|
+
check "tasks.md has at least one task" tasks_has_any
|
|
413
|
+
|
|
414
|
+
if [ "$workflow" = "full" ]; then
|
|
415
|
+
check "design_doc is recorded for full workflow" design_doc_recorded
|
|
416
|
+
fi
|
|
417
|
+
|
|
418
|
+
local design_doc
|
|
419
|
+
design_doc=$(yaml_field_value "design_doc" 2>/dev/null || true)
|
|
420
|
+
if [ -n "$design_doc" ] && [ "$design_doc" != "null" ]; then
|
|
421
|
+
check "PRD file ($design_doc) exists" file_nonempty "$design_doc"
|
|
422
|
+
elif [ "$workflow" != "full" ]; then
|
|
423
|
+
warn " [WARN] No design_doc recorded in .xwang.yaml (optional for quick/tweak)"
|
|
424
|
+
fi
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
guard_build() {
|
|
428
|
+
echo "=== Guard: build → verify ===" >&2
|
|
429
|
+
|
|
430
|
+
check "isolation selected" isolation_selected
|
|
431
|
+
check "build_mode selected" build_mode_selected
|
|
432
|
+
check "build_mode allowed for workflow" build_mode_allowed_for_workflow
|
|
433
|
+
check "subagent dispatch confirmed" subagent_dispatch_confirmed
|
|
434
|
+
check "tdd_mode selected" tdd_mode_selected
|
|
435
|
+
check "tasks.md all tasks checked" tasks_all_done
|
|
436
|
+
check "plan file all tasks checked" plan_tasks_all_done
|
|
437
|
+
check "proposal.md exists" file_nonempty "$CHANGE_DIR/proposal.md"
|
|
438
|
+
check "Build passes" build_passes
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
guard_verify() {
|
|
442
|
+
echo "=== Guard: verify → archive ===" >&2
|
|
443
|
+
|
|
444
|
+
check "tasks.md all tasks checked" tasks_all_done
|
|
445
|
+
check "Build/verify passes" verification_command_passes
|
|
446
|
+
check "verification_report exists" verification_report_exists
|
|
447
|
+
check "branch_status=handled" branch_status_handled
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
guard_archive() {
|
|
451
|
+
echo "=== Guard: archive completeness ===" >&2
|
|
452
|
+
|
|
453
|
+
check "archived is true" archived_is_true
|
|
454
|
+
check "proposal.md exists" file_nonempty "$CHANGE_DIR/proposal.md"
|
|
455
|
+
check "design.md exists" file_nonempty "$CHANGE_DIR/design.md"
|
|
456
|
+
check "tasks.md all tasks checked" tasks_all_done
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
locate_state_script() {
|
|
460
|
+
local state_sh="$SCRIPT_DIR/xwang-state.sh"
|
|
461
|
+
if [ -f "$state_sh" ]; then
|
|
462
|
+
echo "$state_sh"
|
|
463
|
+
return 0
|
|
464
|
+
fi
|
|
465
|
+
state_sh="$(find "$SCRIPT_DIR" "$SCRIPT_DIR/.." "$HOME" -path '*/xwang/scripts/xwang-state.sh' -type f -print -quit 2>/dev/null || true)"
|
|
466
|
+
if [ -n "$state_sh" ] && [ -f "$state_sh" ]; then
|
|
467
|
+
echo "$state_sh"
|
|
468
|
+
return 0
|
|
469
|
+
fi
|
|
470
|
+
return 1
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
apply_state_update() {
|
|
474
|
+
local state_sh
|
|
475
|
+
if ! state_sh=$(locate_state_script); then
|
|
476
|
+
red "FATAL: xwang-state.sh not found; cannot apply state transition"
|
|
477
|
+
exit 1
|
|
478
|
+
fi
|
|
479
|
+
|
|
480
|
+
local p="$1"
|
|
481
|
+
case "$p" in
|
|
482
|
+
design) "$XWANG_BASH" "$state_sh" set "$CHANGE" phase build ;;
|
|
483
|
+
build)
|
|
484
|
+
"$XWANG_BASH" "$state_sh" set "$CHANGE" phase verify
|
|
485
|
+
"$XWANG_BASH" "$state_sh" set "$CHANGE" verify_result pending
|
|
486
|
+
;;
|
|
487
|
+
verify)
|
|
488
|
+
"$XWANG_BASH" "$state_sh" set "$CHANGE" phase archive
|
|
489
|
+
"$XWANG_BASH" "$state_sh" set "$CHANGE" verify_result pass
|
|
490
|
+
;;
|
|
491
|
+
esac
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
# --- Main ---
|
|
495
|
+
|
|
496
|
+
if [ "${XWANG_GUARD_SOURCE_ONLY:-0}" = "1" ]; then
|
|
497
|
+
return 0 2>/dev/null
|
|
498
|
+
red "ERROR: XWANG_GUARD_SOURCE_ONLY=1 is only for sourcing, not direct execution" >&2
|
|
499
|
+
exit 1
|
|
500
|
+
fi
|
|
501
|
+
|
|
502
|
+
case "$PHASE" in
|
|
503
|
+
design) preflight ; guard_design ;;
|
|
504
|
+
build) preflight ; guard_build ;;
|
|
505
|
+
verify) preflight ; guard_verify ;;
|
|
506
|
+
archive) preflight ; guard_archive ;;
|
|
507
|
+
*)
|
|
508
|
+
red "Unknown phase: $PHASE"
|
|
509
|
+
echo "Valid phases: design, build, verify, archive" >&2
|
|
510
|
+
exit 1
|
|
511
|
+
;;
|
|
512
|
+
esac
|
|
513
|
+
|
|
514
|
+
if [ "$BLOCK" -eq 1 ]; then
|
|
515
|
+
echo "" >&2
|
|
516
|
+
red "BLOCKED — fix failing checks before proceeding to next phase"
|
|
517
|
+
exit 1
|
|
518
|
+
else
|
|
519
|
+
echo "" >&2
|
|
520
|
+
green "ALL CHECKS PASSED — ready for next phase"
|
|
521
|
+
if [ "$APPLY" -eq 1 ]; then
|
|
522
|
+
apply_state_update "$PHASE"
|
|
523
|
+
case "$PHASE" in
|
|
524
|
+
design) green " [APPLY] .xwang.yaml updated: phase=build" ;;
|
|
525
|
+
build) green " [APPLY] .xwang.yaml updated: phase=verify, verify_result=pending" ;;
|
|
526
|
+
verify) green " [APPLY] .xwang.yaml updated: phase=archive, verify_result=pass" ;;
|
|
527
|
+
esac
|
|
528
|
+
fi
|
|
529
|
+
exit 0
|
|
530
|
+
fi
|