apphx 1.1.0 → 2.0.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 (121) hide show
  1. package/README.md +50 -182
  2. package/benchmarks/peer-context-worker.mjs +186 -0
  3. package/benchmarks/peer-context.mjs +150 -0
  4. package/dist/apple-types.js.map +1 -1
  5. package/dist/cli.js +370 -118
  6. package/dist/cli.js.map +1 -1
  7. package/dist/config.js +42 -4
  8. package/dist/config.js.map +1 -1
  9. package/dist/dependencies.js +455 -16
  10. package/dist/dependencies.js.map +1 -1
  11. package/dist/export-options.js +44 -2
  12. package/dist/export-options.js.map +1 -1
  13. package/dist/flutter.js +13 -25
  14. package/dist/flutter.js.map +1 -1
  15. package/dist/go-dependencies.js +389 -0
  16. package/dist/go-dependencies.js.map +1 -0
  17. package/dist/inbox.js +7 -8
  18. package/dist/inbox.js.map +1 -1
  19. package/dist/integrations.js +30 -3
  20. package/dist/integrations.js.map +1 -1
  21. package/dist/isolation.js +190 -0
  22. package/dist/isolation.js.map +1 -0
  23. package/dist/macos.js +57 -0
  24. package/dist/macos.js.map +1 -0
  25. package/dist/mygo-build.js +55 -0
  26. package/dist/mygo-build.js.map +1 -0
  27. package/dist/policy.js +22 -0
  28. package/dist/policy.js.map +1 -0
  29. package/dist/preflight.js +1 -1
  30. package/dist/process.js +3 -3
  31. package/dist/project-profiles.js +163 -0
  32. package/dist/project-profiles.js.map +1 -0
  33. package/dist/proof.js +33 -0
  34. package/dist/proof.js.map +1 -0
  35. package/dist/remote.js +1 -1
  36. package/dist/runner.js +269 -287
  37. package/dist/runner.js.map +1 -1
  38. package/dist/scheduler.js +0 -3
  39. package/dist/scheduler.js.map +1 -1
  40. package/dist/schemas.js +156 -44
  41. package/dist/schemas.js.map +1 -1
  42. package/dist/store.js +166 -44
  43. package/dist/store.js.map +1 -1
  44. package/dist/vendor/obfuscator/dist/cli.js +72 -0
  45. package/dist/vendor/obfuscator/dist/cli.js.map +1 -0
  46. package/dist/vendor/obfuscator/dist/code.js +585 -0
  47. package/dist/vendor/obfuscator/dist/code.js.map +1 -0
  48. package/dist/vendor/obfuscator/dist/common.js +262 -0
  49. package/dist/vendor/obfuscator/dist/common.js.map +1 -0
  50. package/dist/vendor/obfuscator/dist/config.js +123 -0
  51. package/dist/vendor/obfuscator/dist/config.js.map +1 -0
  52. package/dist/vendor/obfuscator/dist/content.js +89 -0
  53. package/dist/vendor/obfuscator/dist/content.js.map +1 -0
  54. package/dist/vendor/obfuscator/dist/obfuscate.js +93 -0
  55. package/dist/vendor/obfuscator/dist/obfuscate.js.map +1 -0
  56. package/dist/vendor/obfuscator/dist/png.js +283 -0
  57. package/dist/vendor/obfuscator/dist/png.js.map +1 -0
  58. package/dist/vendor/obfuscator/dist/project.js +159 -0
  59. package/dist/vendor/obfuscator/dist/project.js.map +1 -0
  60. package/dist/vendor/obfuscator/dist/resources.js +335 -0
  61. package/dist/vendor/obfuscator/dist/resources.js.map +1 -0
  62. package/dist/vendor/obfuscator/package.json +22 -0
  63. package/dist/workspace.js +113 -0
  64. package/dist/workspace.js.map +1 -0
  65. package/evals/control.mjs +73 -0
  66. package/evals/expectations.mjs +37 -0
  67. package/evals/host.mjs +51 -0
  68. package/evals/oracle.mjs +80 -0
  69. package/evals/policy.mjs +268 -0
  70. package/examples/android-java.factory.json +18 -0
  71. package/examples/android-java.json +16 -0
  72. package/examples/existing-project.factory.json +18 -0
  73. package/examples/existing-project.json +17 -0
  74. package/examples/expo.eas.json +17 -0
  75. package/examples/expo.factory.json +18 -0
  76. package/examples/expo.json +16 -0
  77. package/examples/factory.json +0 -2
  78. package/examples/flutter.factory.json +13 -0
  79. package/examples/flutter.json +16 -0
  80. package/examples/luggage.json +5 -3
  81. package/examples/mygo-web.factory.json +18 -0
  82. package/examples/mygo-web.json +16 -0
  83. package/examples/mygo.factory.json +18 -0
  84. package/examples/mygo.json +16 -0
  85. package/examples/react-native.factory.json +18 -0
  86. package/examples/react-native.json +16 -0
  87. package/examples/runtime/android-java/app/build.gradle +7 -0
  88. package/examples/runtime/android-java/app/src/main/AndroidManifest.xml +8 -0
  89. package/examples/runtime/android-java/app/src/main/java/dev/apphx/runtime/MainActivity.java +44 -0
  90. package/examples/runtime/android-java/sdk-build.mjs +29 -0
  91. package/examples/runtime/android-java/settings.gradle +2 -0
  92. package/examples/runtime/android-ui.mjs +70 -0
  93. package/examples/runtime/mygo-native/go.mod +13 -0
  94. package/examples/runtime/mygo-native/go.sum +10 -0
  95. package/examples/runtime/mygo-native/main.go +173 -0
  96. package/examples/runtime/mygo-native/main_test.go +14 -0
  97. package/examples/runtime/mygo-native/mygo.json +1 -0
  98. package/examples/runtime/mygo-web/bun.lock +15 -0
  99. package/examples/runtime/mygo-web/go.mod +9 -0
  100. package/examples/runtime/mygo-web/go.sum +8 -0
  101. package/examples/runtime/mygo-web/index.html +1 -0
  102. package/examples/runtime/mygo-web/main.go +60 -0
  103. package/examples/runtime/mygo-web/mygo.config.ts +10 -0
  104. package/examples/runtime/mygo-web/package.json +1 -0
  105. package/examples/runtime/mygo-web/src/main.ts +23 -0
  106. package/examples/runtime/mygo-web/src/mygo.ts +14 -0
  107. package/examples/swift.factory.json +18 -0
  108. package/examples/swift.json +16 -0
  109. package/examples/uni-app.factory.json +18 -0
  110. package/examples/uni-app.json +16 -0
  111. package/package.json +12 -5
  112. package/scripts/bundle-obfuscator.mjs +19 -0
  113. package/scripts/runtime-smoke.mjs +209 -0
  114. package/skills/apphx/SKILL.md +94 -17
  115. package/skills/apphx/resources/google-play-research-result.json +6 -0
  116. package/skills/apphx/resources/local-research-result.json +6 -0
  117. package/skills/apphx/resources/requirement.json +5 -8
  118. package/skills/apphx/resources/research-result.json +5 -14
  119. package/skills/apphx/resources/review-result.json +1 -1
  120. package/dist/apple.js +0 -900
  121. package/dist/apple.js.map +0 -1
