apphx 2.0.0 → 2.1.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 (51) hide show
  1. package/README.md +11 -9
  2. package/dist/cli.js +41 -11
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config.js +4 -2
  5. package/dist/config.js.map +1 -1
  6. package/dist/integrations.js +177 -91
  7. package/dist/integrations.js.map +1 -1
  8. package/dist/notify.js +267 -0
  9. package/dist/notify.js.map +1 -0
  10. package/dist/policy.js +3 -16
  11. package/dist/policy.js.map +1 -1
  12. package/dist/runner.js +119 -20
  13. package/dist/runner.js.map +1 -1
  14. package/dist/schemas.js +57 -2
  15. package/dist/schemas.js.map +1 -1
  16. package/dist/vendor/obfuscator/dist/config.js +119 -6
  17. package/dist/vendor/obfuscator/dist/config.js.map +1 -1
  18. package/dist/vendor/obfuscator/dist/dfa.js +528 -0
  19. package/dist/vendor/obfuscator/dist/dfa.js.map +1 -0
  20. package/dist/vendor/obfuscator/dist/flutter.js +3266 -0
  21. package/dist/vendor/obfuscator/dist/flutter.js.map +1 -0
  22. package/dist/vendor/obfuscator/dist/image-utils.js +335 -0
  23. package/dist/vendor/obfuscator/dist/image-utils.js.map +1 -0
  24. package/dist/vendor/obfuscator/dist/junk.js +1501 -0
  25. package/dist/vendor/obfuscator/dist/junk.js.map +1 -0
  26. package/dist/vendor/obfuscator/dist/lexicon-data.js +972 -0
  27. package/dist/vendor/obfuscator/dist/lexicon-data.js.map +1 -0
  28. package/dist/vendor/obfuscator/dist/obfuscate.js +51 -2
  29. package/dist/vendor/obfuscator/dist/obfuscate.js.map +1 -1
  30. package/dist/vendor/obfuscator/dist/objc-symbols.js +1819 -0
  31. package/dist/vendor/obfuscator/dist/objc-symbols.js.map +1 -0
  32. package/dist/vendor/obfuscator/dist/project-identity.js +852 -0
  33. package/dist/vendor/obfuscator/dist/project-identity.js.map +1 -0
  34. package/dist/vendor/obfuscator/dist/string-encrypt.js +330 -0
  35. package/dist/vendor/obfuscator/dist/string-encrypt.js.map +1 -0
  36. package/examples/android-java.factory.json +40 -5
  37. package/examples/expo.factory.json +68 -5
  38. package/examples/flutter.factory.json +40 -4
  39. package/examples/luggage.json +1 -0
  40. package/examples/multi-role-agents.md +129 -0
  41. package/examples/mygo.factory.json +54 -4
  42. package/examples/queue-loop-demo.md +146 -0
  43. package/examples/react-native.factory.json +58 -5
  44. package/examples/roles.example.json +31 -0
  45. package/examples/swift.factory.json +60 -5
  46. package/examples/uni-app.factory.json +58 -5
  47. package/package.json +1 -1
  48. package/skills/apphx/SKILL.md +39 -4
  49. package/skills/apphx/resources/requirement.json +1 -0
  50. package/skills/apphx-obfuscator/LICENSE +21 -0
  51. package/skills/apphx-obfuscator/SKILL.md +72 -0
