@frankzhang2026/opencode-android-orchestrator 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +50 -24
  3. package/dist/cli.js +64 -2
  4. package/dist/cli.js.map +1 -1
  5. package/dist/config/long-command-timeout.d.ts +20 -0
  6. package/dist/config/long-command-timeout.d.ts.map +1 -0
  7. package/dist/config/long-command-timeout.js +65 -0
  8. package/dist/config/long-command-timeout.js.map +1 -0
  9. package/dist/doctor/installation.d.ts +1 -1
  10. package/dist/doctor/installation.d.ts.map +1 -1
  11. package/dist/doctor/installation.js +3 -1
  12. package/dist/doctor/installation.js.map +1 -1
  13. package/dist/index.d.ts +1 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/installer/adaptive-templates.d.ts +4 -1
  18. package/dist/installer/adaptive-templates.d.ts.map +1 -1
  19. package/dist/installer/adaptive-templates.js +8 -0
  20. package/dist/installer/adaptive-templates.js.map +1 -1
  21. package/dist/installer/init.d.ts.map +1 -1
  22. package/dist/installer/init.js +4 -1
  23. package/dist/installer/init.js.map +1 -1
  24. package/dist/installer/opencode-config.d.ts +3 -3
  25. package/dist/installer/opencode-config.js +1 -1
  26. package/dist/installer/upgrade.d.ts +1 -0
  27. package/dist/installer/upgrade.d.ts.map +1 -1
  28. package/dist/installer/upgrade.js +7 -0
  29. package/dist/installer/upgrade.js.map +1 -1
  30. package/dist/plugin/index.d.ts.map +1 -1
  31. package/dist/plugin/index.js +5 -0
  32. package/dist/plugin/index.js.map +1 -1
  33. package/docs/MIGRATION.md +19 -13
  34. package/docs/SECURITY.md +17 -6
  35. package/docs/TROUBLESHOOTING.md +16 -2
  36. package/package.json +1 -1
  37. package/templates/.opencode/agents/scheduled-planner.md +5 -0
  38. package/templates/.opencode/commands/resume-task.md +28 -0
  39. package/templates/.opencode/skills/scheduled-quality-orchestrator/SKILL.md +38 -3
  40. package/templates/README.md +3 -3
  41. package/templates/automation/config.json +3 -1
  42. package/templates/automation/config.schema.json +9 -2
  43. package/templates/scripts/automation/lib.sh +2 -0
  44. package/templates/scripts/automation/preflight.sh +1 -0
  45. package/templates/scripts/automation/resume-task.sh +135 -0
  46. package/templates/scripts/automation/tests/run-tests.sh +39 -2
@@ -36,6 +36,7 @@
36
36
  "maxFixLoops",
37
37
  "maxReviewCycles",
38
38
  "maxReviewerRestarts",
39
+ "longCommandTimeoutMs",
39
40
  "autoCleanupWorktrees",
40
41
  "pushAfterAcceptance",
41
42
  "approvalPhrases",
@@ -57,17 +58,23 @@
57
58
  "maxFixLoops": { "type": "integer", "minimum": 0, "maximum": 1 },
58
59
  "maxReviewCycles": { "type": "integer", "minimum": 0, "maximum": 2 },
59
60
  "maxReviewerRestarts": { "type": "integer", "minimum": 0, "maximum": 3 },
61
+ "longCommandTimeoutMs": {
62
+ "type": "integer",
63
+ "minimum": 120000,
64
+ "maximum": 7200000
65
+ },
60
66
  "autoCleanupWorktrees": { "type": "boolean" },
61
67
  "pushAfterAcceptance": { "const": false },
62
68
  "approvalPhrases": {
63
69
  "type": "object",
64
70
  "additionalProperties": false,
65
- "required": ["proposal", "contract", "acceptance", "abort"],
71
+ "required": ["proposal", "contract", "acceptance", "abort", "resume"],
66
72
  "properties": {
67
73
  "proposal": { "type": "string", "minLength": 4 },
68
74
  "contract": { "type": "string", "minLength": 4 },
69
75
  "acceptance": { "type": "string", "minLength": 4 },
70
- "abort": { "type": "string", "minLength": 4 }
76
+ "abort": { "type": "string", "minLength": 4 },
77
+ "resume": { "type": "string", "minLength": 4 }
71
78
  }
72
79
  },
