@worca/ui 0.17.0 → 0.18.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.
- package/app/main.bundle.js +243 -229
- package/app/main.bundle.js.map +3 -3
- package/app/styles.css +21 -0
- package/bin/worca-ui.js +24 -10
- package/package.json +1 -1
- package/server/argv-parser.js +28 -0
- package/server/index.js +13 -14
- package/server/process-manager.js +42 -4
- package/server/ws-message-router.js +7 -6
- package/server/ws-status-watcher.js +42 -4
package/app/styles.css
CHANGED
|
@@ -3415,6 +3415,27 @@ sl-details.learnings-panel::part(content) {
|
|
|
3415
3415
|
margin: 8px 0 4px;
|
|
3416
3416
|
}
|
|
3417
3417
|
|
|
3418
|
+
.pr-verification-banner {
|
|
3419
|
+
margin: 8px 0 4px;
|
|
3420
|
+
}
|
|
3421
|
+
|
|
3422
|
+
.pr-verified-row {
|
|
3423
|
+
display: flex;
|
|
3424
|
+
align-items: center;
|
|
3425
|
+
gap: 8px;
|
|
3426
|
+
margin-top: 6px;
|
|
3427
|
+
font-size: 13px;
|
|
3428
|
+
}
|
|
3429
|
+
|
|
3430
|
+
.pr-verified-row .meta-label {
|
|
3431
|
+
color: var(--muted);
|
|
3432
|
+
}
|
|
3433
|
+
|
|
3434
|
+
.pr-verified-badge {
|
|
3435
|
+
display: inline-flex;
|
|
3436
|
+
align-items: center;
|
|
3437
|
+
}
|
|
3438
|
+
|
|
3418
3439
|
.classification-strip {
|
|
3419
3440
|
margin-top: 10px;
|
|
3420
3441
|
padding: 8px 10px;
|
package/bin/worca-ui.js
CHANGED
|
@@ -113,6 +113,23 @@ export function parseArgs(argv) {
|
|
|
113
113
|
return args;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/** Build the argv array for spawning server/index.js. Exported for testing. */
|
|
117
|
+
export function buildSpawnArgs({
|
|
118
|
+
serverScript,
|
|
119
|
+
port,
|
|
120
|
+
host,
|
|
121
|
+
isGlobal,
|
|
122
|
+
projectPath,
|
|
123
|
+
}) {
|
|
124
|
+
const args = [serverScript, '--port', String(port), '--host', host];
|
|
125
|
+
if (isGlobal) {
|
|
126
|
+
args.push('--global');
|
|
127
|
+
} else if (projectPath) {
|
|
128
|
+
args.push('--project', projectPath);
|
|
129
|
+
}
|
|
130
|
+
return args;
|
|
131
|
+
}
|
|
132
|
+
|
|
116
133
|
/** Resolve log file path based on mode (mirrors PID file location). */
|
|
117
134
|
function resolveLogPath(isGlobal) {
|
|
118
135
|
if (isGlobal) {
|
|
@@ -242,7 +259,7 @@ function describePortOccupant(port) {
|
|
|
242
259
|
return null;
|
|
243
260
|
}
|
|
244
261
|
|
|
245
|
-
async function start({ port, host, open, global: isGlobal }) {
|
|
262
|
+
async function start({ port, host, open, global: isGlobal, projectPath }) {
|
|
246
263
|
const { pidDir, pidFile } = resolvePidPaths(isGlobal);
|
|
247
264
|
|
|
248
265
|
const existing = readPidFile(pidFile);
|
|
@@ -281,16 +298,13 @@ async function start({ port, host, open, global: isGlobal }) {
|
|
|
281
298
|
}
|
|
282
299
|
}
|
|
283
300
|
|
|
284
|
-
const spawnArgs =
|
|
285
|
-
SERVER_SCRIPT,
|
|
286
|
-
|
|
287
|
-
String(availablePort),
|
|
288
|
-
'--host',
|
|
301
|
+
const spawnArgs = buildSpawnArgs({
|
|
302
|
+
serverScript: SERVER_SCRIPT,
|
|
303
|
+
port: availablePort,
|
|
289
304
|
host,
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
305
|
+
isGlobal,
|
|
306
|
+
projectPath,
|
|
307
|
+
});
|
|
294
308
|
|
|
295
309
|
// Capture child stdout+stderr to a log file so startup crashes are visible.
|
|
296
310
|
// Without this, errors thrown during module load (missing files, bad imports,
|
package/package.json
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse server startup arguments from an argv array.
|
|
3
|
+
* Returns { port, host, isGlobal, projectPath }.
|
|
4
|
+
* Pass defaults to seed port/host from env vars before argv overrides them.
|
|
5
|
+
*/
|
|
6
|
+
export function parseServerArgv(argv, defaults = {}) {
|
|
7
|
+
let port = defaults.port ?? 3400;
|
|
8
|
+
let host = defaults.host ?? '127.0.0.1';
|
|
9
|
+
let isGlobal = true;
|
|
10
|
+
let projectPath = null;
|
|
11
|
+
|
|
12
|
+
for (let i = 0; i < argv.length; i++) {
|
|
13
|
+
if (argv[i] === '--port' && argv[i + 1]) {
|
|
14
|
+
port = parseInt(argv[++i], 10);
|
|
15
|
+
} else if (argv[i] === '--host' && argv[i + 1]) {
|
|
16
|
+
host = argv[++i];
|
|
17
|
+
} else if (argv[i] === '--global') {
|
|
18
|
+
isGlobal = true;
|
|
19
|
+
} else if (argv[i] === '--project') {
|
|
20
|
+
isGlobal = false;
|
|
21
|
+
if (argv[i + 1] && !argv[i + 1].startsWith('-')) {
|
|
22
|
+
projectPath = argv[++i];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return { port, host, isGlobal, projectPath };
|
|
28
|
+
}
|
package/server/index.js
CHANGED
|
@@ -4,21 +4,20 @@ import { createServer } from 'node:http';
|
|
|
4
4
|
import { homedir, platform } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { createApp } from './app.js';
|
|
7
|
+
import { parseServerArgv } from './argv-parser.js';
|
|
7
8
|
import { createIntegrations } from './integrations/index.js';
|
|
8
9
|
import { attachWsServer } from './ws.js';
|
|
9
10
|
|
|
10
|
-
// Parse argv
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (process.argv[i] === '--project') isGlobal = false;
|
|
21
|
-
}
|
|
11
|
+
// Parse argv (env vars provide defaults; argv flags take precedence)
|
|
12
|
+
const {
|
|
13
|
+
port,
|
|
14
|
+
host,
|
|
15
|
+
isGlobal,
|
|
16
|
+
projectPath: explicitProjectPath,
|
|
17
|
+
} = parseServerArgv(process.argv, {
|
|
18
|
+
port: parseInt(process.env.PORT, 10) || 3400,
|
|
19
|
+
host: process.env.HOST || '127.0.0.1',
|
|
20
|
+
});
|
|
22
21
|
|
|
23
22
|
// Resolve project root: walk up from cwd until we find .claude/settings.json
|
|
24
23
|
import { existsSync } from 'node:fs';
|
|
@@ -44,8 +43,8 @@ if (isGlobal) {
|
|
|
44
43
|
worcaDir = null;
|
|
45
44
|
settingsPath = null;
|
|
46
45
|
} else {
|
|
47
|
-
// Per-project mode:
|
|
48
|
-
projectRoot = findProjectRoot(process.cwd());
|
|
46
|
+
// Per-project mode: use explicit path when provided, otherwise walk up from cwd
|
|
47
|
+
projectRoot = explicitProjectPath ?? findProjectRoot(process.cwd());
|
|
49
48
|
worcaDir = join(projectRoot, '.worca');
|
|
50
49
|
settingsPath = join(projectRoot, '.claude', 'settings.json');
|
|
51
50
|
}
|
|
@@ -201,12 +201,48 @@ export class ProcessManager {
|
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
// Also scan pipelines.d/ for worktree PIDs (worktree runs never appear in runs/)
|
|
205
|
+
const pipelinesDir = join(this.worcaDir, 'multi', 'pipelines.d');
|
|
206
|
+
if (existsSync(pipelinesDir)) {
|
|
207
|
+
try {
|
|
208
|
+
for (const entry of readdirSync(pipelinesDir)) {
|
|
209
|
+
if (!entry.endsWith('.json')) continue;
|
|
210
|
+
const runId = entry.slice(0, -5);
|
|
211
|
+
try {
|
|
212
|
+
const reg = JSON.parse(
|
|
213
|
+
readFileSync(join(pipelinesDir, entry), 'utf8'),
|
|
214
|
+
);
|
|
215
|
+
if (reg.worktree_path) {
|
|
216
|
+
const wtPidPath = join(
|
|
217
|
+
reg.worktree_path,
|
|
218
|
+
'.worca',
|
|
219
|
+
'runs',
|
|
220
|
+
runId,
|
|
221
|
+
'pipeline.pid',
|
|
222
|
+
);
|
|
223
|
+
if (existsSync(wtPidPath)) runIds.add(runId);
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
/* ignore malformed registry entry */
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
} catch {
|
|
230
|
+
/* ignore */
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
204
234
|
for (const runId of runIds) {
|
|
205
235
|
// Check if this run's process is alive
|
|
206
236
|
const alive = this.getRunningPid(runId);
|
|
207
237
|
if (alive) continue;
|
|
208
238
|
|
|
209
|
-
|
|
239
|
+
// Route all paths through resolveRunContext so worktree runs use
|
|
240
|
+
// their worktree dir rather than the project-root runs/ dir.
|
|
241
|
+
const ctx = this.resolveRunContext(runId);
|
|
242
|
+
if (!ctx) continue;
|
|
243
|
+
const { runDir } = ctx;
|
|
244
|
+
|
|
245
|
+
const statusPath = join(runDir, 'status.json');
|
|
210
246
|
if (!existsSync(statusPath)) continue;
|
|
211
247
|
|
|
212
248
|
let status;
|
|
@@ -236,7 +272,7 @@ export class ProcessManager {
|
|
|
236
272
|
|
|
237
273
|
// Append synthetic terminal event if none exists yet.
|
|
238
274
|
// Use pipeline.run.interrupted for signal-killed runs, pipeline.run.failed otherwise.
|
|
239
|
-
const eventsPath = join(
|
|
275
|
+
const eventsPath = join(runDir, 'events.jsonl');
|
|
240
276
|
let hasTerminalEvent = false;
|
|
241
277
|
if (existsSync(eventsPath)) {
|
|
242
278
|
try {
|
|
@@ -269,7 +305,7 @@ export class ProcessManager {
|
|
|
269
305
|
if (this.settingsPath) {
|
|
270
306
|
dispatches.push(
|
|
271
307
|
dispatchExternal({
|
|
272
|
-
runDir
|
|
308
|
+
runDir,
|
|
273
309
|
settingsPath: this.settingsPath,
|
|
274
310
|
eventType,
|
|
275
311
|
payload,
|
|
@@ -744,7 +780,9 @@ export class ProcessManager {
|
|
|
744
780
|
* @param {string} runId
|
|
745
781
|
*/
|
|
746
782
|
_killAgentSubprocess(runId) {
|
|
747
|
-
const
|
|
783
|
+
const ctx = this.resolveRunContext(runId);
|
|
784
|
+
const runDir = ctx ? ctx.runDir : join(this.worcaDir, 'runs', runId);
|
|
785
|
+
const pidPath = join(runDir, 'agent.pid');
|
|
748
786
|
if (!existsSync(pidPath)) return;
|
|
749
787
|
try {
|
|
750
788
|
const agentPid = parseInt(readFileSync(pidPath, 'utf8').trim(), 10);
|
|
@@ -158,6 +158,7 @@ export function createMessageRouter({
|
|
|
158
158
|
}
|
|
159
159
|
const agentName = run.stages?.[stage]?.agent || stage;
|
|
160
160
|
const effectiveRunId = run.run_id || runId;
|
|
161
|
+
const effectiveWorcaDir = run.worktree_worca_dir || proj.worcaDir;
|
|
161
162
|
|
|
162
163
|
const iterations = run.stages?.[stage]?.iterations || [];
|
|
163
164
|
const iterationPrompts = iterations.map((iter, idx) => {
|
|
@@ -189,7 +190,7 @@ export function createMessageRouter({
|
|
|
189
190
|
const iterNum = iter.number ?? 0;
|
|
190
191
|
const resolvedCandidates = [
|
|
191
192
|
join(
|
|
192
|
-
|
|
193
|
+
effectiveWorcaDir,
|
|
193
194
|
'runs',
|
|
194
195
|
effectiveRunId,
|
|
195
196
|
'agents',
|
|
@@ -197,7 +198,7 @@ export function createMessageRouter({
|
|
|
197
198
|
`${stage}-${agentName}-iter-${iterNum}.md`,
|
|
198
199
|
),
|
|
199
200
|
join(
|
|
200
|
-
|
|
201
|
+
effectiveWorcaDir,
|
|
201
202
|
'results',
|
|
202
203
|
effectiveRunId,
|
|
203
204
|
'agents',
|
|
@@ -206,7 +207,7 @@ export function createMessageRouter({
|
|
|
206
207
|
),
|
|
207
208
|
// Fallback for early W-037 runs without stage prefix
|
|
208
209
|
join(
|
|
209
|
-
|
|
210
|
+
effectiveWorcaDir,
|
|
210
211
|
'runs',
|
|
211
212
|
effectiveRunId,
|
|
212
213
|
'agents',
|
|
@@ -214,7 +215,7 @@ export function createMessageRouter({
|
|
|
214
215
|
`${agentName}-iter-${iterNum}.md`,
|
|
215
216
|
),
|
|
216
217
|
join(
|
|
217
|
-
|
|
218
|
+
effectiveWorcaDir,
|
|
218
219
|
'results',
|
|
219
220
|
effectiveRunId,
|
|
220
221
|
'agents',
|
|
@@ -241,14 +242,14 @@ export function createMessageRouter({
|
|
|
241
242
|
if (!resolvedPrompt) {
|
|
242
243
|
const templateCandidates = [
|
|
243
244
|
join(
|
|
244
|
-
|
|
245
|
+
effectiveWorcaDir,
|
|
245
246
|
'runs',
|
|
246
247
|
effectiveRunId,
|
|
247
248
|
'agents',
|
|
248
249
|
`${agentName}.md`,
|
|
249
250
|
),
|
|
250
251
|
join(
|
|
251
|
-
|
|
252
|
+
effectiveWorcaDir,
|
|
252
253
|
'results',
|
|
253
254
|
effectiveRunId,
|
|
254
255
|
'agents',
|
|
@@ -37,9 +37,11 @@ const TERMINAL_STATUSES = new Set([
|
|
|
37
37
|
* @returns {string}
|
|
38
38
|
*/
|
|
39
39
|
export function resolveLatestRunDir(worcaDir) {
|
|
40
|
+
// Collect (runId → runDir) for all live runs from local runs/ and worktree pipelines.d/
|
|
41
|
+
const liveRuns = new Map();
|
|
42
|
+
|
|
40
43
|
const runsDir = join(worcaDir, 'runs');
|
|
41
44
|
if (existsSync(runsDir)) {
|
|
42
|
-
let latest = null;
|
|
43
45
|
try {
|
|
44
46
|
for (const entry of readdirSync(runsDir, { withFileTypes: true })) {
|
|
45
47
|
if (!entry.isDirectory()) continue;
|
|
@@ -49,7 +51,36 @@ export function resolveLatestRunDir(worcaDir) {
|
|
|
49
51
|
const pid = parseInt(readFileSync(pidPath, 'utf8').trim(), 10);
|
|
50
52
|
if (!Number.isNaN(pid) && pid > 0) {
|
|
51
53
|
process.kill(pid, 0); // throws if dead
|
|
52
|
-
|
|
54
|
+
liveRuns.set(entry.name, join(runsDir, entry.name));
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
/* dead process or invalid PID */
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
/* ignore */
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Also scan pipelines.d/ for worktree PIDs (worktree runs never appear in runs/)
|
|
66
|
+
const pipelinesDir = join(worcaDir, 'multi', 'pipelines.d');
|
|
67
|
+
if (existsSync(pipelinesDir)) {
|
|
68
|
+
try {
|
|
69
|
+
for (const entry of readdirSync(pipelinesDir)) {
|
|
70
|
+
if (!entry.endsWith('.json')) continue;
|
|
71
|
+
const runId = entry.slice(0, -5);
|
|
72
|
+
try {
|
|
73
|
+
const reg = JSON.parse(
|
|
74
|
+
readFileSync(join(pipelinesDir, entry), 'utf8'),
|
|
75
|
+
);
|
|
76
|
+
if (!reg.worktree_path) continue;
|
|
77
|
+
const wtRunDir = join(reg.worktree_path, '.worca', 'runs', runId);
|
|
78
|
+
const pidPath = join(wtRunDir, 'pipeline.pid');
|
|
79
|
+
if (!existsSync(pidPath)) continue;
|
|
80
|
+
const pid = parseInt(readFileSync(pidPath, 'utf8').trim(), 10);
|
|
81
|
+
if (!Number.isNaN(pid) && pid > 0) {
|
|
82
|
+
process.kill(pid, 0); // throws if dead
|
|
83
|
+
liveRuns.set(runId, wtRunDir);
|
|
53
84
|
}
|
|
54
85
|
} catch {
|
|
55
86
|
/* dead process or invalid PID */
|
|
@@ -58,9 +89,16 @@ export function resolveLatestRunDir(worcaDir) {
|
|
|
58
89
|
} catch {
|
|
59
90
|
/* ignore */
|
|
60
91
|
}
|
|
61
|
-
if (latest) return join(runsDir, latest);
|
|
62
92
|
}
|
|
63
|
-
|
|
93
|
+
|
|
94
|
+
if (liveRuns.size === 0) return worcaDir; // legacy fallback
|
|
95
|
+
|
|
96
|
+
// Return the runDir of the latest (alphabetically largest) live run ID
|
|
97
|
+
let latestId = null;
|
|
98
|
+
for (const id of liveRuns.keys()) {
|
|
99
|
+
if (!latestId || id > latestId) latestId = id;
|
|
100
|
+
}
|
|
101
|
+
return liveRuns.get(latestId);
|
|
64
102
|
}
|
|
65
103
|
|
|
66
104
|
/**
|