package/dist/notify.js ADDED
@@ -0,0 +1,267 @@
1
+ import { toError } from "./schemas.js";
2
+ import { connect } from "node:net";
3
+ import { connect as connect$1 } from "node:tls";
4
+ // src/notify.ts
5
+ function replyFrom(lines) {
6
+ const status = Number(lines.at(-1)?.slice(0, 3));
7
+ if (!Number.isInteger(status)) throw new Error(`SMTP server sent an unreadable reply: ${JSON.stringify(lines)}`);
8
+ return {
9
+ code: status,
10
+ lines
11
+ };
12
+ }
13
+ function readReply(connection, buffer) {
14
+ return new Promise((resolve, reject) => {
15
+ let current = buffer;
16
+ const complete = [];
17
+ const onData = (chunk) => {
18
+ current += chunk.toString("utf8");
19
+ const lines = current.split("\r\n");
20
+ current = lines.pop() ?? "";
21
+ while (lines.length) {
22
+ const line = lines.shift();
23
+ complete.push(line);
24
+ if (line.length < 4 || line[3] !== "-") {
25
+ cleanup();
26
+ try {
27
+ resolve({
28
+ result: replyFrom(complete),
29
+ rest: current
30
+ });
31
+ } catch (cause) {
32
+ reject(cause);
33
+ }
34
+ return;
35
+ }
36
+ }
37
+ };
38
+ const onError = (error) => {
39
+ cleanup();
40
+ reject(error);
41
+ };
42
+ const onClose = () => {
43
+ cleanup();
44
+ reject(/* @__PURE__ */ new Error("SMTP connection closed while reading a reply"));
45
+ };
46
+ const cleanup = () => {
47
+ connection.removeListener("data", onData);
48
+ connection.removeListener("error", onError);
49
+ connection.removeListener("close", onClose);
50
+ };
51
+ connection.on("data", onData);
52
+ connection.on("error", onError);
53
+ connection.on("close", onClose);
54
+ });
55
+ }
56
+ function expect(result, allowed, context) {
57
+ if (!allowed.includes(result.code)) throw new Error(`SMTP ${context} failed (${result.code}): ${result.lines.join(" ")}`);
58
+ }
59
+ async function command(connection, text, allowed, context, buffer) {
60
+ if (text !== null) connection.write(`${text}\r\n`);
61
+ const { result, rest } = await readReply(connection, buffer);
62
+ expect(result, allowed, context);
63
+ return {
64
+ rest,
65
+ result
66
+ };
67
+ }
68
+ async function connectSocket(options) {
69
+ if (options.secure) {
70
+ const socket = connect$1({
71
+ host: options.host,
72
+ port: options.port,
73
+ servername: options.host,
74
+ rejectUnauthorized: true
75
+ });
76
+ socket.setTimeout(options.timeoutMs, () => socket.destroy(/* @__PURE__ */ new Error(`SMTP TLS handshake timed out after ${options.timeoutMs}ms`)));
77
+ return await new Promise((resolve, reject) => {
78
+ socket.once("secureConnect", () => resolve(socket));
79
+ socket.once("error", reject);
80
+ socket.once("close", () => reject(/* @__PURE__ */ new Error("SMTP TLS connection closed before the handshake completed")));
81
+ });
82
+ }
83
+ const socket = connect({
84
+ host: options.host,
85
+ port: options.port
86
+ });
87
+ socket.setTimeout(options.timeoutMs, () => socket.destroy(/* @__PURE__ */ new Error(`SMTP connection timed out after ${options.timeoutMs}ms`)));
88
+ return await new Promise((resolve, reject) => {
89
+ socket.once("connect", () => resolve(socket));
90
+ socket.once("error", reject);
91
+ });
92
+ }
93
+ async function upgradeTls(socket, options) {
94
+ const upgraded = connect$1({
95
+ socket,
96
+ servername: options.host,
97
+ rejectUnauthorized: true
98
+ });
99
+ await new Promise((resolve, reject) => {
100
+ upgraded.once("secureConnect", () => resolve());
101
+ upgraded.once("error", reject);
102
+ });
103
+ return upgraded;
104
+ }
105
+ /** Minimal SMTP client: EHLO, optional STARTTLS, AUTH PLAIN, MAIL/RCPT/DATA, QUIT. */
106
+ async function sendMail(options, envelope) {
107
+ let connection = await connectSocket(options);
108
+ try {
109
+ let buffer = "";
110
+ const greeting = await readReply(connection, buffer);
111
+ buffer = greeting.rest;
112
+ expect(greeting.result, [220], "greeting");
113
+ let ehlo = await command(connection, "EHLO apphx", [250], "EHLO", buffer);
114
+ buffer = ehlo.rest;
115
+ const capabilities = ehlo.result.lines.join(" ").toUpperCase();
116
+ if (!options.secure && capabilities.includes("STARTTLS")) {
117
+ buffer = (await command(connection, "STARTTLS", [220], "STARTTLS", buffer)).rest;
118
+ connection = await upgradeTls(connection, options);
119
+ ehlo = await command(connection, "EHLO apphx", [250], "EHLO", buffer);
120
+ buffer = ehlo.rest;
121
+ }
122
+ if (options.user) {
123
+ if (options.password === void 0) throw new Error("SMTP user is configured without a resolvable password; set the configured passwordEnv in the host environment");
124
+ const payload = Buffer.from(`\0${options.user}\0${options.password}`, "utf8").toString("base64");
125
+ buffer = (await command(connection, "AUTH PLAIN", [334], "AUTH challenge", buffer)).rest;
126
+ buffer = (await command(connection, payload, [235], "AUTH", buffer)).rest;
127
+ }
128
+ buffer = (await command(connection, `MAIL FROM:<${envelope.from}>`, [250], "MAIL FROM", buffer)).rest;
129
+ buffer = (await command(connection, `RCPT TO:<${envelope.to}>`, [250, 251], "RCPT TO", buffer)).rest;
130
+ buffer = (await command(connection, "DATA", [354], "DATA", buffer)).rest;
131
+ const dotStuffed = `${[
132
+ `From: ${envelope.from}`,
133
+ `To: ${envelope.to}`,
134
+ `Subject: ${envelope.subject}`,
135
+ "MIME-Version: 1.0",
136
+ "Content-Type: text/plain; charset=utf-8",
137
+ `Message-ID: <${Date.now().toString(36)}-${Buffer.from(envelope.to).toString("hex").slice(0, 12)}@apphx.local>`
138
+ ].join("\r\n")}\r\n\r\n${envelope.body}`.replace(/\r?\n/g, "\r\n").replace(/^\./gm, "..");
139
+ const accepted = await new Promise((resolve, reject) => {
140
+ let current = "";
141
+ const onData = (chunk) => {
142
+ current += chunk.toString("utf8");
143
+ const end = current.indexOf("\r\n");
144
+ if (end === -1) return;
145
+ connection.removeListener("data", onData);
146
+ const line = current.slice(0, end + 2);
147
+ const status = Number(line.slice(0, 3));
148
+ if (!Number.isInteger(status)) {
149
+ reject(/* @__PURE__ */ new Error(`SMTP DATA got an unreadable reply: ${JSON.stringify(line)}`));
150
+ return;
151
+ }
152
+ resolve({
153
+ code: status,
154
+ lines: [line.trim()]
155
+ });
156
+ };
157
+ connection.on("data", onData);
158
+ connection.write(`${dotStuffed}\r\n.\r\n`);
159
+ });
160
+ expect(accepted, [250], "DATA acceptance");
161
+ const queued = accepted.lines.join(" ").match(/queued as (?:<)?([^ >]+)/i);
162
+ await command(connection, "QUIT", [221], "QUIT", buffer).catch(() => {});
163
+ return { messageId: queued?.[1] ?? null };
164
+ } finally {
165
+ connection.destroy();
166
+ }
167
+ }
168
+ function template(value, job) {
169
+ return value.replaceAll("{product}", job.spec.id).replaceAll("{name}", job.spec.name).replaceAll("{version}", job.spec.version).replaceAll("{platform}", job.spec.platform).replaceAll("{channel}", job.spec.channel).replaceAll("{status}", job.status).replaceAll("{artifact}", job.artifact ? `${job.artifact.kind} ${job.artifact.digest}` : "none");
170
+ }
171
+ function notifyConfig(config) {
172
+ if (!config.notify) throw Object.assign(/* @__PURE__ */ new Error("No notify configuration; add notify.email (from, to, smtpHost, smtpPort) to the control configuration before notifying"), { code: "SETUP_REQUIRED" });
173
+ return config.notify.email;
174
+ }
175
+ async function notifyJob(store, config, id) {
176
+ const email = notifyConfig(config);
177
+ const job = store.transition(id, (current) => {
178
+ if (current.status !== "built" || !current.artifact) throw new Error("Email notification requires a built task with a recorded artifact; packaging runs after the verification and review gates, so only gate-passed tasks are notified");
179
+ if (current.notifiedAt) return {};
180
+ if (current.notifySendingAt) throw new Error("Notification already in flight for this task; retry after the current send settles");
181
+ return { notifySendingAt: (/* @__PURE__ */ new Date()).toISOString() };
182
+ });
183
+ if (job.notifiedAt) return {
184
+ id,
185
+ notifiedTo: job.notifiedTo ?? null,
186
+ messageId: null,
187
+ error: null
188
+ };
189
+ const to = job.spec.authorEmail ?? email.to;
190
+ const password = email.passwordEnv ? process.env[email.passwordEnv] : void 0;
191
+ const subject = template(email.subject ?? "[apphx] {name} v{version} packaged", job);
192
+ const body = template(email.body ?? `The local packaging pipeline finished for {name} {version} ({platform}/{channel}).\n\nArtifact: {artifact}\nStatus: {status}\n\nVerification and independent review gates passed; this email is a local notification. Store upload, submission and review remain manual human work outside this tool.`, job);
193
+ try {
194
+ const { messageId } = await sendMail({
195
+ host: email.smtpHost,
196
+ port: email.smtpPort,
197
+ secure: email.secure,
198
+ user: email.user,
199
+ password,
200
+ timeoutMs: email.timeoutMs
201
+ }, {
202
+ from: email.from,
203
+ to,
204
+ subject,
205
+ body
206
+ });
207
+ store.update(id, {
208
+ notifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
209
+ notifiedTo: to,
210
+ notifySendingAt: null,
211
+ notifyError: null
212
+ });
213
+ store.event(id, {
214
+ notified: true,
215
+ notifiedTo: to,
216
+ messageId
217
+ });
218
+ return {
219
+ id,
220
+ notifiedTo: to,
221
+ messageId,
222
+ error: null
223
+ };
224
+ } catch (cause) {
225
+ const error = toError(cause).message;
226
+ store.update(id, {
227
+ notifyError: error,
228
+ notifySendingAt: null
229
+ });
230
+ store.event(id, {
231
+ notified: false,
232
+ notifiedTo: to,
233
+ error
234
+ });
235
+ throw new Error(`Email notification failed for ${to}: ${error}`);
236
+ }
237
+ }
238
+ async function notifyBuiltJobs(store, config) {
239
+ notifyConfig(config);
240
+ const outcomes = [];
241
+ for (const job of store.list()) {
242
+ if (job.status !== "built" || !job.artifact) continue;
243
+ if (job.notifiedAt) {
244
+ outcomes.push({
245
+ id: job.id,
246
+ notifiedTo: job.notifiedTo ?? null,
247
+ messageId: null,
248
+ error: null
249
+ });
250
+ continue;
251
+ }
252
+ try {
253
+ outcomes.push(await notifyJob(store, config, job.id));
254
+ } catch (cause) {
255
+ outcomes.push({
256
+ id: job.id,
257
+ notifiedTo: job.spec.authorEmail ?? config.notify.email.to,
258
+ messageId: null,
259
+ error: toError(cause).message
260
+ });
261
+ }
262
+ }
263
+ return outcomes;
264
+ }
265
+ export { notifyBuiltJobs, notifyJob, sendMail };
266
+
267
+ //# sourceMappingURL=notify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notify.js","names":["tlsConnect"],"sources":["../src/notify.ts"],"sourcesContent":["import { connect, type Socket } from 'node:net';\nimport { connect as tlsConnect, type TLSSocket } from 'node:tls';\nimport { toError, type Config, type Job } from './schemas.js';\nimport type { Store } from './store.js';\n\ntype Connection = Socket | TLSSocket;\n\ninterface SmtpResult { code: number; lines: string[] }\n\nfunction replyFrom(lines: string[]): SmtpResult {\n const status = Number(lines.at(-1)?.slice(0, 3));\n if (!Number.isInteger(status)) throw new Error(`SMTP server sent an unreadable reply: ${JSON.stringify(lines)}`);\n return { code: status, lines };\n}\n\nfunction readReply(connection: Connection, buffer: string): Promise<{ result: SmtpResult; rest: string }> {\n return new Promise((resolve, reject) => {\n let current = buffer;\n const complete: string[] = [];\n const onData = (chunk: Buffer): void => {\n current += chunk.toString('utf8');\n const lines = current.split('\\r\\n');\n current = lines.pop() ?? '';\n while (lines.length) {\n const line = lines.shift()!;\n complete.push(line);\n if (line.length < 4 || line[3] !== '-') {\n cleanup();\n try { resolve({ result: replyFrom(complete), rest: current }); } catch (cause) { reject(cause); }\n return;\n }\n }\n };\n const onError = (error: Error): void => { cleanup(); reject(error); };\n const onClose = (): void => { cleanup(); reject(new Error('SMTP connection closed while reading a reply')); };\n const cleanup = (): void => {\n connection.removeListener('data', onData);\n connection.removeListener('error', onError);\n connection.removeListener('close', onClose);\n };\n connection.on('data', onData);\n connection.on('error', onError);\n connection.on('close', onClose);\n });\n}\n\nfunction expect(result: SmtpResult, allowed: number[], context: string): void {\n if (!allowed.includes(result.code)) throw new Error(`SMTP ${context} failed (${result.code}): ${result.lines.join(' ')}`);\n}\n\nasync function command(connection: Connection, text: string | null, allowed: number[], context: string, buffer: string): Promise<{ rest: string; result: SmtpResult }> {\n if (text !== null) connection.write(`${text}\\r\\n`);\n const { result, rest } = await readReply(connection, buffer);\n expect(result, allowed, context);\n return { rest, result };\n}\n\ninterface SmtpOptions {\n host: string; port: number; secure: boolean; user?: string; password?: string; timeoutMs: number;\n}\n\nasync function connectSocket(options: SmtpOptions): Promise<Connection> {\n if (options.secure) {\n const socket = tlsConnect({ host: options.host, port: options.port, servername: options.host, rejectUnauthorized: true });\n socket.setTimeout(options.timeoutMs, () => socket.destroy(new Error(`SMTP TLS handshake timed out after ${options.timeoutMs}ms`)));\n return await new Promise<Connection>((resolve, reject) => {\n socket.once('secureConnect', () => resolve(socket));\n socket.once('error', reject);\n socket.once('close', () => reject(new Error('SMTP TLS connection closed before the handshake completed')));\n });\n }\n const socket = connect({ host: options.host, port: options.port });\n socket.setTimeout(options.timeoutMs, () => socket.destroy(new Error(`SMTP connection timed out after ${options.timeoutMs}ms`)));\n return await new Promise<Connection>((resolve, reject) => {\n socket.once('connect', () => resolve(socket));\n socket.once('error', reject);\n });\n}\n\nasync function upgradeTls(socket: Connection, options: SmtpOptions): Promise<TLSSocket> {\n const upgraded = tlsConnect({ socket, servername: options.host, rejectUnauthorized: true });\n await new Promise<void>((resolve, reject) => {\n upgraded.once('secureConnect', () => resolve());\n upgraded.once('error', reject);\n });\n return upgraded;\n}\n\n/** Minimal SMTP client: EHLO, optional STARTTLS, AUTH PLAIN, MAIL/RCPT/DATA, QUIT. */\nexport async function sendMail(options: SmtpOptions, envelope: { from: string; to: string; subject: string; body: string }): Promise<{ messageId: string | null }> {\n let connection = await connectSocket(options);\n try {\n let buffer = '';\n const greeting = await readReply(connection, buffer);\n buffer = greeting.rest;\n expect(greeting.result, [220], 'greeting');\n let ehlo = await command(connection, 'EHLO apphx', [250], 'EHLO', buffer);\n buffer = ehlo.rest;\n const capabilities = ehlo.result.lines.join(' ').toUpperCase();\n if (!options.secure && capabilities.includes('STARTTLS')) {\n buffer = (await command(connection, 'STARTTLS', [220], 'STARTTLS', buffer)).rest;\n connection = await upgradeTls(connection, options);\n ehlo = await command(connection, 'EHLO apphx', [250], 'EHLO', buffer);\n buffer = ehlo.rest;\n }\n if (options.user) {\n if (options.password === undefined) throw new Error('SMTP user is configured without a resolvable password; set the configured passwordEnv in the host environment');\n const payload = Buffer.from(`\\0${options.user}\\0${options.password}`, 'utf8').toString('base64');\n buffer = (await command(connection, 'AUTH PLAIN', [334], 'AUTH challenge', buffer)).rest;\n buffer = (await command(connection, payload, [235], 'AUTH', buffer)).rest;\n }\n buffer = (await command(connection, `MAIL FROM:<${envelope.from}>`, [250], 'MAIL FROM', buffer)).rest;\n buffer = (await command(connection, `RCPT TO:<${envelope.to}>`, [250, 251], 'RCPT TO', buffer)).rest;\n buffer = (await command(connection, 'DATA', [354], 'DATA', buffer)).rest;\n const headers = [\n `From: ${envelope.from}`,\n `To: ${envelope.to}`,\n `Subject: ${envelope.subject}`,\n 'MIME-Version: 1.0',\n 'Content-Type: text/plain; charset=utf-8',\n `Message-ID: <${Date.now().toString(36)}-${Buffer.from(envelope.to).toString('hex').slice(0, 12)}@apphx.local>`,\n ].join('\\r\\n');\n const dotStuffed = `${headers}\\r\\n\\r\\n${envelope.body}`.replace(/\\r?\\n/g, '\\r\\n').replace(/^\\./gm, '..');\n const accepted = await new Promise<SmtpResult>((resolve, reject) => {\n let current = '';\n const onData = (chunk: Buffer): void => {\n current += chunk.toString('utf8');\n const end = current.indexOf('\\r\\n');\n if (end === -1) return;\n connection.removeListener('data', onData);\n const line = current.slice(0, end + 2);\n const status = Number(line.slice(0, 3));\n if (!Number.isInteger(status)) { reject(new Error(`SMTP DATA got an unreadable reply: ${JSON.stringify(line)}`)); return; }\n resolve({ code: status, lines: [line.trim()] });\n };\n connection.on('data', onData);\n connection.write(`${dotStuffed}\\r\\n.\\r\\n`);\n });\n expect(accepted, [250], 'DATA acceptance');\n const queued = accepted.lines.join(' ').match(/queued as (?:<)?([^ >]+)/i);\n await command(connection, 'QUIT', [221], 'QUIT', buffer).catch(() => {});\n return { messageId: queued?.[1] ?? null };\n } finally {\n connection.destroy();\n }\n}\n\nfunction template(value: string, job: Job): string {\n return value\n .replaceAll('{product}', job.spec.id)\n .replaceAll('{name}', job.spec.name)\n .replaceAll('{version}', job.spec.version)\n .replaceAll('{platform}', job.spec.platform)\n .replaceAll('{channel}', job.spec.channel)\n .replaceAll('{status}', job.status)\n .replaceAll('{artifact}', job.artifact ? `${job.artifact.kind} ${job.artifact.digest}` : 'none');\n}\n\nexport type NotifyOutcome = { id: string; notifiedTo: string | null; messageId: string | null; error: string | null };\n\nfunction notifyConfig(config: Config) {\n if (!config.notify) throw Object.assign(new Error('No notify configuration; add notify.email (from, to, smtpHost, smtpPort) to the control configuration before notifying'), { code: 'SETUP_REQUIRED' });\n return config.notify.email;\n}\n\nexport async function notifyJob(store: Store, config: Config, id: string): Promise<NotifyOutcome> {\n const email = notifyConfig(config);\n // Claim the send first so concurrent notifiers cannot double-send; the claim\n // is released on success (notifiedAt) or failure (notifyError).\n const job = store.transition(id, current => {\n if (current.status !== 'built' || !current.artifact) throw new Error('Email notification requires a built task with a recorded artifact; packaging runs after the verification and review gates, so only gate-passed tasks are notified');\n if (current.notifiedAt) return {};\n if (current.notifySendingAt) throw new Error('Notification already in flight for this task; retry after the current send settles');\n return { notifySendingAt: new Date().toISOString() };\n });\n if (job.notifiedAt) return { id, notifiedTo: job.notifiedTo ?? null, messageId: null, error: null };\n const to = job.spec.authorEmail ?? email.to;\n const password = email.passwordEnv ? process.env[email.passwordEnv] : undefined;\n const subject = template(email.subject ?? '[apphx] {name} v{version} packaged', job);\n const body = template(email.body ?? `The local packaging pipeline finished for {name} {version} ({platform}/{channel}).\\n\\nArtifact: {artifact}\\nStatus: {status}\\n\\nVerification and independent review gates passed; this email is a local notification. Store upload, submission and review remain manual human work outside this tool.`, job);\n try {\n const { messageId } = await sendMail({ host: email.smtpHost, port: email.smtpPort, secure: email.secure, user: email.user, password, timeoutMs: email.timeoutMs }, { from: email.from, to, subject, body });\n store.update(id, { notifiedAt: new Date().toISOString(), notifiedTo: to, notifySendingAt: null, notifyError: null });\n store.event(id, { notified: true, notifiedTo: to, messageId });\n return { id, notifiedTo: to, messageId, error: null };\n } catch (cause) {\n const error = toError(cause).message;\n store.update(id, { notifyError: error, notifySendingAt: null });\n store.event(id, { notified: false, notifiedTo: to, error });\n throw new Error(`Email notification failed for ${to}: ${error}`);\n }\n}\n\nexport async function notifyBuiltJobs(store: Store, config: Config): Promise<NotifyOutcome[]> {\n notifyConfig(config);\n const outcomes: NotifyOutcome[] = [];\n for (const job of store.list()) {\n if (job.status !== 'built' || !job.artifact) continue;\n if (job.notifiedAt) { outcomes.push({ id: job.id, notifiedTo: job.notifiedTo ?? null, messageId: null, error: null }); continue; }\n try { outcomes.push(await notifyJob(store, config, job.id)); }\n catch (cause) { outcomes.push({ id: job.id, notifiedTo: job.spec.authorEmail ?? config.notify!.email.to, messageId: null, error: toError(cause).message }); }\n }\n return outcomes;\n}\n"],"mappings":"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"}
package/dist/policy.js CHANGED
@@ -10,26 +10,13 @@ function parseResearch(text, channel) {
10
10
  }
