@frankzhang2026/opencode-android-orchestrator 0.8.1 → 0.10.0

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 (62) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +85 -14
  3. package/dist/cli.js +9 -1
  4. package/dist/cli.js.map +1 -1
  5. package/dist/config/commit-message-prefix.d.ts +31 -0
  6. package/dist/config/commit-message-prefix.d.ts.map +1 -0
  7. package/dist/config/commit-message-prefix.js +155 -0
  8. package/dist/config/commit-message-prefix.js.map +1 -0
  9. package/dist/config/verification-policy.d.ts +5 -3
  10. package/dist/config/verification-policy.d.ts.map +1 -1
  11. package/dist/config/verification-policy.js +32 -13
  12. package/dist/config/verification-policy.js.map +1 -1
  13. package/dist/doctor/index.d.ts +2 -0
  14. package/dist/doctor/index.d.ts.map +1 -1
  15. package/dist/doctor/index.js +2 -1
  16. package/dist/doctor/index.js.map +1 -1
  17. package/dist/doctor/installation.d.ts.map +1 -1
  18. package/dist/doctor/installation.js +78 -0
  19. package/dist/doctor/installation.js.map +1 -1
  20. package/dist/index.d.ts +2 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +2 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/installer/adaptive-templates.d.ts +8 -2
  25. package/dist/installer/adaptive-templates.d.ts.map +1 -1
  26. package/dist/installer/adaptive-templates.js +9 -1
  27. package/dist/installer/adaptive-templates.js.map +1 -1
  28. package/dist/installer/gradle-verification.d.ts +10 -1
  29. package/dist/installer/gradle-verification.d.ts.map +1 -1
  30. package/dist/installer/gradle-verification.js +192 -27
  31. package/dist/installer/gradle-verification.js.map +1 -1
  32. package/dist/installer/index.d.ts +1 -0
  33. package/dist/installer/index.d.ts.map +1 -1
  34. package/dist/installer/index.js +1 -0
  35. package/dist/installer/index.js.map +1 -1
  36. package/dist/installer/init.d.ts +4 -1
  37. package/dist/installer/init.d.ts.map +1 -1
  38. package/dist/installer/init.js +73 -14
  39. package/dist/installer/init.js.map +1 -1
  40. package/dist/installer/opencode-config.d.ts +3 -3
  41. package/dist/installer/opencode-config.d.ts.map +1 -1
  42. package/dist/installer/opencode-config.js +1 -1
  43. package/dist/installer/opencode-config.js.map +1 -1
  44. package/dist/installer/upgrade.d.ts +12 -1
  45. package/dist/installer/upgrade.d.ts.map +1 -1
  46. package/dist/installer/upgrade.js +127 -15
  47. package/dist/installer/upgrade.js.map +1 -1
  48. package/docs/MIGRATION.md +46 -12
  49. package/docs/SECURITY.md +14 -5
  50. package/docs/TROUBLESHOOTING.md +25 -11
  51. package/package.json +1 -1
  52. package/templates/README.md +9 -1
  53. package/templates/automation/config.json +2 -1
  54. package/templates/automation/config.schema.json +7 -2
  55. package/templates/scripts/automation/abort-task.sh +2 -1
  56. package/templates/scripts/automation/accept-and-integrate.sh +4 -2
  57. package/templates/scripts/automation/approve-and-run.sh +1 -0
  58. package/templates/scripts/automation/lib.sh +93 -2
  59. package/templates/scripts/automation/orchestrate-task.sh +1 -0
  60. package/templates/scripts/automation/preflight.sh +11 -0
  61. package/templates/scripts/automation/show-acceptance-review.sh +3 -0
  62. package/templates/scripts/automation/tests/run-tests.sh +59 -4
package/docs/SECURITY.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # Security model
2
2
 
3
3
  This document describes the security properties of
4
- `@frankzhang2026/opencode-android-orchestrator@0.8.1`. The lifecycle foundation
4
+ `@frankzhang2026/opencode-android-orchestrator@0.10.0`. The lifecycle foundation
5
5
  completed the real OpenCode `1.14.22` and `1.15.13` release matrix in `0.2.0`;
6
- `0.8.1` retains that compatibility boundary.
6
+ `0.10.0` retains that compatibility boundary.
7
7
 
8
8
  ## Security goals and non-goals
9
9
 
@@ -130,14 +130,23 @@ while Gradle settings/build files and orchestration resources remain protected.
130
130
  legacy configuration with no scope field as `primary`, preventing an implicit
131
131
  permission expansion.
132
132
 
133
- `unitTestsEnabled` and `lintEnabled` are the only operator-editable fields in
134
- the otherwise manifest-managed `automation/config.json`. Upgrade authenticates
133
+ `unitTestsEnabled`, `lintEnabled`, and `commitMessagePrefixMode` are the
134
+ operator-editable fields in the otherwise manifest-managed
135
+ `automation/config.json`. Upgrade authenticates
135
136
  the remaining generated content before preserving those values, and doctor
