@pixelbyte-software/pixcode 1.43.0 → 1.44.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,233 @@
1
+ import crypto from 'node:crypto';
2
+ import { appConfigDb } from '../database/db.js';
3
+ const CONFIG_KEY = 'production_agent_loop';
4
+ export const DESKTOP_RELEASE_ASSET_TYPES = [
5
+ { id: 'windows-x64', extension: '.exe', required: true },
6
+ { id: 'linux-x64', extension: '.AppImage', required: true },
7
+ { id: 'linux-deb', extension: '.deb', required: true },
8
+ { id: 'macos-x64', extension: 'x64.dmg', required: true },
9
+ { id: 'macos-arm64', extension: 'arm64.dmg', required: true },
10
+ ];
11
+ function nowIso() {
12
+ return new Date().toISOString();
13
+ }
14
+ function readStore() {
15
+ const raw = appConfigDb.get(CONFIG_KEY);
16
+ if (!raw) {
17
+ return {
18
+ issueRuns: [],
19
+ reviewQueue: [],
20
+ schedulerJobs: [],
21
+ checkpoints: [],
22
+ };
23
+ }
24
+ try {
25
+ const parsed = JSON.parse(raw);
26
+ return {
27
+ issueRuns: Array.isArray(parsed.issueRuns) ? parsed.issueRuns : [],
28
+ reviewQueue: Array.isArray(parsed.reviewQueue) ? parsed.reviewQueue : [],
29
+ schedulerJobs: Array.isArray(parsed.schedulerJobs) ? parsed.schedulerJobs : [],
30
+ checkpoints: Array.isArray(parsed.checkpoints) ? parsed.checkpoints : [],
31
+ };
32
+ }
33
+ catch {
34
+ return {
35
+ issueRuns: [],
36
+ reviewQueue: [],
37
+ schedulerJobs: [],
38
+ checkpoints: [],
39
+ };
40
+ }
41
+ }
42
+ function writeStore(store) {
43
+ appConfigDb.set(CONFIG_KEY, JSON.stringify(store));
44
+ }
45
+ function compact(text, max = 90) {
46
+ const value = String(text || '').replace(/\s+/g, ' ').trim();
47
+ return value.length > max ? value.slice(0, max).replace(/[-_\s]+$/g, '') : value;
48
+ }
49
+ function slugify(value) {
50
+ const slug = compact(value, 64)
51
+ .toLowerCase()
52
+ .replace(/[^a-z0-9]+/g, '-')
53
+ .replace(/^-+|-+$/g, '');
54
+ return slug || 'agent-task';
55
+ }
56
+ export function parseGitHubIssueRef(input = {}) {
57
+ const url = typeof input.issueUrl === 'string' ? input.issueUrl.trim() : '';
58
+ const directNumber = Number.parseInt(String(input.issueNumber || ''), 10);
59
+ const urlMatch = url.match(/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/i);
60
+ return {
61
+ owner: input.owner || urlMatch?.[1] || null,
62
+ repo: input.repo || urlMatch?.[2] || null,
63
+ issueNumber: Number.isFinite(directNumber) ? directNumber : Number.parseInt(urlMatch?.[3] || '0', 10) || null,
64
+ issueUrl: url || null,
65
+ };
66
+ }
67
+ export function createIssueToPrRun(input = {}, userId = null) {
68
+ const issue = parseGitHubIssueRef(input);
69
+ if (!issue.issueNumber && !input.title) {
70
+ throw new Error('Issue-to-PR run requires an issue number, issue URL, or title.');
71
+ }
72
+ const title = compact(input.title || `Issue ${issue.issueNumber}`);
73
+ const branchName = input.branchName || `pixcode/issue-${issue.issueNumber || 'manual'}-${slugify(title)}`;
74
+ const run = {
75
+ id: crypto.randomUUID(),
76
+ type: 'github_issue_to_pr',
77
+ status: 'queued',
78
+ createdAt: nowIso(),
79
+ updatedAt: nowIso(),
80
+ userId,
81
+ issue,
82
+ projectName: input.projectName || null,
83
+ projectPath: input.projectPath || null,
84
+ provider: input.provider || 'opencode',
85
+ model: input.model || null,
86
+ branchName,
87
+ baseBranch: input.baseBranch || 'main',
88
+ acceptanceCriteria: Array.isArray(input.acceptanceCriteria) ? input.acceptanceCriteria : [],
89
+ agentRequest: {
90
+ projectPath: input.projectPath || undefined,
91
+ githubUrl: input.githubUrl || undefined,
92
+ message: [
93
+ `Resolve ${issue.issueUrl || `GitHub issue #${issue.issueNumber || ''}`}`.trim(),
94
+ input.body || title,
95
+ 'Create a branch, run verification, and prepare a pull request summary.',
96
+ ].filter(Boolean).join('\n\n'),
97
+ provider: input.provider || 'opencode',
98
+ model: input.model || undefined,
99
+ branchName,
100
+ createBranch: true,
101
+ createPR: true,
102
+ },
103
+ };
104
+ const store = readStore();
105
+ store.issueRuns.unshift(run);
106
+ writeStore(store);
107
+ return run;
108
+ }
109
+ export function parseCiRepairSignals(logText = '') {
110
+ const text = String(logText || '');
111
+ const lines = text.split(/\r?\n/);
112
+ const failedCommands = [];
113
+ const files = new Set();
114
+ const errors = [];
115
+ for (const line of lines) {
116
+ const trimmed = line.trim();
117
+ if (!trimmed)
118
+ continue;
119
+ if (/npm ERR!|error TS\d+|FAIL|failed|exit code/i.test(trimmed)) {
120
+ errors.push(trimmed);
121
+ }
122
+ const command = trimmed.match(/(?:run|command|script)\s+[`'"]?([a-z0-9:_-]+)[`'"]?/i)?.[1];
123
+ if (command)
124
+ failedCommands.push(command);
125
+ const file = trimmed.match(/((?:src|server|shared|scripts|desktop)\/[^\s:)]+)/)?.[1];
126
+ if (file)
127
+ files.add(file);
128
+ }
129
+ return {
130
+ failedCommands: Array.from(new Set(failedCommands)),
131
+ files: Array.from(files),
132
+ errors: errors.slice(0, 25),
133
+ repairPrompt: [
134
+ 'CI-aware repair loop:',
135
+ '1. Reproduce the failing command locally.',
136
+ '2. Fix only the failing behavior.',
137
+ '3. Re-run the failed command plus related smoke checks.',
138
+ '',
139
+ errors.slice(0, 8).join('\n'),
140
+ ].join('\n').trim(),
141
+ };
142
+ }
143
+ export function createReviewQueueItem(input = {}, userId = null) {
144
+ const item = {
145
+ id: crypto.randomUUID(),
146
+ status: input.status || 'review_requested',
147
+ createdAt: nowIso(),
148
+ updatedAt: nowIso(),
149
+ userId,
150
+ projectName: input.projectName || null,
151
+ projectPath: input.projectPath || null,
152
+ title: compact(input.title || 'Review requested'),
153
+ changedFiles: Array.isArray(input.changedFiles) ? input.changedFiles : [],
154
+ notes: input.notes || '',
155
+ };
156
+ const store = readStore();
157
+ store.reviewQueue.unshift(item);
158
+ writeStore(store);
159
+ return item;
160
+ }
161
+ export function updateReviewQueueItem(itemId, patch = {}) {
162
+ const store = readStore();
163
+ let updated = null;
164
+ store.reviewQueue = store.reviewQueue.map((item) => {
165
+ if (item.id !== itemId)
166
+ return item;
167
+ updated = {
168
+ ...item,
169
+ ...patch,
170
+ id: item.id,
171
+ updatedAt: nowIso(),
172
+ };
173
+ return updated;
174
+ });
175
+ writeStore(store);
176
+ return updated;
177
+ }
178
+ export function scheduleBackgroundAgentJob(input = {}, userId = null) {
179
+ const job = {
180
+ id: crypto.randomUUID(),
181
+ status: 'scheduled',
182
+ createdAt: nowIso(),
183
+ updatedAt: nowIso(),
184
+ userId,
185
+ name: compact(input.name || 'Background agent job'),
186
+ mode: input.mode || 'manual',
187
+ cron: input.cron || null,
188
+ watch: input.watch || null,
189
+ projectName: input.projectName || null,
190
+ provider: input.provider || 'opencode',
191
+ prompt: input.prompt || '',
192
+ nextRunAt: input.nextRunAt || null,
193
+ };
194
+ const store = readStore();
195
+ store.schedulerJobs.unshift(job);
196
+ writeStore(store);
197
+ return job;
198
+ }
199
+ export function createWorkspaceCheckpoint(input = {}, userId = null) {
200
+ const checkpoint = {
201
+ id: crypto.randomUUID(),
202
+ protocol: 'pixcode.workspace-checkpoint.v1',
203
+ createdAt: nowIso(),
204
+ userId,
205
+ projectName: input.projectName || null,
206
+ projectPath: input.projectPath || null,
207
+ reason: compact(input.reason || 'manual checkpoint'),
208
+ gitHead: input.gitHead || null,
209
+ changedFiles: Array.isArray(input.changedFiles) ? input.changedFiles : [],
210
+ metadata: input.metadata && typeof input.metadata === 'object' ? input.metadata : {},
211
+ };
212
+ const store = readStore();
213
+ store.checkpoints.unshift(checkpoint);
214
+ writeStore(store);
215
+ return checkpoint;
216
+ }
217
+ export function getProductionAgentLoopState() {
218
+ return readStore();
219
+ }
220
+ export function evaluateDesktopReleaseAssetPolicy(assetNames = []) {
221
+ const names = Array.isArray(assetNames) ? assetNames.map(String) : [];
222
+ const required = DESKTOP_RELEASE_ASSET_TYPES.map((assetType) => ({
223
+ ...assetType,
224
+ present: names.some((name) => name.endsWith(assetType.extension)),
225
+ }));
226
+ return {
227
+ protocol: 'pixcode.desktop-release-assets.v1',
228
+ required,
229
+ complete: required.every((asset) => asset.present),
230
+ 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.',
231
+ };
232
+ }
233
+ //# sourceMappingURL=production-agent-loop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"production-agent-loop.js","sourceRoot":"","sources":["../../../server/services/production-agent-loop.js"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixelbyte-software/pixcode",
3
- "version": "1.43.0",
3
+ "version": "1.44.0",
4
4
  "description": "Self-hosted AI coding agent control room for Claude Code, Cursor CLI, OpenAI Codex, Gemini CLI, Qwen Code, and OpenCode with chat, files, shell, Git, orchestration, API keys, Telegram, MCP, plugins, themes, and desktop/server deployment.",
5
5
  "type": "module",
6
6
  "main": "dist-server/server/index.js",
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+
3
+ import assert from 'node:assert/strict';
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+
7
+ const root = process.cwd();
8
+
9
+ function read(relativePath) {
10
+ return fs.readFileSync(path.join(root, relativePath), 'utf8');
11
+ }
12
+
13
+ const service = read('server/services/production-agent-loop.js');
14
+ assert.match(service, /createIssueToPrRun/, 'Production loop should create GitHub issue-to-PR runs.');
15
+ assert.match(service, /parseCiRepairSignals/, 'Production loop should parse CI repair signals.');
16
+ assert.match(service, /createReviewQueueItem/, 'Production loop should create code review queue items.');
17
+ assert.match(service, /scheduleBackgroundAgentJob/, 'Production loop should schedule background agent jobs.');
18
+ assert.match(service, /createWorkspaceCheckpoint/, 'Production loop should create workspace checkpoints.');
19
+ assert.match(service, /DESKTOP_RELEASE_ASSET_TYPES/, 'Production loop should define required desktop release assets.');
20
+
21
+ const routes = read('server/routes/production-agent-loop.js');
22
+ assert.match(routes, /\/github\/issue-to-pr/, 'Production loop routes should expose issue-to-PR kickoff.');
23
+ assert.match(routes, /\/ci\/repair-plan/, 'Production loop routes should expose CI repair planning.');
24
+ assert.match(routes, /\/review-queue/, 'Production loop routes should expose review queue APIs.');
25
+ assert.match(routes, /\/scheduler\/jobs/, 'Production loop routes should expose background scheduler jobs.');
26
+ assert.match(routes, /\/snapshots/, 'Production loop routes should expose workspace snapshots.');
27
+ assert.match(routes, /\/desktop-release\/assets-policy/, 'Production loop routes should expose desktop asset policy.');
28
+
29
+ const server = read('server/index.js');
30
+ assert.match(server, /productionAgentLoopRoutes/, 'Server should import production loop routes.');
31
+ assert.match(server, /\/api\/production-agent-loop/, 'Server should mount production loop routes.');
32
+
33
+ const diffAnchors = read('src/utils/diffAnchors.ts');
34
+ assert.match(diffAnchors, /firstChangedLine/, 'Frontend should compute first changed diff line.');
35
+ assert.match(diffAnchors, /buildDiffLineHref/, 'Frontend should build editor line anchors for changed files.');
36
+
37
+ const changesRail = read('src/components/main-content/view/subcomponents/ChangedFilesActivityRail.tsx');
38
+ assert.match(changesRail, /firstChangedLine/, 'Changed files rail should compute changed-line anchors.');
39
+ assert.match(changesRail, /lineHint/, 'Changed files rail should show changed-line hints.');
40
+
41
+ const docs = read('docs/production-agent-loop.md');
42
+ assert.match(docs, /Issue-to-PR/, 'Docs should explain issue-to-PR flow.');
43
+ assert.match(docs, /CI-aware repair/, 'Docs should explain CI-aware repair.');
44
+ assert.match(docs, /checkpoint/i, 'Docs should explain workspace checkpoints.');
45
+ assert.match(docs, /desktop asset/i, 'Docs should explain desktop asset requirements.');
46
+
47
+ console.log('v1.44 production loop smoke passed');
package/server/index.js CHANGED
@@ -79,6 +79,7 @@ import diagnosticsRoutes from './routes/diagnostics.js';
79
79
  import remoteRoutes from './routes/remote.js';
80
80
  import publicApiRoutes from './routes/public-api.js';
81
81
  import webhooksRoutes from './routes/webhooks.js';
82
+ import productionAgentLoopRoutes from './routes/production-agent-loop.js';
82
83
  import liveViewRoutes, { createLiveViewPublicRouter } from './routes/live-view.js';
83
84
  import providerRoutes from './modules/providers/provider.routes.js';
84
85
  import {
@@ -410,6 +411,9 @@ app.use('/api/public', authenticateToken, publicApiRoutes);
410
411
  // Outbound webhook automation (protected)
411
412
  app.use('/api/webhooks', authenticateToken, webhooksRoutes);
412
413
 
414
+ // Production agent loop APIs (protected)
415
+ app.use('/api/production-agent-loop', authenticateToken, productionAgentLoopRoutes);
416
+
413
417
  // Project Live View (protected control API + public share proxy)
414
418
  app.use('/api/live-view', authenticateToken, liveViewRoutes);
415
419
 
@@ -0,0 +1,90 @@
1
+ import express from 'express';
2
+
3
+ import {
4
+ createIssueToPrRun,
5
+ createReviewQueueItem,
6
+ createWorkspaceCheckpoint,
7
+ evaluateDesktopReleaseAssetPolicy,
8
+ getProductionAgentLoopState,
9
+ parseCiRepairSignals,
10
+ scheduleBackgroundAgentJob,
11
+ updateReviewQueueItem,
12
+ } from '../services/production-agent-loop.js';
13
+
14
+ const router = express.Router();
15
+
16
+ function userId(req) {
17
+ return req.user?.id ?? req.user?.userId ?? null;
18
+ }
19
+
20
+ router.get('/', (_req, res) => {
21
+ res.json({ success: true, state: getProductionAgentLoopState() });
22
+ });
23
+
24
+ router.post('/github/issue-to-pr', (req, res) => {
25
+ try {
26
+ const run = createIssueToPrRun(req.body || {}, userId(req));
27
+ res.status(202).json({ success: true, run });
28
+ } catch (error) {
29
+ res.status(400).json({ success: false, error: error.message });
30
+ }
31
+ });
32
+
33
+ router.post('/ci/repair-plan', (req, res) => {
34
+ res.json({
35
+ success: true,
36
+ repairPlan: parseCiRepairSignals(req.body?.log || req.body?.output || ''),
37
+ });
38
+ });
39
+
40
+ router.get('/review-queue', (_req, res) => {
41
+ res.json({ success: true, reviewQueue: getProductionAgentLoopState().reviewQueue });
42
+ });
43
+
44
+ router.post('/review-queue', (req, res) => {
45
+ const item = createReviewQueueItem(req.body || {}, userId(req));
46
+ res.status(201).json({ success: true, item });
47
+ });
48
+
49
+ router.patch('/review-queue/:id', (req, res) => {
50
+ const item = updateReviewQueueItem(req.params.id, req.body || {});
51
+ if (!item) {
52
+ res.status(404).json({ success: false, error: 'Review queue item not found.' });
53
+ return;
54
+ }
55
+ res.json({ success: true, item });
56
+ });
57
+
58
+ router.get('/scheduler/jobs', (_req, res) => {
59
+ res.json({ success: true, jobs: getProductionAgentLoopState().schedulerJobs });
60
+ });
61
+
62
+ router.post('/scheduler/jobs', (req, res) => {
63
+ const job = scheduleBackgroundAgentJob(req.body || {}, userId(req));
64
+ res.status(201).json({ success: true, job });
65
+ });
66
+
67
+ router.get('/snapshots', (_req, res) => {
68
+ res.json({ success: true, checkpoints: getProductionAgentLoopState().checkpoints });
69
+ });
70
+
71
+ router.post('/snapshots', (req, res) => {
72
+ const checkpoint = createWorkspaceCheckpoint(req.body || {}, userId(req));
73
+ res.status(201).json({ success: true, checkpoint });
74
+ });
75
+
76
+ router.post('/desktop-release/assets-policy', (req, res) => {
77
+ res.json({
78
+ success: true,
79
+ policy: evaluateDesktopReleaseAssetPolicy(req.body?.assets || req.body?.assetNames || []),
80
+ });
81
+ });
82
+
83
+ router.get('/desktop-release/assets-policy', (_req, res) => {
84
+ res.json({
85
+ success: true,
86
+ policy: evaluateDesktopReleaseAssetPolicy([]),
87
+ });
88
+ });
89
+
90
+ export default router;
@@ -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
+ }