73
80
  "plugins": {
@@ -120,12 +120,14 @@ automation_validate_config() {
120
120
  (.maxFixLoops | type == "number" and . >= 0 and . <= 1 and floor == .) and
121
121
  (.maxReviewCycles | type == "number" and . >= 0 and . <= 2 and floor == .) and
122
122
  (.maxReviewerRestarts | type == "number" and . >= 0 and . <= 3 and floor == .) and
123
+ (.longCommandTimeoutMs | type == "number" and . >= 120000 and . <= 7200000 and floor == .) and
123
124
  (.autoCleanupWorktrees | type == "boolean") and
124
125
  .pushAfterAcceptance == false and
125
126
  (.approvalPhrases.proposal | type == "string" and length >= 4) and
126
127
  (.approvalPhrases.contract | type == "string" and length >= 4) and
127
128
  (.approvalPhrases.acceptance | type == "string" and length >= 4) and
128
129
  (.approvalPhrases.abort | type == "string" and length >= 4) and
130
+ (.approvalPhrases.resume | type == "string" and length >= 4) and
129
131
  (.plugins | type == "object" and keys == ["superpowers"]) and
130
132
  (.plugins.superpowers | type == "string" and length > 0) and
131
133
  (.requiredSkills | type == "array" and length >= 6) and
@@ -155,6 +155,7 @@ else
155
155
  (last_rule("edit"; "**/src/**") == "deny") and
156
156
  (last_rule("bash"; "./scripts/automation/prepare-contract-review.sh *") == "allow") and
157
157
  (last_rule("bash"; "./scripts/automation/approve-and-run.sh *") == "allow") and
158
+ (last_rule("bash"; "./scripts/automation/resume-task.sh *") == "allow") and
158
159
  (last_rule("bash"; "./scripts/automation/resume-review.sh *") == "allow") and
159
160
  (last_rule("bash"; "./scripts/automation/accept-and-integrate.sh *") == "allow") and
160
161
  (last_rule("bash"; "./scripts/automation/abort-task.sh *") == "allow") and
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
5
+ # shellcheck source=lib.sh
6
+ source "$SCRIPT_DIR/lib.sh"
7
+
8
+ task_id="${1:-}"
9
+ approval="${2:-}"
10
+ if [[ "$#" -ne 2 ]]; then
11
+ printf 'Usage: %s TASK-ID "RESUME-APPROVAL"\n' "$0" >&2
12
+ exit 2
13
+ fi
14
+
15
+ automation_validate_task_id "$task_id"
16
+ automation_require_orchestrated
17
+ automation_require_approval resume "$approval"
18
+
19
+ workspace_file="$(automation_workspace_path "$task_id")"
20
+ origin_file="$(automation_origin_path "$task_id")"
21
+ state_file="$(automation_state_path "$task_id")"
22
+ evidence_dir="$(automation_evidence_path "$task_id")"
23
+ transitions_file="$evidence_dir/transitions.jsonl"
24
+ resumptions_file="$evidence_dir/baseline-resumptions.jsonl"
25
+
26
+ [[ -f "$workspace_file" ]] || automation_die "workspace metadata is missing for $task_id"
27
+ [[ -f "$origin_file" ]] || automation_die "origin evidence is missing for $task_id"
28
+ [[ -f "$state_file" ]] || automation_die "state is missing for $task_id"
29
+ [[ -f "$transitions_file" ]] || automation_die "transition evidence is missing for $task_id"
30
+
31
+ automation_acquire_run_lock "$task_id"
32
+ trap 'automation_release_run_lock' EXIT
33
+
34
+ [[ "$(automation_read_state "$task_id")" == "BLOCKED" ]] || automation_die "$task_id is not BLOCKED"
35
+
36
+ source_root="$(jq -er '.sourceRoot' "$workspace_file")"
37
+ task_root="$(automation_workspace_task_root "$workspace_file")"
38
+ workspace_strategy="$(automation_workspace_strategy "$workspace_file")"
39
+ original_branch="$(jq -er '.originalBranch' "$workspace_file")"
40
+ task_branch="$(jq -er '.taskBranch' "$workspace_file")"
41
+ baseline_head="$(jq -er '.baselineHead' "$workspace_file")"
42
+
43
+ [[ "$(jq -r '.repositoryLeaseRequired // false' "$workspace_file")" == "true" ]] || \
44
+ automation_die "baseline recovery requires a leased transactional workspace"
45
+ [[ -d "$task_root" ]] || automation_die "task root is missing: $task_root"
46
+ automation_require_repository_lease "$task_id" "$source_root" "$workspace_strategy"
47
+ task_root="$(cd "$task_root" && pwd -P)"
48
+ source_root="$(cd "$source_root" && pwd -P)"
49
+ [[ "$source_root" == "$AUTOMATION_ROOT" ]] || automation_die "workspace sourceRoot does not match this repository"
50
+ [[ "$(jq -er '.sourceRoot' "$origin_file")" == "$source_root" ]] || automation_die "origin sourceRoot does not match workspace metadata"
51
+ [[ "$(jq -er '.originalBranch' "$origin_file")" == "$original_branch" ]] || automation_die "origin branch does not match workspace metadata"
52
+ [[ "$(automation_current_branch "$task_root")" == "$task_branch" ]] || automation_die "task branch changed"
53
+ [[ "$(git -C "$task_root" rev-parse HEAD)" == "$baseline_head" ]] || automation_die "task HEAD changed before baseline sealing"
54
+ git -C "$source_root" show-ref --verify --quiet "refs/heads/$original_branch" || automation_die "recorded original branch no longer exists"
55
+ [[ "$(git -C "$source_root" rev-parse "refs/heads/$original_branch")" == "$baseline_head" ]] || automation_die "original branch drifted after task preparation"
56
+
57
+ for sealed_evidence in baseline.json red.json ready.json review.json reviews.jsonl; do
58
+ [[ ! -e "$evidence_dir/$sealed_evidence" ]] || \
59
+ automation_die "$sealed_evidence already exists; baseline-only recovery is forbidden"
60
+ done
61
+
62
+ state_revision="$(jq -er '.revision' "$state_file")"
63
+ if ! jq -s -e --argjson revision "$state_revision" '
64
+ length == $revision and
65
+ length >= 2 and
66
+ .[-2].from == "PENDING" and
67
+ .[-2].to == "CODING" and
68
+ .[-2].actor == "coder-launcher" and
69
+ .[-2].note == "preflight passed; capturing baseline" and
70
+ .[-1].from == "CODING" and
71
+ .[-1].to == "BLOCKED" and
72
+ (.[-1].actor == "coder" or .[-1].actor == "preflight" or .[-1].actor == "orchestrator") and
73
+ (.[-1].note | test("baseline|claim|基线"; "i"))
74
+ ' "$transitions_file" >/dev/null; then
75
+ automation_die "BLOCKED state is not a recoverable baseline-capture interruption"
76
+ fi
77
+
78
+ automation_assert_planning_artifacts_sealed "$task_id" "$task_root"
79
+ changed_paths=()
80
+ while IFS= read -r path; do
81
+ [[ -n "$path" ]] && changed_paths+=("$path")
82
+ done < <(automation_changed_paths_at "$task_root")
83
+ [[ "${#changed_paths[@]}" -eq 2 ]] || automation_die "baseline recovery requires exactly the sealed plan and contract changes"
84
+ for path in "${changed_paths[@]}"; do
85
+ automation_is_planning_artifact "$task_id" "$path" || automation_die "product or unrelated change blocks baseline recovery: $path"
86
+ if git -C "$task_root" ls-files --error-unmatch -- "$path" >/dev/null 2>&1; then
87
+ automation_die "planning artifact became tracked before baseline recovery: $path"
88
+ fi
89
+ done
90
+
91
+ restart_count=0
92
+ if [[ -f "$resumptions_file" ]]; then
93
+ restart_count="$(jq -s --arg taskId "$task_id" '[.[] | select(.taskId == $taskId)] | length' "$resumptions_file")"
94
+ fi
95
+ max_restarts=1
96
+ [[ "$restart_count" -lt "$max_restarts" ]] || automation_die "baseline restart limit exhausted"
97
+ next_restart=$((restart_count + 1))
98
+ blocked_revision="$state_revision"
99
+ current_diff_sha="$(automation_worktree_diff_sha "$task_root")"
100
+
101
+ jq -nc \
102
+ --arg taskId "$task_id" \
103
+ --arg kind "resume-baseline" \
104
+ --arg at "$(automation_now)" \
105
+ --arg diffSha256 "$current_diff_sha" \
106
+ --argjson blockedRevision "$blocked_revision" \
107
+ '{taskId: $taskId, kind: $kind, at: $at,
108
+ blockedRevision: $blockedRevision, diffSha256: $diffSha256}' \
109
+ | automation_append_json "$(automation_approvals_path "$task_id")"
110
+
111
+ automation_transition_state \
112
+ "$task_id" \
113
+ "BLOCKED" \
114
+ "PENDING" \
115
+ "human" \
116
+ "baseline-only recovery $next_restart/$max_restarts; no product diff and baseline evidence absent"
117
+
118
+ jq -nc \
119
+ --arg taskId "$task_id" \
120
+ --arg at "$(automation_now)" \
121
+ --arg diffSha256 "$current_diff_sha" \
122
+ --argjson blockedRevision "$blocked_revision" \
123
+ --argjson restart "$next_restart" \
124
+ --argjson maxRestarts "$max_restarts" \
125
+ '{taskId: $taskId, at: $at, blockedRevision: $blockedRevision,
126
+ diffSha256: $diffSha256, restart: $restart,
127
+ maxRestarts: $maxRestarts, route: "BLOCKED->PENDING",
128
+ baselineEvidencePresent: false, productDiffPresent: false}' \
129
+ | automation_append_json "$resumptions_file"
130
+
131
+ automation_info "$task_id baseline-only recovery accepted; restarting deterministic claim flow"
132
+ automation_release_run_lock
133
+ trap - EXIT
134
+
135
+ "$SCRIPT_DIR/orchestrate-task.sh" "$task_id"
@@ -119,13 +119,15 @@ jq -n '{
119
119
  maxFixLoops: 1,