136
137
  validates the resulting adaptive configuration. Task agents still cannot edit
137
138
  the protected file. Unit tests default on and lint defaults off; disabling unit
138
139
  verification does not remove the mandatory RED evidence step. Assemble, scope,
139
140
  evidence, and required device-test gates are unaffected.
140
141
 
142
+ When commit prefix mode is `required`, the regular UTF-8 sidecar
143
+ `automation/automation-commit-prefix` must contain exactly one active line.
144
+ The installer creates a comments-only template without failing installation;
145
+ normal preflight and contract execution then block until a human fills it.
146
+ The file is not manifest-managed and is excluded from worktree, evidence, and
147
+ commit pathsets. Accepted and abort-recovery commits read it from the recorded
148
+ source worktree; manual Git commits remain outside this enforcement boundary.
149
+
141
150
  The optional repository-root `.automation-worktree-allowlist` is controlled by
142
151
  the local human operator, not by task agents. It accepts at most 256 exact,
143
152
  normalized repository-relative file paths in a regular file no larger than 64
@@ -157,7 +166,7 @@ writes the target package modes.
157
166
  ## Transaction and recovery safety
158
167
 
159
168
  `init` writes original-file backups before publishing a prepared manifest. It
160
- then applies validated files, runs the 44-case transaction suite and a shadow
169
+ then applies validated files, runs the 46-case transaction suite and a shadow
161
170
  run, verifies final hashes/modes, and only then marks the manifest installed.
162
171
  Failure before completion restores originals and removes safely unchanged new
163
172
  files.
@@ -1,7 +1,7 @@
1
1
  # Troubleshooting
2
2
 
3
3
  Use this guide for
4
- `@frankzhang2026/opencode-android-orchestrator@0.8.1`.
4
+ `@frankzhang2026/opencode-android-orchestrator@0.10.0`.
5
5
 
6
6
  ## Start with read-only evidence
7
7
 
@@ -11,7 +11,7 @@ From the repository root, capture:
11
11
  git status --short --branch
12
12
  git rev-parse HEAD
13
13
  opencode --version
14
- npx @frankzhang2026/opencode-android-orchestrator@0.8.1 doctor . --json
14
+ npx @frankzhang2026/opencode-android-orchestrator@0.10.0 doctor . --json
15
15
  ```
16
16
 
17
17
  If installation never completed, doctor will correctly report a missing or
@@ -39,9 +39,9 @@ command-scoped override:
39
39
 
40
40
  ```sh
41
41
  npm --registry=https://registry.npmjs.org/ view \
42
- @frankzhang2026/opencode-android-orchestrator@0.8.1 version
42
+ @frankzhang2026/opencode-android-orchestrator@0.10.0 version
43
43
  npx --yes --registry=https://registry.npmjs.org/ \
44
- @frankzhang2026/opencode-android-orchestrator@0.8.1 upgrade . --json
44
+ @frankzhang2026/opencode-android-orchestrator@0.10.0 upgrade . --json
45
45
  ```
46
46
 
47
47
  This leaves the company's saved npm configuration unchanged. Use the option
@@ -60,15 +60,18 @@ Git-backed Superpowers plugin at runtime.
60
60
  | `MODULE_SCOPE_INVALID` or an invalid `--module-scope` argument | The value is not `all` or `primary`. | Use `all` for the default all-module contract or `primary` for an intentional single-module restriction. |
61
61
  | `PRIMARY_MODULE_AMBIGUOUS` | Restrictive `primary` scope has multiple possible Android modules. | Supply an exact Gradle path, for example `--module-scope primary --primary-module :mobile`, or use the default `all` scope. |
62
62
  | `PRIMARY_MODULE_NOT_FOUND` | The selected Gradle path was not detected. | Use a module path reported by doctor/project detection; do not pass a filesystem directory. |
63
- | `GRADLE_DISCOVERY_FAILED` | The temporary read-only Gradle configuration failed, or registered tasks could not form every required verification group. | Run `./gradlew help --no-configuration-cache --console=plain` and fix the reported project/JDK/dependency issue. Use `--gradle-verification-config` only for an intentional nonstandard task policy, and keep that file inside the target repository. |
63
+ | `GRADLE_DISCOVERY_FAILED` | The temporary read-only Gradle configuration failed, or registered tasks could not form every required verification group. | Run `./gradlew help --no-configuration-cache --console=plain` and fix the reported project/JDK/dependency issue. Do not use the much larger `tasks --all` report as a discovery workaround. Use `--gradle-verification-config` only for an intentional nonstandard task policy. |
64
+ | A real task exists but `jq '.gradleVerification.focusedTestTasks | index(":component_me:testDebugUnitTest")' automation/config.json` prints `null` | A version through `0.8.1` generated configuration from a partial static module set, commonly because settings logic was dynamic or a convention plugin applied Android indirectly. | Upgrade with `--refresh-gradle-discovery`; do not add 30+ modules or tasks by hand. Then rerun the `jq` check and review the regenerated module/path/task diff. |
64
65
  | Unit tests did not run | `unitTestsEnabled` is false. | Set it to `true` in `automation/config.json`, commit the change, and rerun the gate. |
65
66
  | Unit tests run but should be skipped | `unitTestsEnabled` is true, which is the default. | Set it to `false` in `automation/config.json`; RED evidence remains mandatory. |
66
67
  | Lint did not run | `lintEnabled` is false, which is the default. | Set it to `true` in `automation/config.json`, commit the change, and rerun the gate. |
67
68
  | Lint runs but should be skipped | `lintEnabled` is true. | Set it to `false` in `automation/config.json` and commit the change. |
69
+ | Doctor warns that the commit prefix is unconfigured, or preflight says it is required | `commitMessagePrefixMode` defaults to `required`, and init/upgrade created a comments-only `automation/automation-commit-prefix` template. | Edit that file once and put the current company prefix on exactly one non-comment line. Do not start the task until preflight passes. |
70
+ | Commit-prefix validation reports multiple lines, whitespace, size, or a symlink | The human-owned prefix file is ambiguous or unsafe. | Replace it with a regular UTF-8 text file containing one active line of at most 256 bytes; blank and `#` comment lines are allowed. |
68
71
  | Invalid `--long-command-timeout-ms` | The value is not an integer from `120000` through `7200000`. | Use the `1800000` ms default or pass an intentional bounded value to `init`/`upgrade`; do not edit the generated config directly. |