11
11
  return ResearchSchema.parse(value);
12
12
  }
13
- /** Honest pre-delivery differentiation and quality checklist. Store submission and review stay manual. */
14
- function internalQaChecklist() {
15
- return [
16
- "Internal QA checklist for a normal, honest local package handoff. Store submission, account setup and platform review are manual human work outside this tool; nothing here bypasses, influences or pre-approves a store reviewer.",
17
- "Real value: each package has its own genuine purpose, audience and functional behavior. Identical functionality with only renamed labels is not meaningful differentiation.",
18
- "Visual identity: themes, layouts, colors and interaction flows are real product choices for this app, not templated reskins produced to look different.",
19
- "Developer and identity metadata: app name, title, icon, bundle/package id, keywords and description truthfully describe this app and its actual developer. Never forge identities, accounts, locations or submission sources.",
20
- "Functionality: every advertised feature works and is exercised by the frozen runtime acceptance; no hidden functionality for any audience and no fabricated screenshots or evidence.",
21
- "Metadata: price, region, category and description reflect the actual product. Design rationale notes, if any, are the owner's honest answers, not scripted reply templates.",
22
- "Code identity: project, folder, class, function and resource names follow this project's real conventions. Do not mutate hashes, names or resource bytes solely to defeat similarity checks; that would be deceptive.",
23
- "Quality gate: source, dependency and runtime proofs must be current; independent review must cover every frozen acceptance requirement with observed evidence. Only then may the artifact be handed off to a human for store submission."
24
- ].join("\n");
25
- }
26
13
  function researchPrompt(job, portfolio) {
27
14
  const siblings = `Other same-channel products ${JSON.stringify(portfolio)}; earlier versions of this same app are its source baseline, not competing products.`;
28
- return `Research only with your own host tools. Preserve the requested ${job.spec.framework} ${job.spec.platform} project and its existing architecture. ${internalQaChecklist()} ${siblings} Assess technical feasibility, dependencies, licensing, privacy and real acceptance for this local package. Return typed JSON {"viable":boolean,"distinctness":"specific honest differentiation","risks":["..."],"plan":["concrete implementation and device acceptance step"]}. Do not promise store approval and do not add unrequested features.`;
15
+ return `Research only with your own host tools. Preserve the requested ${job.spec.framework} ${job.spec.platform} project and its existing architecture. ${siblings} Assess technical feasibility, dependencies, licensing, privacy and real acceptance for this local package. Return typed JSON {"viable":boolean,"distinctness":"specific honest differentiation","risks":["..."],"plan":["concrete implementation and device acceptance step"]}. Do not promise store approval and do not add unrequested features.`;
29
16
  }
