apphx 2.0.1 → 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 (49) hide show
  1. package/README.md +10 -8
  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 +154 -95
  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/runner.js +119 -20
  11. package/dist/runner.js.map +1 -1
  12. package/dist/schemas.js +57 -2
  13. package/dist/schemas.js.map +1 -1
  14. package/dist/vendor/obfuscator/dist/config.js +119 -6
  15. package/dist/vendor/obfuscator/dist/config.js.map +1 -1
  16. package/dist/vendor/obfuscator/dist/dfa.js +528 -0
  17. package/dist/vendor/obfuscator/dist/dfa.js.map +1 -0
  18. package/dist/vendor/obfuscator/dist/flutter.js +3266 -0
  19. package/dist/vendor/obfuscator/dist/flutter.js.map +1 -0
  20. package/dist/vendor/obfuscator/dist/image-utils.js +335 -0
  21. package/dist/vendor/obfuscator/dist/image-utils.js.map +1 -0
  22. package/dist/vendor/obfuscator/dist/junk.js +1501 -0
  23. package/dist/vendor/obfuscator/dist/junk.js.map +1 -0
  24. package/dist/vendor/obfuscator/dist/lexicon-data.js +972 -0
  25. package/dist/vendor/obfuscator/dist/lexicon-data.js.map +1 -0
  26. package/dist/vendor/obfuscator/dist/obfuscate.js +51 -2
  27. package/dist/vendor/obfuscator/dist/obfuscate.js.map +1 -1
  28. package/dist/vendor/obfuscator/dist/objc-symbols.js +1819 -0
  29. package/dist/vendor/obfuscator/dist/objc-symbols.js.map +1 -0
  30. package/dist/vendor/obfuscator/dist/project-identity.js +852 -0
  31. package/dist/vendor/obfuscator/dist/project-identity.js.map +1 -0
  32. package/dist/vendor/obfuscator/dist/string-encrypt.js +330 -0
  33. package/dist/vendor/obfuscator/dist/string-encrypt.js.map +1 -0
  34. package/examples/android-java.factory.json +40 -5
  35. package/examples/expo.factory.json +68 -5
  36. package/examples/flutter.factory.json +40 -4
  37. package/examples/luggage.json +1 -0
  38. package/examples/multi-role-agents.md +129 -0
  39. package/examples/mygo.factory.json +54 -4
  40. package/examples/queue-loop-demo.md +146 -0
  41. package/examples/react-native.factory.json +58 -5
  42. package/examples/roles.example.json +31 -0
  43. package/examples/swift.factory.json +60 -5
  44. package/examples/uni-app.factory.json +58 -5
  45. package/package.json +1 -1
  46. package/skills/apphx/SKILL.md +39 -4
  47. package/skills/apphx/resources/requirement.json +1 -0
  48. package/skills/apphx-obfuscator/LICENSE +21 -0
  49. 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/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