@aipper/aiws-spec 0.0.28 → 0.0.29

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 (101) 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 +68 -1
  8. package/docs/workflow-delegation-contracts.md +3 -0
  9. package/docs/workflow-delegation-contracts.schema.json +95 -0
  10. package/docs/workflow-governance-rules.json +47 -6
  11. package/docs/workflow-governance-rules.md +7 -6
  12. package/docs/workflow-governance-rules.schema.json +39 -1
  13. package/docs/workflow-router-rules.json +36 -4
  14. package/docs/workflow-router-rules.md +8 -4
  15. package/docs/workflow-stage-contracts.json +2 -3
  16. package/docs/workflow-stage-contracts.md +2 -3
  17. package/package.json +1 -1
  18. package/templates/workspace/.agents/skills/using-aiws/SKILL.md +8 -6
  19. package/templates/workspace/.agents/skills/ws-commit/SKILL.md +6 -118
  20. package/templates/workspace/.agents/skills/ws-deliver/SKILL.md +6 -218
  21. package/templates/workspace/.agents/skills/ws-dev/SKILL.md +52 -141
  22. package/templates/workspace/.agents/skills/ws-finish/SKILL.md +6 -205
  23. package/templates/workspace/.agents/skills/ws-handoff/SKILL.md +10 -44
  24. package/templates/workspace/.agents/skills/ws-plan-verify/SKILL.md +6 -58
  25. package/templates/workspace/.agents/skills/ws-review/SKILL.md +6 -1
  26. package/templates/workspace/.agents/skills/ws-verify-before-complete/SKILL.md +12 -53
  27. package/templates/workspace/.claude/commands/ws-review.md +5 -1
  28. package/templates/workspace/.claude/settings.json.example +26 -0
  29. package/templates/workspace/.claude/skills/ws-commit/SKILL.md +6 -118
  30. package/templates/workspace/.claude/skills/ws-deliver/SKILL.md +6 -218
  31. package/templates/workspace/.claude/skills/ws-dev/SKILL.md +52 -141
  32. package/templates/workspace/.claude/skills/ws-finish/SKILL.md +6 -205
  33. package/templates/workspace/.claude/skills/ws-handoff/SKILL.md +10 -44
  34. package/templates/workspace/.claude/skills/ws-plan-verify/SKILL.md +6 -49
  35. package/templates/workspace/.claude/skills/ws-review/SKILL.md +6 -1
  36. package/templates/workspace/.claude/skills/ws-verify-before-complete/SKILL.md +12 -53
  37. package/templates/workspace/.opencode/command/ws-auto.md +33 -0
  38. package/templates/workspace/.opencode/command/ws-autonomy.md +25 -0
  39. package/templates/workspace/.opencode/command/ws-review.md +5 -1
  40. package/templates/workspace/.opencode/commands/ws-auto.md +33 -0
  41. package/templates/workspace/.opencode/commands/ws-autonomy.md +25 -0
  42. package/templates/workspace/.opencode/commands/ws-commit.md +4 -56
  43. package/templates/workspace/.opencode/commands/ws-deliver.md +10 -50
  44. package/templates/workspace/.opencode/commands/ws-finish.md +8 -65
  45. package/templates/workspace/.opencode/commands/ws-handoff.md +9 -17
  46. package/templates/workspace/.opencode/commands/ws-migrate.md +10 -17
  47. package/templates/workspace/.opencode/commands/ws-plan-verify.md +5 -15
  48. package/templates/workspace/.opencode/commands/ws-pull.md +6 -75
  49. package/templates/workspace/.opencode/commands/ws-push.md +7 -82
  50. package/templates/workspace/.opencode/commands/ws-review.md +5 -1
  51. package/templates/workspace/.opencode/commands/ws-submodule-setup.md +8 -47
  52. package/templates/workspace/.opencode/commands/ws-verify-before-complete.md +10 -19
  53. package/templates/workspace/.opencode/helpers/approval-whitelist-check.sh +148 -0
  54. package/templates/workspace/.opencode/helpers/approval-whitelist-run.sh +82 -0
  55. package/templates/workspace/.opencode/helpers/approval-whitelist-watchdog.sh +144 -0
  56. package/templates/workspace/.opencode/helpers/tmux-swarm-rescue.sh +56 -0
  57. package/templates/workspace/.opencode/helpers/tmux-swarm-scan.sh +46 -0
  58. package/templates/workspace/.opencode/oh-my-opencode.json.example +64 -4
  59. package/templates/workspace/.opencode/skills/using-aiws/SKILL.md +93 -77
  60. package/templates/workspace/.opencode/skills/ws-analyze/SKILL.md +1 -1
  61. package/templates/workspace/.opencode/skills/ws-auto/SKILL.md +46 -0
  62. package/templates/workspace/.opencode/skills/ws-autonomy/SKILL.md +62 -0
  63. package/templates/workspace/.opencode/skills/ws-bugfix/SKILL.md +1 -1
  64. package/templates/workspace/.opencode/skills/ws-commit/SKILL.md +6 -118
  65. package/templates/workspace/.opencode/skills/ws-delegate/SKILL.md +93 -40
  66. package/templates/workspace/.opencode/skills/ws-deliver/SKILL.md +6 -218
  67. package/templates/workspace/.opencode/skills/ws-dev/SKILL.md +53 -142
  68. package/templates/workspace/.opencode/skills/ws-dev-lite/SKILL.md +19 -6
  69. package/templates/workspace/.opencode/skills/ws-finish/SKILL.md +6 -205
  70. package/templates/workspace/.opencode/skills/ws-frontend-design/SKILL.md +1 -1
  71. package/templates/workspace/.opencode/skills/ws-handoff/SKILL.md +10 -44
  72. package/templates/workspace/.opencode/skills/ws-intake/SKILL.md +11 -2
  73. package/templates/workspace/.opencode/skills/ws-migrate/SKILL.md +6 -42
  74. package/templates/workspace/.opencode/skills/ws-plan/SKILL.md +4 -2
  75. package/templates/workspace/.opencode/skills/ws-plan-verify/SKILL.md +6 -49
  76. package/templates/workspace/.opencode/skills/ws-preflight/SKILL.md +1 -1
  77. package/templates/workspace/.opencode/skills/ws-pull/SKILL.md +8 -109
  78. package/templates/workspace/.opencode/skills/ws-push/SKILL.md +8 -100
  79. package/templates/workspace/.opencode/skills/ws-quality-review/SKILL.md +1 -1
  80. package/templates/workspace/.opencode/skills/ws-req-change/SKILL.md +1 -1
  81. package/templates/workspace/.opencode/skills/ws-req-contract-sync/SKILL.md +1 -1
  82. package/templates/workspace/.opencode/skills/ws-req-contract-validate/SKILL.md +1 -1
  83. package/templates/workspace/.opencode/skills/ws-req-flow-sync/SKILL.md +1 -1
  84. package/templates/workspace/.opencode/skills/ws-req-review/SKILL.md +1 -1
  85. package/templates/workspace/.opencode/skills/ws-review/SKILL.md +14 -3
  86. package/templates/workspace/.opencode/skills/ws-rule/SKILL.md +1 -1
  87. package/templates/workspace/.opencode/skills/ws-spec-review/SKILL.md +1 -1
  88. package/templates/workspace/.opencode/skills/ws-submodule-setup/SKILL.md +10 -57
  89. package/templates/workspace/.opencode/skills/ws-verify-before-complete/SKILL.md +12 -53
  90. package/templates/workspace/AGENTS.md +5 -2
  91. package/templates/workspace/AI_PROJECT.md +1 -1
  92. package/templates/workspace/changes/README.md +9 -12
  93. package/templates/workspace/manifest.json +265 -207
  94. package/templates/workspace/.agents/skills/ws-migrate/SKILL.md +0 -54
  95. package/templates/workspace/.agents/skills/ws-pull/SKILL.md +0 -119
  96. package/templates/workspace/.agents/skills/ws-push/SKILL.md +0 -110
  97. package/templates/workspace/.agents/skills/ws-submodule-setup/SKILL.md +0 -65
  98. package/templates/workspace/.claude/skills/ws-migrate/SKILL.md +0 -54
  99. package/templates/workspace/.claude/skills/ws-pull/SKILL.md +0 -119
  100. package/templates/workspace/.claude/skills/ws-push/SKILL.md +0 -110
  101. package/templates/workspace/.claude/skills/ws-submodule-setup/SKILL.md +0 -65