30
17
  function reviewInstructions(job) {
31
- return `Use an independent reviewer with fresh context when supported; otherwise disclose the limitation. Review actual source and acceptance read-only. Verified source: ${job.verifiedDigest}; dependency resolution: ${job.verifiedDependencyDigest}; actual checks: ${JSON.stringify(job.config.verification)}. ${internalQaChecklist()} Reject incomplete requirements, data loss, fake screenshots, accessibility failures, placeholders, weak tests and hidden features. Return typed JSON {"approved":boolean,"findings":["blocking issue"],"acceptanceEvidence":[{"requirement":"exact acceptance string","evidence":"file/symbol + observed command or device scenario"}]}. Cover every acceptance requirement. Reviewer identity is advisory; the Harness gates typed results and unchanged execution evidence.`;
18
+ return `Use an independent reviewer with fresh context when supported; otherwise disclose the limitation. Review actual source and acceptance read-only. Verified source: ${job.verifiedDigest}; dependency resolution: ${job.verifiedDependencyDigest}; actual checks: ${JSON.stringify(job.config.verification)}. Reject incomplete requirements, data loss, fake screenshots, accessibility failures, placeholders, weak tests and hidden features. Return typed JSON {"approved":boolean,"findings":["blocking issue"],"acceptanceEvidence":[{"requirement":"exact acceptance string","evidence":"file/symbol + observed command or device scenario"}]}. Cover every acceptance requirement. Reviewer identity is advisory; the Harness gates typed results and unchanged execution evidence.`;
32
19
  }
