@worca/ui 0.23.0 → 0.24.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 +2341 -1205
- package/app/main.bundle.js.map +4 -4
- package/app/protocol.js +4 -1
- package/app/styles.css +446 -8
- package/app/utils/state-actions.js +21 -3
- package/app/utils/status-constants.js +11 -0
- package/bin/worca-ui.js +2 -2
- package/package.json +2 -1
- package/scripts/build-frontend.js +48 -1
- package/server/app.js +92 -1
- package/server/fleet-routes.js +5 -3
- package/server/index.js +4 -3
- package/server/integrations/commands/fleet.js +1 -1
- package/server/integrations/commands/global.js +9 -0
- package/server/integrations/commands/workspace.js +295 -0
- package/server/integrations/index.js +6 -0
- package/server/integrations/renderers.js +291 -3
- package/server/paths.js +78 -0
- package/server/project-routes.js +68 -5
- package/server/workspace-routes.js +1554 -0
- package/server/worktree-ops.js +12 -1
- package/server/ws-fleet-manifest-watcher.js +4 -3
- package/server/ws-modular.js +10 -2
- package/server/ws-workspace-manifest-watcher.js +136 -0
|
@@ -238,6 +238,272 @@ function renderFleetFailed(envelope) {
|
|
|
238
238
|
return mdMsg(parts.join('\n'), 'error');
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
// Workspace event renderers
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// Mirror the fleet renderers' shape: short title line + indented meta rows.
|
|
245
|
+
// workspace_id replaces fleet_id; envelopes carry workspace_id at the top
|
|
246
|
+
// level (no `pipeline` wrapper). See src/worca/events/workspace_emitter.py
|
|
247
|
+
// for the envelope schema.
|
|
248
|
+
|
|
249
|
+
function workspaceId(envelope) {
|
|
250
|
+
return envelope.workspace_id ?? 'workspace';
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function workspaceTitle(envelope) {
|
|
254
|
+
const name = envelope.payload?.workspace_name;
|
|
255
|
+
return name
|
|
256
|
+
? `${name} (\`${workspaceId(envelope)}\`)`
|
|
257
|
+
: `\`${workspaceId(envelope)}\``;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function renderWorkspaceLaunched(envelope) {
|
|
261
|
+
const p = envelope.payload ?? {};
|
|
262
|
+
const projects = Array.isArray(p.projects) ? p.projects : [];
|
|
263
|
+
const projectsLabel = projects.length
|
|
264
|
+
? projects.slice(0, 5).join(', ') +
|
|
265
|
+
(projects.length > 5 ? `, +${projects.length - 5} more` : '')
|
|
266
|
+
: '(none)';
|
|
267
|
+
const parts = [
|
|
268
|
+
`\u{1F680} **Workspace launched:** ${workspaceTitle(envelope)}`,
|
|
269
|
+
];
|
|
270
|
+
parts.push(` **Projects:** ${projects.length} — ${projectsLabel}`);
|
|
271
|
+
if (p.tier_count != null) parts.push(` **Tiers:** ${p.tier_count}`);
|
|
272
|
+
if (p.guide_attached) parts.push(' **Guide:** attached');
|
|
273
|
+
if (p.skip_planning) parts.push(' **Plan:** skipped (per-project plans)');
|
|
274
|
+
return mdMsg(parts.join('\n'), 'info');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function renderWorkspaceCompleted(envelope) {
|
|
278
|
+
const p = envelope.payload ?? {};
|
|
279
|
+
const parts = [`✅ **Workspace completed:** ${workspaceTitle(envelope)}`];
|
|
280
|
+
if (p.child_count != null) {
|
|
281
|
+
parts.push(` **Projects:** ${p.child_count}`);
|
|
282
|
+
}
|
|
283
|
+
if (p.integration_passed === false) {
|
|
284
|
+
parts.push(' **Integration test:** not run');
|
|
285
|
+
} else if (p.integration_passed) {
|
|
286
|
+
parts.push(' **Integration test:** passed');
|
|
287
|
+
}
|
|
288
|
+
if (p.duration_ms != null) {
|
|
289
|
+
parts.push(` **Duration:** ${fmtMs(p.duration_ms)}`);
|
|
290
|
+
}
|
|
291
|
+
if (p.umbrella_issue_url) {
|
|
292
|
+
parts.push(` **Umbrella:** ${p.umbrella_issue_url}`);
|
|
293
|
+
}
|
|
294
|
+
return mdMsg(parts.join('\n'), 'success');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function renderWorkspaceFailed(envelope) {
|
|
298
|
+
const p = envelope.payload ?? {};
|
|
299
|
+
const parts = [`❌ **Workspace failed:** ${workspaceTitle(envelope)}`];
|
|
300
|
+
if (p.failed_tier != null) {
|
|
301
|
+
parts.push(` **Failed tier:** ${p.failed_tier}`);
|
|
302
|
+
}
|
|
303
|
+
if (Array.isArray(p.failed_projects) && p.failed_projects.length > 0) {
|
|
304
|
+
parts.push(` **Failed projects:** ${p.failed_projects.join(', ')}`);
|
|
305
|
+
}
|
|
306
|
+
if (p.completed_count != null && p.tier_count != null) {
|
|
307
|
+
parts.push(
|
|
308
|
+
` **Progress:** ${p.completed_count} project(s) completed, ${p.failed_count ?? 0} failed`,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
if (p.duration_ms != null) {
|
|
312
|
+
parts.push(` **Duration:** ${fmtMs(p.duration_ms)}`);
|
|
313
|
+
}
|
|
314
|
+
return mdMsg(parts.join('\n'), 'error');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function renderWorkspaceHalted(envelope) {
|
|
318
|
+
const p = envelope.payload ?? {};
|
|
319
|
+
const reason = p.halt_reason || 'unknown';
|
|
320
|
+
const sev = reason === 'circuit_breaker' ? 'error' : 'warning';
|
|
321
|
+
const parts = [`\u{1F6D1} **Workspace halted:** ${workspaceTitle(envelope)}`];
|
|
322
|
+
parts.push(` **Reason:** ${reason}`);
|
|
323
|
+
if (p.completed_tiers != null) {
|
|
324
|
+
parts.push(` **Completed tiers:** ${p.completed_tiers}`);
|
|
325
|
+
}
|
|
326
|
+
if (p.pending_tiers != null && p.pending_tiers > 0) {
|
|
327
|
+
parts.push(` **Pending (not dispatched):** ${p.pending_tiers} tier(s)`);
|
|
328
|
+
}
|
|
329
|
+
return mdMsg(parts.join('\n'), sev);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function renderWorkspacePaused(envelope) {
|
|
333
|
+
const p = envelope.payload ?? {};
|
|
334
|
+
const parts = [`\u{1F7E1} **Workspace paused:** ${workspaceTitle(envelope)}`];
|
|
335
|
+
if (p.reason) parts.push(` **Reason:** ${p.reason}`);
|
|
336
|
+
return mdMsg(parts.join('\n'), 'warning');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function renderWorkspaceResumed(envelope) {
|
|
340
|
+
const p = envelope.payload ?? {};
|
|
341
|
+
const parts = [
|
|
342
|
+
`\u{1F7E2} **Workspace resumed:** ${workspaceTitle(envelope)}`,
|
|
343
|
+
];
|
|
344
|
+
if (p.from_state) parts.push(` **From state:** ${p.from_state}`);
|
|
345
|
+
if (p.redispatch_count != null && p.redispatch_count > 0) {
|
|
346
|
+
parts.push(` **Re-dispatched:** ${p.redispatch_count} project(s)`);
|
|
347
|
+
}
|
|
348
|
+
if (p.skip_count != null && p.skip_count > 0) {
|
|
349
|
+
parts.push(` **Skipped (already complete):** ${p.skip_count}`);
|
|
350
|
+
}
|
|
351
|
+
return mdMsg(parts.join('\n'), 'info');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function renderWorkspaceTierFailed(envelope) {
|
|
355
|
+
const p = envelope.payload ?? {};
|
|
356
|
+
const parts = [`❌ **Workspace tier failed:** ${workspaceTitle(envelope)}`];
|
|
357
|
+
if (p.tier != null) parts.push(` **Tier:** ${p.tier}`);
|
|
358
|
+
if (Array.isArray(p.failed_projects) && p.failed_projects.length > 0) {
|
|
359
|
+
parts.push(` **Failed:** ${p.failed_projects.join(', ')}`);
|
|
360
|
+
}
|
|
361
|
+
if (Array.isArray(p.blocked_projects) && p.blocked_projects.length > 0) {
|
|
362
|
+
parts.push(
|
|
363
|
+
` **Blocked (dep failure):** ${p.blocked_projects.join(', ')}`,
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
if (p.duration_ms != null) {
|
|
367
|
+
parts.push(` **Duration:** ${fmtMs(p.duration_ms)}`);
|
|
368
|
+
}
|
|
369
|
+
return mdMsg(parts.join('\n'), 'error');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function renderWorkspaceIntegrationFailed(envelope) {
|
|
373
|
+
const p = envelope.payload ?? {};
|
|
374
|
+
const parts = [
|
|
375
|
+
`❌ **Workspace integration test failed:** ${workspaceTitle(envelope)}`,
|
|
376
|
+
];
|
|
377
|
+
if (p.exit_code != null) {
|
|
378
|
+
parts.push(` **Exit code:** ${p.exit_code}`);
|
|
379
|
+
}
|
|
380
|
+
if (p.duration_ms != null) {
|
|
381
|
+
parts.push(` **Duration:** ${fmtMs(p.duration_ms)}`);
|
|
382
|
+
}
|
|
383
|
+
if (p.log_tail) {
|
|
384
|
+
// Render as a code block so chat clients preserve formatting.
|
|
385
|
+
parts.push(` **Tail:**\n\`\`\`\n${p.log_tail}\n\`\`\``);
|
|
386
|
+
}
|
|
387
|
+
return mdMsg(parts.join('\n'), 'error');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function renderWorkspaceUmbrellaIssueCreated(envelope) {
|
|
391
|
+
const p = envelope.payload ?? {};
|
|
392
|
+
const parts = [
|
|
393
|
+
`\u{1F517} **Workspace umbrella issue created:** ${workspaceTitle(envelope)}`,
|
|
394
|
+
];
|
|
395
|
+
if (p.issue_number != null && p.nwo) {
|
|
396
|
+
parts.push(` **Issue:** [${p.nwo}#${p.issue_number}](${p.issue_url})`);
|
|
397
|
+
} else if (p.issue_url) {
|
|
398
|
+
parts.push(` **Issue:** ${p.issue_url}`);
|
|
399
|
+
}
|
|
400
|
+
if (p.child_pr_count != null) {
|
|
401
|
+
parts.push(` **Linked PRs:** ${p.child_pr_count}`);
|
|
402
|
+
}
|
|
403
|
+
return mdMsg(parts.join('\n'), 'info');
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function renderWorkspaceCircuitBreakerTripped(envelope) {
|
|
407
|
+
const p = envelope.payload ?? {};
|
|
408
|
+
const parts = [
|
|
409
|
+
`⚠ **Workspace circuit breaker tripped:** ${workspaceTitle(envelope)}`,
|
|
410
|
+
];
|
|
411
|
+
const ratio =
|
|
412
|
+
p.failure_ratio != null ? `${Math.round(p.failure_ratio * 100)}%` : '?';
|
|
413
|
+
const threshold =
|
|
414
|
+
p.threshold != null ? `${Math.round(p.threshold * 100)}%` : '?';
|
|
415
|
+
parts.push(
|
|
416
|
+
` **Failures:** ${p.failed_count ?? 0}/${p.terminal_count ?? 0} (${ratio} ≥ threshold ${threshold})`,
|
|
417
|
+
);
|
|
418
|
+
return mdMsg(parts.join('\n'), 'error');
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Opt-in workspace renderers — quieter for chat by default but available
|
|
422
|
+
// for webhook consumers that want the full coordinator stream.
|
|
423
|
+
|
|
424
|
+
function renderWorkspacePlanStarted(envelope) {
|
|
425
|
+
const p = envelope.payload ?? {};
|
|
426
|
+
const parts = [`⚙ **Workspace planning:** ${workspaceTitle(envelope)}`];
|
|
427
|
+
parts.push(
|
|
428
|
+
` **Decomposing** prompt into per-project sub-plans${p.project_count != null ? ` (${p.project_count} project${p.project_count === 1 ? '' : 's'})` : ''}…`,
|
|
429
|
+
);
|
|
430
|
+
return mdMsg(parts.join('\n'), 'info');
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function renderWorkspacePlanCompleted(envelope) {
|
|
434
|
+
const p = envelope.payload ?? {};
|
|
435
|
+
const parts = [`✅ **Workspace plan ready:** ${workspaceTitle(envelope)}`];
|
|
436
|
+
parts.push(
|
|
437
|
+
` **Plans written:** ${p.project_count}${p.skipped_count ? ` (+${p.skipped_count} skipped)` : ''}`,
|
|
438
|
+
);
|
|
439
|
+
if (p.duration_ms != null) {
|
|
440
|
+
parts.push(` **Duration:** ${fmtMs(p.duration_ms)}`);
|
|
441
|
+
}
|
|
442
|
+
return mdMsg(parts.join('\n'), 'success');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function renderWorkspacePlanFailed(envelope) {
|
|
446
|
+
const p = envelope.payload ?? {};
|
|
447
|
+
const parts = [`❌ **Workspace plan failed:** ${workspaceTitle(envelope)}`];
|
|
448
|
+
if (p.error_type) parts.push(` **Error type:** ${p.error_type}`);
|
|
449
|
+
if (p.error) parts.push(` **Error:** ${p.error}`);
|
|
450
|
+
return mdMsg(parts.join('\n'), 'error');
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function renderWorkspaceTierStarted(envelope) {
|
|
454
|
+
const p = envelope.payload ?? {};
|
|
455
|
+
const projects = Array.isArray(p.projects) ? p.projects : [];
|
|
456
|
+
const parts = [`⚙ **Workspace tier started:** ${workspaceTitle(envelope)}`];
|
|
457
|
+
parts.push(` **Tier:** ${p.tier ?? '?'} (${projects.length} project(s))`);
|
|
458
|
+
if (projects.length > 0) {
|
|
459
|
+
parts.push(` **Projects:** ${projects.join(', ')}`);
|
|
460
|
+
}
|
|
461
|
+
return mdMsg(parts.join('\n'), 'info');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function renderWorkspaceTierCompleted(envelope) {
|
|
465
|
+
const p = envelope.payload ?? {};
|
|
466
|
+
const projects = Array.isArray(p.projects) ? p.projects : [];
|
|
467
|
+
const parts = [
|
|
468
|
+
`✅ **Workspace tier completed:** ${workspaceTitle(envelope)}`,
|
|
469
|
+
];
|
|
470
|
+
parts.push(` **Tier:** ${p.tier ?? '?'} (${projects.length} project(s))`);
|
|
471
|
+
if (p.duration_ms != null) {
|
|
472
|
+
parts.push(` **Duration:** ${fmtMs(p.duration_ms)}`);
|
|
473
|
+
}
|
|
474
|
+
return mdMsg(parts.join('\n'), 'success');
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function renderWorkspaceIntegrationStarted(envelope) {
|
|
478
|
+
const p = envelope.payload ?? {};
|
|
479
|
+
const parts = [
|
|
480
|
+
`⚙ **Workspace integration test started:** ${workspaceTitle(envelope)}`,
|
|
481
|
+
];
|
|
482
|
+
if (p.command) parts.push(` **Command:** \`${p.command}\``);
|
|
483
|
+
return mdMsg(parts.join('\n'), 'info');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function renderWorkspaceIntegrationPassed(envelope) {
|
|
487
|
+
const p = envelope.payload ?? {};
|
|
488
|
+
const parts = [
|
|
489
|
+
`✅ **Workspace integration test passed:** ${workspaceTitle(envelope)}`,
|
|
490
|
+
];
|
|
491
|
+
if (p.duration_ms != null) {
|
|
492
|
+
parts.push(` **Duration:** ${fmtMs(p.duration_ms)}`);
|
|
493
|
+
}
|
|
494
|
+
return mdMsg(parts.join('\n'), 'success');
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function renderWorkspaceGuideConflict(envelope) {
|
|
498
|
+
const p = envelope.payload ?? {};
|
|
499
|
+
const parts = [`⚠ **Workspace guide conflict:** ${workspaceTitle(envelope)}`];
|
|
500
|
+
if (p.stage) parts.push(` **Stage:** ${p.stage}`);
|
|
501
|
+
if (p.source) parts.push(` **Source:** ${p.source}`);
|
|
502
|
+
if (p.run_id) parts.push(` **Run:** \`${p.run_id}\``);
|
|
503
|
+
if (p.message) parts.push(` **Conflict:** ${p.message}`);
|
|
504
|
+
return mdMsg(parts.join('\n'), 'warning');
|
|
505
|
+
}
|
|
506
|
+
|
|
241
507
|
// ---------------------------------------------------------------------------
|
|
242
508
|
// Registry
|
|
243
509
|
// ---------------------------------------------------------------------------
|
|
@@ -264,13 +530,35 @@ const EVENT_RENDERERS = {
|
|
|
264
530
|
'fleet.halted': renderFleetHalted,
|
|
265
531
|
'fleet.completed': renderFleetCompleted,
|
|
266
532
|
'fleet.failed': renderFleetFailed,
|
|
533
|
+
// Workspace events — terminal / attention defaults. See OPT_IN_RENDERERS
|
|
534
|
+
// for the lifecycle-progress events (plan.*, tier.started/.completed,
|
|
535
|
+
// integration_test.started/.passed) — kept out by default because a wide
|
|
536
|
+
// DAG can fire 6+ tier transitions in minutes.
|
|
537
|
+
'workspace.completed': renderWorkspaceCompleted,
|
|
538
|
+
'workspace.failed': renderWorkspaceFailed,
|
|
539
|
+
'workspace.halted': renderWorkspaceHalted,
|
|
540
|
+
'workspace.paused': renderWorkspacePaused,
|
|
541
|
+
'workspace.resumed': renderWorkspaceResumed,
|
|
542
|
+
'workspace.tier.failed': renderWorkspaceTierFailed,
|
|
543
|
+
'workspace.integration_test.failed': renderWorkspaceIntegrationFailed,
|
|
544
|
+
'workspace.umbrella_issue.created': renderWorkspaceUmbrellaIssueCreated,
|
|
545
|
+
'workspace.circuit_breaker.tripped': renderWorkspaceCircuitBreakerTripped,
|
|
546
|
+
'workspace.guide_conflict': renderWorkspaceGuideConflict,
|
|
267
547
|
};
|
|
268
548
|
|
|
269
|
-
// fleet.launched
|
|
270
|
-
//
|
|
271
|
-
// register
|
|
549
|
+
// fleet.launched + workspace progress events ship as opt-in renderers rather
|
|
550
|
+
// than Tier-1 defaults. Callers that want them can pull from this export and
|
|
551
|
+
// register them in their own pipeline.
|
|
272
552
|
export const OPT_IN_RENDERERS = {
|
|
273
553
|
'fleet.launched': renderFleetLaunched,
|
|
554
|
+
'workspace.launched': renderWorkspaceLaunched,
|
|
555
|
+
'workspace.plan.started': renderWorkspacePlanStarted,
|
|
556
|
+
'workspace.plan.completed': renderWorkspacePlanCompleted,
|
|
557
|
+
'workspace.plan.failed': renderWorkspacePlanFailed,
|
|
558
|
+
'workspace.tier.started': renderWorkspaceTierStarted,
|
|
559
|
+
'workspace.tier.completed': renderWorkspaceTierCompleted,
|
|
560
|
+
'workspace.integration_test.started': renderWorkspaceIntegrationStarted,
|
|
561
|
+
'workspace.integration_test.passed': renderWorkspaceIntegrationPassed,
|
|
274
562
|
};
|
|
275
563
|
|
|
276
564
|
export const TIER1_EVENTS = Object.keys(EVENT_RENDERERS);
|
package/server/paths.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy resolvers for ~/.worca/ subdirectories.
|
|
3
|
+
*
|
|
4
|
+
* Mirror of src/worca/utils/paths.py — every helper re-reads the
|
|
5
|
+
* environment on each call so $WORCA_HOME is honored consistently by
|
|
6
|
+
* both halves of the system. See issue #162.
|
|
7
|
+
*
|
|
8
|
+
* Resolution order for every subdir helper:
|
|
9
|
+
* 1. `override` arg (e.g. a constant set by the caller / tests)
|
|
10
|
+
* 2. $WORCA_HOME/<subdir>
|
|
11
|
+
* 3. ~/.worca/<subdir>
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { homedir } from 'node:os';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @returns {string} The worca state directory. Honors $WORCA_HOME, else ~/.worca.
|
|
19
|
+
*/
|
|
20
|
+
export function worcaHome() {
|
|
21
|
+
const override = process.env.WORCA_HOME;
|
|
22
|
+
if (override) return override;
|
|
23
|
+
return join(homedir(), '.worca');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {string=} override Caller-supplied path that wins over $WORCA_HOME.
|
|
28
|
+
* @returns {string} Absolute path to the fleet-runs directory.
|
|
29
|
+
*/
|
|
30
|
+
export function fleetRunsDir(override) {
|
|
31
|
+
if (override) return override;
|
|
32
|
+
return join(worcaHome(), 'fleet-runs');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {string=} override
|
|
37
|
+
* @returns {string} Absolute path to the workspace-runs directory.
|
|
38
|
+
*/
|
|
39
|
+
export function workspaceRunsDir(override) {
|
|
40
|
+
if (override) return override;
|
|
41
|
+
return join(worcaHome(), 'workspace-runs');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {string=} override
|
|
46
|
+
* @returns {string} Absolute path to the workspaces.d directory.
|
|
47
|
+
*/
|
|
48
|
+
export function workspacesDir(override) {
|
|
49
|
+
if (override) return override;
|
|
50
|
+
return join(worcaHome(), 'workspaces.d');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {string=} override
|
|
55
|
+
* @returns {string} Absolute path to the preferences/prefs root (= worca home).
|
|
56
|
+
*/
|
|
57
|
+
export function prefsDir(override) {
|
|
58
|
+
if (override) return override;
|
|
59
|
+
return worcaHome();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {string=} override
|
|
64
|
+
* @returns {string} Absolute path to the user templates directory.
|
|
65
|
+
*/
|
|
66
|
+
export function templatesDir(override) {
|
|
67
|
+
if (override) return override;
|
|
68
|
+
return join(worcaHome(), 'templates');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {string=} override
|
|
73
|
+
* @returns {string} Absolute path to preferences.json.
|
|
74
|
+
*/
|
|
75
|
+
export function preferencesPath(override) {
|
|
76
|
+
if (override) return override;
|
|
77
|
+
return join(worcaHome(), 'preferences.json');
|
|
78
|
+
}
|
package/server/project-routes.js
CHANGED
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
unlinkSync,
|
|
17
17
|
writeFileSync,
|
|
18
18
|
} from 'node:fs';
|
|
19
|
-
import { homedir } from 'node:os';
|
|
20
19
|
import { dirname, join } from 'node:path';
|
|
21
20
|
import { Router } from 'express';
|
|
22
21
|
import lockfile from 'proper-lockfile';
|
|
@@ -28,6 +27,7 @@ import { ensureWebhookForUi } from './ensure-webhook.js';
|
|
|
28
27
|
import { extractAndStripGlobalKeys } from './global-keys.js';
|
|
29
28
|
import { LaunchLock } from './launch-lock.js';
|
|
30
29
|
import { createModelEnvRouter } from './model-env-routes.js';
|
|
30
|
+
import { preferencesPath, templatesDir } from './paths.js';
|
|
31
31
|
import { readPreferences } from './preferences.js';
|
|
32
32
|
import { ProcessManager } from './process-manager.js';
|
|
33
33
|
import { countRunningPipelinesAcrossProjects } from './process-registry.js';
|
|
@@ -726,6 +726,71 @@ export function createProjectScopedRoutes({
|
|
|
726
726
|
}
|
|
727
727
|
});
|
|
728
728
|
|
|
729
|
+
// GET /api/projects/:projectId/runs/:runId/plan — per-run plan markdown
|
|
730
|
+
//
|
|
731
|
+
// Returns the markdown content of the run's plan file. Two sources, in
|
|
732
|
+
// priority order:
|
|
733
|
+
// 1. stages.plan.plan_file — set by workspace children that received a
|
|
734
|
+
// pre-built per-repo plan from the workspace planner. May point at
|
|
735
|
+
// an absolute path under the workspace run dir.
|
|
736
|
+
// 2. <worktree>/MASTER_PLAN.md — generated by a standalone pipeline's
|
|
737
|
+
// own PLAN stage. status.json doesn't always carry the path so we
|
|
738
|
+
// reconstruct it from the worktree.
|
|
739
|
+
//
|
|
740
|
+
// Returns 404 when neither path exists / is readable. Replies in
|
|
741
|
+
// text/markdown so the client just streams it into the dialog body.
|
|
742
|
+
router.get('/runs/:runId/plan', requireWorcaDir, (req, res) => {
|
|
743
|
+
const { runId } = req.params;
|
|
744
|
+
if (!validateRunId(runId)) {
|
|
745
|
+
return res.status(400).json({ ok: false, error: 'Invalid runId' });
|
|
746
|
+
}
|
|
747
|
+
const { worcaDir } = req.project;
|
|
748
|
+
const statusPath = findRunStatusPath(worcaDir, runId);
|
|
749
|
+
if (!statusPath) {
|
|
750
|
+
return res
|
|
751
|
+
.status(404)
|
|
752
|
+
.json({ ok: false, error: `Run "${runId}" not found` });
|
|
753
|
+
}
|
|
754
|
+
let status;
|
|
755
|
+
try {
|
|
756
|
+
status = JSON.parse(readFileSync(statusPath, 'utf8'));
|
|
757
|
+
} catch (err) {
|
|
758
|
+
return res
|
|
759
|
+
.status(500)
|
|
760
|
+
.json({ ok: false, error: `Failed to read status: ${err.message}` });
|
|
761
|
+
}
|
|
762
|
+
const planFile = status?.stages?.plan?.plan_file ?? null;
|
|
763
|
+
// `status.worktree` is a boolean flag (true when the run is hosted in a
|
|
764
|
+
// worktree), not a path. The actual worktree path lives in the
|
|
765
|
+
// pipelines.d registry entry. Walk it via the overlay so we can offer
|
|
766
|
+
// the MASTER_PLAN.md fallback even when stage.plan_file isn't set.
|
|
767
|
+
const overlay = readPipelineOverlay(worcaDir, runId);
|
|
768
|
+
const worktreePath =
|
|
769
|
+
typeof overlay?.worktree_path === 'string' ? overlay.worktree_path : null;
|
|
770
|
+
const fallback = worktreePath ? join(worktreePath, 'MASTER_PLAN.md') : null;
|
|
771
|
+
const candidates = [planFile, fallback].filter(
|
|
772
|
+
(p) => typeof p === 'string' && p.length > 0,
|
|
773
|
+
);
|
|
774
|
+
for (const p of candidates) {
|
|
775
|
+
if (existsSync(p)) {
|
|
776
|
+
try {
|
|
777
|
+
res.setHeader('Content-Type', 'text/markdown; charset=utf-8');
|
|
778
|
+
res.send(readFileSync(p, 'utf8'));
|
|
779
|
+
return;
|
|
780
|
+
} catch (err) {
|
|
781
|
+
return res.status(500).json({
|
|
782
|
+
ok: false,
|
|
783
|
+
error: `Failed to read plan file: ${err.message}`,
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return res.status(404).json({
|
|
789
|
+
ok: false,
|
|
790
|
+
error: 'No plan file found for this run',
|
|
791
|
+
});
|
|
792
|
+
});
|
|
793
|
+
|
|
729
794
|
// POST /api/projects/:projectId/runs — start a new pipeline
|
|
730
795
|
router.post('/runs', requireWorcaDir, async (req, res) => {
|
|
731
796
|
// Block parallel pipelines on the same project (GH #82)
|
|
@@ -1467,7 +1532,7 @@ export function createProjectScopedRoutes({
|
|
|
1467
1532
|
const tiers = [
|
|
1468
1533
|
{ tier: 'worca', dir: join(root, '.claude', 'worca', 'templates') },
|
|
1469
1534
|
{ tier: 'project', dir: join(root, '.claude', 'templates') },
|
|
1470
|
-
{ tier: 'user', dir:
|
|
1535
|
+
{ tier: 'user', dir: templatesDir() },
|
|
1471
1536
|
];
|
|
1472
1537
|
|
|
1473
1538
|
const templates = [];
|
|
@@ -1539,9 +1604,7 @@ export function createProjectScopedRoutes({
|
|
|
1539
1604
|
// Fall back to source_repo from global preferences
|
|
1540
1605
|
if (!source) {
|
|
1541
1606
|
try {
|
|
1542
|
-
const prefs = readPreferences(
|
|
1543
|
-
join(homedir(), '.worca', 'preferences.json'),
|
|
1544
|
-
);
|
|
1607
|
+
const prefs = readPreferences(preferencesPath());
|
|
1545
1608
|
source = prefs.source_repo || undefined;
|
|
1546
1609
|
} catch {
|
|
1547
1610
|
/* ignore — worca init will use its own resolution chain */
|