120
120
  maxReviewCycles: 1,
121
121
  maxReviewerRestarts: 2,
122
+ longCommandTimeoutMs: 1800000,
122
123
  autoCleanupWorktrees: true,
123
124
  pushAfterAcceptance: false,
124
125
  approvalPhrases: {
125
126
  proposal: "批准方案,生成计划和任务合同。",
126
127
  contract: "合同已复核,批准自动执行到人工验收阶段。",
127
128
  acceptance: "验收通过,提交到原分支。",
128
- abort: "中止任务,封存修改并恢复原分支。"
129
+ abort: "中止任务,封存修改并恢复原分支。",
130
+ resume: "恢复任务,重新捕获基线并继续自动执行。"
129
131
  },
130
132
  plugins: {
131
133
  superpowers: "superpowers@git+https://github.com/obra/superpowers.git#v6.2.0"
@@ -483,9 +485,12 @@ fi
483
485
  if run_fixture env AUTOMATION_SKIP_AGENT_RUN=1 ./scripts/automation/resume-review.sh TASK-TEST-003 >/dev/null 2>&1; then
484
486
  fail 'reviewer-only recovery accepted a non-review blocker'
485
487
  fi
488
+ if run_fixture env AUTOMATION_SKIP_AGENT_RUN=1 ./scripts/automation/resume-task.sh TASK-TEST-003 '恢复任务,重新捕获基线并继续自动执行。' >/dev/null 2>&1; then
489
+ fail 'baseline-only recovery accepted a preflight blocker'
490
+ fi
486
491
  rm "$fixture/unapproved-change.txt"
487
492
  pass 'preflight blocks dirty task roots without retrying implicitly'
488
- pass 'reviewer-only recovery rejects unrelated BLOCKED states'
493
+ pass 'reviewer and baseline recovery reject unrelated BLOCKED states'
489
494
 
490
495
  printf '%s\n' '# Approved reviewer-fix plan' > "$fixture/docs/plans/TASK-TEST-005.md"
491
496
  jq \
@@ -745,6 +750,38 @@ run_fixture ./scripts/automation/abort-task.sh TASK-TEST-009 '中止任务,封
745
750
  [[ ! -e "$fixture/docs/plans/TASK-TEST-009.md" && ! -e "$fixture/automation/tasks/TASK-TEST-009.json" ]] || fail 'planning-only abort left generated artifacts in the original worktree'
746
751
  pass 'aborting before product edits archives planning artifacts without creating a planning-only commit'
747
752
 
753
+ printf '%s\n' '# Baseline interruption recovery plan' > "$fixture/docs/plans/TASK-TEST-010.md"
754
+ jq \
755
+ '.id = "TASK-TEST-010" |
756
+ .title = "Recover one interrupted baseline capture" |
757
+ .planPath = "docs/plans/TASK-TEST-010.md" |
758
+ .targetTests = [{gradleTask: "testDebugUnitTest", filter: "dev.example.orchestratorfixture.BaselineRecoveryTest"}]' \
759
+ "$fixture/automation/tasks/TASK-TEST-001.json" \
760
+ > "$fixture/automation/tasks/TASK-TEST-010.json"
761
+ run_fixture ./scripts/automation/prepare-contract-review.sh TASK-TEST-010 '批准方案,生成计划和任务合同。' >/dev/null
762
+ run_fixture env AUTOMATION_SKIP_AGENT_RUN=1 ./scripts/automation/approve-and-run.sh TASK-TEST-010 '合同已复核,批准自动执行到人工验收阶段。' >/dev/null
763
+ run_fixture ./scripts/automation/transition-state.sh TASK-TEST-010 PENDING CODING coder-launcher 'preflight passed; capturing baseline' >/dev/null
764
+ run_fixture ./scripts/automation/block-task.sh TASK-TEST-010 'claim baseline capture interrupted before metadata sealing' >/dev/null
765
+ if run_fixture env AUTOMATION_SKIP_AGENT_RUN=1 ./scripts/automation/resume-task.sh TASK-TEST-010 '拒绝' >/dev/null 2>&1; then
766
+ fail 'baseline recovery accepted an invalid confirmation'
767
+ fi
768
+ [[ "$(jq -r '.state' "$runtime_root/state/TASK-TEST-010.json")" == "BLOCKED" ]] || fail 'rejected baseline recovery changed task state'
769
+ run_fixture env AUTOMATION_SKIP_AGENT_RUN=1 ./scripts/automation/resume-task.sh TASK-TEST-010 '恢复任务,重新捕获基线并继续自动执行。' >/dev/null
770
+ [[ "$(jq -r '.state' "$runtime_root/state/TASK-TEST-010.json")" == "PENDING" ]] || fail 'baseline recovery did not return to PENDING'
771
+ [[ "$(jq -s 'length' "$runtime_root/evidence/TASK-TEST-010/baseline-resumptions.jsonl")" -eq 1 ]] || fail 'baseline recovery did not record one bounded resumption'
772
+ [[ "$(jq -s -r '.[-1].kind' "$runtime_root/evidence/TASK-TEST-010/approvals.jsonl")" == "resume-baseline" ]] || fail 'baseline recovery approval was not audited'
773
+ [[ ! -f "$runtime_root/evidence/TASK-TEST-010/baseline.json" ]] || fail 'baseline recovery manufactured baseline evidence'
774
+ pass 'one explicitly approved baseline interruption resumes through PENDING without product changes'
775
+
776
+ run_fixture ./scripts/automation/transition-state.sh TASK-TEST-010 PENDING CODING coder-launcher 'preflight passed; capturing baseline' >/dev/null
777
+ run_fixture ./scripts/automation/block-task.sh TASK-TEST-010 'claim baseline capture interrupted a second time before metadata sealing' >/dev/null
778
+ if run_fixture env AUTOMATION_SKIP_AGENT_RUN=1 ./scripts/automation/resume-task.sh TASK-TEST-010 '恢复任务,重新捕获基线并继续自动执行。' >/dev/null 2>&1; then
779
+ fail 'baseline recovery exceeded its one-restart limit'
780
+ fi
781
+ [[ "$(jq -r '.state' "$runtime_root/state/TASK-TEST-010.json")" == "BLOCKED" ]] || fail 'exhausted baseline recovery changed task state'
782
+ pass 'baseline recovery stops after one auditable restart'
783
+ run_fixture ./scripts/automation/abort-task.sh TASK-TEST-010 '中止任务,封存修改并恢复原分支。' >/dev/null
784
+
748
785
  isolated_config_tmp="$(mktemp "$fixture/automation/.config.XXXXXX")"
749
786
  jq '.workspaceStrategy = "isolatedWorktree"' "$fixture/automation/config.json" > "$isolated_config_tmp"
750
787
  mv "$isolated_config_tmp" "$fixture/automation/config.json"