package/dist/runner.js CHANGED
@@ -1,106 +1,29 @@
1
- import { ResearchSchema, ReviewSchema, toError } from "./schemas.js";
1
+ import { validateMyGoCommand, validateMyGoPackaging } from "./mygo-build.js";
2
+ import { ReviewSchema, toError, validateJobConfig } from "./schemas.js";
3
+ import { canonicalPath, expandArgv, inside } from "./config.js";
2
4
  import { executionEnvironment, processGroupAlive, run } from "./process.js";
3
- import { pollApple, shipApple } from "./apple.js";
4
- import { expandArgv, inside } from "./config.js";
5
+ import { closestOwnedGoWorkspace } from "./go-dependencies.js";
6
+ import { dependencyDigest, generatedModulePaths, isGeneratedPath } from "./dependencies.js";
7
+ import { artifactOutputPath, prepareWorkspace, projectDirectory, sourceDigest } from "./workspace.js";
5
8
  import { checkPrerequisites } from "./preflight.js";
9
+ import { prepareIsolatedCommand } from "./isolation.js";
6
10
  import { flutterEnvironment, prepareFlutter } from "./flutter.js";
7
- import { prepareExportOptions } from "./export-options.js";
8
- import { GENERATED_PATHS, dependencyDigest, isGeneratedPath, validateLocalDependencies } from "./dependencies.js";
11
+ import { prepareExportOptions, prepareNativeExportCommands } from "./export-options.js";
12
+ import { parseResearch, researchPrompt, reviewInstructions } from "./policy.js";
13
+ import { assertProject, buildInstructions } from "./project-profiles.js";
14
+ import { validateMacOSArtifact } from "./macos.js";
15
+ import { invalidateVerification } from "./proof.js";
16
+ import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
17
+ import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
18
+ import { fileURLToPath } from "node:url";
9
19
  import { createHash, randomUUID } from "node:crypto";
10
- import { delimiter, join, relative } from "node:path";
11
- import { constants, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync } from "node:fs";
12
20
  // src/runner.ts
13
- const REQUIRED_POLICY_RULES = [
14
- "2.1",
15
- "2.3.1",
16
- "2.5.19",
17
- "4.2",
18
- "4.2.6",
19
- "4.3",
20
- "5.1.1"
21
- ];
22
- function sourceDigest(root, omitGenerated = true) {
23
- if (lstatSync(root).isSymbolicLink()) throw new Error("A source or screenshot root cannot be a symlink");
24
- if (omitGenerated) {
25
- for (const path of GENERATED_PATHS) inside(root, path);
26
- validateLocalDependencies(root);
27
- }
28
- const hash = createHash("sha256");
29
- function visit(directory) {
30
- for (const entry of readdirSync(directory).sort()) {
31
- const path = join(directory, entry);
32
- if (omitGenerated && isGeneratedPath(relative(root, path))) continue;
33
- const stat = lstatSync(path);
34
- if (stat.isSymbolicLink()) throw new Error(`Source symlink is not permitted: ${relative(root, path)}`);
35
- if (stat.isDirectory()) visit(path);
36
- else if (stat.isFile()) hash.update(relative(root, path)).update("\0").update(`${stat.size}:${stat.mode & 511}\0`).update(readFileSync(path));
37
- }
38
- }
39
- visit(root);
40
- return hash.digest("hex");
41
- }
42
21
  function requireDependencyProof(job) {
43
22
  if (!job.verifiedDependencyDigest) throw Object.assign(/* @__PURE__ */ new Error("Dependency resolution proof is missing; retry this stopped task to re-run verification"), { code: "SETUP_REQUIRED" });
44
- if (dependencyDigest(job.workspace) !== job.verifiedDependencyDigest) throw Object.assign(/* @__PURE__ */ new Error("Dependency resolution changed since verification; re-verification is required"), { code: "EVIDENCE_CHANGED" });
45
- }
46
- function parseVerdict(text) {
47
- const content = text.trim().replace(/^```(?:json)?\s*\n?/, "").replace(/\n?```$/, "");
48
- try {
49
- return JSON.parse(content);
50
- } catch {
51
- throw new Error("Host did not return a JSON verdict; no approval was inferred from text or an exit code");
52
- }
53
- }
54
- function parseResearch(text) {
55
- const research = ResearchSchema.parse(parseVerdict(text));
56
- const cited = /* @__PURE__ */ new Set();
57
- for (const source of Array.isArray(research?.policySources) ? research.policySources : []) {
58
- if (typeof source?.url !== "string") continue;
59
- try {
60
- const url = new URL(source.url);
61
- if (url.protocol === "https:" && url.hostname === "developer.apple.com") cited.add(source.rule);
62
- } catch {}
63
- }
64
- if (typeof research?.viable !== "boolean" || !REQUIRED_POLICY_RULES.every((rule) => cited.has(rule)) || typeof research.distinctness !== "string" || !research.distinctness.trim() || !Array.isArray(research.plan) || !research.plan.length || research.plan.some((step) => typeof step !== "string" || !step.trim())) throw new Error("Research verdict requires boolean viability, all required official policy references, distinctness reasoning and a concrete implementation plan");
65
- return research;
66
- }
67
- function prepareWorkspace(store, job) {
68
- if (!job.baseJobId || job.basePrepared) return;
69
- const base = store.get(job.baseJobId);
70
- if (!job.baseDigest || sourceDigest(base.workspace) !== job.baseDigest) throw Object.assign(/* @__PURE__ */ new Error("Predecessor source changed since intake; refusing to copy unverified code"), { code: "SETUP_REQUIRED" });
71
- if (existsSync(job.workspace)) {
72
- if (sourceDigest(job.workspace) !== job.baseDigest) throw Object.assign(/* @__PURE__ */ new Error("Uninitialized successor workspace differs from its verified predecessor; inspect it before retrying"), { code: "SETUP_REQUIRED" });
73
- } else {
74
- mkdirSync(job.config.workspaceRoot, {
75
- recursive: true,
76
- mode: 448
77
- });
78
- const staging = mkdtempSync(join(job.config.workspaceRoot, `.${job.id}.base-`));
79
- const prepared = join(staging, "source");
80
- try {
81
- cpSync(base.workspace, prepared, {
82
- recursive: true,
83
- force: false,
84
- errorOnExist: true,
85
- mode: constants.COPYFILE_FICLONE,
86
- filter: (path) => !isGeneratedPath(relative(base.workspace, path))
87
- });
88
- if (sourceDigest(prepared) !== job.baseDigest || sourceDigest(base.workspace) !== job.baseDigest) throw new Error("Predecessor changed while copying; no successor was initialized");
89
- renameSync(prepared, job.workspace);
90
- } finally {
91
- rmSync(staging, {
92
- recursive: true,
93
- force: true
94
- });
95
- }
96
- }
97
- store.update(job.id, { basePrepared: true });
23
+ if (dependencyDigest(job.workspace, job.spec.framework, job.spec.project?.directory) !== job.verifiedDependencyDigest) throw Object.assign(/* @__PURE__ */ new Error("Dependency resolution changed since verification; re-verification is required"), { code: "EVIDENCE_CHANGED" });
98
24
  }
