@aipper/aiws-spec 0.0.28 → 0.0.30

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 (138) hide show
  1. package/docs/cli-interface.md +10 -12
  2. package/docs/opencode-autonomous-swarm.md +178 -0
  3. package/docs/opencode-omo-adapter.md +123 -4
  4. package/docs/opencode-omo-validation-checklist.md +47 -0
  5. package/docs/opencode-subagent-first.md +187 -0
  6. package/docs/workflow-delegation-context-injection.md +217 -0
  7. package/docs/workflow-delegation-contracts.json +108 -2
  8. package/docs/workflow-delegation-contracts.md +13 -0
  9. package/docs/workflow-delegation-contracts.schema.json +152 -0
  10. package/docs/workflow-governance-rules.json +47 -6
  11. package/docs/workflow-governance-rules.md +8 -7
  12. package/docs/workflow-governance-rules.schema.json +39 -1
  13. package/docs/workflow-review-gates.json +6 -0
  14. package/docs/workflow-review-gates.md +14 -5
  15. package/docs/workflow-review-gates.schema.json +4 -0
  16. package/docs/workflow-router-rules.json +72 -8
  17. package/docs/workflow-router-rules.md +21 -14
  18. package/docs/workflow-router-rules.schema.json +4 -0
  19. package/docs/workflow-stage-contracts.json +24 -5
  20. package/docs/workflow-stage-contracts.md +16 -15
  21. package/docs/workflow-stage-contracts.schema.json +12 -1
  22. package/package.json +1 -1
  23. package/templates/workspace/.agents/skills/using-aiws/SKILL.md +16 -10
  24. package/templates/workspace/.agents/skills/ws-commit/SKILL.md +6 -118
  25. package/templates/workspace/.agents/skills/ws-deliver/SKILL.md +6 -218
  26. package/templates/workspace/.agents/skills/ws-dev/SKILL.md +82 -141
  27. package/templates/workspace/.agents/skills/ws-finish/SKILL.md +9 -204
  28. package/templates/workspace/.agents/skills/ws-handoff/SKILL.md +10 -44
  29. package/templates/workspace/.agents/skills/ws-migrate/SKILL.md +6 -42
  30. package/templates/workspace/.agents/skills/ws-plan-verify/SKILL.md +6 -58
  31. package/templates/workspace/.agents/skills/ws-pull/SKILL.md +8 -109
  32. package/templates/workspace/.agents/skills/ws-push/SKILL.md +8 -100
  33. package/templates/workspace/.agents/skills/ws-review/SKILL.md +6 -1
  34. package/templates/workspace/.agents/skills/ws-submodule-setup/SKILL.md +10 -57
  35. package/templates/workspace/.agents/skills/ws-verify-before-complete/SKILL.md +12 -53
  36. package/templates/workspace/{changes → .aiws/changes}/README.md +20 -23
  37. package/templates/workspace/{changes → .aiws/changes}/templates/proposal.md +12 -12
  38. package/templates/workspace/{changes → .aiws/changes}/templates/tasks.md +8 -8
  39. package/templates/workspace/{issues → .aiws/issues}/problem-issues.csv +1 -1
  40. package/templates/workspace/.aiws/journal/.gitkeep +1 -0
  41. package/templates/workspace/{requirements → .aiws/requirements}/requirements-issues.csv +1 -1
  42. package/templates/workspace/.aiws/tmp/.gitkeep +0 -0
  43. package/templates/workspace/{tools → .aiws/tools}/requirements_contract.py +2 -2
  44. package/templates/workspace/{tools → .aiws/tools}/requirements_contract_sync.py +2 -2
  45. package/templates/workspace/{tools → .aiws/tools}/requirements_flow_gen.py +1 -1
  46. package/templates/workspace/{tools → .aiws/tools}/server_test_runner.py +10 -10
  47. package/templates/workspace/{tools → .aiws/tools}/ws_change_check.py +23 -23
  48. package/templates/workspace/{tools → .aiws/tools}/ws_resolve_sub_target.sh +2 -2
  49. package/templates/workspace/{tools → .aiws/tools}/ws_tasks_plan.py +3 -3
  50. package/templates/workspace/.claude/commands/ws-review.md +5 -1
  51. package/templates/workspace/.claude/settings.json.example +26 -0
  52. package/templates/workspace/.claude/skills/ws-commit/SKILL.md +6 -118
  53. package/templates/workspace/.claude/skills/ws-deliver/SKILL.md +6 -218
  54. package/templates/workspace/.claude/skills/ws-dev/SKILL.md +52 -141
  55. package/templates/workspace/.claude/skills/ws-finish/SKILL.md +9 -204
  56. package/templates/workspace/.claude/skills/ws-handoff/SKILL.md +10 -44
  57. package/templates/workspace/.claude/skills/ws-migrate/SKILL.md +6 -42
  58. package/templates/workspace/.claude/skills/ws-plan-verify/SKILL.md +6 -49
  59. package/templates/workspace/.claude/skills/ws-pull/SKILL.md +8 -109
  60. package/templates/workspace/.claude/skills/ws-push/SKILL.md +8 -100
  61. package/templates/workspace/.claude/skills/ws-review/SKILL.md +6 -1
  62. package/templates/workspace/.claude/skills/ws-submodule-setup/SKILL.md +10 -57
  63. package/templates/workspace/.claude/skills/ws-verify-before-complete/SKILL.md +12 -53
  64. package/templates/workspace/.githooks/pre-commit +13 -0
  65. package/templates/workspace/.githooks/pre-push +7 -0
  66. package/templates/workspace/.opencode/command/ws-auto.md +33 -0
  67. package/templates/workspace/.opencode/command/ws-autonomy.md +25 -0
  68. package/templates/workspace/.opencode/command/ws-review.md +5 -1
  69. package/templates/workspace/.opencode/commands/ws-auto.md +33 -0
  70. package/templates/workspace/.opencode/commands/ws-autonomy.md +25 -0
  71. package/templates/workspace/.opencode/commands/ws-commit.md +4 -56
  72. package/templates/workspace/.opencode/commands/ws-deliver.md +10 -50
  73. package/templates/workspace/.opencode/commands/ws-finish.md +11 -3
  74. package/templates/workspace/.opencode/commands/ws-handoff.md +9 -17
  75. package/templates/workspace/.opencode/commands/ws-migrate.md +10 -17
  76. package/templates/workspace/.opencode/commands/ws-plan-verify.md +5 -15
  77. package/templates/workspace/.opencode/commands/ws-pull.md +6 -75
  78. package/templates/workspace/.opencode/commands/ws-push.md +7 -82
  79. package/templates/workspace/.opencode/commands/ws-review.md +5 -1
  80. package/templates/workspace/.opencode/commands/ws-submodule-setup.md +8 -47
  81. package/templates/workspace/.opencode/commands/ws-verify-before-complete.md +10 -19
  82. package/templates/workspace/.opencode/helpers/approval-whitelist-check.sh +148 -0
  83. package/templates/workspace/.opencode/helpers/approval-whitelist-run.sh +82 -0
  84. package/templates/workspace/.opencode/helpers/approval-whitelist-watchdog.sh +144 -0
  85. package/templates/workspace/.opencode/helpers/tmux-swarm-rescue.sh +56 -0
  86. package/templates/workspace/.opencode/helpers/tmux-swarm-scan.sh +46 -0
  87. package/templates/workspace/.opencode/lib/aiws-context.js +953 -0
  88. package/templates/workspace/.opencode/oh-my-opencode.json.example +64 -4
  89. package/templates/workspace/.opencode/plugins/aiws-inject-context.js +201 -0
  90. package/templates/workspace/.opencode/plugins/aiws-session-start.js +86 -0
  91. package/templates/workspace/.opencode/plugins/aiws-workflow-state.js +116 -0
  92. package/templates/workspace/.opencode/scripts/ws-plan-gen-submodule-targets.sh +27 -0
  93. package/templates/workspace/.opencode/scripts/ws-plan-setup-worktree.sh +31 -0
  94. package/templates/workspace/.opencode/skills/using-aiws/SKILL.md +130 -77
  95. package/templates/workspace/.opencode/skills/ws-analyze/SKILL.md +1 -1
  96. package/templates/workspace/.opencode/skills/ws-auto/SKILL.md +46 -0
  97. package/templates/workspace/.opencode/skills/ws-autonomy/SKILL.md +62 -0
  98. package/templates/workspace/.opencode/skills/ws-bugfix/SKILL.md +1 -1
  99. package/templates/workspace/.opencode/skills/ws-commit/SKILL.md +6 -118
  100. package/templates/workspace/.opencode/skills/ws-delegate/SKILL.md +115 -40
  101. package/templates/workspace/.opencode/skills/ws-deliver/SKILL.md +6 -218
  102. package/templates/workspace/.opencode/skills/ws-dev/SKILL.md +105 -146
  103. package/templates/workspace/.opencode/skills/ws-dev-lite/SKILL.md +23 -7
  104. package/templates/workspace/.opencode/skills/ws-finish/SKILL.md +9 -204
  105. package/templates/workspace/.opencode/skills/ws-frontend-design/SKILL.md +1 -1
  106. package/templates/workspace/.opencode/skills/ws-handoff/SKILL.md +10 -44
  107. package/templates/workspace/.opencode/skills/ws-intake/SKILL.md +43 -4
  108. package/templates/workspace/.opencode/skills/ws-migrate/SKILL.md +6 -42
  109. package/templates/workspace/.opencode/skills/ws-plan/SKILL.md +15 -55
  110. package/templates/workspace/.opencode/skills/ws-plan-verify/SKILL.md +6 -49
  111. package/templates/workspace/.opencode/skills/ws-preflight/SKILL.md +1 -1
  112. package/templates/workspace/.opencode/skills/ws-pull/SKILL.md +8 -109
  113. package/templates/workspace/.opencode/skills/ws-push/SKILL.md +8 -100
  114. package/templates/workspace/.opencode/skills/ws-quality-review/SKILL.md +7 -2
  115. package/templates/workspace/.opencode/skills/ws-req-change/SKILL.md +1 -1
  116. package/templates/workspace/.opencode/skills/ws-req-contract-sync/SKILL.md +1 -1
  117. package/templates/workspace/.opencode/skills/ws-req-contract-validate/SKILL.md +1 -1
  118. package/templates/workspace/.opencode/skills/ws-req-flow-sync/SKILL.md +1 -1
  119. package/templates/workspace/.opencode/skills/ws-req-review/SKILL.md +1 -1
  120. package/templates/workspace/.opencode/skills/ws-review/SKILL.md +35 -4
  121. package/templates/workspace/.opencode/skills/ws-rule/SKILL.md +1 -1
  122. package/templates/workspace/.opencode/skills/ws-spec-review/SKILL.md +1 -1
  123. package/templates/workspace/.opencode/skills/ws-submodule-setup/SKILL.md +10 -57
  124. package/templates/workspace/.opencode/skills/ws-verify-before-complete/SKILL.md +45 -55
  125. package/templates/workspace/AGENTS.md +32 -84
  126. package/templates/workspace/AI_PROJECT.md +5 -5
  127. package/templates/workspace/AI_WORKSPACE.md +1 -1
  128. package/templates/workspace/REQUIREMENTS.md +1 -1
  129. package/templates/workspace/gitignore +1 -1
  130. package/templates/workspace/manifest.json +215 -395
  131. /package/templates/workspace/{changes → .aiws/changes}/templates/design.md +0 -0
  132. /package/templates/workspace/{memory-bank → .aiws/memory-bank}/README.md +0 -0
  133. /package/templates/workspace/{memory-bank → .aiws/memory-bank}/architecture.md +0 -0
  134. /package/templates/workspace/{memory-bank → .aiws/memory-bank}/implementation-plan.md +0 -0
  135. /package/templates/workspace/{memory-bank → .aiws/memory-bank}/progress.md +0 -0
  136. /package/templates/workspace/{memory-bank → .aiws/memory-bank}/tech-stack.md +0 -0
  137. /package/templates/workspace/{requirements → .aiws/requirements}/CHANGELOG.md +0 -0
  138. /package/templates/workspace/{secrets → .aiws/secrets}/test-accounts.example.json +0 -0
