@frankzhang2026/opencode-android-orchestrator 1.0.5 → 1.2.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 (109) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +79 -15
  3. package/dist/config/queue-policy.d.ts.map +1 -1
  4. package/dist/config/queue-policy.js +1 -2
  5. package/dist/config/queue-policy.js.map +1 -1
  6. package/dist/doctor/index.d.ts.map +1 -1
  7. package/dist/doctor/index.js +27 -3
  8. package/dist/doctor/index.js.map +1 -1
  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 +11 -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 +42 -13
  20. package/dist/installer/adaptive-templates.js.map +1 -1
  21. package/dist/installer/android-project.d.ts +3 -1
  22. package/dist/installer/android-project.d.ts.map +1 -1
  23. package/dist/installer/android-project.js.map +1 -1
  24. package/dist/installer/capabilities-script.d.ts +2 -0
  25. package/dist/installer/capabilities-script.d.ts.map +1 -0
  26. package/dist/installer/capabilities-script.js +84 -0
  27. package/dist/installer/capabilities-script.js.map +1 -0
  28. package/dist/installer/gradle-verification.d.ts.map +1 -1
  29. package/dist/installer/gradle-verification.js +13 -6
  30. package/dist/installer/gradle-verification.js.map +1 -1
  31. package/dist/installer/init.d.ts.map +1 -1
  32. package/dist/installer/init.js +3 -2
  33. package/dist/installer/init.js.map +1 -1
  34. package/dist/installer/opencode-config.d.ts +3 -3
  35. package/dist/installer/opencode-config.js +1 -1
  36. package/dist/installer/project-capabilities.d.ts +35 -0
  37. package/dist/installer/project-capabilities.d.ts.map +1 -0
  38. package/dist/installer/project-capabilities.js +82 -0
  39. package/dist/installer/project-capabilities.js.map +1 -0
  40. package/dist/installer/upgrade.d.ts.map +1 -1
  41. package/dist/installer/upgrade.js +10 -2
  42. package/dist/installer/upgrade.js.map +1 -1
  43. package/dist/queue/cli.d.ts.map +1 -1
  44. package/dist/queue/cli.js +32 -5
  45. package/dist/queue/cli.js.map +1 -1
  46. package/dist/queue/executor.d.ts +1 -0
  47. package/dist/queue/executor.d.ts.map +1 -1
  48. package/dist/queue/executor.js +160 -11
  49. package/dist/queue/executor.js.map +1 -1
  50. package/dist/queue/process-ownership.d.ts +12 -0
  51. package/dist/queue/process-ownership.d.ts.map +1 -0
  52. package/dist/queue/process-ownership.js +58 -0
  53. package/dist/queue/process-ownership.js.map +1 -0
  54. package/dist/queue/queue.d.ts +25 -1
  55. package/dist/queue/queue.d.ts.map +1 -1
  56. package/dist/queue/queue.js +96 -11
  57. package/dist/queue/queue.js.map +1 -1
  58. package/dist/queue/service.d.ts.map +1 -1
  59. package/dist/queue/service.js +19 -2
  60. package/dist/queue/service.js.map +1 -1
  61. package/dist/queue/storage.d.ts +3 -1
  62. package/dist/queue/storage.d.ts.map +1 -1
  63. package/dist/queue/storage.js +25 -15
  64. package/dist/queue/storage.js.map +1 -1
  65. package/dist/queue/supervision.d.ts +38 -0
  66. package/dist/queue/supervision.d.ts.map +1 -0
  67. package/dist/queue/supervision.js +247 -0
  68. package/dist/queue/supervision.js.map +1 -0
  69. package/dist/queue/tools.d.ts.map +1 -1
  70. package/dist/queue/tools.js +4 -2
  71. package/dist/queue/tools.js.map +1 -1
  72. package/docs/GRADLE-6.7.1.md +112 -0
  73. package/docs/MIGRATION.md +125 -11
  74. package/docs/QUEUE.md +350 -16
  75. package/docs/SECURITY.md +5 -2
  76. package/docs/TROUBLESHOOTING.md +81 -8
  77. package/package.json +1 -1
  78. package/templates/.opencode/agents/scheduled-coder.md +14 -1
  79. package/templates/.opencode/agents/scheduled-planner.md +64 -3
  80. package/templates/.opencode/skills/scheduled-quality-coder/SKILL.md +36 -3
  81. package/templates/.opencode/skills/scheduled-quality-orchestrator/SKILL.md +65 -4
  82. package/templates/.opencode/skills/scheduled-quality-reviewer/SKILL.md +23 -1
  83. package/templates/AGENTS.md.fragment +10 -4
  84. package/templates/README.md +3 -1
  85. package/templates/automation/config.schema.json +212 -22
  86. package/templates/automation/task-contract.schema.json +659 -49
  87. package/templates/automation/tasks/TASK-TEMPLATE.json.example +14 -8
  88. package/templates/automation/verification/collect.init.gradle +108 -0
  89. package/templates/automation/verification/contract.cjs +119 -0
  90. package/templates/automation/verification/inventory.cjs +536 -0
  91. package/templates/automation/verification/project.cjs +230 -0
  92. package/templates/automation/verification/recovery.cjs +425 -0
  93. package/templates/automation/verification/runtime.init.gradle +58 -0
  94. package/templates/scripts/automation/acceptance-report.sh +17 -0
  95. package/templates/scripts/automation/claim-task.sh +35 -2
  96. package/templates/scripts/automation/integration-scope-gate.sh +3 -3
  97. package/templates/scripts/automation/lib.sh +61 -19
  98. package/templates/scripts/automation/orchestrate-task.sh +7 -1
  99. package/templates/scripts/automation/preflight.sh +1 -2
  100. package/templates/scripts/automation/quality-gate.sh +10 -1
  101. package/templates/scripts/automation/record-red.sh +13 -1
  102. package/templates/scripts/automation/resume-task.sh +3 -0
  103. package/templates/scripts/automation/scope-gate.sh +4 -4
  104. package/templates/scripts/automation/show-acceptance-review.sh +1 -0
  105. package/templates/scripts/automation/status.sh +35 -0
  106. package/templates/scripts/automation/submit-review.sh +10 -1
  107. package/templates/scripts/automation/validate-contract.sh +10 -1
  108. package/templates/scripts/automation/verify-integration.sh +3 -0
  109. package/templates/scripts/automation/verify-task.sh +3 -0