69
72
  | Android SDK failure | No valid explicit SDK, `ANDROID_HOME`, `ANDROID_SDK_ROOT`, or `local.properties` `sdk.dir` was found. | Configure one real SDK root containing `platforms/` and `build-tools/`. Do not publish `local.properties`. |
70
73
  | Missing `git`, `jq`, `rg`, `shasum`, or Java | Required deterministic command is unavailable on `PATH`. | Install or restore the missing command, record its version, and rerun the read-only checks. |
71
- | `Bundled Orchestrator skill is unavailable` | The installed `0.8.1` package is incomplete, damaged, or loaded from an unsupported partial copy. | Reinstall the exact package, inspect its `resources/third-party/superpowers-v6.2.0/skills/` entries, restart OpenCode, and rerun `opencode debug skill`. Do not add an external Superpowers plugin as a fallback. |
74
+ | `Bundled Orchestrator skill is unavailable` | The installed `0.10.0` package is incomplete, damaged, or loaded from an unsupported partial copy. | Reinstall the exact package, inspect its `resources/third-party/superpowers-v6.2.0/skills/` entries, restart OpenCode, and rerun `opencode debug skill`. Do not add an external Superpowers plugin as a fallback. |
72
75
  | The exact Superpowers v6.2.0 plugin remains after upgrade | That entry existed in the verified pre-install OpenCode file and is therefore user-owned. | Leave it in place or remove it as a separate reviewed configuration change. Upgrade only removes the old Orchestrator-managed entry. |
73
76
 
74
77
  Version `0.6.0` always passes `--no-configuration-cache` to its temporary
@@ -77,12 +80,23 @@ Gradle task-discovery invocation. This overrides a repository-level
77
80
  the project's files or normal build behavior. Deleting the project's
78
81
  Configuration Cache is therefore neither required nor a durable repair.
79
82
 
83
+ Version `0.9.0` makes the same bounded Gradle invocation authoritative for
84
+ both modules and tasks. It checks applied `com.android.*` plugins after project
85
+ evaluation and reads `TaskContainer.names`, avoiding the volume and apparent
86
+ silence of `./gradlew tasks --all --console=plain | rg ...` in a large build.
87
+ For an existing installation, run:
88
+
89
+ ```sh
90
+ npx @frankzhang2026/opencode-android-orchestrator@0.10.0 upgrade . \
91
+ --refresh-gradle-discovery
92
+ ```
93
+
80
94
  Version `0.7.0` raises direct managed long-running lifecycle commands to at
81
95
  least `1800000` milliseconds. A higher timeout already supplied by the caller
82
96
  is preserved; unrelated Bash commands are unchanged. To configure one hour,
83
97
  run `upgrade . --long-command-timeout-ms 3600000` on a healthy installation.
84
98
  If a command still reports `120000 ms`, confirm that the project manifest and
85
- OpenCode plugin reference are both `0.8.1`, restart the OpenCode session so the
99
+ OpenCode plugin reference are both `0.10.0`, restart the OpenCode session so the
86
100
  plugin reloads, and rerun doctor before attempting recovery.
87
101
 
