@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 +1 -1
- package/server/process-manager.js +47 -5
- package/server/project-routes.js +29 -7
package/package.json
CHANGED
|
@@ -371,7 +371,21 @@ export class ProcessManager {
|
|
|
371
371
|
* @returns {Promise<{ pid: number }>}
|
|
372
372
|
*/
|
|
373
373
|
async startPipeline(opts = {}) {
|
|
374
|
-
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
package/server/project-routes.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|