@@ -4,84 +4,15 @@ description: 拉取:fast-forward 拉取并对齐 submodules(尽量避免 det
4
4
  <!-- AIWS_MANAGED_BEGIN:opencode:ws-pull -->
5
5
  # ws pull
6
6
 
7
- 用中文输出(命令/路径/代码标识符保持原样不翻译)。
7
+ Thin CLI wrapper. Delegates to `aiws pull`.
8
8
 
9
- 目标:安全拉取 superproject,并对齐 submodules,尽量把 submodule 从 detached HEAD “挂回”分支(仅当该分支包含 gitlink commit),减少人为操作差异。
10
-
11
- 安全约束(强制):
12
- - 不执行破坏性命令;不自动提交/不自动 push
13
- - 若工作区不干净:停止并要求用户先 commit 或 stash
14
- - `git pull` 默认只允许 fast-forward(避免隐式 merge/rebase)
15
-
16
- 步骤(建议):
17
- 1) 输出上下文并检查工作区干净:
18
- ```bash
19
- git rev-parse --show-toplevel
20
- git branch --show-current
21
- git status --porcelain
22
- ```
23
- 若 `git status --porcelain` 非空:停止。
24
-
25
- 2) 拉取 superproject(fast-forward only):
26
9
  ```bash
27
- git pull --ff-only
28
- ```
29
- 若失败:停止并说明需要用户明确选择 `git pull --rebase` 或手动处理。
30
-
31
- 3) 更新 submodules 到 superproject 记录的 gitlink commit:
32
- ```bash
33
- if [[ -f .gitmodules ]]; then
34
- git submodule sync --recursive
35
- git submodule update --init --recursive
10
+ if [[ -x "./node_modules/.bin/aiws" ]]; then
11
+ ./node_modules/.bin/aiws pull
12
+ elif command -v aiws >/dev/null 2>&1; then
13
+ aiws pull
36
14
  else
37
- echo "[info] no .gitmodules"
38
- fi
39
- ```
40
-
41
- 4) (可选但推荐)把 submodule 从 detached HEAD 尽量“挂回分支”(不改动 gitlink commit):
42
- 为避免 origin 多分支时“猜错分支”,本步骤只在 `.gitmodules` 明确配置了 `submodule.<name>.branch` 时才执行;否则保持 detached 并提示先运行 `/ws-submodule-setup` 对齐配置。
43
- ```bash
44
- if [[ -f .gitmodules ]]; then
45
- base_branch="$(git branch --show-current)"
46
- git config --file .gitmodules --get-regexp '^submodule\\..*\\.path$' 2>/dev/null \
47
- | while read -r key sub_path; do
48
- [[ -z "${sub_path:-}" ]] && continue
49
- name="${key#submodule.}"; name="${name%.path}"
50
- echo "== submodule: ${sub_path} (${name}) =="
51
- sub_sha="$(git rev-parse "HEAD:${sub_path}" 2>/dev/null || true)"
52
- [[ -z "${sub_sha:-}" ]] && { echo "[warn] no gitlink sha"; continue; }
53
-
54
- cfg_branch="$(git config --file .gitmodules --get "submodule.${name}.branch" 2>/dev/null || true)"
55
- if [[ "${cfg_branch:-}" == "." ]]; then cfg_branch="$base_branch"; fi
56
- if [[ -z "${cfg_branch:-}" ]]; then
57
- echo "[warn] ${sub_path}: missing .gitmodules submodule.${name}.branch; keep detached (run ws-submodule-setup)"
58
- continue
59
- fi
60
- target_branch="${cfg_branch}"
61
- pin_branch="aiws/pin/${target_branch}"
62
-
63
- git -C "${sub_path}" fetch origin --prune || true
64
- if git -C "${sub_path}" show-ref --verify --quiet "refs/remotes/origin/${target_branch}"; then
65
- if git -C "${sub_path}" merge-base --is-ancestor "${sub_sha}" "origin/${target_branch}"; then
66
- git -C "${sub_path}" checkout -B "${pin_branch}" "${sub_sha}"
67
- git -C "${sub_path}" branch --set-upstream-to "origin/${target_branch}" "${pin_branch}" >/dev/null 2>&1 || true
68
- echo "[ok] attached ${sub_path} to ${pin_branch} (upstream=origin/${target_branch}) at ${sub_sha}"
69
- else
70
- echo "[warn] ${sub_path}: ${sub_sha} is not in origin/${target_branch}; keep detached"
71
- fi
72
- else
73
- echo "[warn] ${sub_path}: origin/${target_branch} not found; keep detached"
74
- fi
75
- done
15
+ npx @aipper/aiws pull
76
16
  fi
77
17
  ```
