@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.
- package/docs/cli-interface.md +10 -12
- package/docs/opencode-autonomous-swarm.md +178 -0
- package/docs/opencode-omo-adapter.md +123 -4
- package/docs/opencode-omo-validation-checklist.md +47 -0
- package/docs/opencode-subagent-first.md +187 -0
- package/docs/workflow-delegation-context-injection.md +217 -0
- package/docs/workflow-delegation-contracts.json +108 -2
- package/docs/workflow-delegation-contracts.md +13 -0
- package/docs/workflow-delegation-contracts.schema.json +152 -0
- package/docs/workflow-governance-rules.json +47 -6
- package/docs/workflow-governance-rules.md +8 -7
- package/docs/workflow-governance-rules.schema.json +39 -1
- package/docs/workflow-review-gates.json +6 -0
- package/docs/workflow-review-gates.md +14 -5
- package/docs/workflow-review-gates.schema.json +4 -0
- package/docs/workflow-router-rules.json +72 -8
- package/docs/workflow-router-rules.md +21 -14
- package/docs/workflow-router-rules.schema.json +4 -0
- package/docs/workflow-stage-contracts.json +24 -5
- package/docs/workflow-stage-contracts.md +16 -15
- package/docs/workflow-stage-contracts.schema.json +12 -1
- package/package.json +1 -1
- package/templates/workspace/.agents/skills/using-aiws/SKILL.md +16 -10
- package/templates/workspace/.agents/skills/ws-commit/SKILL.md +6 -118
- package/templates/workspace/.agents/skills/ws-deliver/SKILL.md +6 -218
- package/templates/workspace/.agents/skills/ws-dev/SKILL.md +82 -141
- package/templates/workspace/.agents/skills/ws-finish/SKILL.md +9 -204
- package/templates/workspace/.agents/skills/ws-handoff/SKILL.md +10 -44
- package/templates/workspace/.agents/skills/ws-migrate/SKILL.md +6 -42
- package/templates/workspace/.agents/skills/ws-plan-verify/SKILL.md +6 -58
- package/templates/workspace/.agents/skills/ws-pull/SKILL.md +8 -109
- package/templates/workspace/.agents/skills/ws-push/SKILL.md +8 -100
- package/templates/workspace/.agents/skills/ws-review/SKILL.md +6 -1
- package/templates/workspace/.agents/skills/ws-submodule-setup/SKILL.md +10 -57
- package/templates/workspace/.agents/skills/ws-verify-before-complete/SKILL.md +12 -53
- package/templates/workspace/{changes → .aiws/changes}/README.md +20 -23
- package/templates/workspace/{changes → .aiws/changes}/templates/proposal.md +12 -12
- package/templates/workspace/{changes → .aiws/changes}/templates/tasks.md +8 -8
- package/templates/workspace/{issues → .aiws/issues}/problem-issues.csv +1 -1
- package/templates/workspace/.aiws/journal/.gitkeep +1 -0
- package/templates/workspace/{requirements → .aiws/requirements}/requirements-issues.csv +1 -1
- package/templates/workspace/.aiws/tmp/.gitkeep +0 -0
- package/templates/workspace/{tools → .aiws/tools}/requirements_contract.py +2 -2
- package/templates/workspace/{tools → .aiws/tools}/requirements_contract_sync.py +2 -2
- package/templates/workspace/{tools → .aiws/tools}/requirements_flow_gen.py +1 -1
- package/templates/workspace/{tools → .aiws/tools}/server_test_runner.py +10 -10
- package/templates/workspace/{tools → .aiws/tools}/ws_change_check.py +23 -23
- package/templates/workspace/{tools → .aiws/tools}/ws_resolve_sub_target.sh +2 -2
- package/templates/workspace/{tools → .aiws/tools}/ws_tasks_plan.py +3 -3
- package/templates/workspace/.claude/commands/ws-review.md +5 -1
- package/templates/workspace/.claude/settings.json.example +26 -0
- package/templates/workspace/.claude/skills/ws-commit/SKILL.md +6 -118
- package/templates/workspace/.claude/skills/ws-deliver/SKILL.md +6 -218
- package/templates/workspace/.claude/skills/ws-dev/SKILL.md +52 -141
- package/templates/workspace/.claude/skills/ws-finish/SKILL.md +9 -204
- package/templates/workspace/.claude/skills/ws-handoff/SKILL.md +10 -44
- package/templates/workspace/.claude/skills/ws-migrate/SKILL.md +6 -42
- package/templates/workspace/.claude/skills/ws-plan-verify/SKILL.md +6 -49
- package/templates/workspace/.claude/skills/ws-pull/SKILL.md +8 -109
- package/templates/workspace/.claude/skills/ws-push/SKILL.md +8 -100
- package/templates/workspace/.claude/skills/ws-review/SKILL.md +6 -1
- package/templates/workspace/.claude/skills/ws-submodule-setup/SKILL.md +10 -57
- package/templates/workspace/.claude/skills/ws-verify-before-complete/SKILL.md +12 -53
- package/templates/workspace/.githooks/pre-commit +13 -0
- package/templates/workspace/.githooks/pre-push +7 -0
- package/templates/workspace/.opencode/command/ws-auto.md +33 -0
- package/templates/workspace/.opencode/command/ws-autonomy.md +25 -0
- package/templates/workspace/.opencode/command/ws-review.md +5 -1
- package/templates/workspace/.opencode/commands/ws-auto.md +33 -0
- package/templates/workspace/.opencode/commands/ws-autonomy.md +25 -0
- package/templates/workspace/.opencode/commands/ws-commit.md +4 -56
- package/templates/workspace/.opencode/commands/ws-deliver.md +10 -50
- package/templates/workspace/.opencode/commands/ws-finish.md +11 -3
- package/templates/workspace/.opencode/commands/ws-handoff.md +9 -17
- package/templates/workspace/.opencode/commands/ws-migrate.md +10 -17
- package/templates/workspace/.opencode/commands/ws-plan-verify.md +5 -15
- package/templates/workspace/.opencode/commands/ws-pull.md +6 -75
- package/templates/workspace/.opencode/commands/ws-push.md +7 -82
- package/templates/workspace/.opencode/commands/ws-review.md +5 -1
- package/templates/workspace/.opencode/commands/ws-submodule-setup.md +8 -47
- package/templates/workspace/.opencode/commands/ws-verify-before-complete.md +10 -19
- package/templates/workspace/.opencode/helpers/approval-whitelist-check.sh +148 -0
- package/templates/workspace/.opencode/helpers/approval-whitelist-run.sh +82 -0
- package/templates/workspace/.opencode/helpers/approval-whitelist-watchdog.sh +144 -0
- package/templates/workspace/.opencode/helpers/tmux-swarm-rescue.sh +56 -0
- package/templates/workspace/.opencode/helpers/tmux-swarm-scan.sh +46 -0
- package/templates/workspace/.opencode/lib/aiws-context.js +953 -0
- package/templates/workspace/.opencode/oh-my-opencode.json.example +64 -4
- package/templates/workspace/.opencode/plugins/aiws-inject-context.js +201 -0
- package/templates/workspace/.opencode/plugins/aiws-session-start.js +86 -0
- package/templates/workspace/.opencode/plugins/aiws-workflow-state.js +116 -0
- package/templates/workspace/.opencode/scripts/ws-plan-gen-submodule-targets.sh +27 -0
- package/templates/workspace/.opencode/scripts/ws-plan-setup-worktree.sh +31 -0
- package/templates/workspace/.opencode/skills/using-aiws/SKILL.md +130 -77
- package/templates/workspace/.opencode/skills/ws-analyze/SKILL.md +1 -1
- package/templates/workspace/.opencode/skills/ws-auto/SKILL.md +46 -0
- package/templates/workspace/.opencode/skills/ws-autonomy/SKILL.md +62 -0
- package/templates/workspace/.opencode/skills/ws-bugfix/SKILL.md +1 -1
- package/templates/workspace/.opencode/skills/ws-commit/SKILL.md +6 -118
- package/templates/workspace/.opencode/skills/ws-delegate/SKILL.md +115 -40
- package/templates/workspace/.opencode/skills/ws-deliver/SKILL.md +6 -218
- package/templates/workspace/.opencode/skills/ws-dev/SKILL.md +105 -146
- package/templates/workspace/.opencode/skills/ws-dev-lite/SKILL.md +23 -7
- package/templates/workspace/.opencode/skills/ws-finish/SKILL.md +9 -204
- package/templates/workspace/.opencode/skills/ws-frontend-design/SKILL.md +1 -1
- package/templates/workspace/.opencode/skills/ws-handoff/SKILL.md +10 -44
- package/templates/workspace/.opencode/skills/ws-intake/SKILL.md +43 -4
- package/templates/workspace/.opencode/skills/ws-migrate/SKILL.md +6 -42
- package/templates/workspace/.opencode/skills/ws-plan/SKILL.md +15 -55
- package/templates/workspace/.opencode/skills/ws-plan-verify/SKILL.md +6 -49
- package/templates/workspace/.opencode/skills/ws-preflight/SKILL.md +1 -1
- package/templates/workspace/.opencode/skills/ws-pull/SKILL.md +8 -109
- package/templates/workspace/.opencode/skills/ws-push/SKILL.md +8 -100
- package/templates/workspace/.opencode/skills/ws-quality-review/SKILL.md +7 -2
- package/templates/workspace/.opencode/skills/ws-req-change/SKILL.md +1 -1
- package/templates/workspace/.opencode/skills/ws-req-contract-sync/SKILL.md +1 -1
- package/templates/workspace/.opencode/skills/ws-req-contract-validate/SKILL.md +1 -1
- package/templates/workspace/.opencode/skills/ws-req-flow-sync/SKILL.md +1 -1
- package/templates/workspace/.opencode/skills/ws-req-review/SKILL.md +1 -1
- package/templates/workspace/.opencode/skills/ws-review/SKILL.md +35 -4
- package/templates/workspace/.opencode/skills/ws-rule/SKILL.md +1 -1
- package/templates/workspace/.opencode/skills/ws-spec-review/SKILL.md +1 -1
- package/templates/workspace/.opencode/skills/ws-submodule-setup/SKILL.md +10 -57
- package/templates/workspace/.opencode/skills/ws-verify-before-complete/SKILL.md +45 -55
- package/templates/workspace/AGENTS.md +32 -84
- package/templates/workspace/AI_PROJECT.md +5 -5
- package/templates/workspace/AI_WORKSPACE.md +1 -1
- package/templates/workspace/REQUIREMENTS.md +1 -1
- package/templates/workspace/gitignore +1 -1
- package/templates/workspace/manifest.json +215 -395
- /package/templates/workspace/{changes → .aiws/changes}/templates/design.md +0 -0
- /package/templates/workspace/{memory-bank → .aiws/memory-bank}/README.md +0 -0
- /package/templates/workspace/{memory-bank → .aiws/memory-bank}/architecture.md +0 -0
- /package/templates/workspace/{memory-bank → .aiws/memory-bank}/implementation-plan.md +0 -0
- /package/templates/workspace/{memory-bank → .aiws/memory-bank}/progress.md +0 -0
- /package/templates/workspace/{memory-bank → .aiws/memory-bank}/tech-stack.md +0 -0
- /package/templates/workspace/{requirements → .aiws/requirements}/CHANGELOG.md +0 -0
- /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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
1
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|