@worca/ui 0.15.1 → 0.15.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@worca/ui",
3
- "version": "0.15.1",
3
+ "version": "0.15.3",
4
4
  "description": "Pipeline monitoring UI for worca-cc",
5
5
  "license": "MIT",
6
6
  "author": "Sinisha Djukic",
@@ -371,7 +371,21 @@ export class ProcessManager {
371
371
  * @returns {Promise<{ pid: number }>}
372
372
  */
373
373
  async startPipeline(opts = {}) {
374
- const cwd = opts.projectRoot || this.projectRoot;
374
+ // Resume must spawn inside the run's own working tree. Worktree-hosted
375
+ // runs live under <worktree>/.worca/runs/<id>; if we spawn from the parent
376
+ // project root, git operations and relative settings paths target the
377
+ // wrong tree and the resumed pipeline corrupts state on the parent
378
+ // branch. Worktree wins over opts.projectRoot for resume — callers
379
+ // routinely pass proj.projectRoot without knowing whether the run is
380
+ // worktree-hosted. Mirrors the cwd derivation in restartStage.
381
+ let resumeCtx = null;
382
+ if (opts.resume && opts.runId) {
383
+ resumeCtx = this.resolveRunContext(opts.runId);
384
+ }
385
+ const cwd =
386
+ resumeCtx && resumeCtx.worcaDir !== this.worcaDir
387
+ ? join(resumeCtx.worcaDir, '..')
388
+ : opts.projectRoot || this.projectRoot;
375
389
  const pipelineScriptRel = '.claude/worca/scripts/run_pipeline.py';
376
390
  const worktreeScriptRel = '.claude/worca/scripts/run_worktree.py';
377
391
 
@@ -408,11 +422,39 @@ export class ProcessManager {
408
422
  if (opts.resume) {
409
423
  args.push('--resume');
410
424
  if (opts.runId) {
411
- const ctx = this.resolveRunContext(opts.runId);
412
- const statusDir = ctx
413
- ? ctx.runDir
414
- : join(this.worcaDir, 'runs', opts.runId);
425
+ // The runner derives worca_dir from os.path.dirname(status_path) and
426
+ // builds the per-run dir as <worca_dir>/runs/<run_id>/. We must pass
427
+ // the worca root, not the per-run dir — passing <worca>/runs/<id>
428
+ // would make the runner compute a nested <worca>/runs/<id>/runs/<id>/
429
+ // and write status updates there while the UI keeps reading the
430
+ // original. _find_active_runs(worca_root) then locates the run.
431
+ const statusDir = resumeCtx ? resumeCtx.worcaDir : this.worcaDir;
415
432
  args.push('--status-dir', statusDir);
433
+
434
+ // _find_active_runs filters out runs whose pipeline_status is in
435
+ // {completed, interrupted}. To resume an interrupted/failed run, flip
436
+ // the top-level status to "resuming" so the runner can pick it up;
437
+ // it'll transition to "running" once it's processing.
438
+ const statusPath = resumeCtx
439
+ ? join(resumeCtx.runDir, 'status.json')
440
+ : join(this.worcaDir, 'runs', opts.runId, 'status.json');
441
+ try {
442
+ const s = JSON.parse(readFileSync(statusPath, 'utf8'));
443
+ if (
444
+ s.pipeline_status === 'interrupted' ||
445
+ s.pipeline_status === 'failed'
446
+ ) {
447
+ s.pipeline_status = 'resuming';
448
+ delete s.stop_reason;
449
+ writeFileSync(
450
+ statusPath,
451
+ `${JSON.stringify(s, null, 2)}\n`,
452
+ 'utf8',
453
+ );
454
+ }
455
+ } catch {
456
+ /* non-fatal — runner will surface a clearer error if the file is missing */
457
+ }
416
458
  }
417
459
  } else if (opts.sourceType !== undefined) {
418
460
  // New format: separate source and prompt args
@@ -71,7 +71,7 @@ function validateRunId(runId) {
71
71
  // Re-exported from run-dir-resolver so callers (including older tests) can
72
72
  // continue importing from project-routes. The implementation now overlays
73
73
  // worktree runs registered in <worcaDir>/multi/pipelines.d/.
74
- import { findRunStatusPath } from './run-dir-resolver.js';
74
+ import { findRunStatusPath, readPipelineOverlay } from './run-dir-resolver.js';
75
75
  export { findRunStatusPath };
76
76
 
77
77
  /** Validate a branch name — alphanumeric, dots, hyphens, underscores, slashes */
@@ -686,8 +686,14 @@ export function createProjectScopedRoutes({
686
686
  }
687
687
  try {
688
688
  let status = JSON.parse(readFileSync(statusPath, 'utf8'));
689
- // Reconcile stale "running" status when no process is alive
690
- if (status.pipeline_status === 'running' && pm && !pm.getRunningPid()) {
689
+ // Reconcile stale "running" status when no process is alive.
690
+ // Pass runId so worktree-hosted pipelines (PID lives under
691
+ // <worktree>/.worca/runs/<id>/pipeline.pid) are detected correctly.
692
+ if (
693
+ status.pipeline_status === 'running' &&
694
+ pm &&
695
+ !pm.getRunningPid(runId)
696
+ ) {
691
697
  try {
692
698
  pm.reconcileStatus();
693
699
  status = JSON.parse(readFileSync(statusPath, 'utf8'));
@@ -929,7 +935,14 @@ export function createProjectScopedRoutes({
929
935
  }
930
936
  const { worcaDir, settingsPath } = req.project;
931
937
  try {
932
- const controlDir = join(worcaDir, 'runs', runId);
938
+ // Worktree runs read control.json from <worktree>/.worca/runs/<id>/.
939
+ // Writing it to the parent project's worcaDir leaves the runner
940
+ // unaware of the stop request — SIGTERM still works, but we lose
941
+ // graceful-shutdown semantics.
942
+ const overlay = readPipelineOverlay(worcaDir, runId);
943
+ const controlDir = overlay?.worktree_path
944
+ ? join(overlay.worktree_path, '.worca', 'runs', runId)
945
+ : join(worcaDir, 'runs', runId);
933
946
  mkdirSync(controlDir, { recursive: true });
934
947
  writeFileSync(
935
948
  join(controlDir, 'control.json'),
@@ -1135,7 +1148,12 @@ export function createProjectScopedRoutes({
1135
1148
  pipeline_status: st.pipeline_status,
1136
1149
  });
1137
1150
  }
1138
- const controlDir = join(worcaDir, 'runs', runId);
1151
+ // Worktree runs read control.json from <worktree>/.worca/runs/<id>/;
1152
+ // writing to the parent's worcaDir is invisible to the runner.
1153
+ const overlay = readPipelineOverlay(worcaDir, runId);
1154
+ const controlDir = overlay?.worktree_path
1155
+ ? join(overlay.worktree_path, '.worca', 'runs', runId)
1156
+ : join(worcaDir, 'runs', runId);
1139
1157
  mkdirSync(controlDir, { recursive: true });
1140
1158
  writeFileSync(
1141
1159
  join(controlDir, 'control.json'),
@@ -1337,7 +1355,7 @@ export function createProjectScopedRoutes({
1337
1355
  .json({ ok: false, error: `Run "${runId}" not found` });
1338
1356
  }
1339
1357
 
1340
- const running = req.project.pm.getRunningPid();
1358
+ const running = req.project.pm.getRunningPid(runId);
1341
1359
  if (running) {
1342
1360
  return res.status(409).json({
1343
1361
  ok: false,
@@ -1366,7 +1384,11 @@ export function createProjectScopedRoutes({
1366
1384
  }
1367
1385
  }
1368
1386
 
1369
- const cwd = projectRoot || process.cwd();
1387
+ // Worktree-hosted runs live outside the parent project. Spawn the learner
1388
+ // inside the worktree so its default --status-dir=.worca and any git
1389
+ // operations land on the right tree, mirroring run_pipeline.py resume.
1390
+ const overlay = readPipelineOverlay(worcaDir, runId);
1391
+ const cwd = overlay?.worktree_path || projectRoot || process.cwd();
1370
1392
  const env = { ...process.env };
1371
1393
  delete env.CLAUDECODE;
1372
1394