@worca/ui 0.31.0 → 0.33.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/README.md +10 -0
- package/app/main.bundle.js +1812 -1767
- package/app/main.bundle.js.map +4 -4
- package/app/styles.css +100 -5
- package/package.json +2 -1
- package/server/app.js +34 -23
- package/server/beads-reader.js +12 -4
- package/server/integrations/renderers.js +28 -0
- package/server/workspace-routes.js +170 -2
- package/server/ws-beads-watcher.js +131 -21
- package/server/ws-modular.js +4 -2
package/app/styles.css
CHANGED
|
@@ -2425,6 +2425,13 @@ sl-details.agent-prompt-section::part(content) {
|
|
|
2425
2425
|
margin-bottom: 12px;
|
|
2426
2426
|
}
|
|
2427
2427
|
|
|
2428
|
+
/* Divider + gap before the user message header */
|
|
2429
|
+
.agent-prompt-block + .agent-prompt-block {
|
|
2430
|
+
margin-top: 18px;
|
|
2431
|
+
padding-top: 16px;
|
|
2432
|
+
border-top: 1px solid var(--border-subtle);
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2428
2435
|
.agent-prompt-label-row {
|
|
2429
2436
|
display: flex;
|
|
2430
2437
|
align-items: center;
|
|
@@ -2455,12 +2462,14 @@ sl-details.agent-prompt-section::part(content) {
|
|
|
2455
2462
|
flex-shrink: 0;
|
|
2456
2463
|
}
|
|
2457
2464
|
|
|
2465
|
+
/* Accent the section headers (not the message bodies) so each block reads as
|
|
2466
|
+
a labelled section; the bodies stay neutral. */
|
|
2458
2467
|
.agent-prompt-label {
|
|
2459
|
-
font-size:
|
|
2460
|
-
font-weight:
|
|
2461
|
-
color: var(--
|
|
2468
|
+
font-size: 12px;
|
|
2469
|
+
font-weight: 700;
|
|
2470
|
+
color: var(--accent);
|
|
2462
2471
|
text-transform: uppercase;
|
|
2463
|
-
letter-spacing: 0.
|
|
2472
|
+
letter-spacing: 0.6px;
|
|
2464
2473
|
margin-bottom: 4px;
|
|
2465
2474
|
}
|
|
2466
2475
|
|
|
@@ -4833,7 +4842,7 @@ sl-tooltip.bead-tooltip::part(body) {
|
|
|
4833
4842
|
|
|
4834
4843
|
.bead-tooltip-excerpt {
|
|
4835
4844
|
font-size: 12px;
|
|
4836
|
-
white-space:
|
|
4845
|
+
white-space: normal;
|
|
4837
4846
|
margin-bottom: 2px;
|
|
4838
4847
|
}
|
|
4839
4848
|
|
|
@@ -5642,6 +5651,36 @@ sl-tooltip.bead-tooltip::part(body) {
|
|
|
5642
5651
|
gap: 4px;
|
|
5643
5652
|
}
|
|
5644
5653
|
|
|
5654
|
+
/* Workspace plan upload (existing mode) */
|
|
5655
|
+
.workspace-plan-upload {
|
|
5656
|
+
display: flex;
|
|
5657
|
+
flex-direction: column;
|
|
5658
|
+
gap: 8px;
|
|
5659
|
+
}
|
|
5660
|
+
.workspace-plan-advanced {
|
|
5661
|
+
font-size: 13px;
|
|
5662
|
+
}
|
|
5663
|
+
.workspace-plan-advanced sl-input {
|
|
5664
|
+
margin-top: 4px;
|
|
5665
|
+
}
|
|
5666
|
+
|
|
5667
|
+
/* Per-project plan rows (per-repo mode) */
|
|
5668
|
+
.per-project-plans {
|
|
5669
|
+
display: flex;
|
|
5670
|
+
flex-direction: column;
|
|
5671
|
+
gap: 6px;
|
|
5672
|
+
}
|
|
5673
|
+
.per-project-plan-row {
|
|
5674
|
+
display: flex;
|
|
5675
|
+
align-items: center;
|
|
5676
|
+
gap: 8px;
|
|
5677
|
+
}
|
|
5678
|
+
.per-project-plan-name {
|
|
5679
|
+
min-width: 100px;
|
|
5680
|
+
font-size: 13px;
|
|
5681
|
+
font-weight: 500;
|
|
5682
|
+
}
|
|
5683
|
+
|
|
5645
5684
|
/* Base-branch validation states */
|
|
5646
5685
|
.base-branch-validating {
|
|
5647
5686
|
display: flex;
|
|
@@ -6248,6 +6287,62 @@ sl-dialog.markdown-dialog::part(body) {
|
|
|
6248
6287
|
text-align: left;
|
|
6249
6288
|
}
|
|
6250
6289
|
|
|
6290
|
+
/* --- Markdown context overrides --- */
|
|
6291
|
+
|
|
6292
|
+
.markdown-inline.markdown-body {
|
|
6293
|
+
display: inline;
|
|
6294
|
+
font-size: inherit;
|
|
6295
|
+
line-height: inherit;
|
|
6296
|
+
}
|
|
6297
|
+
.markdown-inline.markdown-body p {
|
|
6298
|
+
display: inline;
|
|
6299
|
+
margin: 0;
|
|
6300
|
+
}
|
|
6301
|
+
.markdown-inline.markdown-body code {
|
|
6302
|
+
font-size: 0.85em;
|
|
6303
|
+
}
|
|
6304
|
+
|
|
6305
|
+
.agent-prompt-content .markdown-body {
|
|
6306
|
+
white-space: normal;
|
|
6307
|
+
font-size: 12px;
|
|
6308
|
+
line-height: 1.5;
|
|
6309
|
+
}
|
|
6310
|
+
.agent-prompt-content .markdown-body h1,
|
|
6311
|
+
.agent-prompt-content .markdown-body h2,
|
|
6312
|
+
.agent-prompt-content .markdown-body h3 {
|
|
6313
|
+
margin: 0.8em 0 0.3em;
|
|
6314
|
+
}
|
|
6315
|
+
|
|
6316
|
+
.bead-tooltip-excerpt.markdown-body {
|
|
6317
|
+
white-space: normal;
|
|
6318
|
+
font-size: 12px;
|
|
6319
|
+
max-height: 200px;
|
|
6320
|
+
overflow-y: auto;
|
|
6321
|
+
/* Inherit the tooltip's (light) text color instead of --fg, which is the
|
|
6322
|
+
main-panel foreground and renders dark-on-dark inside the tooltip. */
|
|
6323
|
+
color: inherit;
|
|
6324
|
+
}
|
|
6325
|
+
.bead-tooltip-excerpt.markdown-body p {
|
|
6326
|
+
margin: 0.3em 0;
|
|
6327
|
+
}
|
|
6328
|
+
.bead-tooltip-excerpt.markdown-body pre {
|
|
6329
|
+
font-size: 11px;
|
|
6330
|
+
max-height: 120px;
|
|
6331
|
+
overflow: auto;
|
|
6332
|
+
}
|
|
6333
|
+
/* The base markdown code/blockquote/link colors assume the light main panel;
|
|
6334
|
+
re-tint them for the dark tooltip surface so they stay legible. */
|
|
6335
|
+
.bead-tooltip-excerpt.markdown-body code,
|
|
6336
|
+
.bead-tooltip-excerpt.markdown-body pre {
|
|
6337
|
+
background: rgba(255, 255, 255, 0.1);
|
|
6338
|
+
border-color: rgba(255, 255, 255, 0.15);
|
|
6339
|
+
}
|
|
6340
|
+
.bead-tooltip-excerpt.markdown-body blockquote {
|
|
6341
|
+
color: inherit;
|
|
6342
|
+
opacity: 0.8;
|
|
6343
|
+
border-left-color: rgba(255, 255, 255, 0.3);
|
|
6344
|
+
}
|
|
6345
|
+
|
|
6251
6346
|
.workspace-run-card .workspace-card-root,
|
|
6252
6347
|
.workspace-run-card .workspace-card-name {
|
|
6253
6348
|
font-family: var(--font-mono);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@worca/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"description": "Pipeline monitoring UI for worca-cc",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Sinisha Djukic",
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
"@xterm/addon-fit": "^0.11.0",
|
|
66
66
|
"@xterm/addon-search": "^0.16.0",
|
|
67
67
|
"@xterm/xterm": "^6.0.0",
|
|
68
|
+
"dompurify": "^3.4.5",
|
|
68
69
|
"express": "^5.2.1",
|
|
69
70
|
"lit-html": "^3.3.1",
|
|
70
71
|
"lucide": "^0.577.0",
|
package/server/app.js
CHANGED
|
@@ -65,6 +65,39 @@ function runWorcaCleanupSubprocess(flag, id) {
|
|
|
65
65
|
});
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
export function buildWorkspaceArgs(workspace_root, workspace_id, manifest) {
|
|
69
|
+
const args = [
|
|
70
|
+
'-m',
|
|
71
|
+
'worca.scripts.run_workspace',
|
|
72
|
+
workspace_root,
|
|
73
|
+
'--workspace-id',
|
|
74
|
+
workspace_id,
|
|
75
|
+
];
|
|
76
|
+
if (manifest.work_request?.source) {
|
|
77
|
+
args.push('--source', manifest.work_request.source);
|
|
78
|
+
} else {
|
|
79
|
+
args.push('--prompt', manifest.work_request?.description ?? '');
|
|
80
|
+
}
|
|
81
|
+
if (manifest.branch_template) {
|
|
82
|
+
args.push('--branch', manifest.branch_template);
|
|
83
|
+
}
|
|
84
|
+
if (manifest.skip_integration) args.push('--skip-integration');
|
|
85
|
+
if (manifest.skip_planning) args.push('--skip-planning');
|
|
86
|
+
if (manifest.max_parallel) {
|
|
87
|
+
args.push('--max-parallel', String(manifest.max_parallel));
|
|
88
|
+
}
|
|
89
|
+
for (const p of manifest.guide?.paths || []) {
|
|
90
|
+
args.push('--guide', p);
|
|
91
|
+
}
|
|
92
|
+
if (manifest.workspace_plan_path) {
|
|
93
|
+
args.push('--workspace-plan', manifest.workspace_plan_path);
|
|
94
|
+
}
|
|
95
|
+
for (const [name, path] of Object.entries(manifest.project_plans || {})) {
|
|
96
|
+
args.push('--project-plan', `${name}=${path}`);
|
|
97
|
+
}
|
|
98
|
+
return args;
|
|
99
|
+
}
|
|
100
|
+
|
|
68
101
|
export function createApp(options = {}) {
|
|
69
102
|
const app = express();
|
|
70
103
|
const appDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'app');
|
|
@@ -733,29 +766,7 @@ export function createApp(options = {}) {
|
|
|
733
766
|
child.unref();
|
|
734
767
|
return;
|
|
735
768
|
}
|
|
736
|
-
const args =
|
|
737
|
-
'-m',
|
|
738
|
-
'worca.scripts.run_workspace',
|
|
739
|
-
workspace_root,
|
|
740
|
-
'--workspace-id',
|
|
741
|
-
workspace_id,
|
|
742
|
-
];
|
|
743
|
-
if (manifest.work_request?.source) {
|
|
744
|
-
args.push('--source', manifest.work_request.source);
|
|
745
|
-
} else {
|
|
746
|
-
args.push('--prompt', manifest.work_request?.description ?? '');
|
|
747
|
-
}
|
|
748
|
-
if (manifest.branch_template) {
|
|
749
|
-
args.push('--branch', manifest.branch_template);
|
|
750
|
-
}
|
|
751
|
-
if (manifest.skip_integration) args.push('--skip-integration');
|
|
752
|
-
if (manifest.skip_planning) args.push('--skip-planning');
|
|
753
|
-
if (manifest.max_parallel) {
|
|
754
|
-
args.push('--max-parallel', String(manifest.max_parallel));
|
|
755
|
-
}
|
|
756
|
-
for (const p of manifest.guide?.paths || []) {
|
|
757
|
-
args.push('--guide', p);
|
|
758
|
-
}
|
|
769
|
+
const args = buildWorkspaceArgs(workspace_root, workspace_id, manifest);
|
|
759
770
|
const child = spawn('python3', args, {
|
|
760
771
|
detached: true,
|
|
761
772
|
stdio: 'ignore',
|
package/server/beads-reader.js
CHANGED
|
@@ -40,7 +40,7 @@ function transformIssue(issue, deps) {
|
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
async function
|
|
43
|
+
export async function enrichIssuesWithDeps(issues, dbPath) {
|
|
44
44
|
if (issues.length === 0) return [];
|
|
45
45
|
const detailed = await runBd(['show', ...issues.map((i) => i.id)], dbPath);
|
|
46
46
|
const detailMap = new Map(detailed.map((d) => [d.id, d]));
|
|
@@ -54,13 +54,21 @@ export function dbExists(beadsDb) {
|
|
|
54
54
|
return existsSync(beadsDb);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
export async function listIssuesShallow(beadsDb) {
|
|
58
|
+
try {
|
|
59
|
+
return await runBd(['list', '--limit', '0'], beadsDb);
|
|
60
|
+
} catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
57
65
|
export async function listIssues(beadsDb) {
|
|
58
66
|
try {
|
|
59
67
|
const issues = await runBd(['list', '--limit', '0'], beadsDb);
|
|
60
|
-
// Must await here — without it, an
|
|
68
|
+
// Must await here — without it, an enrichIssuesWithDeps rejection (e.g. bd show
|
|
61
69
|
// SIGTERM under daemon contention) escapes the try/catch and propagates
|
|
62
70
|
// to the WS handler as an unhandled rejection, crashing Node.
|
|
63
|
-
return await
|
|
71
|
+
return await enrichIssuesWithDeps(issues, beadsDb);
|
|
64
72
|
} catch {
|
|
65
73
|
return [];
|
|
66
74
|
}
|
|
@@ -72,7 +80,7 @@ export async function listIssuesByLabel(beadsDb, label) {
|
|
|
72
80
|
['list', '--label-any', label, '--all', '--limit', '0'],
|
|
73
81
|
beadsDb,
|
|
74
82
|
);
|
|
75
|
-
return await
|
|
83
|
+
return await enrichIssuesWithDeps(issues, beadsDb);
|
|
76
84
|
};
|
|
77
85
|
try {
|
|
78
86
|
return await attempt();
|
|
@@ -453,6 +453,32 @@ function renderWorkspacePlanFailed(envelope) {
|
|
|
453
453
|
return mdMsg(parts.join('\n'), 'error');
|
|
454
454
|
}
|
|
455
455
|
|
|
456
|
+
function renderWorkspacePlanLoaded(envelope) {
|
|
457
|
+
const p = envelope.payload ?? {};
|
|
458
|
+
const parts = [`📋 **Workspace plan loaded:** ${workspaceTitle(envelope)}`];
|
|
459
|
+
parts.push(
|
|
460
|
+
` **Mode:** ${p.mode ?? '?'} (${p.project_count ?? '?'} project${p.project_count === 1 ? '' : 's'})`,
|
|
461
|
+
);
|
|
462
|
+
return mdMsg(parts.join('\n'), 'info');
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function renderWorkspacePlanPartial(envelope) {
|
|
466
|
+
const p = envelope.payload ?? {};
|
|
467
|
+
const uncovered = Array.isArray(p.uncovered_projects)
|
|
468
|
+
? p.uncovered_projects
|
|
469
|
+
: [];
|
|
470
|
+
const parts = [
|
|
471
|
+
`⚠ **Workspace plan partial coverage:** ${workspaceTitle(envelope)}`,
|
|
472
|
+
];
|
|
473
|
+
parts.push(
|
|
474
|
+
` **Mode:** ${p.mode ?? '?'} — ${uncovered.length} uncovered project${uncovered.length === 1 ? '' : 's'}`,
|
|
475
|
+
);
|
|
476
|
+
if (uncovered.length > 0) {
|
|
477
|
+
parts.push(` **Uncovered:** ${uncovered.join(', ')}`);
|
|
478
|
+
}
|
|
479
|
+
return mdMsg(parts.join('\n'), 'warning');
|
|
480
|
+
}
|
|
481
|
+
|
|
456
482
|
function renderWorkspaceTierStarted(envelope) {
|
|
457
483
|
const p = envelope.payload ?? {};
|
|
458
484
|
const projects = Array.isArray(p.projects) ? p.projects : [];
|
|
@@ -570,6 +596,8 @@ export const OPT_IN_RENDERERS = {
|
|
|
570
596
|
'workspace.plan.started': renderWorkspacePlanStarted,
|
|
571
597
|
'workspace.plan.completed': renderWorkspacePlanCompleted,
|
|
572
598
|
'workspace.plan.failed': renderWorkspacePlanFailed,
|
|
599
|
+
'workspace.plan.loaded': renderWorkspacePlanLoaded,
|
|
600
|
+
'workspace.plan.partial': renderWorkspacePlanPartial,
|
|
573
601
|
'workspace.tier.started': renderWorkspaceTierStarted,
|
|
574
602
|
'workspace.tier.completed': renderWorkspaceTierCompleted,
|
|
575
603
|
'workspace.integration_test.started': renderWorkspaceIntegrationStarted,
|
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
} from './paths.js';
|
|
30
30
|
|
|
31
31
|
const GUIDE_CAP_BYTES_DEFAULT = 64 * 1024;
|
|
32
|
+
const PROJECT_PLAN_CAP_BYTES = 256 * 1024;
|
|
33
|
+
const WORKSPACE_PLAN_CAP_BYTES = 256 * 1024;
|
|
32
34
|
|
|
33
35
|
const WS_ID_RE = /^ws_\d{12}_[0-9a-f]{1,32}$/;
|
|
34
36
|
|
|
@@ -557,6 +559,105 @@ function parseMultipart(body, contentType) {
|
|
|
557
559
|
return parts;
|
|
558
560
|
}
|
|
559
561
|
|
|
562
|
+
// ─── plan strategy resolution ─────────────────────────────────────────────
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Map a plan_mode to the manifest fields it implies and validate
|
|
566
|
+
* mode-specific preconditions.
|
|
567
|
+
*
|
|
568
|
+
* @param {string} plan_mode - 'master' | 'existing' | 'per-repo' | 'independent'
|
|
569
|
+
* @param {string|null} workspace_plan_path
|
|
570
|
+
* @param {Object|null} project_plans - { projectName: filePath, ... }
|
|
571
|
+
* @param {Object} ws - parsed workspace.json (needs ws.projects)
|
|
572
|
+
* @returns {{ ok: true, fields: object } | { ok: false, status: number, error: string }}
|
|
573
|
+
*/
|
|
574
|
+
export function _resolvePlanStrategy(
|
|
575
|
+
plan_mode,
|
|
576
|
+
workspace_plan_path,
|
|
577
|
+
project_plans,
|
|
578
|
+
ws,
|
|
579
|
+
) {
|
|
580
|
+
const mode = plan_mode || 'master';
|
|
581
|
+
const projectNames = new Set((ws.projects ?? []).map((p) => p.name));
|
|
582
|
+
|
|
583
|
+
if (mode === 'existing') {
|
|
584
|
+
if (!workspace_plan_path) {
|
|
585
|
+
return {
|
|
586
|
+
ok: false,
|
|
587
|
+
status: 400,
|
|
588
|
+
error:
|
|
589
|
+
'existing mode requires a workspace plan (upload or server-side path)',
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
return {
|
|
593
|
+
ok: true,
|
|
594
|
+
fields: {
|
|
595
|
+
plan_mode: 'existing',
|
|
596
|
+
workspace_plan_path,
|
|
597
|
+
project_plans: null,
|
|
598
|
+
skip_planning: false,
|
|
599
|
+
},
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (mode === 'per-repo') {
|
|
604
|
+
const plans = project_plans ?? {};
|
|
605
|
+
const planKeys = Object.keys(plans);
|
|
606
|
+
if (planKeys.length === 0) {
|
|
607
|
+
return {
|
|
608
|
+
ok: false,
|
|
609
|
+
status: 400,
|
|
610
|
+
error: 'per-repo mode requires at least one project plan',
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
const unknown = planKeys.filter((name) => !projectNames.has(name));
|
|
614
|
+
if (unknown.length > 0) {
|
|
615
|
+
return {
|
|
616
|
+
ok: false,
|
|
617
|
+
status: 422,
|
|
618
|
+
error: `Unknown project(s) in per-repo plans: ${unknown.join(', ')}`,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
return {
|
|
622
|
+
ok: true,
|
|
623
|
+
fields: {
|
|
624
|
+
plan_mode: 'per-repo',
|
|
625
|
+
workspace_plan_path: null,
|
|
626
|
+
project_plans: plans,
|
|
627
|
+
skip_planning: false,
|
|
628
|
+
},
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (mode === 'independent') {
|
|
633
|
+
return {
|
|
634
|
+
ok: true,
|
|
635
|
+
fields: {
|
|
636
|
+
plan_mode: 'independent',
|
|
637
|
+
workspace_plan_path: null,
|
|
638
|
+
project_plans: null,
|
|
639
|
+
skip_planning: true,
|
|
640
|
+
},
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// master (default) — ignore any stray plan inputs. master mode always runs
|
|
645
|
+
// the master planner; the Python CLI infers its mode from which flags are
|
|
646
|
+
// present, so passing --workspace-plan/--project-plan through here would
|
|
647
|
+
// silently run the job as existing/per-repo while the manifest (and badge)
|
|
648
|
+
// still claimed "master". Null them out to keep declared mode and actual
|
|
649
|
+
// behavior in lockstep.
|
|
650
|
+
return {
|
|
651
|
+
ok: true,
|
|
652
|
+
fields: {
|
|
653
|
+
plan_mode: 'master',
|
|
654
|
+
workspace_plan_path: null,
|
|
655
|
+
project_plans: null,
|
|
656
|
+
skip_planning: false,
|
|
657
|
+
},
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
560
661
|
// ─── default injectables ──────────────────────────────────────────────────
|
|
561
662
|
|
|
562
663
|
function defaultValidateBaseBranch(repoPath, branch) {
|
|
@@ -929,6 +1030,8 @@ export function createWorkspaceRouter({
|
|
|
929
1030
|
|
|
930
1031
|
let fields = {};
|
|
931
1032
|
const guideFiles = [];
|
|
1033
|
+
let workspacePlanFileData = null;
|
|
1034
|
+
const projectPlanFiles = {};
|
|
932
1035
|
|
|
933
1036
|
if (isMultipart) {
|
|
934
1037
|
const rawBody = await readRawBody(req);
|
|
@@ -939,7 +1042,21 @@ export function createWorkspaceRouter({
|
|
|
939
1042
|
.json({ ok: false, error: 'Failed to parse multipart body' });
|
|
940
1043
|
}
|
|
941
1044
|
for (const part of parts) {
|
|
942
|
-
if (part.filename != null) {
|
|
1045
|
+
if (part.name === 'workspace_plan_file' && part.filename != null) {
|
|
1046
|
+
workspacePlanFileData = {
|
|
1047
|
+
filename: part.filename,
|
|
1048
|
+
content: part.content,
|
|
1049
|
+
};
|
|
1050
|
+
} else if (
|
|
1051
|
+
part.name?.startsWith('project_plan_') &&
|
|
1052
|
+
part.filename != null
|
|
1053
|
+
) {
|
|
1054
|
+
const projectName = part.name.slice('project_plan_'.length);
|
|
1055
|
+
projectPlanFiles[projectName] = {
|
|
1056
|
+
filename: part.filename,
|
|
1057
|
+
content: part.content,
|
|
1058
|
+
};
|
|
1059
|
+
} else if (part.filename != null) {
|
|
943
1060
|
guideFiles.push({ filename: part.filename, content: part.content });
|
|
944
1061
|
} else if (part.name) {
|
|
945
1062
|
fields[part.name] = part.content.toString('utf8');
|
|
@@ -1035,6 +1152,57 @@ export function createWorkspaceRouter({
|
|
|
1035
1152
|
guideEntry = { paths, bytes: totalBytes, filenames, uploaded: true };
|
|
1036
1153
|
}
|
|
1037
1154
|
|
|
1155
|
+
// ── workspace plan file upload / server-side path ──────────────
|
|
1156
|
+
let workspacePlanPath = null;
|
|
1157
|
+
if (workspacePlanFileData) {
|
|
1158
|
+
if (workspacePlanFileData.content.length > WORKSPACE_PLAN_CAP_BYTES) {
|
|
1159
|
+
return res.status(400).json({
|
|
1160
|
+
ok: false,
|
|
1161
|
+
error: 'Workspace plan exceeds 256 KB limit',
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
workspacePlanPath = join(wsRunDir, 'workspace-plan.json');
|
|
1165
|
+
writeFileSync(workspacePlanPath, workspacePlanFileData.content);
|
|
1166
|
+
} else if (fields.workspace_plan) {
|
|
1167
|
+
if (!existsSync(fields.workspace_plan)) {
|
|
1168
|
+
return res.status(400).json({
|
|
1169
|
+
ok: false,
|
|
1170
|
+
error: `workspace_plan path not found: ${fields.workspace_plan}`,
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
workspacePlanPath = fields.workspace_plan;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// ── per-project plan file uploads ──────────────────────────────
|
|
1177
|
+
const projectPlans = {};
|
|
1178
|
+
for (const [name, file] of Object.entries(projectPlanFiles)) {
|
|
1179
|
+
if (file.content.length > PROJECT_PLAN_CAP_BYTES) {
|
|
1180
|
+
return res.status(400).json({
|
|
1181
|
+
ok: false,
|
|
1182
|
+
error: `Project plan for "${name}" exceeds 256 KB limit`,
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
const safeName = sanitizeFilename(name);
|
|
1186
|
+
const plansDir = join(wsRunDir, 'plans');
|
|
1187
|
+
mkdirSync(plansDir, { recursive: true });
|
|
1188
|
+
const planPath = join(plansDir, `${safeName}.md`);
|
|
1189
|
+
writeFileSync(planPath, file.content);
|
|
1190
|
+
projectPlans[name] = planPath;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// ── resolve plan strategy + validate ──────────────────────────
|
|
1194
|
+
const planResult = _resolvePlanStrategy(
|
|
1195
|
+
plan_mode,
|
|
1196
|
+
workspacePlanPath,
|
|
1197
|
+
Object.keys(projectPlans).length > 0 ? projectPlans : null,
|
|
1198
|
+
ws,
|
|
1199
|
+
);
|
|
1200
|
+
if (!planResult.ok) {
|
|
1201
|
+
return res
|
|
1202
|
+
.status(planResult.status)
|
|
1203
|
+
.json({ ok: false, error: planResult.error });
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1038
1206
|
const tiers = computeTiers(ws.projects);
|
|
1039
1207
|
const dagTiers = tiers.map((projects, i) => ({
|
|
1040
1208
|
tier: i,
|
|
@@ -1054,10 +1222,10 @@ export function createWorkspaceRouter({
|
|
|
1054
1222
|
source: source ?? null,
|
|
1055
1223
|
},
|
|
1056
1224
|
guide: guideEntry,
|
|
1225
|
+
...planResult.fields,
|
|
1057
1226
|
branch_template: branch_template ?? 'workspace/{slug}/{project}',
|
|
1058
1227
|
max_parallel: Number(max_parallel) || 5,
|
|
1059
1228
|
skip_integration: false,
|
|
1060
|
-
skip_planning: plan_mode === 'skip',
|
|
1061
1229
|
status: 'planning',
|
|
1062
1230
|
halt_reason: null,
|
|
1063
1231
|
dag: { tiers: dagTiers },
|