@aipper/aiws-spec 0.0.27 → 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 (107) 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 +63 -8
  14. package/docs/workflow-router-rules.md +15 -6
  15. package/docs/workflow-stage-contracts.json +16 -8
  16. package/docs/workflow-stage-contracts.md +7 -7
  17. package/package.json +1 -1
  18. package/templates/workspace/.agents/skills/using-aiws/SKILL.md +22 -8
  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-intake/SKILL.md +87 -0
  25. package/templates/workspace/.agents/skills/ws-plan/SKILL.md +15 -9
  26. package/templates/workspace/.agents/skills/ws-plan-verify/SKILL.md +6 -49
  27. package/templates/workspace/.agents/skills/ws-review/SKILL.md +6 -1
  28. package/templates/workspace/.agents/skills/ws-verify-before-complete/SKILL.md +12 -53
  29. package/templates/workspace/.claude/commands/ws-intake.md +19 -0
  30. package/templates/workspace/.claude/commands/ws-review.md +5 -1
  31. package/templates/workspace/.claude/settings.json.example +26 -0
  32. package/templates/workspace/.claude/skills/ws-commit/SKILL.md +6 -118
  33. package/templates/workspace/.claude/skills/ws-deliver/SKILL.md +6 -218
  34. package/templates/workspace/.claude/skills/ws-dev/SKILL.md +52 -141
  35. package/templates/workspace/.claude/skills/ws-finish/SKILL.md +6 -205
  36. package/templates/workspace/.claude/skills/ws-handoff/SKILL.md +10 -44
  37. package/templates/workspace/.claude/skills/ws-intake/SKILL.md +31 -0
  38. package/templates/workspace/.claude/skills/ws-plan-verify/SKILL.md +6 -49
  39. package/templates/workspace/.claude/skills/ws-review/SKILL.md +6 -1
  40. package/templates/workspace/.claude/skills/ws-verify-before-complete/SKILL.md +12 -53
  41. package/templates/workspace/.opencode/command/ws-auto.md +33 -0
  42. package/templates/workspace/.opencode/command/ws-autonomy.md +25 -0
  43. package/templates/workspace/.opencode/command/ws-intake.md +22 -0
  44. package/templates/workspace/.opencode/command/ws-review.md +5 -1
  45. package/templates/workspace/.opencode/commands/ws-auto.md +33 -0
  46. package/templates/workspace/.opencode/commands/ws-autonomy.md +25 -0
  47. package/templates/workspace/.opencode/commands/ws-commit.md +4 -56
  48. package/templates/workspace/.opencode/commands/ws-deliver.md +10 -50
  49. package/templates/workspace/.opencode/commands/ws-finish.md +8 -65
  50. package/templates/workspace/.opencode/commands/ws-handoff.md +9 -17
  51. package/templates/workspace/.opencode/commands/ws-intake.md +22 -0
  52. package/templates/workspace/.opencode/commands/ws-migrate.md +10 -17
  53. package/templates/workspace/.opencode/commands/ws-plan-verify.md +5 -15
  54. package/templates/workspace/.opencode/commands/ws-pull.md +6 -75
  55. package/templates/workspace/.opencode/commands/ws-push.md +7 -82
  56. package/templates/workspace/.opencode/commands/ws-review.md +5 -1
  57. package/templates/workspace/.opencode/commands/ws-submodule-setup.md +8 -47
  58. package/templates/workspace/.opencode/commands/ws-verify-before-complete.md +10 -19
  59. package/templates/workspace/.opencode/helpers/approval-whitelist-check.sh +148 -0
  60. package/templates/workspace/.opencode/helpers/approval-whitelist-run.sh +82 -0
  61. package/templates/workspace/.opencode/helpers/approval-whitelist-watchdog.sh +144 -0
  62. package/templates/workspace/.opencode/helpers/tmux-swarm-rescue.sh +56 -0
  63. package/templates/workspace/.opencode/helpers/tmux-swarm-scan.sh +46 -0
  64. package/templates/workspace/.opencode/oh-my-opencode.json.example +64 -4
  65. package/templates/workspace/.opencode/skills/using-aiws/SKILL.md +93 -77
  66. package/templates/workspace/.opencode/skills/ws-analyze/SKILL.md +1 -1
  67. package/templates/workspace/.opencode/skills/ws-auto/SKILL.md +46 -0
  68. package/templates/workspace/.opencode/skills/ws-autonomy/SKILL.md +62 -0
  69. package/templates/workspace/.opencode/skills/ws-bugfix/SKILL.md +1 -1
  70. package/templates/workspace/.opencode/skills/ws-commit/SKILL.md +6 -118
  71. package/templates/workspace/.opencode/skills/ws-delegate/SKILL.md +93 -40
  72. package/templates/workspace/.opencode/skills/ws-deliver/SKILL.md +6 -218
  73. package/templates/workspace/.opencode/skills/ws-dev/SKILL.md +53 -142
  74. package/templates/workspace/.opencode/skills/ws-dev-lite/SKILL.md +19 -6
  75. package/templates/workspace/.opencode/skills/ws-finish/SKILL.md +6 -205
  76. package/templates/workspace/.opencode/skills/ws-frontend-design/SKILL.md +1 -1
  77. package/templates/workspace/.opencode/skills/ws-handoff/SKILL.md +10 -44
  78. package/templates/workspace/.opencode/skills/ws-intake/SKILL.md +40 -0
  79. package/templates/workspace/.opencode/skills/ws-migrate/SKILL.md +6 -42
  80. package/templates/workspace/.opencode/skills/ws-plan/SKILL.md +4 -2
  81. package/templates/workspace/.opencode/skills/ws-plan-verify/SKILL.md +6 -49
  82. package/templates/workspace/.opencode/skills/ws-preflight/SKILL.md +1 -1
  83. package/templates/workspace/.opencode/skills/ws-pull/SKILL.md +8 -109
  84. package/templates/workspace/.opencode/skills/ws-push/SKILL.md +8 -100
  85. package/templates/workspace/.opencode/skills/ws-quality-review/SKILL.md +1 -1
  86. package/templates/workspace/.opencode/skills/ws-req-change/SKILL.md +1 -1
  87. package/templates/workspace/.opencode/skills/ws-req-contract-sync/SKILL.md +1 -1
  88. package/templates/workspace/.opencode/skills/ws-req-contract-validate/SKILL.md +1 -1
  89. package/templates/workspace/.opencode/skills/ws-req-flow-sync/SKILL.md +1 -1
  90. package/templates/workspace/.opencode/skills/ws-req-review/SKILL.md +1 -1
  91. package/templates/workspace/.opencode/skills/ws-review/SKILL.md +14 -3
  92. package/templates/workspace/.opencode/skills/ws-rule/SKILL.md +1 -1
  93. package/templates/workspace/.opencode/skills/ws-spec-review/SKILL.md +1 -1
  94. package/templates/workspace/.opencode/skills/ws-submodule-setup/SKILL.md +10 -57
  95. package/templates/workspace/.opencode/skills/ws-verify-before-complete/SKILL.md +12 -53
  96. package/templates/workspace/AGENTS.md +12 -5
  97. package/templates/workspace/AI_PROJECT.md +1 -1
  98. package/templates/workspace/changes/README.md +9 -12
  99. package/templates/workspace/manifest.json +277 -203
  100. package/templates/workspace/.agents/skills/ws-migrate/SKILL.md +0 -54
  101. package/templates/workspace/.agents/skills/ws-pull/SKILL.md +0 -119
  102. package/templates/workspace/.agents/skills/ws-push/SKILL.md +0 -110
  103. package/templates/workspace/.agents/skills/ws-submodule-setup/SKILL.md +0 -65
  104. package/templates/workspace/.claude/skills/ws-migrate/SKILL.md +0 -54
  105. package/templates/workspace/.claude/skills/ws-pull/SKILL.md +0 -119
  106. package/templates/workspace/.claude/skills/ws-push/SKILL.md +0 -110
  107. package/templates/workspace/.claude/skills/ws-submodule-setup/SKILL.md +0 -65