@@ -0,0 +1,230 @@
1
+ "use strict";
2
+ // Shared validation for discovery, installation, doctor and managed Shell.
3
+ const { lstatSync } = require("node:fs");
4
+ const { dirname, isAbsolute, resolve } = require("node:path");
5
+ exports.validateProjectCapabilities = validateProjectCapabilities;
6
+ const fail = (message) => { throw new Error(`Invalid Android capability model: ${message}`); };
7
+ const object = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
8
+ const modulePath = (v) => typeof v === "string" && /^:(?:[A-Za-z0-9_.-]+(?::[A-Za-z0-9_.-]+)*)?$/.test(v);
9
+ const types = ["application", "library", "dynamic-feature", "test", "asset-pack", "jvm-library"];
10
+ function keys(value, expected) {
11
+ if (!object(value) || Object.keys(value).length !== expected.length || !expected.every(k => Object.hasOwn(value, k)))
12
+ fail("unexpected fields");
13
+ }
14
+ function safePath(root, value, pattern = false) {
15
+ if (typeof value !== "string" || !value || isAbsolute(value) || !/^[A-Za-z0-9._/*-]+$/.test(value))
16
+ fail("unsafe repository path");
17
+ const text = value;
18
+ const bare = pattern && text.endsWith("/**") ? text.slice(0, -3) : text;
19
+ if (bare.includes("*") || (bare !== "." && bare.split("/").some(p => !p || p === "." || p === ".." || [".git", ".gradle", ".automation-plugin", ".opencode"].includes(p))))
20
+ fail("unsafe repository path");
21
+ let path = resolve(root, bare);
22
+ for (; path !== resolve(root); path = dirname(path)) {
23
+ try {
24
+ if (lstatSync(path).isSymbolicLink()) fail("symbolic-link source or module path");
25
+ } catch (error) { if (error.code !== "ENOENT") throw error; }
26
+ if (dirname(path) === path)
27
+ fail("path outside repository");
28
+ }
29
+ return text;
30
+ }
31
+ const under = (parent, child) => parent === "." || child === parent || child.startsWith(`${parent}/`);
32
+ const base = (p) => p.endsWith("/**") ? p.slice(0, -3) : p;
33
+ const overlap = (a, b) => a === b || (a.endsWith("/**") && under(base(a), base(b))) || (b.endsWith("/**") && under(base(b), base(a)));
34
+ /** Validate persisted portable snapshots independently of Gradle output parsing. */
35
+ function validateProjectCapabilities(value, root) {
36
+ keys(value, ["version", "buildRoot", "modules"]);
37
+ if (value.version !== 1 || !Array.isArray(value.modules) || !value.modules.length)
38
+ fail("unsupported or empty model");
39
+ const buildRoot = safePath(root, value.buildRoot);
40
+ const modules = value.modules;
41
+ const ids = new Set();
42
+ for (const raw of modules) {
43
+ keys(raw, ["gradlePath", "directory", "buildFile", "type", "namespace", "applicationId", "dependencies", "sources", "tasks"]);
44
+ if (!modulePath(raw.gradlePath) || ids.has(raw.gradlePath))
45
+ fail("duplicate or invalid module identity");
46
+ ids.add(raw.gradlePath);
47
+ if (!types.includes(raw.type))
48
+ fail("unsupported module type");
49
+ for (const name of ["namespace", "applicationId"])
50
+ if (raw[name] !== null && (typeof raw[name] !== "string" || !raw[name]))
51
+ fail("invalid module identifier");
52
+ const directory = safePath(root, raw.directory), buildFile = safePath(root, raw.buildFile);
53
+ if (!under(buildRoot, directory) || !under(buildRoot, buildFile))
54
+ fail("module outside selected build");
55
+ if (!Array.isArray(raw.dependencies) || !raw.dependencies.every(modulePath) || new Set(raw.dependencies).size !== raw.dependencies.length)
56
+ fail("invalid dependencies");
57
+ if (!Array.isArray(raw.sources) || !Array.isArray(raw.tasks))
58
+ fail("missing sources or tasks");
59
+ const names = new Set();
60
+ for (const source of raw.sources) {
61
+ keys(source, ["name", "kind", "paths"]);
62
+ if (typeof source.name !== "string" || !/^[A-Za-z0-9_.-]+$/.test(source.name) || names.has(source.name))
63
+ fail("invalid source set");
64
+ names.add(source.name);
65
+ if (!["production", "test"].includes(source.kind) || !Array.isArray(source.paths))
66
+ fail("invalid source classification");
67
+ for (const p of source.paths) {
68
+ const path = safePath(root, p, true);
69
+ if (!under(buildRoot, base(path)) || base(path) === buildRoot || /(^|\/)(build|automation|scripts)(\/|$)/.test(path))
70
+ fail("generated or protected source root");
71
+ }
72
+ }
73
+ const tasks = new Set();
74
+ for (const task of raw.tasks) {
75
+ keys(task, ["path", "kind", "variant"]);
76
+ const prefix = raw.gradlePath === ":" ? ":" : `${raw.gradlePath}:`;
77
+ if (typeof task.path !== "string" || !task.path.startsWith(prefix) || !/^[A-Za-z][A-Za-z0-9_.-]*$/.test(task.path.slice(prefix.length)) || tasks.has(task.path))
78
+ fail("invalid task identity");
79
+ tasks.add(task.path);
80
+ if (!["unit", "assemble", "lint", "device"].includes(task.kind) || (task.variant !== null && (typeof task.variant !== "string" || !/^[A-Za-z0-9]+$/.test(task.variant))))
81
+ fail("invalid task capability");
82
+ }
83
+ }
84
+ const model = value;
85
+ const reached = new Set(model.modules.filter(m => m.type !== "jvm-library").map(m => m.gradlePath));
86
+ if (!reached.size)
87
+ fail("a standalone JVM build is unsupported");
88
+ for (let changed = true; changed;) {
89
+ changed = false;
90
+ for (const m of model.modules) {
91
+ for (const dep of m.dependencies) {
92
+ if (!ids.has(dep))
93
+ fail("unresolved project dependency");
94
+ if (reached.has(m.gradlePath) && !reached.has(dep)) {
95
+ reached.add(dep);
96
+ changed = true;
97
+ }
98
+ }
99
+ }
100
+ }
101
+ if (reached.size !== model.modules.length)
102
+ fail("unrelated JVM module");
103
+ const production = model.modules.flatMap(m => m.sources.filter(s => s.kind === "production").flatMap(s => s.paths));
104
+ const tests = model.modules.flatMap(m => m.sources.filter(s => s.kind === "test").flatMap(s => s.paths));
105
+ if (production.some(p => tests.some(t => overlap(p, t))))
106
+ fail("production and test roots overlap");
107
+ const protectedFiles = model.modules.map(m => m.buildFile);
108
+ if ([...production, ...tests].some(p => protectedFiles.some(f => overlap(p, f))))
109
+ fail("source roots contain build configuration");
110
+ return model;
111
+ }
112
+
113
+ exports.safePath = safePath;
114
+
115
+ function buildProtectedPaths(buildRoot) {
116
+ const prefix = buildRoot === "." ? "" : buildRoot + "/";
117
+ return ["gradle/**", "gradlew", "gradlew.bat", "settings.gradle", "settings.gradle.kts",
118
+ "build.gradle", "build.gradle.kts", "gradle.properties", "local.properties", "buildSrc/**", "build-logic/**"]
119
+ .map(p => prefix + p);
120
+ }
121
+ exports.buildProtectedPaths = buildProtectedPaths;
122
+
123
+ // The argument is always a repository/worktree root. Resolve the same portable
124
+ // build selection inside that checkout; never retain an absolute source path.
125
+ function gradleBuildRoot(config, root) {
126
+ validateConfigurationModel(config, root);
127
+ return resolve(root, config.androidProject?.capabilities?.buildRoot ?? ".");
128
+ }
129
+ exports.gradleBuildRoot = gradleBuildRoot;
130
+
131
+ function validateConfigurationModel(config, root) {
132
+ if (config.androidProject?.capabilities === undefined) return;
133
+ const model = validateProjectCapabilities(config.androidProject.capabilities, root);
134
+ const prefix = model.buildRoot === "." ? "" : model.buildRoot + "/";
135
+ if (![prefix + "settings.gradle", prefix + "settings.gradle.kts"].includes(config.androidProject.settingsFile))
136
+ fail("settings file differs from selected build");
137
+ // Legacy root snapshots retain their original policy. Nested builds require
138
+ // protection of their wrapper, settings, local environment and build logic.
139
+ if (model.buildRoot !== "." && buildProtectedPaths(model.buildRoot).some(p => !config.protectedPaths.includes(p)))
140
+ fail("selected build configuration is not protected");
141
+ const equal = (a, b) => JSON.stringify(a) === JSON.stringify(b);
142
+ const paths = kind => [...new Set(model.modules.flatMap(m => m.sources.filter(s => s.kind === kind).flatMap(s => s.paths)))];
143
+ for (const [key, kind] of [["productionPaths", "production"], ["testPaths", "test"]])
144
+ if (!equal(config.androidProject[key], paths(kind))) fail("source paths differ from capability snapshot");
145
+ const modules = model.modules.map(m => ({gradlePath:m.gradlePath, directory:m.directory, buildFile:m.buildFile,
146
+ dsl:m.buildFile.endsWith(".kts") ? "kotlin" : "groovy", type:m.type, namespace:m.namespace, applicationId:m.applicationId}));
147
+ if (!equal(config.androidProject.modules, modules)) fail("module identities differ from capability snapshot");
148
+ const groups = {fullUnitTestTasks:"unit", focusedTestTasks:"unit", assembleTasks:"assemble", lintTasks:"lint", deviceTestTasks:"device"};
149
+ for (const [group, kind] of Object.entries(groups)) {
150
+ const allowed = new Set(model.modules.flatMap(m => m.tasks.filter(t => t.kind === kind).map(t => t.path)));
151
+ if (!config.gradleVerification[group].every(t => allowed.has(t))) fail("verification task was not discovered: " + group);
152
+ }
153
+ for (const m of model.modules) {
154
+ const required = m.tasks.filter(t => t.kind === "unit");
155
+ if (required.some(t => !config.gradleVerification.fullUnitTestTasks.includes(t.path)))
156
+ fail("full verification omits module: " + m.gradlePath);
157
+ const builds = m.tasks.filter(t => t.kind === "assemble");
158
+ if (!builds.length || builds.some(t => !config.gradleVerification.assembleTasks.includes(t.path)))
159
+ fail("build verification omits module: " + m.gradlePath);
160
+ }
161
+ const sources = [...paths("production"), ...paths("test")];
162
+ if (sources.some(p => config.protectedPaths.some(f => overlap(p, f.endsWith("/") ? f + "**" : f)))) fail("source paths overlap protected configuration");
163
+ }
164
+ exports.validateConfigurationModel = validateConfigurationModel;
165
+
166
+ // Shared actual-runtime diagnostics. PATH java is only a launcher candidate;
167
+ // it is never substituted for the wrapper's build JVM attestation.
168
+ function parseBuildRuntime(output) {
169
+ const lines = output.split(/\r?\n/).filter(line => line.startsWith("ORCHESTRATOR_BUILD_RUNTIME="));
170
+ if (lines.length !== 1) throw new Error("Gradle did not produce exactly one actual build JVM record");
171
+ const value = JSON.parse(lines[0].slice("ORCHESTRATOR_BUILD_RUNTIME=".length));
172
+ if (value.version !== 1 || typeof value.gradleVersion !== "string" || !/^\d+\.\d+/.test(value.gradleVersion) ||
173
+ typeof value.javaVersion !== "string" || !/^(?:1\.)?\d+/.test(value.javaVersion) ||
174
+ typeof value.javaHome !== "string" || !isAbsolute(value.javaHome) || typeof value.javaVendor !== "string" ||
175
+ !Array.isArray(value.plugins) || !Array.isArray(value.tests)) throw new Error("Invalid actual Gradle/JVM record");
176
+ const java = Number(value.javaVersion.replace(/^1\./, "").match(/^\d+/)[0]);
177
+ if (/^6\.7(?:\.|$)/.test(value.gradleVersion) && (java < 8 || java > 15))
178
+ throw new Error("Gradle 6.7 requires an actual build JVM from Java 8 through 15");
179
+ for (const item of value.tests) if (!modulePath(item.task) || typeof item.executable !== "string" || !isAbsolute(item.executable))
180
+ throw new Error("Invalid test JVM record");
181
+ return value;
182
+ }
183
+ function inspectBuildRuntime(root, runner) {
184
+ const fs = require("node:fs"), path = require("node:path");
185
+ const executable = path.join(root, "gradlew");
186
+ const args = ["help", "--no-configuration-cache", "--console=plain", "--quiet", "--project-dir", root,
187
+ "--init-script", path.join(__dirname, "runtime.init.gradle")];
188
+ if (process.env.AUTOMATION_WORKER_TOKEN) args.push("--no-daemon");
189
+ const result = runner ? runner(executable, args) : require("node:child_process").spawnSync(executable, args,
190
+ { cwd: root, encoding: "utf8", timeout: 180000, maxBuffer: 8 * 1024 * 1024 });
191
+ if (result.status !== 0 || result.error) throw new Error(`Cannot verify the wrapper's actual Gradle/JVM: ${String(result.error ?? result.stderr ?? '').slice(-2000)}`);
192
+ return parseBuildRuntime(result.stdout);
193
+ }
194
+ function buildEnvironmentBinding(root, runtime) {
195
+ const fs = require("node:fs"), path = require("node:path"), crypto = require("node:crypto");
196
+ const hash = value => crypto.createHash("sha256").update(value).digest("hex");
197
+ const env = Object.fromEntries(["PATH", "JAVA_HOME", "JAVA_TOOL_OPTIONS", "JDK_JAVA_OPTIONS", "GRADLE_OPTS", "GRADLE_USER_HOME", "ANDROID_HOME", "ANDROID_SDK_ROOT"]
198
+ .map(key => [key, process.env[key] ?? null]));
199
+ const home = process.env.GRADLE_USER_HOME ?? path.join(require("node:os").homedir(), ".gradle");
200
+ const files = [path.join(home, "gradle.properties"), path.join(home, "init.gradle"), path.join(home, "init.gradle.kts"),
201
+ path.join(root, "gradle.properties"), path.join(root, "local.properties"), path.join(root, "gradle/gradle-daemon-jvm.properties")];
202
+ const init = path.join(home, "init.d");
203
+ if (fs.existsSync(init)) files.push(...fs.readdirSync(init).sort().filter(name => /\.gradle(?:\.kts)?$/.test(name)).map(name => path.join(init, name)));
204
+ const homes = [process.env.JAVA_HOME, runtime?.javaHome].filter(Boolean);
205
+ for (const javaHome of homes) files.push(path.join(javaHome, "bin/java"), path.join(javaHome, "release"));
206
+ if (runtime) files.push(...runtime.tests.map(test => test.executable));
207
+ const inputs = [...new Set(files)].sort().map(file => [file, fs.existsSync(file) ? hash(fs.readFileSync(file)) : null]);
208
+ return hash(JSON.stringify({ env, inputs }));
209
+ }
210
+ exports.parseBuildRuntime = parseBuildRuntime;
211
+ exports.inspectBuildRuntime = inspectBuildRuntime;
212
+ exports.buildEnvironmentBinding = buildEnvironmentBinding;
213
+ function assertIsolatedBuildEnvironment(root) {
214
+ const fs = require("node:fs"), path = require("node:path");
215
+ const local = path.join(root, "local.properties");
216
+ if (!fs.existsSync(local)) return;
217
+ if (fs.lstatSync(local).isSymbolicLink()) throw new Error("Isolated builds require a regular local.properties file");
218
+ const lines = fs.readFileSync(local, "utf8").split(/\r?\n/).filter(line => line.trim() && !/^\s*[#!]/.test(line));
219
+ if (!lines.every(line => /^\s*sdk\.dir\s*[=:]/.test(line) && !/\\$/.test(line)))
220
+ throw new Error("ISOLATED_ENVIRONMENT_UNDECLARED: local.properties contains non-SDK inputs. Configure them through reviewed tracked build configuration or use an explicitly approved fixed-branch task; ignored files are not copied to worktrees.");
221
+ }
222
+ exports.assertIsolatedBuildEnvironment = assertIsolatedBuildEnvironment;
223
+ if (require.main === module) {
224
+ try {
225
+ const config = JSON.parse(require("node:fs").readFileSync(process.argv[2], "utf8"));
226
+ const root = gradleBuildRoot(config, process.argv[3]);
227
+ if (process.argv[4] === "--build-root") console.log(root);
228
+ }
229
+ catch (error) { console.error(error.message); process.exitCode = 1; }
230
+ }