78
-
79
- 可选(方案 2,一次性设置 submodule 跟踪分支,会改 `.gitmodules`,需提交):
80
- ```bash
81
- git submodule set-branch --branch main <sub_path>
82
- git add .gitmodules
83
- git commit -m "chore(submodule): set tracking branch"
84
- ```
85
18
  <!-- AIWS_MANAGED_END:opencode:ws-pull -->
86
-
87
- 可在下方追加本项目对 OpenCode 的额外说明(托管块外内容会被保留)。
@@ -4,90 +4,15 @@ description: 推送:submodule 感知(先 submodules 后 superproject;fast-
4
4
  <!-- AIWS_MANAGED_BEGIN:opencode:ws-push -->
5
5
  # ws push
6
6
 
7
- 用中文输出(命令/路径/代码标识符保持原样不翻译)。
7
+ Thin CLI wrapper. Delegates to `aiws push`.
8
8
 
9
- 目标:安全 push 当前仓库;若仓库包含 submodules,则先 push submodules,再 push superproject(默认 fast-forward;不 force)。
10
-
11
- 强制约束:
12
- - 不自动提交、不自动 `git add -A`
13
- - 不使用 `--force` / `--force-with-lease`
14
- - 若工作区不干净:停止并要求先 commit 或 stash
15
-
16
- 步骤(建议):
17
- 1) 输出上下文并检查工作区干净:
18
- ```bash
19
- git branch --show-current
20
- git status --porcelain
21
- git status -sb
22
- ```
23
- 若 `git status --porcelain` 非空:停止。
24
-
25
- 2) 判断是否存在 submodules:
26
- ```bash
27
- if [[ -f .gitmodules ]]; then
28
- git config --file .gitmodules --get-regexp '^submodule\\..*\\.path$' || true
29
- fi
30
- ```
31
-
32
- 3) 若没有 submodules:正常 push(仍需用户确认远端/分支):
33
- ```bash
34
- git remote -v
35
- git push
36
- ```
37
-
38
- 4) 若有 submodules:先检查 `.gitmodules` 的 `submodule.<name>.branch` 是否齐全(缺失则停止并提示 `/ws-submodule-setup`):
39
9
  ```bash
40
- missing=0
41
- while read -r key sub_path; do
42
- name="${key#submodule.}"; name="${name%.path}"
43
- b="$(git config --file .gitmodules --get "submodule.${name}.branch" 2>/dev/null || true)"
44
- if [[ -z "${b:-}" ]]; then
45
- echo "error: missing .gitmodules submodule.${name}.branch (path=${sub_path})"
46
- missing=1
47
- fi
48
- done < <(git config --file .gitmodules --get-regexp '^submodule\\..*\\.path$' 2>/dev/null || true)
49
- if [[ "$missing" -ne 0 ]]; then
50
- echo "hint: run /ws-submodule-setup (and commit .gitmodules), then retry"
51
- exit 2
10
+ if [[ -x "./node_modules/.bin/aiws" ]]; then
11
+ ./node_modules/.bin/aiws push
12
+ elif command -v aiws >/dev/null 2>&1; then
13
+ aiws push
14
+ else
15
+ npx @aipper/aiws push
52
16
  fi
53
17
  ```