99
25
  function taskPrompt(job) {
100
- return `You are the native host of an embedded Flutter app Harness. Use your own context, tools and native delegation in ${job.workspace}; never launch another coding-agent CLI. Implement only this product. Never hide functionality from App Review or use obfuscation to imitate distinct utility. Do not upload, submit, push, read publisher credentials or change factory state. These instructions constrain this workflow, not arbitrary host tools: the workspace is not a security sandbox. Complete real implementations, not placeholders.\nImmutable product requirements:\n${JSON.stringify(job.spec, null, 2)}\nPrevious verification/review failure (repair the mechanism, never weaken acceptance):\n${job.feedback ?? job.error ?? "none"}\nPersisted execution logs:\n${JSON.stringify(job.logs.slice(-3))}\nContinue existing work. Only actual deterministic checks performed by the Harness establish verification; host claims do not.\n`;
101
- }
102
- function assertTurnPermission(job, options) {
103
- if (options.buildOnly && (job.config.allowSubmit || job.config.allowRelease)) throw new Error("--build-only refuses this task with snapshotted publication permission");
26
+ return `You are the native host of an embedded multi-stack app Harness. Use your own context, tools and native delegation in ${projectDirectory(job)}; never launch another coding-agent CLI. Preserve the requested framework and existing architecture. Implement only this product. Never hide functionality from users or reviewers. Do not upload, submit, push, read publisher credentials or change factory state. Remote build commands belong only to the explicitly authorized Harness packaging profile, not host turns. The workspace is not a security sandbox. Complete real implementations, not placeholders.\nImmutable product requirements:\n${JSON.stringify(job.spec, null, 2)}\nPrevious verification/review failure (repair the mechanism, never weaken acceptance):\n${job.feedback ?? job.error ?? "none"}\nPersisted execution logs:\n${JSON.stringify(job.logs.slice(-3))}\nContinue existing work. Only actual deterministic checks performed by the Harness establish verification; host claims do not.\n`;
104
27
  }