33
- export { internalQaChecklist, parseResearch, researchPrompt, reviewInstructions };
20
+ export { parseResearch, researchPrompt, reviewInstructions };
34
21
 
35
22
  //# sourceMappingURL=policy.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"policy.js","names":[],"sources":["../src/policy.ts"],"sourcesContent":["import { ResearchSchema } from './schemas.js';\nimport type { Job, Spec } from './schemas.js';\n\nexport function parseResearch(text: string, channel: Spec['channel']) {\n const content = text.trim().replace(/^```(?:json)?\\s*\\n?/, '').replace(/\\n?```$/, '');\n let value: unknown;\n try { value = JSON.parse(content); }\n catch { throw new Error('Host did not return a JSON verdict; no approval was inferred from text or an exit code'); }\n return ResearchSchema.parse(value);\n}\n\n/** Honest pre-delivery differentiation and quality checklist. Store submission and review stay manual. */\nexport function internalQaChecklist(): string {\n return [\n 'Internal QA checklist for a normal, honest local package handoff. Store submission, account setup and platform review are manual human work outside this tool; nothing here bypasses, influences or pre-approves a store reviewer.',\n 'Real value: each package has its own genuine purpose, audience and functional behavior. Identical functionality with only renamed labels is not meaningful differentiation.',\n 'Visual identity: themes, layouts, colors and interaction flows are real product choices for this app, not templated reskins produced to look different.',\n 'Developer and identity metadata: app name, title, icon, bundle/package id, keywords and description truthfully describe this app and its actual developer. Never forge identities, accounts, locations or submission sources.',\n 'Functionality: every advertised feature works and is exercised by the frozen runtime acceptance; no hidden functionality for any audience and no fabricated screenshots or evidence.',\n 'Metadata: price, region, category and description reflect the actual product. Design rationale notes, if any, are the owner\\'s honest answers, not scripted reply templates.',\n 'Code identity: project, folder, class, function and resource names follow this project\\'s real conventions. Do not mutate hashes, names or resource bytes solely to defeat similarity checks; that would be deceptive.',\n 'Quality gate: source, dependency and runtime proofs must be current; independent review must cover every frozen acceptance requirement with observed evidence. Only then may the artifact be handed off to a human for store submission.',\n ].join('\\n');\n}\n\nexport function researchPrompt(job: Job, portfolio: Array<Pick<Spec, 'id' | 'purpose' | 'audience' | 'differentiators'>>): string {\n const siblings = `Other same-channel products ${JSON.stringify(portfolio)}; earlier versions of this same app are its source baseline, not competing products.`;\n return `Research only with your own host tools. Preserve the requested ${job.spec.framework} ${job.spec.platform} project and its existing architecture. ${internalQaChecklist()} ${siblings} Assess technical feasibility, dependencies, licensing, privacy and real acceptance for this local package. Return typed JSON {\"viable\":boolean,\"distinctness\":\"specific honest differentiation\",\"risks\":[\"...\"],\"plan\":[\"concrete implementation and device acceptance step\"]}. Do not promise store approval and do not add unrequested features.`;\n}\n\nexport function reviewInstructions(job: Job): string {\n return `Use an independent reviewer with fresh context when supported; otherwise disclose the limitation. Review actual source and acceptance read-only. Verified source: ${job.verifiedDigest}; dependency resolution: ${job.verifiedDependencyDigest}; actual checks: ${JSON.stringify(job.config.verification)}. ${internalQaChecklist()} Reject incomplete requirements, data loss, fake screenshots, accessibility failures, placeholders, weak tests and hidden features. Return typed JSON {\"approved\":boolean,\"findings\":[\"blocking issue\"],\"acceptanceEvidence\":[{\"requirement\":\"exact acceptance string\",\"evidence\":\"file/symbol + observed command or device scenario\"}]}. Cover every acceptance requirement. Reviewer identity is advisory; the Harness gates typed results and unchanged execution evidence.`;\n}\n"],"mappings":";;AAGA,SAAgB,cAAc,MAAc,SAA0B;CACpE,MAAM,UAAU,KAAK,KAAK,CAAC,CAAC,QAAQ,uBAAuB,EAAE,CAAC,CAAC,QAAQ,WAAW,EAAE;CACpF,IAAI;CACJ,IAAI;EAAE,QAAQ,KAAK,MAAM,OAAO;CAAG,QAC7B;EAAE,MAAM,IAAI,MAAM,wFAAwF;CAAG;CACnH,OAAO,eAAe,MAAM,KAAK;AACnC;;AAGA,SAAgB,sBAA8B;CAC5C,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,CAAC,CAAC,KAAK,IAAI;AACb;AAEA,SAAgB,eAAe,KAAU,WAAyF;CAChI,MAAM,WAAW,+BAA+B,KAAK,UAAU,SAAS,EAAE;CAC1E,OAAO,kEAAkE,IAAI,KAAK,UAAU,GAAG,IAAI,KAAK,SAAS,0CAA0C,oBAAoB,EAAE,GAAG,SAAS;AAC/L;AAEA,SAAgB,mBAAmB,KAAkB;CACnD,OAAO,qKAAqK,IAAI,eAAe,2BAA2B,IAAI,yBAAyB,mBAAmB,KAAK,UAAU,IAAI,OAAO,YAAY,EAAE,IAAI,oBAAoB,EAAE;AAC9U"}