@@ -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
@@ -0,0 +1,46 @@
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
+ out_file="${out_dir}/tmux-scan.json"
11
+ mkdir -p "${out_dir}"
12
+
13
+ python3 - "$out_file" <<'PY'
14
+ import json
15
+ import subprocess
16
+ import sys
17
+
18
+ out_file = sys.argv[1]
19
+
20
+ def run(cmd):
21
+ return subprocess.run(cmd, check=True, text=True, capture_output=True).stdout
22
+
23
+ panes_raw = run(["tmux", "list-panes", "-a", "-F", "#{session_name}:#{window_index}.#{pane_index}\t#{pane_current_command}\t#{pane_current_path}\t#{pane_in_mode}"]).splitlines()
24
+ panes = []
25
+ for row in panes_raw:
26
+ target, command, current_path, in_mode = row.split("\t", 3)
27
+ capture = run(["tmux", "capture-pane", "-t", target, "-p", "-S", "-80"])
28
+ lowered = capture.lower()
29
+ panes.append(
30
+ {
31
+ "target": target,
32
+ "command": command,
33
+ "current_path": current_path,
34
+ "pane_in_mode": in_mode == "1",
35
+ "waiting_confirm": "(y/n)" in lowered,
36
+ "press_enter": "press enter to continue" in lowered,
37
+ "error_like": any(token in lowered for token in ["traceback", "error", "failed", "fatal"]),
38
+ "tail": capture.splitlines()[-20:],
39
+ }
40
+ )
41
+
42
+ with open(out_file, "w", encoding="utf-8") as fh:
43
+ json.dump({"panes": panes}, fh, ensure_ascii=False, indent=2)
44
+
45
+ print(out_file)
46
+ PY
@@ -2,16 +2,76 @@
2
2
  "agents": {
3
3
  "planner-sisyphus": {
4
4
  "enabled": true,
5
- "replace_plan": false
5
+ "replace_plan": false,
6
+ "categories": [
7
+ "planning",
8
+ "autonomous"
9
+ ],
10
+ "prompt_append": "You are running inside an AIWS workspace. Before coding, read AI_PROJECT.md, REQUIREMENTS.md, and AI_WORKSPACE.md. Stay autonomous until the task is complete. Completion means: truth files read, task/change bound, required verify commands run, review/evidence written, and aiws validate . or change validate completed when applicable. Stop early only for a real blocker: missing user decision, missing credential, external dependency failure, or sandbox/permission failure."
6
11
  },
7
12
  "librarian": {
8
- "enabled": true
13
+ "enabled": true,
14
+ "categories": [
15
+ "docs",
16
+ "research"
17
+ ]
9
18
  },
10
19
  "explore": {
11
- "enabled": true
20
+ "enabled": true,
21
+ "categories": [
22
+ "code",
23
+ "analysis"
24
+ ]
12
25
  },
13
26
  "oracle": {
14
- "enabled": true
27
+ "enabled": true,
28
+ "categories": [
29
+ "review",
30
+ "qa"
31
+ ]
32
+ }
33
+ },
34
+ "backgroundTasks": {
35
+ "enabled": true,
36
+ "maxJobs": 3,
37
+ "defaultTimeoutMs": 1200000
38
+ },
39
+ "experimental": {
40
+ "auto_resume": true
41
+ },
42
+ "aiws": {
43
+ "autonomy": {
44
+ "approval_whitelist": {
45
+ "enabled": true,
46
+ "mode": "assist-only",
47
+ "host_permission_mode": "manual-only",
48
+ "read_only_commands": [
49
+ "rg",
50
+ "cat",
51
+ "sed",
52
+ "ls",
53
+ "find",
54
+ "git status",
55
+ "git diff"
56
+ ],
57
+ "write_allow_paths": [
58
+ ".agentdocs/tmp/",
59
+ "changes/*/analysis/",
60
+ "changes/*/review/",
61
+ "changes/*/evidence/"
62
+ ],
63
+ "deny_paths": [
64
+ "secrets/",
65
+ ".env*"
66
+ ],
67
+ "deny_commands": [
68
+ "rm",
69
+ "mv",
70
+ "git commit",
71
+ "git push",
72
+ "npm publish"
73
+ ]
74
+ }
15
75
  }
16
76
  }
17
77
  }