54
-
55
- 5) 逐个 push submodules(fast-forward only),再 push superproject:
56
- ```bash
57
- base_branch="$(git branch --show-current)"
58
-
59
- git config --file .gitmodules --get-regexp '^submodule\\..*\\.path$' 2>/dev/null \
60
- | while read -r key sub_path; do
61
- name="${key#submodule.}"; name="${name%.path}"
62
- echo "== submodule: ${sub_path} (${name}) =="
63
-
64
- if [[ -n "$(git -C "${sub_path}" status --porcelain 2>/dev/null || true)" ]]; then
65
- echo "error: submodule dirty: ${sub_path}"
66
- exit 2
67
- fi
68
-
69
- cfg_branch="$(git config --file .gitmodules --get "submodule.${name}.branch" 2>/dev/null || true)"
70
- if [[ "${cfg_branch:-}" == "." ]]; then cfg_branch="$base_branch"; fi
71
- target_branch="${cfg_branch}"
72
-
73
- git -C "${sub_path}" fetch origin --prune
74
- if ! git -C "${sub_path}" show-ref --verify --quiet "refs/remotes/origin/${target_branch}"; then
75
- echo "error: missing origin/${target_branch} for ${sub_path}"
76
- exit 2
77
- fi
78
-
79
- if ! git -C "${sub_path}" merge-base --is-ancestor "origin/${target_branch}" HEAD; then
80
- echo "error: non-fast-forward (submodule=${sub_path}, branch=${target_branch})"
81
- exit 2
82
- fi
83
-
84
- git -C "${sub_path}" push origin "HEAD:refs/heads/${target_branch}"
85
- done
86
-
87
- git remote -v
88
- git push
89
- ```
90
18
  <!-- AIWS_MANAGED_END:opencode:ws-push -->