88
102
  After installation, inspect OpenCode discovery separately:
@@ -137,7 +151,7 @@ Common fail-closed codes include:
137
151
  | `PLUGIN_VERSION_CONFLICT` | The same managed package identity has another reference/version. | Review and remove or migrate only the obsolete entry; never let init silently replace it. |
138
152
  | `DUPLICATE_PLUGIN` or `DUPLICATE_PROPERTY` | Configuration identity is ambiguous. | Correct the JSON/JSONC structure without discarding unrelated fields or comments. |
139
153
  | `INVALID_JSONC` or `ROOT_NOT_OBJECT` | OpenCode configuration cannot be merged safely. | Repair the user-owned file and validate it before retrying. |
140
- | `AGENTS_BLOCK_CONFLICT` or `AGENTS_MARKERS_INVALID` | During `init`, the bounded block was modified; or a lifecycle command found partial, out-of-order, or duplicate markers. | Keep project-specific instructions outside one valid marker pair. `0.8.1 upgrade` preserves marker-external changes and replaces the old managed block; malformed marker structure still requires manual repair. |
154
+ | `AGENTS_BLOCK_CONFLICT` or `AGENTS_MARKERS_INVALID` | During `init`, the bounded block was modified; or a lifecycle command found partial, out-of-order, or duplicate markers. | Keep project-specific instructions outside one valid marker pair. `0.10.0 upgrade` preserves marker-external changes and replaces the old managed block; malformed marker structure still requires manual repair. |
141
155
  | `FILE_SYMLINK`, `TARGET_SYMLINK`, or `CONFIG_SYMLINK` | A managed target or ancestor is a symbolic link. | Replace it only after understanding ownership and destination. The installer intentionally does not follow it. |
142
156
  | `PLAN_STALE` or `TARGET_MODIFIED` | A file changed between planning and application. | Stop concurrent edits, inspect the diff, and rerun from a stable state. |
143
157
 
@@ -156,8 +170,8 @@ configuration.
156
170
  | --- | --- | --- |
157
171
  | `MANIFEST_MISSING`, `MANIFEST_INVALID`, or `MANIFEST_STATE` | No trustworthy installed manifest is available. | Do not invent a manifest or copy one from another project. Determine whether this is an uninstalled/manual setup or an interrupted transaction. |
158
172
  | `EXISTING_INSTALLATION_DIFFERENT` | `init` found another installed inventory/version. | Use `upgrade` for a healthy older manifest. |
159
- | `EXISTING_INSTALLATION_INVALID` or `INSTALLATION_INVALID` | Manifest, installed files, or required backups failed structural or content validation. In `0.8.0`, this also reported an active manifest whose mode was not exactly `0600`. | Preserve the project and `.automation-plugin/`; use `0.8.1 upgrade` for mode-only drift, and inspect other doctor details or recovery history. |
160
- | `INSTALLED_FILES_MODIFIED` | Upgrade found content/existence drift in an ordinary managed file or missing/modified backup content. | Move intentional customization out of ordinary managed paths or choose manual recovery. `0.8.1` separately merges user-owned `AGENTS.md` content and does not reject mode-only drift. |
173
+ | `EXISTING_INSTALLATION_INVALID` or `INSTALLATION_INVALID` | Manifest, installed files, or required backups failed structural or content validation. In `0.8.0`, this also reported an active manifest whose mode was not exactly `0600`. | Preserve the project and `.automation-plugin/`; use `0.10.0 upgrade` for mode-only drift, and inspect other doctor details or recovery history. |
174
+ | `INSTALLED_FILES_MODIFIED` | Upgrade found content/existence drift in an ordinary managed file or missing/modified backup content. | Move intentional customization out of ordinary managed paths or choose manual recovery. `0.10.0` separately merges user-owned `AGENTS.md` content and does not reject mode-only drift. |
161
175
  | `VERSION_DOWNGRADE_REFUSED` | Target package is older than the installed manifest. | Use a newer fixed package version; never edit the manifest version. |
162
176
  | `UPGRADE_IN_PROGRESS` or `UNINSTALL_IN_PROGRESS` | `.automation-plugin/upgrade.json` or `uninstall.json` records an unfinished transaction. | Inspect the marker and matching recovery directory. Do not delete the marker merely to retry. |
163
177
  | `POST_UPGRADE_VERIFICATION_FAILED` | New resources failed verification. | The implementation attempts a complete old-version rollback; verify the old manifest and inspect upgrade evidence. |
@@ -168,7 +182,7 @@ configuration.
168
182
  Installer control paths are:
169
183
 
170
184
  - active manifest: `.automation-plugin/manifest.json` (new manifests are
171
- written as `0600`; mode-only drift does not block `0.8.1 upgrade`);
185
+ written as `0600`; mode-only drift does not block `0.10.0 upgrade`);
172
186
  - first-install/original backups: `.automation-plugin/backups/<id>/`;