@@ -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 / ".agentdocs" / "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 / ".agentdocs" / "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}/.agentdocs/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
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ if ! command -v tmux >/dev/null 2>&1; then
5
+ echo "error: tmux not found" >&2
6
+ exit 1
7
+ fi
8
+
9
+ out_dir="${AIWS_OPENCODE_AUTONOMY_DIR:-.agentdocs/tmp/opencode-autonomy}"
10
+ scan_file="${1:-${out_dir}/tmux-scan.json}"
11
+ log_file="${out_dir}/tmux-rescue.log"
12
+ mkdir -p "${out_dir}"
13
+
14
+ if [[ ! -f "${scan_file}" ]]; then
15
+ echo "error: scan file missing: ${scan_file}" >&2
16
+ exit 1
17
+ fi
18
+
19
+ python3 - "${scan_file}" "${log_file}" <<'PY'
20
+ import json
21
+ import subprocess
22
+ import sys
23
+ from datetime import datetime, timezone
24
+
25
+ scan_file, log_file = sys.argv[1:3]
26
+
27
+ def tmux(*args):
28
+ subprocess.run(["tmux", *args], check=True)
29
+
30
+ with open(scan_file, "r", encoding="utf-8") as fh:
31
+ payload = json.load(fh)
32
+
33
+ actions = []
34
+ for pane in payload.get("panes", []):
35
+ target = str(pane.get("target", ""))
36
+ if not target:
37
+ continue
38
+ if pane.get("waiting_confirm") is True:
39
+ tmux("send-keys", "-t", target, "y", "Enter")
40
+ actions.append((target, "confirm_yes"))
41
+ continue
42
+ if pane.get("press_enter") is True:
43
+ tmux("send-keys", "-t", target, "Enter")
44
+ actions.append((target, "press_enter"))
45
+ continue
46
+ if pane.get("pane_in_mode") is True:
47
+ tmux("send-keys", "-t", target, "-X", "cancel")
48
+ actions.append((target, "cancel_copy_mode"))
49
+
50
+ timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
51
+ with open(log_file, "a", encoding="utf-8") as fh:
52
+ for target, action in actions:
53
+ fh.write(f"[{timestamp}] {target} {action}\n")
54
+
55
+ print(log_file)
56
+ PY