91
-
92
- 可在下方追加本项目对 OpenCode 的额外说明(托管块外内容会被保留)。
93
-
@@ -19,7 +19,11 @@ description: 评审:提交前审计改动并落盘证据
19
19
  3) 将审计落盘到(目录不存在则创建):
20
20
  - 默认:`changes/<change-id>/review/codex-review.md`
21
21
  - 回退:`.agentdocs/tmp/review/codex-review.md`(仅在无法确定 `change-id` 时使用)
22
- 4) 回复中输出:
22
+ 4) 若当前任务已进入“准备提交/交付/finish”的语境,继续补齐 dual review gate:
23
+ - 运行/收敛 `/ws-spec-review`,落盘 `changes/<change-id>/review/spec-review.md`
24
+ - 运行/收敛 `/ws-quality-review`,落盘 `changes/<change-id>/review/quality-review.md`
25
+ - 不要把单个 `codex-review.md` 误当成 finish gate 已完成
26
+ 5) 回复中输出:
23
27
  - `证据(Evidence):` 证据文件路径
24
28
  - `主要风险(Top risks):` 3–8 条(高→低)
25
29
  - `下一步(Next):` 最小修复清单 + 最小验证命令
@@ -4,54 +4,15 @@ description: 子模块分支对齐(写入 .gitmodules 的 submodule.<name>.bra
4
4
  <!-- AIWS_MANAGED_BEGIN:opencode:ws-submodule-setup -->
5
5
  # ws submodule setup
6
6
 
7
- 用中文输出(命令/路径/代码标识符保持原样不翻译)。
7
+ Thin CLI wrapper. Delegates to `aiws submodule-setup`.
8
8
 
9
- 目标:为每个 submodule 写入 `.gitmodules` 的 `submodule.<name>.branch`(团队真值),让 `/ws-pull`、`/ws-finish` 能确定性地减少 detached 与人为差异;并使 `aiws validate` 的 submodule 分支门禁通过。
10
-
11
- 约束:
12
- - 不自动提交、不自动 push(必须先输出 diff 并让用户确认)
13
- - 不做破坏性命令
14
-
15
- 步骤(建议):
16
- 1) 确认工作区干净:
17
- ```bash
18
- git status --porcelain
19
- ```
20
- 非空则停止。
21
-
22
- 2) 列出 submodules:
23
9
  ```bash
24
- test -f .gitmodules || { echo "no .gitmodules"; exit 0; }
25
- git config --file .gitmodules --get-regexp '^submodule\\..*\\.path$'
26
- ```
27
-
28
- 3) 对每个 submodule 让用户选择目标分支(推荐具体分支名;可选 `.` 表示跟随 superproject 分支名):
29
- ```bash
30
- while read -r key sub_path; do
31
- name="${key#submodule.}"; name="${name%.path}"
32
- echo "== submodule: ${name} path=${sub_path} =="
33
- echo "[current] branch=$(git config --file .gitmodules --get submodule.${name}.branch || true)"
34
- echo "[origin] HEAD=$(git -C \"${sub_path}\" symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null || true)"
35
- done < <(git config --file .gitmodules --get-regexp '^submodule\\..*\\.path$')
36
- ```
37
-
38
- 4) 写入分支配置:
39
- ```bash
40
- git submodule set-branch --branch <branch-or-dot> <sub_path>
41
- ```
42
-
43
- 5) 输出 diff 并让用户确认是否提交:
44
- ```bash
45
- git diff -- .gitmodules
46
- git status --porcelain
47
- ```
48
-
49
- 6) 用户确认后提交:
50
- ```bash
51
- git add .gitmodules
52
- git commit -m "chore(submodule): set tracking branches"
10
+ if [[ -x "./node_modules/.bin/aiws" ]]; then
11
+ ./node_modules/.bin/aiws submodule-setup
12
+ elif command -v aiws >/dev/null 2>&1; then
13
+ aiws submodule-setup
14
+ else
15
+ npx @aipper/aiws submodule-setup
16
+ fi
53
17
  ```
54
18
  <!-- AIWS_MANAGED_END:opencode:ws-submodule-setup -->
55
-
56
- 可在下方追加本项目对 OpenCode 的额外说明(托管块外内容会被保留)。
57
-
@@ -4,24 +4,15 @@ description: 完成前验证:finish / handoff 前检查双审查与 validate/e
4
4
  <!-- AIWS_MANAGED_BEGIN:opencode:ws-verify-before-complete -->
5
5
  # ws verify before complete
6
6
 
7
- 用中文输出(命令/路径/代码标识符保持原样不翻译)。
7
+ Thin CLI wrapper. Delegates to `aiws verify-bc`.
8
8
 