1
+ {"version":3,"file":"policy.js","names":[],"sources":["../src/policy.ts"],"sourcesContent":["import { ResearchSchema } from './schemas.js';\nimport type { Job, Spec } from './schemas.js';\n\nexport function parseResearch(text: string, channel: Spec['channel']) {\n const content = text.trim().replace(/^```(?:json)?\\s*\\n?/, '').replace(/\\n?```$/, '');\n let value: unknown;\n try { value = JSON.parse(content); }\n catch { throw new Error('Host did not return a JSON verdict; no approval was inferred from text or an exit code'); }\n return ResearchSchema.parse(value);\n}\n\nexport function researchPrompt(job: Job, portfolio: Array<Pick<Spec, 'id' | 'purpose' | 'audience' | 'differentiators'>>): string {\n const siblings = `Other same-channel products ${JSON.stringify(portfolio)}; earlier versions of this same app are its source baseline, not competing products.`;\n return `Research only with your own host tools. Preserve the requested ${job.spec.framework} ${job.spec.platform} project and its existing architecture. ${siblings} Assess technical feasibility, dependencies, licensing, privacy and real acceptance for this local package. Return typed JSON {\"viable\":boolean,\"distinctness\":\"specific honest differentiation\",\"risks\":[\"...\"],\"plan\":[\"concrete implementation and device acceptance step\"]}. Do not promise store approval and do not add unrequested features.`;\n}\n\nexport function reviewInstructions(job: Job): string {\n return `Use an independent reviewer with fresh context when supported; otherwise disclose the limitation. Review actual source and acceptance read-only. Verified source: ${job.verifiedDigest}; dependency resolution: ${job.verifiedDependencyDigest}; actual checks: ${JSON.stringify(job.config.verification)}. Reject incomplete requirements, data loss, fake screenshots, accessibility failures, placeholders, weak tests and hidden features. Return typed JSON {\"approved\":boolean,\"findings\":[\"blocking issue\"],\"acceptanceEvidence\":[{\"requirement\":\"exact acceptance string\",\"evidence\":\"file/symbol + observed command or device scenario\"}]}. Cover every acceptance requirement. Reviewer identity is advisory; the Harness gates typed results and unchanged execution evidence.`;\n}\n"],"mappings":";;AAGA,SAAgB,cAAc,MAAc,SAA0B;CACpE,MAAM,UAAU,KAAK,KAAK,CAAC,CAAC,QAAQ,uBAAuB,EAAE,CAAC,CAAC,QAAQ,WAAW,EAAE;CACpF,IAAI;CACJ,IAAI;EAAE,QAAQ,KAAK,MAAM,OAAO;CAAG,QAC7B;EAAE,MAAM,IAAI,MAAM,wFAAwF;CAAG;CACnH,OAAO,eAAe,MAAM,KAAK;AACnC;AAEA,SAAgB,eAAe,KAAU,WAAyF;CAChI,MAAM,WAAW,+BAA+B,KAAK,UAAU,SAAS,EAAE;CAC1E,OAAO,kEAAkE,IAAI,KAAK,UAAU,GAAG,IAAI,KAAK,SAAS,0CAA0C,SAAS;AACtK;AAEA,SAAgB,mBAAmB,KAAkB;CACnD,OAAO,qKAAqK,IAAI,eAAe,2BAA2B,IAAI,yBAAyB,mBAAmB,KAAK,UAAU,IAAI,OAAO,YAAY,EAAE;AACpT"}
package/dist/runner.js CHANGED
@@ -13,7 +13,7 @@ import { parseResearch, researchPrompt, reviewInstructions } from "./policy.js";
13
13
  import { assertProject, buildInstructions } from "./project-profiles.js";