105
28
  function contextDigest(job) {
106
29
  return createHash("sha256").update(JSON.stringify({
@@ -123,9 +46,9 @@ function deadlineExpired(job) {
123
46
  }
124
47
  function requireReviewProof(job) {
125
48
  try {
126
- if (!job.verifiedDigest || sourceDigest(job.workspace) !== job.verifiedDigest) throw new Error("Source changed since verification; stale evidence cannot approve a release");
49
+ if (!job.verifiedDigest || sourceDigest(job.workspace, true, job.spec.framework, job.spec.project?.directory) !== job.verifiedDigest) throw new Error("Source changed since verification; stale evidence cannot approve a handoff");
127
50
  requireDependencyProof(job);
128
- if (job.spec.apple?.screenshotsDir && sourceDigest(inside(job.workspace, job.spec.apple.screenshotsDir), false) !== job.screenshotDigest) throw new Error("Screenshots changed since runtime verification; stale evidence cannot approve a release");
51
+ if (job.spec.screenshotsDir && sourceDigest(inside(projectDirectory(job), job.spec.screenshotsDir), false) !== job.screenshotDigest) throw new Error("Screenshots changed since runtime verification; stale evidence cannot approve a handoff");
129
52
  } catch (cause) {
130
53
  const error = toError(cause);
131
54
  if (error.code !== "SETUP_REQUIRED") error.code = "EVIDENCE_CHANGED";
@@ -136,17 +59,27 @@ function hostAction(store, job) {
136
59
  const turn = job.turn;
137
60
  let instruction;
138
61
  if (turn.stage === "research") {
139
- const portfolio = store.list().filter((value) => value.id !== job.id).map((value) => ({
62
+ const portfolio = store.list().filter((value) => value.id !== job.id && value.spec.channel === job.spec.channel && value.spec.id !== job.spec.id).map((value) => ({
140
63
  id: value.spec.id,
141
64
  purpose: value.spec.purpose,
142
65
  audience: value.spec.audience,
143
66
  differentiators: value.spec.differentiators
144
67
  }));
145
- instruction = `Research only with your own host tools. Retrieve the current official Apple review guidelines at https://developer.apple.com/app-store/review/guidelines/ and assess rules ${REQUIRED_POLICY_RULES.join(", ")}, offline persistence/backup and distinct utility against portfolio ${JSON.stringify(portfolio)}. Assess content ownership and submitting account compliance with rule 4.2.6; report unknowns rather than inventing attestations. Return a typed JSON result {"viable":boolean,"policySources":[{"rule":"4.2","url":"https://developer.apple.com/..."}],"distinctness":"specific reasoning","risks":["..."],"plan":["concrete step"]}. Cite all required rules. These host-supplied citations are not independently verified policy evidence. Do not promise App Store approval.`;
146
- } else if (turn.stage === "build") instruction = `Research plan: ${JSON.stringify(job.research)}\nCreate a complete iOS Flutter project in this cwd if absent. Implement the entire product, persistence and validated backup/restore, accessibility and realistic empty/error states. Include real integration_test and test_driver acceptance workflows and real simulator screenshots under the requested screenshots directory; never fake screenshots or bypass tests. Use the returned environment with your own tools. The configured SDK, if any, is external to source; keep development caches task-local. Frozen checks: ${JSON.stringify(job.config.verification)}. Complete this turn without a result payload when source is ready; the Harness will then run verification, not infer success.`;
147
- else instruction = `Use a native independent reviewer with a fresh context when your host supports one. Review actual source and acceptance tests read-only; do not edit files. If independent review is unavailable, disclose that limitation rather than claim a separate reviewer. Verified source: ${job.verifiedDigest}; dependency resolution: ${job.verifiedDependencyDigest}; actual checks: ${JSON.stringify(job.config.verification)}. Reject incomplete workflows, data loss, untested backup/corrupt import, fake screenshots, accessibility failures, placeholders, misleading legal claims, weak tests, hidden features and repetitive products. Return typed JSON {"approved":boolean,"findings":["blocking issue"],"acceptanceEvidence":[{"requirement":"exact acceptance string","evidence":"file/symbol + observed command or scenario"}]}. Cover every acceptance requirement. Host reviewer identity is advisory; the Harness gates only typed results and unchanged deterministic evidence.`;
148
- const environment = flutterEnvironment(job.config, job.workspace, { CI: "1" });
149
- if (job.config.toolchain) {
68
+ instruction = researchPrompt(job, portfolio);
69
+ } else if (turn.stage === "build") instruction = `Research plan: ${JSON.stringify(job.research)}\n${buildInstructions(job.spec)} Implement the entire requested product, accessibility and realistic empty/error states. Add genuine device/simulator acceptance workflows and real screenshots when requested; never fake screenshots or bypass tests. Do not add unrequested offline/backup/account features. Use the returned environment with your tools. Frozen verification and packaging: ${JSON.stringify({
70
+ verification: job.config.verification,
71
+ packaging: job.config.packaging,
72
+ packageTarget: job.config.packageTarget
73
+ })}. Complete this turn without a result payload when source is ready; the Harness will then execute verification, not infer success.`;
74
+ else instruction = reviewInstructions(job);
75
+ const environment = job.spec.framework === "flutter" ? flutterEnvironment(job.config, job.workspace, { CI: "1" }) : { CI: "1" };
76
+ if (job.spec.framework === "mygo") Object.assign(environment, {
77
+ GOENV: "off",
78
+ GOTOOLCHAIN: "local",
79
+ GOFLAGS: "-mod=readonly",
80
+ GOWORK: closestOwnedGoWorkspace(job.workspace, job.spec.project?.directory) ?? "off"
81
+ });
82
+ if (job.spec.framework === "flutter" && job.config.toolchain) {
150
83
  environment.FLUTTER_ROOT = job.config.toolchain.flutterSdk;
151
84
  environment.PATH = `${join(job.config.toolchain.flutterSdk, "bin")}${process.env.PATH ? `${delimiter}${process.env.PATH}` : ""}`;
152
85
  }
@@ -157,7 +90,7 @@ function hostAction(store, job) {
157
90
  id: turn.id,
158
91
  stage: turn.stage,
159
92
  prompt: taskPrompt(job) + instruction,
160
- cwd: job.workspace,
93
+ cwd: projectDirectory(job),
161
94
  environment: Object.fromEntries(Object.entries(environment).filter((entry) => entry[1] !== void 0))
162
95
  }
163
96
  };
@@ -166,15 +99,8 @@ function repair(job, message) {
166
99
  return {
167
100
  stage: "build",
168
101
  status: (job.attempts[job.stage] ?? 0) >= job.config.maxAttempts ? "failed" : "waiting",
102
+ ...invalidateVerification(),
169
103
  turn: null,
170
- review: null,
171
- verifiedDigest: null,
172
- verifiedDependencyDigest: null,
173
- screenshotDigest: null,
174
- verifiedAt: void 0,
175
- artifact: null,
176
- ipaPath: null,
177
- archiveDigest: null,
178
104
  feedback: message,
179
105
  error: message,
180
106
  nextAt: Date.now() + job.config.retryDelayMs
@@ -184,13 +110,11 @@ async function nextTurn(store, id, options = {}) {
184
110
  store.acquire();
185
111
  try {
186
112
  let job = store.get(id);
187
- assertTurnPermission(job, options);
188
113
  if (job.turn) {
189
114
  if (job.status !== "running" || job.stage !== job.turn.stage || contextDigest(job) !== job.turn.contextDigest) throw new Error("Active host turn context changed; abandon its token before requesting another turn");
190
115
  if (deadlineExpired(job)) {
191
116
  const claimed = job.turn;
192
117
  job = store.transition(id, (current) => {
193
- assertTurnPermission(current, options);
194
118
  if (current.turn?.id !== claimed.id || contextDigest(current) !== claimed.contextDigest) throw new Error("Host turn changed while checking its deadline; inspect the current task");
195
119
  return {
196
120
  turn: null,
@@ -212,9 +136,7 @@ async function nextTurn(store, id, options = {}) {
212
136
  if ([
213
137
  "failed",
214
138
  "ready",
215
- "built",
216
- "approved",
217
- "released"
139
+ "built"
218
140
  ].includes(job.status)) return {
219
141
  job,
220
142
  action: "done"
@@ -233,7 +155,6 @@ async function nextTurn(store, id, options = {}) {
233
155
  };
234
156
  if (deadlineExpired(job) || job.hostTurns >= job.config.maxHostTurns || (job.attempts[job.stage] ?? 0) >= job.config.maxAttempts) {
235
157
  job = store.transition(id, (current) => {
236
- assertTurnPermission(current, options);
237
158
  return {
238
159
  status: "failed",
239
160
  error: "Host turn, stage attempt or task wall-clock limit reached; no further host work will start"
@@ -247,7 +168,7 @@ async function nextTurn(store, id, options = {}) {
247
168
  try {
248
169
  if (job.stage === "review") requireReviewProof(job);
249
170
  prepareWorkspace(store, job);
250
- mkdirSync(job.workspace, {
171
+ mkdirSync(projectDirectory(job), {
251
172
  recursive: true,
252
173
  mode: 448
253
174
  });
@@ -263,7 +184,6 @@ async function nextTurn(store, id, options = {}) {
263
184
  };
264
185
  }
265
186
  job = store.transition(id, (current) => {
266
- assertTurnPermission(current, options);
267
187
  const eligible = ["queued", "waiting"].includes(current.status) || current.status === "blocked" && current.blockedReason?.kind === "prerequisite";
268
188
  if (current.turn || contextDigest(current) !== contextDigest(job) || current.nextAt > Date.now() || !eligible || current.groups.length || current.manualRecovery) throw new Error("Task changed before the host turn could be claimed");
269
189
  if (deadlineExpired(current) || current.hostTurns >= current.config.maxHostTurns || (current.attempts[current.stage] ?? 0) >= current.config.maxAttempts) return {
@@ -298,7 +218,6 @@ async function nextTurn(store, id, options = {}) {
298
218
  }
299
219
  function completeTurn(store, id, turnId, result, options = {}) {
300
220
  return store.transition(id, (job) => {
301
- assertTurnPermission(job, options);
302
221
  if (!job.turn || job.turn.id !== turnId || job.turn.stage !== job.stage || job.status !== "running") throw new Error("Stale, replayed or wrong-phase host turn; use next to inspect the active token");
303
222
  if (contextDigest(job) !== job.turn.contextDigest) throw new Error("Host turn context changed; abandon this stale token before continuing");
304
223
  if (job.groups.length || job.manualRecovery) throw new Error("Command recovery must finish before completing a host turn");
@@ -309,29 +228,21 @@ function completeTurn(store, id, turnId, result, options = {}) {
309
228
  };
310
229
  let patch;
311
230
  if (job.stage === "research") {
312
- const research = parseResearch(JSON.stringify(ResearchSchema.parse(result)));
231
+ const research = parseResearch(JSON.stringify(result), job.spec.channel);
313
232
  patch = research.viable ? {
314
233
  stage: "build",
315
234
  research
316
235
  } : {
317
236
  status: "failed",
318
237
  research,
319
- error: "Research rejected this product; no replacement product or review evasion will be attempted"
238
+ error: "Research rejected this product; no replacement product will be attempted"
320
239
  };
321
240
  } else if (job.stage === "build") {
322
241
  if (result !== void 0) throw new Error("Build completion accepts no result payload; actual deterministic verification is required");
323
- const project = inside(job.workspace, "pubspec.yaml");
324
- if (!lstatSync(project).isFile()) throw new Error("Build completion requires a regular pubspec.yaml in the task workspace");
242
+ assertProject(projectDirectory(job), job.spec);
325
243
  patch = {
326
- stage: "verify",
327
- verifiedDigest: null,
328
- verifiedDependencyDigest: null,
329
- screenshotDigest: null,
330
- verifiedAt: void 0,
331
- review: null,
332
- artifact: null,
333
- ipaPath: null,
334
- archiveDigest: null
244
+ ...invalidateVerification(),
245
+ stage: job.config.obfuscation ? "obfuscate" : "verify"
335
246
  };
336
247
  } else {
337
248
  const review = ReviewSchema.parse(result);
@@ -365,7 +276,6 @@ function completeTurn(store, id, turnId, result, options = {}) {
365
276
  }
366
277
  function abandonTurn(store, id, turnId, options = {}) {
367
278
  return store.transition(id, (job) => {
368
- assertTurnPermission(job, options);
369
279
  if (!job.turn || job.turn.id !== turnId) throw new Error("Stale or replayed host turn; no active claim was abandoned");
370
280
  if (job.groups.length || job.manualRecovery) throw new Error("Command recovery must finish before abandoning a host turn");
371
281
  store.event(id, {
@@ -406,30 +316,46 @@ async function executeStep(store, job, { signal } = {}) {
406
316
  status: "blocked",
407
317
  error: config.executionWarning ?? "Local execution is disabled. Explicitly enable allowLocalExecution only for trusted deterministic commands in a dedicated account or VM."
408
318
  });
409
- if (deadlineExpired(job) && !["publish", "monitor"].includes(job.stage)) return store.update(job.id, {
319
+ if (deadlineExpired(job)) return store.update(job.id, {
410
320
  status: "failed",
411
321
  error: "Task wall-clock limit reached; no further work will start"
412
322
  });
413
323
  const attemptNumber = (job.attempts[job.stage] ?? 0) + 1;
414
324
  let attempts = { ...job.attempts };
415
325
  let attemptCharged = false;
416
- const developerEnv = flutterEnvironment(config, job.workspace, executionEnvironment(config));
417
- const verifierEnv = flutterEnvironment(config, job.workspace, executionEnvironment(config), true);
326
+ const developerEnv = job.spec.framework === "flutter" ? flutterEnvironment(config, job.workspace, executionEnvironment(config)) : executionEnvironment(config);
327
+ const verifierEnv = job.spec.framework === "flutter" ? flutterEnvironment(config, job.workspace, executionEnvironment(config), true) : developerEnv;
418
328
  const environment = ["verify", "package"].includes(job.stage) ? verifierEnv : developerEnv;
419
329
  const logPath = join(config.stateDir, "logs", job.id, `${job.stage}-${attemptNumber}-${Date.now()}.log`);
420
330
  store.update(job.id, { logs: [...job.logs, logPath] });
421
- const exec = async (argv, options = {}) => {
331
+ const readableFiles = /* @__PURE__ */ new Set();
332
+ const trackedRun = async (argv, options = {}, capabilities = {}) => {
422
333
  let pid;
334
+ let isolated;
423
335
  try {
424
336
  const effectiveEnv = options.env ?? environment;
337
+ if (job.spec.framework === "mygo") validateMyGoCommand(config, argv);
425
338
  const command = effectiveEnv.FLUTTER_ROOT && ["flutter", "dart"].includes(argv[0]) ? [join(effectiveEnv.FLUTTER_ROOT, "bin", argv[0]), ...argv.slice(1)] : argv;
426
- const result = await run(command, {
427
- cwd: job.workspace,
339
+ const cwd = options.cwd ?? projectDirectory(job);
340
+ if (config.isolation && [
341
+ "obfuscate",
342
+ "verify",
343
+ "package"
344
+ ].includes(job.stage)) isolated = prepareIsolatedCommand(command, {
345
+ config,
346
+ workspace: job.workspace,
347
+ cwd,
428
348
  env: effectiveEnv,
349
+ readableFiles: [...readableFiles, ...capabilities.readableFiles ?? []],
350
+ writableDirectories: capabilities.writableDirectories
351
+ });
352
+ const result = await run(isolated?.argv ?? command, {
429
353
  timeoutMs: config.timeoutMs,
430
354
  signal,
431
355
  logPath,
432
356
  ...options,
357
+ cwd,
358
+ env: isolated?.env ?? effectiveEnv,
433
359
  onSpawn: (value, metadata) => {
434
360
  pid = value;
435
361
  const current = store.get(job.id);
@@ -452,33 +378,93 @@ async function executeStep(store, job, { signal } = {}) {
452
378
  }
453
379
  return result;
454
380
  } finally {
455
- const current = store.get(job.id);
456
- const groups = current.groups.filter(processGroupAlive);
457
- const commands = Object.fromEntries(Object.entries(current.commands ?? {}).filter(([pid]) => groups.includes(Number(pid))));
458
- store.update(job.id, {
459
- groups,
460
- commands,
461
- recoveryStage: groups.length ? current.recoveryStage ?? job.stage : void 0
381
+ try {
382
+ const current = store.get(job.id);
383
+ const groups = current.groups.filter(processGroupAlive);
384
+ const commands = Object.fromEntries(Object.entries(current.commands ?? {}).filter(([pid]) => groups.includes(Number(pid))));
385
+ store.update(job.id, {
386
+ groups,
387
+ commands,
388
+ recoveryStage: groups.length ? current.recoveryStage ?? job.stage : void 0
389
+ });
390
+ } finally {
391
+ isolated?.cleanup();
392
+ }
393
+ }
394
+ };
395
+ const exec = (argv, options) => trackedRun(argv, options);
396
+ const utilityExec = (argv, options) => {
397
+ if (!config.isolation) return trackedRun(argv, options);
398
+ if (argv[0] !== "/usr/bin/plutil") throw Object.assign(/* @__PURE__ */ new Error("Unexpected isolated export utility"), { code: "SETUP_REQUIRED" });
399
+ const file = resolve(options?.cwd ?? projectDirectory(job), argv[argv.length - 1]);
400
+ const actual = canonicalPath(file), state = canonicalPath(config.stateDir), directory = dirname(actual);
401
+ const within = (root, target) => {
402
+ const path = relative(root, target);
403
+ return path === "" || path !== ".." && !path.startsWith(`..${sep}`) && !isAbsolute(path);
404
+ };
405
+ if (within(job.workspace, file) && !within(job.workspace, actual) && !config.isolation.readOnlyPaths.some((root) => within(root, actual))) throw Object.assign(/* @__PURE__ */ new Error("Workspace export options cannot grant access to an unapproved external file"), { code: "SETUP_REQUIRED" });
406
+ const snapshot = dirname(directory) === state && basename(directory).startsWith("export-options-") && basename(actual) === "ExportOptions.plist";
407
+ if (within(state, actual) && !snapshot || within(canonicalPath(config.workspaceRoot), actual) && !within(job.workspace, actual) || config.configPath && actual === canonicalPath(config.configPath)) throw Object.assign(/* @__PURE__ */ new Error("Export utility cannot read protected control state or another task"), { code: "SETUP_REQUIRED" });
408
+ const writing = argv[1] === "-convert" && argv[2] === "xml1";
409
+ if (writing && !snapshot) throw Object.assign(/* @__PURE__ */ new Error("Export utility writes require a harness-owned snapshot"), { code: "SETUP_REQUIRED" });
410
+ return trackedRun([...argv.slice(0, -1), actual], options, {
411
+ readableFiles: within(job.workspace, actual) ? [] : [actual],
412
+ writableDirectories: writing ? [directory] : []
413
+ });
414
+ };
415
+ const removeArtifact = async (output) => {
416
+ if (!config.isolation) {
417
+ rmSync(output, {
418
+ recursive: true,
419
+ force: true
462
420
  });
421
+ return;
463
422
  }
423
+ const result = await exec([
424
+ "/bin/rm",
425
+ "-rf",
426
+ "--",
427
+ output
428
+ ]);
429
+ if (result.code !== 0) throw Object.assign(/* @__PURE__ */ new Error(`Isolated artifact cleanup was denied: ${result.stderr || result.stdout}`), { code: "SETUP_REQUIRED" });
464
430
  };
465
431
  try {
432
+ validateJobConfig(job.spec, config);
433
+ if (job.spec.framework === "mygo") validateMyGoPackaging(config);
466
434
  prepareWorkspace(store, job);
467
- mkdirSync(job.workspace, {
435
+ mkdirSync(projectDirectory(job), {
468
436
  recursive: true,
469
437
  mode: 448
470
438
  });
439
+ if (job.spec.framework === "mygo") {
440
+ Object.assign(developerEnv, {
441
+ GOENV: "off",
442
+ GOTOOLCHAIN: "local",
443
+ GOFLAGS: "-mod=readonly",
444
+ GOWORK: closestOwnedGoWorkspace(job.workspace, job.spec.project?.directory) ?? "off",
445
+ GOCACHE: developerEnv.GOCACHE ?? join(config.stateDir, "verification-cache", "go-build"),
446
+ GOMODCACHE: developerEnv.GOMODCACHE ?? join(config.stateDir, "verification-cache", "go-mod")
447
+ });
448
+ for (const key of [
449
+ "MYGO_UPDATER_PRIVATE_KEY",
450
+ "MYGO_WINDOWS_CERTIFICATE_PASSWORD",
451
+ "MYGO_CLI_BINARY",
452
+ "MYGO_LDFLAGS",
453
+ "MYGO_GENERATE",
454
+ "MYGO_INSPECTOR"
455
+ ]) delete developerEnv[key];
456
+ }
471
457
  if (job.stage === "verify" || job.stage === "package") await checkPrerequisites(config.verification.prerequisites ?? [], {
472
- cwd: job.workspace,
458
+ cwd: projectDirectory(job),
473
459
  env: environment,
474
460
  run: exec,
475
461
  signal
476
462
  });
477
- if ((job.attempts[job.stage] ?? 0) >= config.maxAttempts && !["publish", "monitor"].includes(job.stage)) return store.update(job.id, {
463
+ if ((job.attempts[job.stage] ?? 0) >= config.maxAttempts) return store.update(job.id, {
478
464
  status: "failed",
479
465
  error: `Attempt limit reached at ${job.stage}`
480
466
  });
481
- if (config.toolchain && ["verify", "package"].includes(job.stage)) {
467
+ if (job.spec.framework === "flutter" && config.toolchain && ["verify", "package"].includes(job.stage)) {
482
468
  const toolchain = await prepareFlutter(job, {
483
469
  run: exec,
484
470
  env: developerEnv,
@@ -493,7 +479,7 @@ async function executeStep(store, job, { signal } = {}) {
493
479
  store.update(job.id, { attempts });
494
480
  attemptCharged = true;
495
481
  if (job.stage === "verify") {
496
- if (config.toolchain) {
482
+ if (job.spec.framework === "flutter" && config.toolchain) {
497
483
  const dependencies = await exec([
498
484
  "flutter",
499
485
  "pub",
@@ -501,147 +487,163 @@ async function executeStep(store, job, { signal } = {}) {
501
487
  ]);
502
488
  if (dependencies.code !== 0) throw new Error(`Verification dependency resolution failed: ${dependencies.stderr.slice(-4e3)}`);
503
489
  }
504
- const before = sourceDigest(job.workspace);
505
- const dependenciesBefore = dependencyDigest(job.workspace);
490
+ const before = sourceDigest(job.workspace, true, job.spec.framework, job.spec.project?.directory);
491
+ const dependenciesBefore = dependencyDigest(job.workspace, job.spec.framework, job.spec.project?.directory);
506
492
  for (const argv of [...config.verification.checks, ...config.verification.runtime]) {
507
493
  const result = await exec(expandArgv(argv, executionEnvironment(config)));
508
494
  if (result.code !== 0) throw new Error(`Verification failed (${result.code}) ${JSON.stringify(argv)}:\n${result.stderr.slice(-5e3)}\n${result.stdout.slice(-5e3)}`);
509
495
  }
510
- if (sourceDigest(job.workspace) !== before) throw new Error("Verification changed source files; the new revision must be verified again");
511
- if (dependencyDigest(job.workspace) !== dependenciesBefore) throw new Error("Verification changed dependency resolution; the new resolution must be verified again");
496
+ if (sourceDigest(job.workspace, true, job.spec.framework, job.spec.project?.directory) !== before) throw new Error("Verification changed source files; the new revision must be verified again");
497
+ if (dependencyDigest(job.workspace, job.spec.framework, job.spec.project?.directory) !== dependenciesBefore) throw new Error("Verification changed dependency resolution; the new resolution must be verified again");
512
498
  return next(store, job, "review", {
513
499
  verifiedDigest: before,
514
500
  verifiedDependencyDigest: dependenciesBefore,
515
- screenshotDigest: job.spec.apple?.screenshotsDir ? sourceDigest(inside(job.workspace, job.spec.apple.screenshotsDir), false) : null,
501
+ screenshotDigest: job.spec.screenshotsDir ? sourceDigest(inside(projectDirectory(job), job.spec.screenshotsDir), false) : null,
516
502
  verifiedAt: (/* @__PURE__ */ new Date()).toISOString()
517
503
  });
518
504
  }
505
+ if (job.stage === "obfuscate") {
506
+ const obfuscation = job.config.obfuscation;
507
+ if (!obfuscation) throw Object.assign(/* @__PURE__ */ new Error("Obfuscation stage requires config.obfuscation"), { code: "SETUP_REQUIRED" });
508
+ const expanded = expandArgv(obfuscation.command, environment);
509
+ const bundled = fileURLToPath(new URL("./vendor/obfuscator/dist/cli.js", import.meta.url));
510
+ const argv = [
511
+ ...expanded[0] === "obfuscator" && existsSync(bundled) ? [
512
+ process.execPath,
513
+ bundled,
514
+ ...expanded.slice(1)
515
+ ] : expanded,
516
+ "--project",
517
+ projectDirectory(job),
518
+ "--config",
519
+ obfuscation.configPath,
520
+ "--framework",
521
+ job.spec.framework,
522
+ ...obfuscation.seed ? ["--seed", obfuscation.seed] : []
523
+ ];
524
+ const result = await trackedRun(argv, {}, { readableFiles: [obfuscation.configPath] });
525
+ if (result.code !== 0) throw new Error(`Obfuscation failed (${result.code}) ${JSON.stringify(argv)}:\n${result.stderr.slice(-5e3)}\n${result.stdout.slice(-5e3)}`);
526
+ return next(store, job, "verify");
527
+ }
519
528
  if (job.stage === "package") {
520
529
  requireReviewProof(job);
521
530
  if (!job.review?.approved || job.review.findings.length) throw new Error("Packaging requires an approved host review");
522
- if (process.platform !== "darwin") throw Object.assign(/* @__PURE__ */ new Error("iOS packaging requires the Mac worker; use the SSH controller from Linux"), { code: "SETUP_REQUIRED" });
523
531
  const target = config.packageTarget;
524
- if (config.allowSubmit && target !== "ipa") throw Object.assign(/* @__PURE__ */ new Error("Only a verified IPA can enter publication"), { code: "SETUP_REQUIRED" });
525
- const output = inside(job.workspace, target === "ipa" ? "build/ios/ipa" : "build/ios/iphonesimulator");
526
- if (target === "ipa" && !config.apple?.exportOptionsPlist) throw Object.assign(/* @__PURE__ */ new Error("Configure apple.exportOptionsPlist and valid local signing before IPA packaging"), { code: "SETUP_REQUIRED" });
527
- const apple = job.spec.apple;
528
- if (target === "ipa" && (!apple?.version || !apple.buildNumber)) throw Object.assign(/* @__PURE__ */ new Error("apple.version and apple.buildNumber are required for signing"), { code: "SETUP_REQUIRED" });
529
- let exportOptions;
530
- let built;
531
- try {
532
- if (target === "ipa") exportOptions = await prepareExportOptions(config.apple.exportOptionsPlist, config.stateDir, exec);
533
- store.update(job.id, {
534
- artifact: null,
535
- ipaPath: null,
536
- archiveDigest: null
537
- });
538
- rmSync(output, {
539
- recursive: true,
540
- force: true
541
- });
542
- built = await exec(target === "ipa" ? [
543
- "flutter",
544
- "build",
545
- "ipa",
546
- "--release",
547
- "--build-name",
548
- apple.version,
549
- "--build-number",
550
- apple.buildNumber,
551
- "--export-options-plist",
552
- exportOptions.path
553
- ] : [
554
- "flutter",
555
- "build",
556
- "ios",
557
- "--simulator",
558
- "--debug",
559
- "--no-pub"
560
- ]);
561
- } finally {
562
- if (exportOptions) rmSync(exportOptions.directory, {
563
- recursive: true,
564
- force: true
565
- });
532
+ const project = projectDirectory(job);
533
+ const profile = config.packaging;
534
+ if (job.cloudBuildPending) throw Object.assign(/* @__PURE__ */ new Error("A previous cloud build may have remote effects. Reconcile it with the provider before creating a new task; this build will not be replayed."), { code: "SETUP_REQUIRED" });
535
+ if (["ios", "macos"].includes(job.spec.platform) && profile?.provider !== "cloud" && process.platform !== "darwin") throw Object.assign(/* @__PURE__ */ new Error(`Local ${job.spec.platform} packaging requires a macOS worker`), { code: "SETUP_REQUIRED" });
536
+ let path;
537
+ store.update(job.id, {
538
+ artifact: null,
539
+ ipaPath: null,
540
+ archiveDigest: null
541
+ });
542
+ if (profile) {
543
+ const output = artifactOutputPath(project, profile.artifactPath);
544
+ const outputRelative = relative(job.workspace, output).split(sep).join("/");
545
+ let generated = isGeneratedPath(profile.artifactPath, job.spec.framework);
546
+ if (!generated) {
547
+ for (const module of generatedModulePaths(job.workspace, job.spec.framework, job.spec.project?.directory)) if (outputRelative.startsWith(`${module}/`)) {
548
+ generated = true;
549
+ break;
550
+ }
551
+ }
552
+ if (!generated || !/(?:^|\/)(?:build|dist|unpackage)(?:\/|$)/.test(profile.artifactPath) || /(?:^|\/)(?:node_modules|\.git|\.gradle|\.factory-cache|Pods)(?:\/|$)/.test(profile.artifactPath)) throw Object.assign(/* @__PURE__ */ new Error("packaging.artifactPath must name an exact generated build/dist/unpackage artifact, never source or dependency caches"), { code: "SETUP_REQUIRED" });
553
+ if (!output.endsWith(target === "simulator" || target === "app" ? ".app" : `.${target}`)) throw Object.assign(/* @__PURE__ */ new Error(`packaging.artifactPath must end in ${target === "simulator" || target === "app" ? ".app" : `.${target}`}`), { code: "SETUP_REQUIRED" });
554
+ const prepared = await prepareNativeExportCommands(profile.commands.map((argv) => expandArgv(argv, executionEnvironment(config))), project, config.stateDir, utilityExec);
555
+ try {
556
+ for (const file of prepared.readableFiles) readableFiles.add(file);
557
+ await removeArtifact(output);
558
+ if (profile.provider === "cloud") store.update(job.id, { cloudBuildPending: true });
559
+ for (const argv of prepared.commands) {
560
+ const result = await exec(argv);
561
+ if (result.code !== 0) throw new Error(`Packaging failed (${result.code}) ${JSON.stringify(argv)}:\n${result.stderr.slice(-5e3)}\n${result.stdout.slice(-5e3)}`);
562
+ }
563
+ path = artifactOutputPath(project, profile.artifactPath);
564
+ } finally {
565
+ for (const file of prepared.readableFiles) readableFiles.delete(file);
566
+ prepared.cleanup();
567
+ }
568
+ } else {
569
+ const output = artifactOutputPath(project, target === "ipa" ? "build/ios/ipa" : target === "simulator" ? "build/ios/iphonesimulator" : target === "apk" ? "build/app/outputs/flutter-apk" : "build/app/outputs/bundle/release");
570
+ if (target === "ipa" && !config.apple?.exportOptionsPlist) throw Object.assign(/* @__PURE__ */ new Error("Configure apple.exportOptionsPlist and valid local signing before IPA packaging"), { code: "SETUP_REQUIRED" });
571
+ if (target === "ipa" && !job.spec.apple?.buildNumber) throw Object.assign(/* @__PURE__ */ new Error("apple.buildNumber is required for IPA signing"), { code: "SETUP_REQUIRED" });
572
+ let exportOptions;
573
+ try {
574
+ if (target === "ipa") {
575
+ exportOptions = await prepareExportOptions(config.apple.exportOptionsPlist, config.stateDir, utilityExec);
576
+ readableFiles.add(exportOptions.path);
577
+ }
578
+ await removeArtifact(output);
579
+ const built = await exec(target === "ipa" ? [
580
+ "flutter",
581
+ "build",
582
+ "ipa",
583
+ "--release",
584
+ "--build-name",
585
+ job.spec.version,
586
+ "--build-number",
587
+ job.spec.apple.buildNumber,
588
+ "--export-options-plist",
589
+ exportOptions.path
590
+ ] : target === "simulator" ? [
591
+ "flutter",
592
+ "build",
593
+ "ios",
594
+ "--simulator",
595
+ "--debug",
596
+ "--no-pub"
597
+ ] : [
598
+ "flutter",
599
+ "build",
600
+ target === "apk" ? "apk" : "appbundle",
601
+ "--release",
602
+ "--no-pub",
603
+ "--build-name",
604
+ job.spec.version
605
+ ]);
606
+ if (built.code !== 0) throw new Error(`${job.spec.framework} ${target} build failed: ${built.stderr.slice(-5e3)}`);
607
+ } finally {
608
+ if (exportOptions) {
609
+ readableFiles.delete(exportOptions.path);
610
+ rmSync(exportOptions.directory, {
611
+ recursive: true,
612
+ force: true
613
+ });
614
+ }
615
+ }
616
+ const products = readdirSync(output).filter((name) => name.endsWith(target === "simulator" ? ".app" : `.${target}`));
617
+ if (products.length !== 1) throw new Error(`Expected exactly one ${target} artifact in ${relative(job.workspace, output)}`);
618
+ path = artifactOutputPath(project, join(relative(project, output), products[0]));
566
619
  }
567
- if (built.code !== 0) throw new Error(`iOS ${target} build failed: ${built.stderr.slice(-5e3)}`);
568
- if (sourceDigest(job.workspace) !== job.verifiedDigest) throw Object.assign(/* @__PURE__ */ new Error("Packaging changed source; re-verification is required"), { code: "EVIDENCE_CHANGED" });
569
- requireDependencyProof(job);
570
- const products = readdirSync(output).filter((name) => name.endsWith(target === "ipa" ? ".ipa" : ".app"));
571
- if (products.length !== 1) throw new Error(`Expected exactly one ${target === "ipa" ? "IPA" : "simulator app"} in ${relative(job.workspace, output)}`);
572
- const path = inside(job.workspace, join(relative(job.workspace, output), products[0]));
573
- const digest = target === "ipa" ? createHash("sha256").update(readFileSync(path)).digest("hex") : sourceDigest(path, false);
620
+ requireReviewProof(job);
621
+ const stat = lstatSync(path);
622
+ const bundle = target === "simulator" || target === "app";
623
+ if (stat.isSymbolicLink() || (bundle ? !stat.isDirectory() || readdirSync(path).length === 0 : !stat.isFile() || stat.size === 0)) throw new Error("Packaging did not produce a nonempty regular artifact of the requested kind");
624
+ if (target === "app" || target === "dmg") await validateMacOSArtifact(path, target, exec);
625
+ const digest = bundle ? sourceDigest(path, false) : createHash("sha256").update(readFileSync(path)).digest("hex");
574
626
  const artifact = {
575
- kind: target === "ipa" ? "ipa" : "ios-simulator",
627
+ kind: target === "simulator" ? "ios-simulator" : target === "app" ? "macos-app" : target,
576
628
  path,
577
629
  digest,
578
630
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
579
631
  };
580
- if (!config.allowSubmit) return store.update(job.id, {
632
+ return store.update(job.id, {
581
633
  status: "built",
582
634
  artifact,
583
- error: null
584
- });
585
- return next(store, job, "publish", {
586
- artifact,
587
- ipaPath: path,
588
- archiveDigest: digest
589
- });
590
- }
591
- if (job.stage === "publish") {
592
- if (!config.allowSubmit) throw Object.assign(/* @__PURE__ */ new Error("This task has no upload/submission permission"), { code: "SETUP_REQUIRED" });
593
- if (!job.ipaPath || !job.archiveDigest) throw new Error("Publication requires a completed verified IPA artifact");
594
- if (sourceDigest(job.workspace) !== job.verifiedDigest || createHash("sha256").update(readFileSync(job.ipaPath)).digest("hex") !== job.archiveDigest) throw new Error("Source or archive changed after verification; refusing publication");
595
- requireDependencyProof(job);
596
- const spec = structuredClone(job.spec);
597
- if (spec.apple?.screenshotsDir) spec.apple.screenshotsDir = inside(job.workspace, spec.apple.screenshotsDir);
598
- if (spec.apple?.screenshotsDir && sourceDigest(spec.apple.screenshotsDir, false) !== job.screenshotDigest) throw new Error("Screenshots changed after runtime verification; refusing to upload different assets");
599
- return next(store, job, "monitor", {
600
- receipt: await shipApple({
601
- spec,
602
- ipaPath: job.ipaPath,
603
- state: store.get(job.id).receipt,
604
- save: (receipt) => {
605
- store.update(job.id, { receipt });
606
- },
607
- run: exec,
608
- signal,
609
- allowRelease: config.allowRelease
610
- }),
611
- status: "waiting",
612
- nextAt: Date.now() + config.pollMs
613
- });
614
- }
615
- if (job.stage === "monitor") {
616
- const result = await pollApple({
617
- spec: job.spec,
618
- state: job.receipt,
619
- signal
620
- });
621
- if (result.status === "approved" && (result.available || !config.allowRelease)) return store.update(job.id, {
622
- status: result.available ? "released" : "approved",
623
- appleReview: result,
624
- error: null
625
- });
626
- if (result.status === "rejected") return store.update(job.id, {
627
- status: "blocked",
628
- appleReview: result,
629
- error: "Apple rejected this version. Rejection message details are not available through this API; do not invent feedback. Supply the actual App Review message with the feedback command to resume repairs."
630
- });
631
- return store.update(job.id, {
632
- status: "waiting",
633
- appleReview: result,
634
- attempts: {
635
- ...attempts,
636
- monitor: 0
637
- },
638
- nextAt: Date.now() + config.pollMs,
635
+ cloudBuildPending: false,
639
636
  error: null
640
637
  });
641
638
  }
642
639
  throw new Error(`Unknown task stage ${job.stage}`);
643
640
  } catch (cause) {
644
641
  const error = toError(cause);
642
+ if (store.get(job.id).cloudBuildPending) return store.update(job.id, {
643
+ status: "blocked",
644
+ blockedReason: null,
645
+ error: `Cloud build outcome requires provider reconciliation; automatic replay is forbidden. ${error.message}`
646
+ });
645
647
  if (error.code === "PROCESS_CLEANUP_BLOCKED") return store.update(job.id, {
646
648
  status: "blocked",
647
649
  blockedReason: null,
@@ -675,27 +677,7 @@ async function executeStep(store, job, { signal } = {}) {
675
677
  nextAt: Date.now(),
676
678
  error: error.message
677
679
  });
678
- if (error.code === "ENOENT" && error.syscall?.startsWith("spawn") || /SETUP_REQUIRED|UNSUPPORTED|APPLE_WRITE_UNCERTAIN|APPLE_STATE_|PROCESS_CLEANUP_BLOCKED/.test(error.code ?? "")) return store.update(job.id, {
679
- status: "blocked",
680
- error: error.message
681
- });
682
- if (error.code === "APPLE_PROCESSING" || error.code === "APPLE_RATE_LIMIT") return store.update(job.id, {
683
- status: "waiting",
684
- attempts: {
685
- ...attempts,
686
- [job.stage]: Math.max(0, (attempts[job.stage] ?? 0) - 1)
687
- },
688
- error: error.message,
689
- nextAt: Date.now() + config.pollMs
690
- });
691
- const receipt = store.get(job.id).receipt;
692
- const publicationRecovery = ["publish", "monitor"].includes(job.stage) && (job.stage === "monitor" || !!(receipt.upload || receipt.pending || receipt.buildId || receipt.submissionId || receipt.versionId || Object.keys(receipt.effects ?? {}).length));
693
- if (publicationRecovery && error.transient === true) return store.update(job.id, {
694
- status: "waiting",
695
- error: error.message,
696
- nextAt: Date.now() + config.pollMs
697
- });
698
- if (job.stage === "monitor" || publicationRecovery && error.code?.startsWith("APPLE_")) return store.update(job.id, {
680
+ if (error.code === "ENOENT" && error.syscall?.startsWith("spawn") || /SETUP_REQUIRED|UNSUPPORTED|PROCESS_CLEANUP_BLOCKED/.test(error.code ?? "")) return store.update(job.id, {
699
681
  status: "blocked",
700
682
  error: error.message
701
683
  });
@@ -717,6 +699,6 @@ async function executeStep(store, job, { signal } = {}) {
717
699
  });
718
700
  }
719
701
  }
720
- export { abandonTurn, completeTurn, executeStep, nextTurn, parseResearch, parseVerdict, sourceDigest };
702
+ export { abandonTurn, completeTurn, executeStep, nextTurn };
721
703
 
722
704
  //# sourceMappingURL=runner.js.map