9
- 目标:在 `/ws-finish` 或 `/ws-handoff` 前检查双审查、validate stamp 与 evidence 是否齐全。
10
-
11
- 步骤(建议):
12
- 1) 检查以下最小 gate:
13
- - `changes/<change-id>/review/spec-review.md`
14
- - `changes/<change-id>/review/quality-review.md`
15
- - `.agentdocs/tmp/aiws-validate/*.json`
16
- 2) 若存在 `changes/<change-id>/evidence/`,检查 review / validate / collaboration summary 是否已收敛。
17
- 3) 将结果落盘到:
18
- - 默认:`changes/<change-id>/evidence/verify-before-complete.md`
19
- - 回退:`.agentdocs/tmp/review/verify-before-complete.md`
20
- 4) 输出:
21
- - `证据(Evidence):`
22
- - `结论(Result): pass|fail`
23
- - `缺失项(Missing):`
24
- - `下一步(Next):`
9
+ ```bash
10
+ if [[ -x "./node_modules/.bin/aiws" ]]; then
11
+ ./node_modules/.bin/aiws verify-bc
12
+ elif command -v aiws >/dev/null 2>&1; then
13
+ aiws verify-bc
14
+ else
15
+ npx @aipper/aiws verify-bc
16
+ fi
17
+ ```
25
18
  <!-- AIWS_MANAGED_END:opencode:ws-verify-before-complete -->