14
14
  import { validateMacOSArtifact } from "./macos.js";
15
15
  import { invalidateVerification } from "./proof.js";
16
- import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
16
+ import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
17
17
  import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
18
18
  import { fileURLToPath } from "node:url";
19
19
  import { createHash, randomUUID } from "node:crypto";
@@ -25,6 +25,15 @@ function requireDependencyProof(job) {
25
25
  function taskPrompt(job) {
26
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`;
27
27
  }
28
+ const DEFAULT_ROLES = {
29
+ research: ["product-analyst"],
30
+ build: ["developer"],
31
+ review: [
32
+ "architect-review",
33
+ "qa-engineer",
34
+ "acceptance-reviewer"
35
+ ]
36
+ };
28
37
  function contextDigest(job) {
29
38
  return createHash("sha256").update(JSON.stringify({
30
39
  spec: job.spec,
@@ -83,15 +92,18 @@ function hostAction(store, job) {
83
92
  environment.FLUTTER_ROOT = job.config.toolchain.flutterSdk;
84
93
  environment.PATH = `${join(job.config.toolchain.flutterSdk, "bin")}${process.env.PATH ? `${delimiter}${process.env.PATH}` : ""}`;
85
94
  }
95
+ const role = [...job.config.roles?.[turn.stage] ?? DEFAULT_ROLES[turn.stage]];
96
+ const prompt = `${taskPrompt(job)}${instruction}\nTurn roles (adopt every role through your native delegation, with independent contexts where the skill requires them): ${JSON.stringify(role)}`;
86
97
  return {
87
98
  job,
88
99
  action: "host",
89
100
  turn: {
90
101
  id: turn.id,
91
102
  stage: turn.stage,
92
- prompt: taskPrompt(job) + instruction,
103
+ prompt,
93
104
  cwd: projectDirectory(job),
94
- environment: Object.fromEntries(Object.entries(environment).filter((entry) => entry[1] !== void 0))
105
+ environment: Object.fromEntries(Object.entries(environment).filter((entry) => entry[1] !== void 0)),
106
+ role
95
107
  }
96
108
  };
97
109
  }
@@ -216,6 +228,51 @@ async function nextTurn(store, id, options = {}) {
216
228
  store.release();
217
229
  }
218
230
  }
231
+ /**
232
+ * Pick the earliest-due actionable task in the whole queue for a host loop:
233
+ * the first job (ordered by priority lane, then next_at, then insertion) whose
234
+ * `next` returns a host turn or a deterministic step. Jobs that are deferred, stopped or still
235
+ * owned by another execution are skipped; jobs whose persisted turn context no
236
+ * longer matches are surfaced in `pendingErrors` instead of silently blocking
237
+ * the queue. `idle` reports the earliest future due time plus a status summary
238
+ * so the host can audit whether the queue is truly finished or merely blocked,
239
+ * and knows when to wake again. Does not hold the queue worker lock across
240
+ * calls; each host claim stays transactional inside `nextTurn`.
241
+ */
242
+ async function nextQueueTurn(store, options = {}) {
243
+ const pendingErrors = [];
244
+ const rank = (job) => job.spec.priority === "P0" ? 0 : job.spec.priority === "P2" ? 2 : 1;
245
+ const jobs = [...store.list()].sort((left, right) => rank(left) - rank(right) || left.nextAt - right.nextAt);
246
+ for (const job of jobs) try {
247
+ const action = await nextTurn(store, job.id, options);
248
+ if (action.action === "host" || action.action === "step") return action;
249
+ } catch (cause) {
250
+ pendingErrors.push({
251
+ id: job.id,
252
+ error: toError(cause).message
253
+ });
254
+ }
255
+ const summary = {
256
+ total: jobs.length,
257
+ built: 0,
258
+ failed: 0,
259
+ blocked: 0,
260
+ waiting: 0,
261
+ queued: 0,
262
+ running: 0
263
+ };
264
+ for (const job of jobs) {
265
+ const status = job.status;
266
+ if (status === "built" || status === "failed" || status === "blocked" || status === "waiting" || status === "queued" || status === "running") summary[status] += 1;
267
+ }
268
+ const due = jobs.map((job) => job.nextAt).filter((nextAt) => nextAt > Date.now());
269
+ return {
270
+ action: "idle",
271
+ nextAt: due.length ? Math.min(...due) : null,
272
+ pendingErrors,
273
+ summary
274
+ };
275
+ }
219
276
  function completeTurn(store, id, turnId, result, options = {}) {
220
277
  return store.transition(id, (job) => {
221
278
  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");
@@ -505,25 +562,67 @@ async function executeStep(store, job, { signal } = {}) {
505
562
  if (job.stage === "obfuscate") {
506
563
  const obfuscation = job.config.obfuscation;
507
564
  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) ? [
565
+ const obfuscationBase = {
566
+ digest: sourceDigest(job.workspace, true, job.spec.framework, job.spec.project?.directory),
567
+ path: projectDirectory(job)
568
+ };
569
+ let configPath = obfuscation.configPath;
570
+ let snapshot;
571
+ if (!configPath) {
572
+ const directory = join(config.stateDir, "obfuscation");
573
+ mkdirSync(directory, {
574
+ recursive: true,
575
+ mode: 448
576
+ });
577
+ snapshot = join(directory, `${job.id}-${Date.now()}.json`);
578
+ writeFileSync(snapshot, `${JSON.stringify(obfuscation.options ?? {}, null, 2)}\n`, { mode: 384 });
579
+ configPath = snapshot;
580
+ }
581
+ try {
582
+ const expanded = expandArgv(obfuscation.command, environment);
583
+ const bundled = fileURLToPath(new URL("./vendor/obfuscator/dist/cli.js", import.meta.url));
584
+ const resolved = expanded[0] === "obfuscator" && existsSync(bundled) ? [
512
585
  process.execPath,
513
586
  bundled,
514
587
  ...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");
588
+ ] : expanded;
589
+ const argv = [
590
+ ...resolved,
591
+ "--project",
592
+ projectDirectory(job),
593
+ "--config",
594
+ configPath,
595
+ "--framework",
596
+ job.spec.framework,
597
+ ...obfuscation.seed ? ["--seed", obfuscation.seed] : []
598
+ ];
599
+ const result = await trackedRun(argv, {}, { readableFiles: [configPath] });
600
+ if (result.code !== 0) throw new Error(`Obfuscation failed (${result.code}) ${JSON.stringify(argv)}:\n${result.stderr.slice(-5e3)}\n${result.stdout.slice(-5e3)}`);
601
+ let report;
602
+ try {
603
+ const parsed = JSON.parse(result.stdout);
604
+ if (parsed && typeof parsed === "object") {
605
+ const value = parsed;
606
+ const skipped = value.skipped && typeof value.skipped === "object" ? value.skipped : {};
607
+ if (Number.isSafeInteger(value.changedFiles) && value.transforms && typeof value.transforms === "object" && !Array.isArray(value.transforms)) report = {
608
+ changedFiles: value.changedFiles,
609
+ transforms: Object.fromEntries(Object.entries(value.transforms).filter(([key, count]) => typeof key === "string" && Number.isSafeInteger(count))),
610
+ skipped: {
611
+ generated: Number.isSafeInteger(skipped.generated) ? skipped.generated : 0,
612
+ unsupported: Number.isSafeInteger(skipped.unsupported) ? skipped.unsupported : 0
613
+ }
614
+ };
615
+ }
616
+ } catch {}
617
+ if (!report && resolved[0] === process.execPath && resolved[1] === bundled) throw new Error("Bundled obfuscator produced no valid report; the transformed workspace cannot be evidenced");
618
+ return next(store, job, "verify", {
619
+ obfuscationBase,
620
+ obfuscationDigest: sourceDigest(job.workspace, true, job.spec.framework, job.spec.project?.directory),
621
+ ...report ? { obfuscationReport: report } : {}
622
+ });
623
+ } finally {
624
+ if (snapshot) rmSync(snapshot, { force: true });
625
+ }
527
626
  }
528
627
  if (job.stage === "package") {
529
628
  requireReviewProof(job);
@@ -699,6 +798,6 @@ async function executeStep(store, job, { signal } = {}) {
699
798
  });
700
799
  }
701
800
  }
702
- export { abandonTurn, completeTurn, executeStep, nextTurn };
801
+ export { abandonTurn, completeTurn, executeStep, nextQueueTurn, nextTurn };
703
802
 
704
803
  //# sourceMappingURL=runner.js.map