173
187
  - upgrade marker and snapshots: `.automation-plugin/upgrade.json` and
174
188
  `.automation-plugin/upgrades/<id>/`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frankzhang2026/opencode-android-orchestrator",
3
- "version": "0.8.1",
3
+ "version": "0.10.0",
4
4
  "description": "Reusable OpenCode orchestration for Android projects",
5
5
  "license": "MIT",
6
6
  "author": "frankzhang2026",
@@ -14,7 +14,7 @@ Migrated template roots:
14
14
  - `.opencode/skills`: the three `scheduled-quality-*` skills
15
15
  - `scripts/automation`: all 29 deterministic V3 Bash transactions and their
16
16
  test runner, preserved as executable files
17
- - `automation`: the portable V4 configuration render source, both JSON Schemas,
17
+ - `automation`: the portable V5 configuration render source, both JSON Schemas,
18
18
  and the task contract example
19
19
  - `docs/plans/README.md`: the human-approved plan authoring contract
20
20
  - `AGENTS.md.fragment`: a bounded managed block for non-destructive
@@ -43,6 +43,14 @@ policy. Verification switches live in `automation/config.json`:
43
43
  Discovered task lists are retained even while their corresponding gate is
44
44
  disabled.
45
45
 
46
+ Schema V5 also sets `commitMessagePrefixMode` to `required`. After managed
47
+ resources are written, init/upgrade creates the human-owned
48
+ `automation/automation-commit-prefix` comments-only template when missing.
49
+ That sidecar is not a packaged template or manifest-managed resource: users
50
+ fill one active line, may change it between commits, and upgrade/uninstall
51
+ preserve it. An unconfigured required file blocks task startup but remains a
52
+ warning during install/upgrade shadow verification.
53
+
46
54
  After read-only planning succeeds, `init` creates a comment-only
47
55
  `.automation-worktree-allowlist` if it is missing and preserves any existing
48
56
  regular file. This user-editable control file remains outside the managed
@@ -1,5 +1,5 @@
1
1
  {
2
- "schemaVersion": 4,
2
+ "schemaVersion": 5,
3
3
  "enabled": true,
4
4
  "mode": "orchestrated",
5
5
  "workspaceStrategy": "inPlaceExclusive",
@@ -10,6 +10,7 @@
10
10
  "maxReviewerRestarts": 2,
11
11
  "unitTestsEnabled": true,
12
12
  "lintEnabled": false,
13
+ "commitMessagePrefixMode": "required",
13
14
  "longCommandTimeoutMs": 1800000,
14
15
  "autoCleanupWorktrees": true,
15
16
  "pushAfterAcceptance": false,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "urn:frankzhang2026:opencode-android-orchestrator:automation-config:v4",
3
+ "$id": "urn:frankzhang2026:opencode-android-orchestrator:automation-config:v5",
4
4
  "title": "OpenCode task orchestration configuration",
5
5
  "type": "object",
6
6
  "additionalProperties": false,
@@ -38,6 +38,7 @@
38
38
  "maxReviewerRestarts",
39
39
  "unitTestsEnabled",
40
40
  "lintEnabled",
41
+ "commitMessagePrefixMode",
41
42
  "longCommandTimeoutMs",
42
43
  "autoCleanupWorktrees",
43
44
  "pushAfterAcceptance",
@@ -48,7 +49,7 @@
48
49
  "protectedPaths"
49
50
  ],