26
-
27
- 可在下方追加本项目对 OpenCode 的额外说明(托管块外内容会被保留)。
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ workspace_root="${1:-.}"
5
+ shift || true
6
+
7
+ command_text=""
8
+ decision_kind="unknown"
9
+ paths=()
10
+
11
+ while [[ $# -gt 0 ]]; do
12
+ case "$1" in
13
+ --command)
14
+ command_text="${2:-}"
15
+ shift 2
16
+ ;;
17
+ --kind)
18
+ decision_kind="${2:-unknown}"
19
+ shift 2
20
+ ;;
21
+ --path)
22
+ paths+=("${2:-}")
23
+ shift 2
24
+ ;;
25
+ *)
26
+ echo "error: unknown argument: $1" >&2
27
+ exit 2
28
+ ;;
29
+ esac
30
+ done
31
+
32
+ python3 - "$workspace_root" "$command_text" "$decision_kind" "${paths[@]-}" <<'PY'
33
+ import json
34
+ import os
35
+ import shlex
36
+ import sys
37
+ from datetime import datetime, timezone
38
+ from fnmatch import fnmatch
39
+ from pathlib import Path
40
+
41
+ workspace_root = Path(sys.argv[1]).resolve()
42
+ command_text = sys.argv[2]
43
+ decision_kind = sys.argv[3]
44
+ paths = sys.argv[4:]
45
+
46
+ config_path = workspace_root / ".opencode" / "oh-my-opencode.json"
47
+ out_dir = Path(os.environ.get("AIWS_OPENCODE_AUTONOMY_DIR", workspace_root / ".aiws" / "tmp" / "opencode-autonomy"))
48
+ out_dir.mkdir(parents=True, exist_ok=True)
49
+ log_file = out_dir / "approval-whitelist.log"
50
+ result_file = out_dir / "approval-whitelist-last.json"
51
+
52
+ decision = "manual"
53
+ reason = "missing_policy"
54
+ policy = None
55
+
56
+ def normalize_signature(command: str) -> str:
57
+ if not command.strip():
58
+ return ""
59
+ tokens = shlex.split(command)
60
+ if not tokens:
61
+ return ""
62
+ if tokens[0] == "git" and len(tokens) >= 2:
63
+ return f"git {tokens[1]}"
64
+ return tokens[0]
65
+
66
+ if config_path.exists():
67
+ try:
68
+ raw = json.loads(config_path.read_text(encoding="utf-8"))
69
+ except json.JSONDecodeError:
70
+ payload = {
71
+ "workspace_root": str(workspace_root),
72
+ "config_path": str(config_path),
73
+ "command": command_text,
74
+ "command_signature": normalize_signature(command_text) if command_text.strip() else "",
75
+ "kind": decision_kind,
76
+ "paths": paths,
77
+ "decision": "manual",
78
+ "reason": "invalid_config_json",
79
+ }
80
+ result_file.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
81
+ timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
82
+ with log_file.open("a", encoding="utf-8") as fh:
83
+ fh.write(f"[{timestamp}] decision=manual reason=invalid_config_json kind={decision_kind} signature={(payload['command_signature'] or '(empty)')} paths={paths}\n")
84
+ print(json.dumps(payload, ensure_ascii=False))
85
+ sys.exit(0)
86
+ policy = raw.get("aiws", {}).get("autonomy", {}).get("approval_whitelist", {})
87
+
88
+ def path_matches(target: str, patterns: list[str]) -> bool:
89
+ normalized = target.replace("\\", "/")
90
+ for pattern in patterns:
91
+ if fnmatch(normalized, pattern):
92
+ return True
93
+ if pattern.endswith("/") and fnmatch(normalized, f"{pattern}*"):
94
+ return True
95
+ return False
96
+
97
+ command_signature = normalize_signature(command_text)
98
+
99
+ payload = {
100
+ "workspace_root": str(workspace_root),
101
+ "config_path": str(config_path),
102
+ "command": command_text,
103
+ "command_signature": command_signature,
104
+ "kind": decision_kind,
105
+ "paths": paths,
106
+ "decision": decision,
107
+ "reason": reason,
108
+ }
109
+
110
+ if policy and policy.get("enabled") is True:
111
+ deny_commands = [item for item in policy.get("deny_commands", []) if isinstance(item, str) and item.strip()]
112
+ deny_paths = [item for item in policy.get("deny_paths", []) if isinstance(item, str) and item.strip()]
113
+ read_only_commands = [item for item in policy.get("read_only_commands", []) if isinstance(item, str) and item.strip()]
114
+ write_allow_paths = [item for item in policy.get("write_allow_paths", []) if isinstance(item, str) and item.strip()]
115
+ host_permission_mode = policy.get("host_permission_mode", "")
116
+
117
+ if decision_kind == "host-permission":
118
+ decision = "manual"
119
+ reason = f"host_permission_mode={host_permission_mode or 'missing'}"
120
+ elif command_signature in deny_commands:
121
+ decision = "deny"
122
+ reason = "deny_command"
123
+ elif any(path_matches(path, deny_paths) for path in paths):
124
+ decision = "deny"
125
+ reason = "deny_path"
126
+ elif decision_kind == "read" and command_signature in read_only_commands:
127
+ decision = "allow"
128
+ reason = "read_only_command"
129
+ elif decision_kind == "write" and paths and all(path_matches(path, write_allow_paths) for path in paths):
130
+ decision = "allow"
131
+ reason = "write_allow_path"
132
+ else:
133
+ decision = "manual"
134
+ reason = "policy_requires_manual_review"
135
+
136
+ payload["host_permission_mode"] = host_permission_mode
137
+ payload["policy_mode"] = policy.get("mode", "")
138
+
139
+ payload["decision"] = decision
140
+ payload["reason"] = reason
141
+ result_file.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
142
+
143
+ timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
144
+ with log_file.open("a", encoding="utf-8") as fh:
145
+ fh.write(f"[{timestamp}] decision={decision} reason={reason} kind={decision_kind} signature={command_signature or '(empty)'} paths={paths}\n")
146
+
147
+ print(json.dumps(payload, ensure_ascii=False))
148
+ PY
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ workspace_root="${1:-.}"
5
+ shift || true
6
+
7
+ helper_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
8
+ check_script="${helper_dir}/approval-whitelist-check.sh"
9
+
10
+ if [[ ! -x "${check_script}" && ! -f "${check_script}" ]]; then
11
+ echo "error: missing checker: ${check_script}" >&2
12
+ exit 2
13
+ fi
14
+
15
+ decision_json="$(bash "${check_script}" "${workspace_root}" "$@")"
16
+
17
+ python3 - "${workspace_root}" "${decision_json}" <<'PY'
18
+ import json
19
+ import os
20
+ import shlex
21
+ import subprocess
22
+ import sys
23
+ from datetime import datetime, timezone
24
+ from pathlib import Path
25
+
26
+ workspace_root = Path(sys.argv[1]).resolve()
27
+ payload = json.loads(sys.argv[2])
28
+
29
+ out_dir = Path(os.environ.get("AIWS_OPENCODE_AUTONOMY_DIR", workspace_root / ".aiws" / "tmp" / "opencode-autonomy"))
30
+ out_dir.mkdir(parents=True, exist_ok=True)
31
+ log_file = out_dir / "approval-whitelist-exec.log"
32
+ result_file = out_dir / "approval-whitelist-exec-last.json"
33
+
34
+ decision = payload.get("decision", "manual")
35
+ command_text = str(payload.get("command", ""))
36
+
37
+ def finish(result: dict, exit_code: int) -> None:
38
+ timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
39
+ result_file.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
40
+ with log_file.open("a", encoding="utf-8") as fh:
41
+ fh.write(
42
+ f"[{timestamp}] decision={result.get('decision')} executed={result.get('executed')} exit_code={result.get('exit_code')} reason={result.get('reason')} command={result.get('command_signature') or '(empty)'}\n"
43
+ )
44
+ print(json.dumps(result, ensure_ascii=False))
45
+ sys.exit(exit_code)
46
+
47
+ if decision == "deny":
48
+ finish({**payload, "executed": False, "exit_code": None, "reason": payload.get("reason", "deny_command")}, 4)
49
+
50
+ if decision != "allow":
51
+ finish({**payload, "executed": False, "exit_code": None, "reason": payload.get("reason", "manual_review_required")}, 3)
52
+
53
+ if not command_text.strip():
54
+ finish({**payload, "decision": "manual", "executed": False, "exit_code": None, "reason": "empty_command"}, 3)
55
+
56
+ unsafe_fragments = ["&&", "||", ";", "|", ">", "<", "$(", "`"]
57
+ if any(fragment in command_text for fragment in unsafe_fragments):
58
+ finish({**payload, "decision": "manual", "executed": False, "exit_code": None, "reason": "unsupported_shell_syntax"}, 3)
59
+
60
+ command_args = shlex.split(command_text)
61
+ if not command_args:
62
+ finish({**payload, "decision": "manual", "executed": False, "exit_code": None, "reason": "empty_command"}, 3)
63
+
64
+ proc = subprocess.run(
65
+ command_args,
66
+ cwd=str(workspace_root),
67
+ text=True,
68
+ capture_output=True,
69
+ check=False,
70
+ )
71
+
72
+ result = {
73
+ **payload,
74
+ "executed": True,
75
+ "exit_code": proc.returncode,
76
+ "stdout_line_count": len(proc.stdout.splitlines()),
77
+ "stderr_line_count": len(proc.stderr.splitlines()),
78
+ "reason": "executed",
79
+ }
80
+
81
+ finish(result, proc.returncode)
82
+ PY
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ workspace_root="${1:-.}"
5
+ shift || true
6
+
7
+ out_dir="${AIWS_OPENCODE_AUTONOMY_DIR:-${workspace_root}/.aiws/tmp/opencode-autonomy}"
8
+ queue_file="${out_dir}/approval-watchdog-queue.jsonl"
9
+ once=false
10
+ poll_ms=2000
11
+
12
+ while [[ $# -gt 0 ]]; do
13
+ case "$1" in
14
+ --queue)
15
+ queue_file="${2:-}"
16
+ shift 2
17
+ ;;
18
+ --once)
19
+ once=true
20
+ shift
21
+ ;;
22
+ --poll-ms)
23
+ poll_ms="${2:-2000}"
24
+ shift 2
25
+ ;;
26
+ *)
27
+ echo "error: unknown argument: $1" >&2
28
+ exit 2
29
+ ;;
30
+ esac
31
+ done
32
+
33
+ helper_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
34
+ runner_script="${helper_dir}/approval-whitelist-run.sh"
35
+
36
+ python3 - "$workspace_root" "$queue_file" "$runner_script" "$poll_ms" "$once" <<'PY'
37
+ import json
38
+ import subprocess
39
+ import sys
40
+ import time
41
+ from datetime import datetime, timezone
42
+ from pathlib import Path
43
+
44
+ workspace_root = Path(sys.argv[1]).resolve()
45
+ queue_file = Path(sys.argv[2]).resolve()
46
+ runner_script = Path(sys.argv[3]).resolve()
47
+ poll_ms = max(int(sys.argv[4]), 100)
48
+ once = sys.argv[5].lower() == "true"
49
+
50
+ out_dir = queue_file.parent
51
+ out_dir.mkdir(parents=True, exist_ok=True)
52
+ results_file = out_dir / "approval-watchdog-results.jsonl"
53
+ state_file = out_dir / "approval-watchdog-state.json"
54
+ log_file = out_dir / "approval-watchdog.log"
55
+
56
+ def now() -> str:
57
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
58
+
59
+ def load_state() -> dict:
60
+ if not state_file.exists():
61
+ return {"processed_ids": [], "last_run_at": None, "processed_count": 0}
62
+ return json.loads(state_file.read_text(encoding="utf-8"))
63
+
64
+ def save_state(state: dict) -> None:
65
+ state_file.write_text(json.dumps(state, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
66
+
67
+ def append_log(message: str) -> None:
68
+ with log_file.open("a", encoding="utf-8") as fh:
69
+ fh.write(f"[{now()}] {message}\n")
70
+
71
+ def normalize_entry(raw: dict, index: int) -> dict:
72
+ entry_id = str(raw.get("id") or f"queue-{index}")
73
+ kind = str(raw.get("kind") or "unknown")
74
+ command = str(raw.get("command") or "")
75
+ paths = raw.get("paths") if isinstance(raw.get("paths"), list) else []
76
+ paths = [str(item) for item in paths if str(item).strip()]
77
+ return {"id": entry_id, "kind": kind, "command": command, "paths": paths}
78
+
79
+ def run_entry(entry: dict) -> dict:
80
+ args = ["bash", str(runner_script), str(workspace_root), "--kind", entry["kind"], "--command", entry["command"]]
81
+ for path in entry["paths"]:
82
+ args.extend(["--path", path])
83
+ proc = subprocess.run(args, cwd=str(workspace_root), text=True, capture_output=True, check=False)
84
+ stdout = proc.stdout.strip()
85
+ payload = json.loads(stdout) if stdout else {}
86
+ result = {
87
+ "id": entry["id"],
88
+ "queued_kind": entry["kind"],
89
+ "queued_command": entry["command"],
90
+ "queued_paths": entry["paths"],
91
+ "runner_exit_code": proc.returncode,
92
+ "result": payload,
93
+ "processed_at": now(),
94
+ }
95
+ with results_file.open("a", encoding="utf-8") as fh:
96
+ fh.write(json.dumps(result, ensure_ascii=False) + "\n")
97
+ append_log(
98
+ f"id={entry['id']} exit_code={proc.returncode} decision={payload.get('decision', 'unknown')} executed={payload.get('executed', False)}"
99
+ )
100
+ return result
101
+
102
+ def process_pending() -> int:
103
+ state = load_state()
104
+ processed_ids = set(str(item) for item in state.get("processed_ids", []))
105
+ processed_now = 0
106
+ if not queue_file.exists():
107
+ state["last_run_at"] = now()
108
+ save_state(state)
109
+ append_log("queue_missing")
110
+ return processed_now
111
+ lines = queue_file.read_text(encoding="utf-8").splitlines()
112
+ for index, line in enumerate(lines, start=1):
113
+ stripped = line.strip()
114
+ if not stripped:
115
+ continue
116
+ raw = json.loads(stripped)
117
+ entry = normalize_entry(raw, index)
118
+ if entry["id"] in processed_ids:
119
+ continue
120
+ run_entry(entry)
121
+ processed_ids.add(entry["id"])
122
+ processed_now += 1
123
+ state["processed_ids"] = sorted(processed_ids)
124
+ state["processed_count"] = len(processed_ids)
125
+ state["last_run_at"] = now()
126
+ save_state(state)
127
+ append_log(f"cycle_complete processed_now={processed_now}")
128
+ return processed_now
129
+
130
+ if once:
131
+ process_pending()
132
+ print(state_file)
133
+ sys.exit(0)
134
+
135
+ append_log(f"watchdog_start poll_ms={poll_ms}")
136
+ try:
137
+ while True:
138
+ process_pending()
139
+ time.sleep(poll_ms / 1000.0)
140
+ except KeyboardInterrupt:
141
+ append_log("watchdog_stop keyboard_interrupt")
142
+ print(state_file)
143
+ sys.exit(0)
144
+ PY