@pixelbyte-software/pixcode 1.43.0 → 1.45.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.
@@ -0,0 +1,460 @@
1
+ import crypto from 'node:crypto';
2
+
3
+ import { appConfigDb } from '../database/db.js';
4
+
5
+ const CONFIG_KEY = 'platformization';
6
+
7
+ export const TEAM_ROLES = {
8
+ owner: [
9
+ 'team:manage',
10
+ 'project:admin',
11
+ 'run:approve',
12
+ 'secret:manage',
13
+ 'marketplace:manage',
14
+ 'eval:run',
15
+ 'usage:view',
16
+ 'security:audit',
17
+ ],
18
+ admin: [
19
+ 'project:admin',
20
+ 'run:approve',
21
+ 'secret:manage',
22
+ 'marketplace:manage',
23
+ 'eval:run',
24
+ 'usage:view',
25
+ 'security:audit',
26
+ ],
27
+ member: [
28
+ 'project:write',
29
+ 'run:create',
30
+ 'secret:use',
31
+ 'eval:run',
32
+ 'usage:view',
33
+ ],
34
+ viewer: [
35
+ 'project:read',
36
+ 'usage:view',
37
+ ],
38
+ };
39
+
40
+ export const SECRET_SCOPES = ['global', 'provider', 'project', 'workflow', 'telegram', 'api'];
41
+
42
+ export const MARKETPLACE_PLUGIN_TYPES = ['mcp-server', 'workflow-template', 'provider-adapter', 'notification-channel'];
43
+
44
+ export const SECURITY_AUDIT_CHECKS = [
45
+ 'dependency_audit',
46
+ 'secret_scan',
47
+ 'permission_audit',
48
+ 'agent_output_leak_detection',
49
+ ];
50
+
51
+ function nowIso() {
52
+ return new Date().toISOString();
53
+ }
54
+
55
+ function emptyStore() {
56
+ return {
57
+ teamMembers: [],
58
+ secrets: [],
59
+ marketplacePlugins: [],
60
+ evaluationSuites: [],
61
+ evaluationRuns: [],
62
+ usageEvents: [],
63
+ securityAuditRuns: [],
64
+ auditLog: [],
65
+ };
66
+ }
67
+
68
+ function readStore() {
69
+ const raw = appConfigDb.get(CONFIG_KEY);
70
+ if (!raw) return emptyStore();
71
+ try {
72
+ const parsed = JSON.parse(raw);
73
+ return {
74
+ teamMembers: Array.isArray(parsed.teamMembers) ? parsed.teamMembers : [],
75
+ secrets: Array.isArray(parsed.secrets) ? parsed.secrets : [],
76
+ marketplacePlugins: Array.isArray(parsed.marketplacePlugins) ? parsed.marketplacePlugins : [],
77
+ evaluationSuites: Array.isArray(parsed.evaluationSuites) ? parsed.evaluationSuites : [],
78
+ evaluationRuns: Array.isArray(parsed.evaluationRuns) ? parsed.evaluationRuns : [],
79
+ usageEvents: Array.isArray(parsed.usageEvents) ? parsed.usageEvents : [],
80
+ securityAuditRuns: Array.isArray(parsed.securityAuditRuns) ? parsed.securityAuditRuns : [],
81
+ auditLog: Array.isArray(parsed.auditLog) ? parsed.auditLog : [],
82
+ };
83
+ } catch {
84
+ return emptyStore();
85
+ }
86
+ }
87
+
88
+ function writeStore(store) {
89
+ appConfigDb.set(CONFIG_KEY, JSON.stringify(store));
90
+ }
91
+
92
+ function compact(text, max = 120) {
93
+ const value = String(text || '').replace(/\s+/g, ' ').trim();
94
+ return value.length > max ? value.slice(0, max).replace(/[-_\s]+$/g, '') : value;
95
+ }
96
+
97
+ function slugify(value) {
98
+ const slug = compact(value, 72)
99
+ .toLowerCase()
100
+ .replace(/[^a-z0-9]+/g, '-')
101
+ .replace(/^-+|-+$/g, '');
102
+ return slug || crypto.randomUUID();
103
+ }
104
+
105
+ function addAudit(store, action, actorId, details = {}) {
106
+ store.auditLog.unshift({
107
+ id: crypto.randomUUID(),
108
+ action,
109
+ actorId: actorId || null,
110
+ createdAt: nowIso(),
111
+ details,
112
+ });
113
+ store.auditLog = store.auditLog.slice(0, 250);
114
+ }
115
+
116
+ function normalizeRole(role) {
117
+ return TEAM_ROLES[role] ? role : 'viewer';
118
+ }
119
+
120
+ function normalizeScope(scope) {
121
+ return SECRET_SCOPES.includes(scope) ? scope : 'project';
122
+ }
123
+
124
+ function vaultKey() {
125
+ const material = process.env.PIXCODE_SECRET_KEY || process.env.JWT_SECRET || appConfigDb.getOrCreateJwtSecret();
126
+ return crypto.createHash('sha256').update(material).digest();
127
+ }
128
+
129
+ function sealSecret(value) {
130
+ const iv = crypto.randomBytes(12);
131
+ const cipher = crypto.createCipheriv('aes-256-gcm', vaultKey(), iv);
132
+ const encrypted = Buffer.concat([cipher.update(String(value || ''), 'utf8'), cipher.final()]);
133
+ return {
134
+ algorithm: 'aes-256-gcm',
135
+ iv: iv.toString('base64'),
136
+ tag: cipher.getAuthTag().toString('base64'),
137
+ ciphertext: encrypted.toString('base64'),
138
+ };
139
+ }
140
+
141
+ function openSecret(sealed) {
142
+ const decipher = crypto.createDecipheriv('aes-256-gcm', vaultKey(), Buffer.from(sealed.iv, 'base64'));
143
+ decipher.setAuthTag(Buffer.from(sealed.tag, 'base64'));
144
+ return Buffer.concat([
145
+ decipher.update(Buffer.from(sealed.ciphertext, 'base64')),
146
+ decipher.final(),
147
+ ]).toString('utf8');
148
+ }
149
+
150
+ function redactSecret(secret) {
151
+ return {
152
+ ...secret,
153
+ sealedValue: undefined,
154
+ redacted: '********',
155
+ };
156
+ }
157
+
158
+ function scopeMatches(secret, input = {}) {
159
+ if (secret.scope === 'global') return true;
160
+ if (secret.scope === 'provider') return !input.provider || secret.target === input.provider;
161
+ if (secret.scope === 'project') return !input.projectPath || secret.target === input.projectPath || secret.target === input.projectName;
162
+ if (secret.scope === 'workflow') return !input.workflowId || secret.target === input.workflowId;
163
+ if (secret.scope === 'telegram') return input.channel === 'telegram';
164
+ if (secret.scope === 'api') return input.channel === 'api';
165
+ return false;
166
+ }
167
+
168
+ export function getPlatformizationState() {
169
+ const store = readStore();
170
+ return {
171
+ roles: TEAM_ROLES,
172
+ secretScopes: SECRET_SCOPES,
173
+ marketplacePluginTypes: MARKETPLACE_PLUGIN_TYPES,
174
+ securityAuditChecks: SECURITY_AUDIT_CHECKS,
175
+ teamMembers: store.teamMembers,
176
+ secrets: store.secrets.map(redactSecret),
177
+ marketplacePlugins: store.marketplacePlugins,
178
+ evaluationSuites: store.evaluationSuites,
179
+ evaluationRuns: store.evaluationRuns,
180
+ usageSummary: summarizeUsageEvents(store.usageEvents),
181
+ securityAuditRuns: store.securityAuditRuns,
182
+ auditLog: store.auditLog,
183
+ };
184
+ }
185
+
186
+ export function createTeamMember(input = {}, actorId = null) {
187
+ const email = compact(input.email || input.username || '');
188
+ if (!email) throw new Error('Team member email or username is required.');
189
+ const store = readStore();
190
+ const member = {
191
+ id: crypto.randomUUID(),
192
+ email,
193
+ displayName: compact(input.displayName || email, 80),
194
+ role: normalizeRole(input.role || 'viewer'),
195
+ projectScopes: Array.isArray(input.projectScopes) ? input.projectScopes : [],
196
+ status: input.status || 'active',
197
+ createdAt: nowIso(),
198
+ updatedAt: nowIso(),
199
+ };
200
+ member.permissions = TEAM_ROLES[member.role];
201
+ store.teamMembers.unshift(member);
202
+ addAudit(store, 'team.member.created', actorId, { memberId: member.id, role: member.role });
203
+ writeStore(store);
204
+ return member;
205
+ }
206
+
207
+ export function updateTeamMember(memberId, patch = {}, actorId = null) {
208
+ const store = readStore();
209
+ let updated = null;
210
+ store.teamMembers = store.teamMembers.map((member) => {
211
+ if (member.id !== memberId) return member;
212
+ updated = {
213
+ ...member,
214
+ ...patch,
215
+ id: member.id,
216
+ role: normalizeRole(patch.role || member.role),
217
+ updatedAt: nowIso(),
218
+ };
219
+ updated.permissions = TEAM_ROLES[updated.role];
220
+ return updated;
221
+ });
222
+ if (updated) {
223
+ addAudit(store, 'team.member.updated', actorId, { memberId, role: updated.role });
224
+ writeStore(store);
225
+ }
226
+ return updated;
227
+ }
228
+
229
+ export function createSecret(input = {}, actorId = null) {
230
+ const name = compact(input.name || input.envName || '');
231
+ const value = input.value;
232
+ if (!name || typeof value !== 'string') throw new Error('Secret name and string value are required.');
233
+ const scope = normalizeScope(input.scope || 'project');
234
+ const store = readStore();
235
+ const secret = {
236
+ id: crypto.randomUUID(),
237
+ name,
238
+ envName: compact(input.envName || name).replace(/[^A-Z0-9_]/gi, '_').toUpperCase(),
239
+ scope,
240
+ target: input.target || input.projectPath || input.provider || null,
241
+ createdAt: nowIso(),
242
+ updatedAt: nowIso(),
243
+ fingerprint: crypto.createHash('sha256').update(value).digest('hex').slice(0, 12),
244
+ sealedValue: sealSecret(value),
245
+ };
246
+ store.secrets = store.secrets.filter((existing) => !(existing.envName === secret.envName && existing.scope === secret.scope && existing.target === secret.target));
247
+ store.secrets.unshift(secret);
248
+ addAudit(store, 'secret.created', actorId, { secretId: secret.id, scope: secret.scope, envName: secret.envName });
249
+ writeStore(store);
250
+ return redactSecret(secret);
251
+ }
252
+
253
+ export function listSecrets() {
254
+ return readStore().secrets.map(redactSecret);
255
+ }
256
+
257
+ export function materializeScopedEnv(input = {}, options = {}) {
258
+ const store = readStore();
259
+ const env = {};
260
+ const included = [];
261
+ for (const secret of store.secrets) {
262
+ if (!scopeMatches(secret, input)) continue;
263
+ included.push({
264
+ id: secret.id,
265
+ envName: secret.envName,
266
+ scope: secret.scope,
267
+ target: secret.target,
268
+ redacted: '********',
269
+ });
270
+ if (options.reveal === true) {
271
+ env[secret.envName] = openSecret(secret.sealedValue);
272
+ }
273
+ }
274
+ return { env, included };
275
+ }
276
+
277
+ export function upsertMarketplacePlugin(input = {}, actorId = null) {
278
+ const pluginId = input.id || slugify(input.name || input.packageName || 'plugin');
279
+ const store = readStore();
280
+ const existing = store.marketplacePlugins.find((plugin) => plugin.id === pluginId);
281
+ const plugin = {
282
+ id: pluginId,
283
+ name: compact(input.name || pluginId, 100),
284
+ type: MARKETPLACE_PLUGIN_TYPES.includes(input.type) ? input.type : 'mcp-server',
285
+ source: input.source || input.packageName || input.repository || null,
286
+ permissionScopes: Array.isArray(input.permissionScopes) ? input.permissionScopes : [],
287
+ installCommand: input.installCommand || null,
288
+ status: input.status || existing?.status || 'available',
289
+ health: input.health || existing?.health || { status: 'unknown', checkedAt: null },
290
+ updatedAt: nowIso(),
291
+ createdAt: existing?.createdAt || nowIso(),
292
+ };
293
+ store.marketplacePlugins = [plugin, ...store.marketplacePlugins.filter((item) => item.id !== pluginId)];
294
+ addAudit(store, 'marketplace.plugin.upserted', actorId, { pluginId, type: plugin.type });
295
+ writeStore(store);
296
+ return plugin;
297
+ }
298
+
299
+ export function updateMarketplacePluginHealth(pluginId, health = {}, actorId = null) {
300
+ const store = readStore();
301
+ let updated = null;
302
+ store.marketplacePlugins = store.marketplacePlugins.map((plugin) => {
303
+ if (plugin.id !== pluginId) return plugin;
304
+ updated = {
305
+ ...plugin,
306
+ health: {
307
+ status: health.status || 'unknown',
308
+ message: health.message || '',
309
+ checkedAt: nowIso(),
310
+ },
311
+ updatedAt: nowIso(),
312
+ };
313
+ return updated;
314
+ });
315
+ if (updated) {
316
+ addAudit(store, 'marketplace.plugin.health_checked', actorId, { pluginId, status: updated.health.status });
317
+ writeStore(store);
318
+ }
319
+ return updated;
320
+ }
321
+
322
+ export function createEvaluationSuite(input = {}, actorId = null) {
323
+ const tasks = Array.isArray(input.tasks) ? input.tasks : [];
324
+ const suite = {
325
+ id: input.id || slugify(input.name || 'evaluation-suite'),
326
+ name: compact(input.name || 'Evaluation suite', 100),
327
+ description: compact(input.description || '', 240),
328
+ tasks: tasks.map((task, index) => ({
329
+ id: task.id || `task-${index + 1}`,
330
+ title: compact(task.title || `Task ${index + 1}`, 120),
331
+ acceptanceCriteria: Array.isArray(task.acceptanceCriteria) ? task.acceptanceCriteria : [],
332
+ projectPath: task.projectPath || null,
333
+ })),
334
+ createdAt: nowIso(),
335
+ updatedAt: nowIso(),
336
+ };
337
+ const store = readStore();
338
+ store.evaluationSuites = [suite, ...store.evaluationSuites.filter((item) => item.id !== suite.id)];
339
+ addAudit(store, 'eval.suite.upserted', actorId, { suiteId: suite.id, tasks: suite.tasks.length });
340
+ writeStore(store);
341
+ return suite;
342
+ }
343
+
344
+ export function createEvaluationRun(input = {}, actorId = null) {
345
+ const results = Array.isArray(input.results) ? input.results : [];
346
+ const passed = results.filter((result) => result.status === 'passed').length;
347
+ const run = {
348
+ id: crypto.randomUUID(),
349
+ suiteId: input.suiteId || null,
350
+ provider: input.provider || null,
351
+ model: input.model || null,
352
+ status: input.status || 'completed',
353
+ createdAt: nowIso(),
354
+ results,
355
+ summary: {
356
+ total: results.length,
357
+ passed,
358
+ failed: results.filter((result) => result.status === 'failed').length,
359
+ passRate: results.length ? Math.round((passed / results.length) * 1000) / 10 : 0,
360
+ averageLatencyMs: average(results.map((result) => Number(result.latencyMs || 0)).filter(Boolean)),
361
+ },
362
+ };
363
+ const store = readStore();
364
+ store.evaluationRuns.unshift(run);
365
+ addAudit(store, 'eval.run.created', actorId, { runId: run.id, suiteId: run.suiteId, passRate: run.summary.passRate });
366
+ writeStore(store);
367
+ return run;
368
+ }
369
+
370
+ function average(values) {
371
+ if (!values.length) return 0;
372
+ return Math.round(values.reduce((sum, value) => sum + value, 0) / values.length);
373
+ }
374
+
375
+ export function recordUsageEvent(input = {}, actorId = null) {
376
+ const event = {
377
+ id: crypto.randomUUID(),
378
+ createdAt: input.createdAt || nowIso(),
379
+ provider: input.provider || 'unknown',
380
+ model: input.model || 'unknown',
381
+ workflow: input.workflow || input.source || 'manual',
382
+ inputTokens: Number(input.inputTokens || 0),
383
+ outputTokens: Number(input.outputTokens || 0),
384
+ costUsd: Number(input.costUsd || 0),
385
+ latencyMs: Number(input.latencyMs || 0),
386
+ status: input.status || 'ok',
387
+ };
388
+ const store = readStore();
389
+ store.usageEvents.unshift(event);
390
+ store.usageEvents = store.usageEvents.slice(0, 2000);
391
+ addAudit(store, 'usage.event.recorded', actorId, { provider: event.provider, model: event.model, status: event.status });
392
+ writeStore(store);
393
+ return event;
394
+ }
395
+
396
+ export function summarizeUsageEvents(events = readStore().usageEvents) {
397
+ const groups = new Map();
398
+ for (const event of events) {
399
+ const key = `${event.provider}:${event.model}:${event.workflow}`;
400
+ const current = groups.get(key) || {
401
+ provider: event.provider,
402
+ model: event.model,
403
+ workflow: event.workflow,
404
+ runs: 0,
405
+ errors: 0,
406
+ inputTokens: 0,
407
+ outputTokens: 0,
408
+ totalTokens: 0,
409
+ costUsd: 0,
410
+ latencyMs: 0,
411
+ };
412
+ current.runs += 1;
413
+ current.errors += event.status === 'error' ? 1 : 0;
414
+ current.inputTokens += event.inputTokens;
415
+ current.outputTokens += event.outputTokens;
416
+ current.totalTokens += event.inputTokens + event.outputTokens;
417
+ current.costUsd += event.costUsd;
418
+ current.latencyMs += event.latencyMs;
419
+ groups.set(key, current);
420
+ }
421
+ return Array.from(groups.values()).map((group) => ({
422
+ ...group,
423
+ costUsd: Math.round(group.costUsd * 10000) / 10000,
424
+ averageLatencyMs: group.runs ? Math.round(group.latencyMs / group.runs) : 0,
425
+ errorRate: group.runs ? Math.round((group.errors / group.runs) * 1000) / 10 : 0,
426
+ latencyMs: undefined,
427
+ }));
428
+ }
429
+
430
+ export function createSecurityAuditRun(input = {}, actorId = null) {
431
+ const checks = Array.isArray(input.checks) && input.checks.length
432
+ ? input.checks.filter((check) => SECURITY_AUDIT_CHECKS.includes(check))
433
+ : SECURITY_AUDIT_CHECKS;
434
+ const findings = Array.isArray(input.findings) ? input.findings : [];
435
+ const run = {
436
+ id: crypto.randomUUID(),
437
+ protocol: 'pixcode.security-audit.v1',
438
+ status: input.status || 'queued',
439
+ projectName: input.projectName || null,
440
+ projectPath: input.projectPath || null,
441
+ checks,
442
+ createdAt: nowIso(),
443
+ findings: findings.map((finding, index) => ({
444
+ id: finding.id || `finding-${index + 1}`,
445
+ severity: finding.severity || 'medium',
446
+ title: compact(finding.title || 'Security finding', 140),
447
+ file: finding.file || null,
448
+ recommendation: finding.recommendation || null,
449
+ })),
450
+ checklist: checks.map((check) => ({
451
+ check,
452
+ status: 'pending',
453
+ })),
454
+ };
455
+ const store = readStore();
456
+ store.securityAuditRuns.unshift(run);
457
+ addAudit(store, 'security.audit.created', actorId, { runId: run.id, checks });
458
+ writeStore(store);
459
+ return run;
460
+ }
@@ -0,0 +1,248 @@
1
+ import crypto from 'node:crypto';
2
+
3
+ import { appConfigDb } from '../database/db.js';
4
+
5
+ const CONFIG_KEY = 'production_agent_loop';
6
+
7
+ export const DESKTOP_RELEASE_ASSET_TYPES = [
8
+ { id: 'windows-x64', extension: '.exe', required: true },
9
+ { id: 'linux-x64', extension: '.AppImage', required: true },
10
+ { id: 'linux-deb', extension: '.deb', required: true },
11
+ { id: 'macos-x64', extension: 'x64.dmg', required: true },
12
+ { id: 'macos-arm64', extension: 'arm64.dmg', required: true },
13
+ ];
14
+
15
+ function nowIso() {
16
+ return new Date().toISOString();
17
+ }
18
+
19
+ function readStore() {
20
+ const raw = appConfigDb.get(CONFIG_KEY);
21
+ if (!raw) {
22
+ return {
23
+ issueRuns: [],
24
+ reviewQueue: [],
25
+ schedulerJobs: [],
26
+ checkpoints: [],
27
+ };
28
+ }
29
+ try {
30
+ const parsed = JSON.parse(raw);
31
+ return {
32
+ issueRuns: Array.isArray(parsed.issueRuns) ? parsed.issueRuns : [],
33
+ reviewQueue: Array.isArray(parsed.reviewQueue) ? parsed.reviewQueue : [],
34
+ schedulerJobs: Array.isArray(parsed.schedulerJobs) ? parsed.schedulerJobs : [],
35
+ checkpoints: Array.isArray(parsed.checkpoints) ? parsed.checkpoints : [],
36
+ };
37
+ } catch {
38
+ return {
39
+ issueRuns: [],
40
+ reviewQueue: [],
41
+ schedulerJobs: [],
42
+ checkpoints: [],
43
+ };
44
+ }
45
+ }
46
+
47
+ function writeStore(store) {
48
+ appConfigDb.set(CONFIG_KEY, JSON.stringify(store));
49
+ }
50
+
51
+ function compact(text, max = 90) {
52
+ const value = String(text || '').replace(/\s+/g, ' ').trim();
53
+ return value.length > max ? value.slice(0, max).replace(/[-_\s]+$/g, '') : value;
54
+ }
55
+
56
+ function slugify(value) {
57
+ const slug = compact(value, 64)
58
+ .toLowerCase()
59
+ .replace(/[^a-z0-9]+/g, '-')
60
+ .replace(/^-+|-+$/g, '');
61
+ return slug || 'agent-task';
62
+ }
63
+
64
+ export function parseGitHubIssueRef(input = {}) {
65
+ const url = typeof input.issueUrl === 'string' ? input.issueUrl.trim() : '';
66
+ const directNumber = Number.parseInt(String(input.issueNumber || ''), 10);
67
+ const urlMatch = url.match(/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/i);
68
+ return {
69
+ owner: input.owner || urlMatch?.[1] || null,
70
+ repo: input.repo || urlMatch?.[2] || null,
71
+ issueNumber: Number.isFinite(directNumber) ? directNumber : Number.parseInt(urlMatch?.[3] || '0', 10) || null,
72
+ issueUrl: url || null,
73
+ };
74
+ }
75
+
76
+ export function createIssueToPrRun(input = {}, userId = null) {
77
+ const issue = parseGitHubIssueRef(input);
78
+ if (!issue.issueNumber && !input.title) {
79
+ throw new Error('Issue-to-PR run requires an issue number, issue URL, or title.');
80
+ }
81
+
82
+ const title = compact(input.title || `Issue ${issue.issueNumber}`);
83
+ const branchName = input.branchName || `pixcode/issue-${issue.issueNumber || 'manual'}-${slugify(title)}`;
84
+ const run = {
85
+ id: crypto.randomUUID(),
86
+ type: 'github_issue_to_pr',
87
+ status: 'queued',
88
+ createdAt: nowIso(),
89
+ updatedAt: nowIso(),
90
+ userId,
91
+ issue,
92
+ projectName: input.projectName || null,
93
+ projectPath: input.projectPath || null,
94
+ provider: input.provider || 'opencode',
95
+ model: input.model || null,
96
+ branchName,
97
+ baseBranch: input.baseBranch || 'main',
98
+ acceptanceCriteria: Array.isArray(input.acceptanceCriteria) ? input.acceptanceCriteria : [],
99
+ agentRequest: {
100
+ projectPath: input.projectPath || undefined,
101
+ githubUrl: input.githubUrl || undefined,
102
+ message: [
103
+ `Resolve ${issue.issueUrl || `GitHub issue #${issue.issueNumber || ''}`}`.trim(),
104
+ input.body || title,
105
+ 'Create a branch, run verification, and prepare a pull request summary.',
106
+ ].filter(Boolean).join('\n\n'),
107
+ provider: input.provider || 'opencode',
108
+ model: input.model || undefined,
109
+ branchName,
110
+ createBranch: true,
111
+ createPR: true,
112
+ },
113
+ };
114
+
115
+ const store = readStore();
116
+ store.issueRuns.unshift(run);
117
+ writeStore(store);
118
+ return run;
119
+ }
120
+
121
+ export function parseCiRepairSignals(logText = '') {
122
+ const text = String(logText || '');
123
+ const lines = text.split(/\r?\n/);
124
+ const failedCommands = [];
125
+ const files = new Set();
126
+ const errors = [];
127
+
128
+ for (const line of lines) {
129
+ const trimmed = line.trim();
130
+ if (!trimmed) continue;
131
+ if (/npm ERR!|error TS\d+|FAIL|failed|exit code/i.test(trimmed)) {
132
+ errors.push(trimmed);
133
+ }
134
+ const command = trimmed.match(/(?:run|command|script)\s+[`'"]?([a-z0-9:_-]+)[`'"]?/i)?.[1];
135
+ if (command) failedCommands.push(command);
136
+ const file = trimmed.match(/((?:src|server|shared|scripts|desktop)\/[^\s:)]+)/)?.[1];
137
+ if (file) files.add(file);
138
+ }
139
+
140
+ return {
141
+ failedCommands: Array.from(new Set(failedCommands)),
142
+ files: Array.from(files),
143
+ errors: errors.slice(0, 25),
144
+ repairPrompt: [
145
+ 'CI-aware repair loop:',
146
+ '1. Reproduce the failing command locally.',
147
+ '2. Fix only the failing behavior.',
148
+ '3. Re-run the failed command plus related smoke checks.',
149
+ '',
150
+ errors.slice(0, 8).join('\n'),
151
+ ].join('\n').trim(),
152
+ };
153
+ }
154
+
155
+ export function createReviewQueueItem(input = {}, userId = null) {
156
+ const item = {
157
+ id: crypto.randomUUID(),
158
+ status: input.status || 'review_requested',
159
+ createdAt: nowIso(),
160
+ updatedAt: nowIso(),
161
+ userId,
162
+ projectName: input.projectName || null,
163
+ projectPath: input.projectPath || null,
164
+ title: compact(input.title || 'Review requested'),
165
+ changedFiles: Array.isArray(input.changedFiles) ? input.changedFiles : [],
166
+ notes: input.notes || '',
167
+ };
168
+ const store = readStore();
169
+ store.reviewQueue.unshift(item);
170
+ writeStore(store);
171
+ return item;
172
+ }
173
+
174
+ export function updateReviewQueueItem(itemId, patch = {}) {
175
+ const store = readStore();
176
+ let updated = null;
177
+ store.reviewQueue = store.reviewQueue.map((item) => {
178
+ if (item.id !== itemId) return item;
179
+ updated = {
180
+ ...item,
181
+ ...patch,
182
+ id: item.id,
183
+ updatedAt: nowIso(),
184
+ };
185
+ return updated;
186
+ });
187
+ writeStore(store);
188
+ return updated;
189
+ }
190
+
191
+ export function scheduleBackgroundAgentJob(input = {}, userId = null) {
192
+ const job = {
193
+ id: crypto.randomUUID(),
194
+ status: 'scheduled',
195
+ createdAt: nowIso(),
196
+ updatedAt: nowIso(),
197
+ userId,
198
+ name: compact(input.name || 'Background agent job'),
199
+ mode: input.mode || 'manual',
200
+ cron: input.cron || null,
201
+ watch: input.watch || null,
202
+ projectName: input.projectName || null,
203
+ provider: input.provider || 'opencode',
204
+ prompt: input.prompt || '',
205
+ nextRunAt: input.nextRunAt || null,
206
+ };
207
+ const store = readStore();
208
+ store.schedulerJobs.unshift(job);
209
+ writeStore(store);
210
+ return job;
211
+ }
212
+
213
+ export function createWorkspaceCheckpoint(input = {}, userId = null) {
214
+ const checkpoint = {
215
+ id: crypto.randomUUID(),
216
+ protocol: 'pixcode.workspace-checkpoint.v1',
217
+ createdAt: nowIso(),
218
+ userId,
219
+ projectName: input.projectName || null,
220
+ projectPath: input.projectPath || null,
221
+ reason: compact(input.reason || 'manual checkpoint'),
222
+ gitHead: input.gitHead || null,
223
+ changedFiles: Array.isArray(input.changedFiles) ? input.changedFiles : [],
224
+ metadata: input.metadata && typeof input.metadata === 'object' ? input.metadata : {},
225
+ };
226
+ const store = readStore();
227
+ store.checkpoints.unshift(checkpoint);
228
+ writeStore(store);
229
+ return checkpoint;
230
+ }
231
+
232
+ export function getProductionAgentLoopState() {
233
+ return readStore();
234
+ }
235
+
236
+ export function evaluateDesktopReleaseAssetPolicy(assetNames = []) {
237
+ const names = Array.isArray(assetNames) ? assetNames.map(String) : [];
238
+ const required = DESKTOP_RELEASE_ASSET_TYPES.map((assetType) => ({
239
+ ...assetType,
240
+ present: names.some((name) => name.endsWith(assetType.extension)),
241
+ }));
242
+ return {
243
+ protocol: 'pixcode.desktop-release-assets.v1',
244
+ required,
245
+ complete: required.every((asset) => asset.present),
246
+ rule: 'Every GitHub release must include Windows exe, Linux AppImage, Linux deb, macOS x64 dmg, and macOS arm64 dmg assets. Assets may be carried forward and renamed when the app updates internally.',
247
+ };
248
+ }