50
51
  "properties": {
51
- "schemaVersion": { "const": 4 },
52
+ "schemaVersion": { "const": 5 },
52
53
  "enabled": { "type": "boolean" },
53
54
  "mode": { "enum": ["shadow", "orchestrated"] },
54
55
  "workspaceStrategy": {
@@ -61,6 +62,10 @@
61
62
  "maxReviewerRestarts": { "type": "integer", "minimum": 0, "maximum": 3 },
62
63
  "unitTestsEnabled": { "type": "boolean", "default": true },
63
64
  "lintEnabled": { "type": "boolean", "default": false },
65
+ "commitMessagePrefixMode": {
66
+ "enum": ["required", "disabled"],
67
+ "default": "required"
68
+ },
64
69
  "longCommandTimeoutMs": {
65
70
  "type": "integer",
66
71
  "minimum": 120000,
@@ -86,8 +86,9 @@ if [[ -d "$task_root" ]]; then
86
86
  fi
87
87
  done
88
88
  if [[ "$product_path_count" -gt 0 ]]; then
89
+ commit_message="$(automation_commit_message_at "$source_root" "Archive aborted work for $task_id")"
89
90
  git -C "$task_root" add -- "${changed_paths[@]}"
90
- git -C "$task_root" commit --only -m "Archive aborted work for $task_id" -- "${changed_paths[@]}"
91
+ git -C "$task_root" commit --only -m "$commit_message" -- "${changed_paths[@]}"
91
92
  recovery_commit="$(git -C "$task_root" rev-parse HEAD)"
92
93
  else
93
94
  plan_rel="$(jq -er '.planPath' "$origin_file")"
@@ -74,6 +74,7 @@ sealed_diff_sha="$(automation_worktree_diff_sha "$task_root")"
74
74
  )
75
75
  report_file="$evidence_dir/acceptance-report.json"
76
76
  [[ "$(jq -er '.sealedDiffSha256' "$report_file")" == "$sealed_diff_sha" ]] || automation_die "acceptance report is stale"
77
+ automation_read_commit_message_prefix_at "$source_root" >/dev/null
77
78
 
78
79
  automation_acquire_run_lock "$task_id"
79
80
  integration_complete=0
@@ -132,9 +133,10 @@ while IFS= read -r path; do
132
133
  [[ -n "$path" ]] && commit_paths+=("$path")
133
134
  done < <(automation_changed_paths_at "$task_root")
134
135
  [[ "${#commit_paths[@]}" -ge 3 ]] || automation_die "final commit must contain product changes and both planning artifacts"
135
- git -C "$task_root" add -- "${commit_paths[@]}"
136
136
  title="$(jq -er '.title' "$task_root/automation/tasks/$task_id.json")"
137
- git -C "$task_root" commit --only -m "Implement $title ($task_id)" -- "${commit_paths[@]}"
137
+ commit_message="$(automation_commit_message_at "$source_root" "Implement $title ($task_id)")"
138
+ git -C "$task_root" add -- "${commit_paths[@]}"
139
+ git -C "$task_root" commit --only -m "$commit_message" -- "${commit_paths[@]}"
138
140
  product_commit="$(git -C "$task_root" rev-parse HEAD)"
139
141
  automation_worktree_is_clean "$task_root" || automation_die "task root is dirty after the combined task commit"
140
142
  git -C "$task_root" merge-base --is-ancestor "$baseline_head" "$product_commit" || automation_die "combined task commit is not based on the recorded pre-task baseline"
@@ -33,6 +33,7 @@ plan="$source_root/$plan_rel"
33
33
  [[ "$(git -C "$source_root" rev-parse HEAD)" == "$original_head" ]] || automation_die "original HEAD changed after contract review began"
34
34
  [[ "$(automation_file_sha256 "$contract")" == "$(jq -er '.contractSha256' "$origin_file")" ]] || automation_die "contract changed after proposal approval"
35
35
  [[ "$(automation_file_sha256 "$plan")" == "$(jq -er '.planSha256' "$origin_file")" ]] || automation_die "plan changed after proposal approval"
36
+ automation_read_commit_message_prefix_at "$source_root" >/dev/null
36
37
 
37
38
  changed_paths=()
38
39
  while IFS= read -r path; do
@@ -50,6 +50,9 @@ AUTOMATION_WORKSPACES_DIR="$AUTOMATION_RUNTIME_ROOT/workspaces"
50
50
  AUTOMATION_WORKTREE_ALLOWLIST_RELATIVE_PATH=".automation-worktree-allowlist"
51
51
  AUTOMATION_WORKTREE_ALLOWLIST_MAX_BYTES=65536
52
52
  AUTOMATION_WORKTREE_ALLOWLIST_MAX_ENTRIES=256
53
+ AUTOMATION_COMMIT_MESSAGE_PREFIX_RELATIVE_PATH="automation/automation-commit-prefix"
54
+ AUTOMATION_COMMIT_MESSAGE_PREFIX_FILE_MAX_BYTES=4096
55
+ AUTOMATION_COMMIT_MESSAGE_PREFIX_MAX_BYTES=256
53
56
 
54
57
  automation_info() {
55
58
  printf '[automation] %s\n' "$*"
@@ -111,7 +114,7 @@ automation_validate_config() {
111
114
  length > 0 and
112
115
  length == (unique | length) and
113
116
  all(.[]; gradle_task);
114
- .schemaVersion == 4 and
117
+ .schemaVersion == 5 and
115
118
  (.enabled | type == "boolean") and
116
119
  (.mode == "shadow" or .mode == "orchestrated") and
117
120
  (.workspaceStrategy == "inPlaceExclusive" or .workspaceStrategy == "isolatedWorktree") and
@@ -122,6 +125,7 @@ automation_validate_config() {
122
125
  (.maxReviewerRestarts | type == "number" and . >= 0 and . <= 3 and floor == .) and
123
126
  (.unitTestsEnabled | type == "boolean") and
124
127
  (.lintEnabled | type == "boolean") and
128
+ (.commitMessagePrefixMode == "required" or .commitMessagePrefixMode == "disabled") and
125
129
  (.longCommandTimeoutMs | type == "number" and . >= 120000 and . <= 7200000 and floor == .) and
126
130
  (.autoCleanupWorktrees | type == "boolean") and
127
131
  .pushAfterAcceptance == false and
@@ -246,6 +250,92 @@ automation_config_value() {
246
250
  jq -r "$query" "$AUTOMATION_CONFIG"
247
251
  }
248
252
 
253
+ automation_read_commit_message_prefix_at() {
254
+ local root="$1"
255
+ local config_file="$root/automation/config.json"
256
+ local prefix_file="$root/$AUTOMATION_COMMIT_MESSAGE_PREFIX_RELATIVE_PATH"
257
+ local mode byte_count line prefix prefix_bytes
258
+ local active_count=0
259
+
260
+ if [[ -L "$config_file" || ! -f "$config_file" ]]; then
261
+ automation_die "missing or unsafe automation configuration: $config_file"
262
+ return 1
263
+ fi
264
+ mode="$(jq -er '.commitMessagePrefixMode' "$config_file")" || {
265
+ automation_die "commitMessagePrefixMode is missing from $config_file"
266
+ return 1
267
+ }
268
+ case "$mode" in
269
+ disabled) return 0 ;;
270
+ required) ;;
271
+ *)
272
+ automation_die "unsupported commitMessagePrefixMode: $mode"
273
+ return 1
274
+ ;;
275
+ esac
276
+
277
+ if [[ ! -e "$prefix_file" && ! -L "$prefix_file" ]]; then
278
+ automation_die "commit-message prefix is required; fill $prefix_file before starting a task"
279
+ return 1
280
+ fi
281
+ if [[ -L "$prefix_file" || ! -f "$prefix_file" ]]; then
282
+ automation_die "commit-message prefix must be a regular file, not a symlink: $prefix_file"
283
+ return 1
284
+ fi
285
+ byte_count="$(wc -c < "$prefix_file" | tr -d '[:space:]')"
286
+ if [[ ! "$byte_count" =~ ^[0-9]+$ ]] || \
287
+ [[ "$byte_count" -gt "$AUTOMATION_COMMIT_MESSAGE_PREFIX_FILE_MAX_BYTES" ]]; then
288
+ automation_die "commit-message prefix file exceeds $AUTOMATION_COMMIT_MESSAGE_PREFIX_FILE_MAX_BYTES bytes: $prefix_file"
289
+ return 1
290
+ fi
291
+
292
+ while IFS= read -r line || [[ -n "$line" ]]; do
293
+ line="${line%$'\r'}"
294
+ [[ -z "$line" || "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]] && continue
295
+ active_count=$((active_count + 1))
296
+ if [[ "$active_count" -gt 1 ]]; then
297
+ automation_die "commit-message prefix must contain exactly one non-comment line: $prefix_file"
298
+ return 1
299
+ fi
300
+ if [[ "$line" == [[:space:]]* || "$line" == *[[:space:]] ]]; then
301
+ automation_die "commit-message prefix must not have leading or trailing whitespace"
302
+ return 1
303
+ fi
304
+ case "$line" in
305
+ *[[:cntrl:]]*)
306
+ automation_die "commit-message prefix must not contain control characters"
307
+ return 1
308
+ ;;
309
+ esac
310
+ prefix="$line"
311
+ done < "$prefix_file"
312
+
313
+ if [[ "$active_count" -eq 0 ]]; then
314
+ automation_die "commit-message prefix is not configured; fill $prefix_file before starting a task"
315
+ return 1
316
+ fi
317
+ prefix_bytes="$(printf '%s' "$prefix" | wc -c | tr -d '[:space:]')"
318
+ if [[ ! "$prefix_bytes" =~ ^[0-9]+$ ]] || \
319
+ [[ "$prefix_bytes" -gt "$AUTOMATION_COMMIT_MESSAGE_PREFIX_MAX_BYTES" ]]; then
320
+ automation_die "commit-message prefix exceeds $AUTOMATION_COMMIT_MESSAGE_PREFIX_MAX_BYTES UTF-8 bytes"
321
+ return 1
322
+ fi
323
+ printf '%s\n' "$prefix"
324
+ }
325
+
326
+ automation_commit_message_at() {
327
+ local root="$1"
328
+ local base_message="$2"
329
+ local prefix
330
+
331
+ prefix="$(automation_read_commit_message_prefix_at "$root")" || return 1
332
+ if [[ -n "$prefix" ]]; then
333
+ printf '%s %s\n' "$prefix" "$base_message"
334
+ else
335
+ printf '%s\n' "$base_message"
336
+ fi
337
+ }
338
+
249
339
  automation_validate_gradle_task() {
250
340
  local task="${1:-}"
251
341
  local safe_task_pattern='^([A-Za-z][A-Za-z0-9_.-]*|(:[A-Za-z0-9_.-]+)+)$'
@@ -453,7 +543,7 @@ automation_validate_worktree_allowlist_entry_at() {
453
543
  ;;
454
544
  esac
455
545
  case "$path" in
456
- /*|./*|.|..|.git|.git/*|docs/plans|docs/plans/*|"$AUTOMATION_WORKTREE_ALLOWLIST_RELATIVE_PATH")
546
+ /*|./*|.|..|.git|.git/*|docs/plans|docs/plans/*|"$AUTOMATION_WORKTREE_ALLOWLIST_RELATIVE_PATH"|"$AUTOMATION_COMMIT_MESSAGE_PREFIX_RELATIVE_PATH")
457
547
  automation_die "worktree allowlist entry is reserved or unsafe: $path"
458
548
  return 1
459
549
  ;;
@@ -608,6 +698,7 @@ automation_worktree_path_is_allowlisted() {
608
698
  local entry
609
699
 
610
700
  [[ "$path" == "$AUTOMATION_WORKTREE_ALLOWLIST_RELATIVE_PATH" ]] && return 0
701
+ [[ "$path" == "$AUTOMATION_COMMIT_MESSAGE_PREFIX_RELATIVE_PATH" ]] && return 0
611
702
  while IFS= read -r entry; do
612
703
  [[ -n "$entry" ]] || continue
613
704
  [[ "$path" == "$entry" ]] && return 0
@@ -19,6 +19,7 @@ source_root="$(jq -er '.sourceRoot' "$workspace_file")"
19
19
  if [[ "$(jq -r '.repositoryLeaseRequired // false' "$workspace_file")" == "true" ]]; then
20
20
  automation_require_repository_lease "$task_id" "$source_root" "$workspace_strategy"
21
21
  fi
22
+ automation_read_commit_message_prefix_at "$source_root" >/dev/null
22
23
 
23
24
  automation_acquire_run_lock "$task_id"
24
25
  trap 'automation_release_run_lock' EXIT
@@ -47,6 +47,17 @@ mode="$(automation_config_value '.mode')"
47
47
 
48
48
  [[ "$enabled" == "true" ]] || fail_or_warn "automation is disabled"
49
49
  [[ "$mode" == "orchestrated" ]] || fail_or_warn "automation mode is $mode, not orchestrated"
50
+ prefix_validation_error=""
51
+ prefix_root="$AUTOMATION_ROOT"
52
+ if [[ -n "$task_id" ]]; then
53
+ prefix_workspace_file="$(automation_workspace_path "$task_id")"
54
+ if [[ -f "$prefix_workspace_file" ]]; then
55
+ prefix_root="$(jq -er '.sourceRoot' "$prefix_workspace_file")"
56
+ fi
57
+ fi
58
+ if ! prefix_validation_error="$(automation_read_commit_message_prefix_at "$prefix_root" 2>&1 >/dev/null)"; then
59
+ fail_or_warn "$prefix_validation_error"
60
+ fi
50
61
 
51
62
  if [[ "${AUTOMATION_TEST_MODE:-0}" != "1" ]]; then
52
63
  [[ -n "${ANDROID_HOME:-}" ]] || fail_or_warn "ANDROID_HOME is not set; configure the Android SDK environment before running OpenCode"
@@ -45,6 +45,8 @@ report_diff_sha="$(jq -er '.sealedDiffSha256' "$report_file")"
45
45
  changed_count="$(jq -er '.changedPaths | length' "$report_file")"
46
46
  max_changed_files="$(jq -er '.maxChangedFiles' "$report_file")"
47
47
  device_tests_required="$(jq -r '.deviceTestsRequired' "$report_file")"
48
+ title="$(jq -er '.title' "$report_file")"
49
+ commit_message="$(automation_commit_message_at "$source_root" "Implement $title ($task_id)")"
48
50
 
49
51
  printf '# 🔔 人工验收提醒\n\n'
50
52
  printf '自动执行已停在 `AWAITING_HUMAN`。下面内容已重新核对 sealed diff;此时代码、计划与任务合同均尚未提交或集成。\n\n'
@@ -58,6 +60,7 @@ printf '| 任务分支收尾 | 成功集成后自动删除;失败或阻塞时
58
60
  printf '| sealed diff SHA | `%s` |\n' "$report_diff_sha"
59
61
  printf '| 变更范围 | 实际 %s 个 / 合同上限 %s 个 |\n' "$changed_count" "$max_changed_files"
60
62
  printf '| 提交策略 | 代码、测试、计划与任务合同合并为一个提交 |\n'
63
+ printf '| 预计提交信息 | `%s` |\n' "$commit_message"
61
64
  printf '| 自动证据 | Baseline ✓ · RED ✓ · G1–G6 ✓ · Reviewer APPROVED ✓ |\n'
62
65
  printf '\n## 必须重点复核\n\n'
63
66
  printf '### P0 · 真实行为是否满足合同\n\n'