@enderfga/claw-orchestrator 4.7.0 → 4.8.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 +5 -5
- package/configs/autoloop-coder-prompt.md +4 -4
- package/configs/autoloop-planner-prompt.md +10 -10
- package/configs/autoloop-reviewer-prompt.md +3 -4
- package/dist/src/autoloop/dispatcher.d.ts +59 -10
- package/dist/src/autoloop/dispatcher.js +261 -40
- package/dist/src/autoloop/dispatcher.js.map +1 -1
- package/dist/src/autoloop/planner-tools.d.ts +4 -3
- package/dist/src/autoloop/planner-tools.js +35 -2
- package/dist/src/autoloop/planner-tools.js.map +1 -1
- package/dist/src/autoloop/types.d.ts +2 -0
- package/dist/src/autoloop/types.js.map +1 -1
- package/dist/src/dashboard/index.html +185 -94
- package/dist/src/embedded-server.js +67 -9
- package/dist/src/embedded-server.js.map +1 -1
- package/dist/src/index.js +84 -52
- package/dist/src/index.js.map +1 -1
- package/dist/src/persistent-agy-session.js +4 -1
- package/dist/src/persistent-agy-session.js.map +1 -1
- package/dist/src/persistent-codex-session.js +3 -0
- package/dist/src/persistent-codex-session.js.map +1 -1
- package/dist/src/persistent-cursor-session.js +7 -2
- package/dist/src/persistent-cursor-session.js.map +1 -1
- package/dist/src/persistent-custom-session.js +13 -0
- package/dist/src/persistent-custom-session.js.map +1 -1
- package/dist/src/persistent-gemini-session.d.ts +4 -0
- package/dist/src/persistent-gemini-session.js +55 -2
- package/dist/src/persistent-gemini-session.js.map +1 -1
- package/dist/src/persistent-opencode-session.js +35 -1
- package/dist/src/persistent-opencode-session.js.map +1 -1
- package/dist/src/persistent-session.js +6 -1
- package/dist/src/persistent-session.js.map +1 -1
- package/dist/src/session-manager.d.ts +44 -5
- package/dist/src/session-manager.js +190 -18
- package/dist/src/session-manager.js.map +1 -1
- package/dist/src/types.d.ts +3 -2
- package/package.json +1 -1
- package/skills/SKILL.md +11 -10
- package/skills/references/autoloop.md +50 -6
- package/skills/references/claude-cli-tracking.md +2 -1
- package/skills/references/multi-engine.md +12 -9
- package/skills/references/tools.md +38 -19
|
@@ -27,7 +27,12 @@
|
|
|
27
27
|
margin: 0;
|
|
28
28
|
background: var(--bg);
|
|
29
29
|
color: var(--text);
|
|
30
|
-
font:
|
|
30
|
+
font:
|
|
31
|
+
14px/1.5 -apple-system,
|
|
32
|
+
BlinkMacSystemFont,
|
|
33
|
+
'Segoe UI',
|
|
34
|
+
Roboto,
|
|
35
|
+
sans-serif;
|
|
31
36
|
}
|
|
32
37
|
a {
|
|
33
38
|
color: var(--info);
|
|
@@ -270,8 +275,7 @@
|
|
|
270
275
|
.entryBody {
|
|
271
276
|
white-space: pre-wrap;
|
|
272
277
|
word-wrap: break-word;
|
|
273
|
-
font-family:
|
|
274
|
-
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
278
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
275
279
|
font-size: 12px;
|
|
276
280
|
background: #fafafa;
|
|
277
281
|
padding: 8px 10px;
|
|
@@ -379,11 +383,23 @@
|
|
|
379
383
|
align-self: flex-start;
|
|
380
384
|
}
|
|
381
385
|
.forge-mode[data-mode='build'],
|
|
382
|
-
.forge-mode[data-mode='building'] {
|
|
386
|
+
.forge-mode[data-mode='building'] {
|
|
387
|
+
background: var(--info);
|
|
388
|
+
color: white;
|
|
389
|
+
}
|
|
383
390
|
.forge-mode[data-mode='done'],
|
|
384
|
-
.forge-mode[data-mode='build-complete'] {
|
|
385
|
-
|
|
386
|
-
|
|
391
|
+
.forge-mode[data-mode='build-complete'] {
|
|
392
|
+
background: var(--accent);
|
|
393
|
+
color: white;
|
|
394
|
+
}
|
|
395
|
+
.forge-mode[data-mode='failed'] {
|
|
396
|
+
background: var(--error);
|
|
397
|
+
color: white;
|
|
398
|
+
}
|
|
399
|
+
.forge-mode[data-mode='queued'] {
|
|
400
|
+
background: var(--warn);
|
|
401
|
+
color: white;
|
|
402
|
+
}
|
|
387
403
|
.forge-empty {
|
|
388
404
|
margin: auto;
|
|
389
405
|
max-width: 440px;
|
|
@@ -402,9 +418,19 @@
|
|
|
402
418
|
border-radius: 4px;
|
|
403
419
|
font-size: 13px;
|
|
404
420
|
}
|
|
405
|
-
.forge-msg.user {
|
|
406
|
-
|
|
407
|
-
|
|
421
|
+
.forge-msg.user {
|
|
422
|
+
background: #e0eaff;
|
|
423
|
+
align-self: flex-end;
|
|
424
|
+
}
|
|
425
|
+
.forge-msg.assistant {
|
|
426
|
+
background: var(--surface);
|
|
427
|
+
border: 1px solid var(--border);
|
|
428
|
+
}
|
|
429
|
+
.forge-msg.system {
|
|
430
|
+
background: var(--surface-2);
|
|
431
|
+
color: var(--muted);
|
|
432
|
+
font-size: 12px;
|
|
433
|
+
}
|
|
408
434
|
.forge-share {
|
|
409
435
|
background: var(--surface);
|
|
410
436
|
border: 1px solid var(--accent);
|
|
@@ -412,9 +438,20 @@
|
|
|
412
438
|
padding: 12px;
|
|
413
439
|
margin: 8px 0;
|
|
414
440
|
}
|
|
415
|
-
.forge-share strong {
|
|
416
|
-
|
|
417
|
-
|
|
441
|
+
.forge-share strong {
|
|
442
|
+
color: var(--accent);
|
|
443
|
+
display: block;
|
|
444
|
+
margin-bottom: 6px;
|
|
445
|
+
}
|
|
446
|
+
.forge-share a {
|
|
447
|
+
color: var(--accent);
|
|
448
|
+
word-break: break-all;
|
|
449
|
+
}
|
|
450
|
+
.forge-share-actions {
|
|
451
|
+
display: flex;
|
|
452
|
+
gap: 8px;
|
|
453
|
+
margin-top: 10px;
|
|
454
|
+
}
|
|
418
455
|
.forge-share-actions button {
|
|
419
456
|
padding: 4px 12px;
|
|
420
457
|
font-size: 12px;
|
|
@@ -436,13 +473,17 @@
|
|
|
436
473
|
font-size: 12px;
|
|
437
474
|
overflow-x: auto;
|
|
438
475
|
}
|
|
439
|
-
.forge-public-modal h4 {
|
|
476
|
+
.forge-public-modal h4 {
|
|
477
|
+
margin-bottom: 4px;
|
|
478
|
+
}
|
|
440
479
|
.runItem .lifecycle-actions {
|
|
441
480
|
display: none;
|
|
442
481
|
gap: 4px;
|
|
443
482
|
margin-top: 4px;
|
|
444
483
|
}
|
|
445
|
-
.runItem:hover .lifecycle-actions {
|
|
484
|
+
.runItem:hover .lifecycle-actions {
|
|
485
|
+
display: flex;
|
|
486
|
+
}
|
|
446
487
|
.lifecycle-actions button {
|
|
447
488
|
font-size: 10px;
|
|
448
489
|
padding: 2px 6px;
|
|
@@ -451,7 +492,9 @@
|
|
|
451
492
|
border-radius: 3px;
|
|
452
493
|
cursor: pointer;
|
|
453
494
|
}
|
|
454
|
-
.lifecycle-actions button:hover {
|
|
495
|
+
.lifecycle-actions button:hover {
|
|
496
|
+
background: var(--surface-2);
|
|
497
|
+
}
|
|
455
498
|
.forge-question {
|
|
456
499
|
background: var(--surface);
|
|
457
500
|
border: 1px solid var(--border);
|
|
@@ -459,9 +502,21 @@
|
|
|
459
502
|
padding: 12px;
|
|
460
503
|
margin: 8px 0;
|
|
461
504
|
}
|
|
462
|
-
.forge-question .ctx {
|
|
463
|
-
|
|
464
|
-
|
|
505
|
+
.forge-question .ctx {
|
|
506
|
+
color: var(--muted);
|
|
507
|
+
font-size: 12px;
|
|
508
|
+
margin-bottom: 8px;
|
|
509
|
+
}
|
|
510
|
+
.forge-question .q {
|
|
511
|
+
font-weight: 600;
|
|
512
|
+
margin-bottom: 8px;
|
|
513
|
+
}
|
|
514
|
+
.forge-question .opts {
|
|
515
|
+
display: flex;
|
|
516
|
+
flex-wrap: wrap;
|
|
517
|
+
gap: 6px;
|
|
518
|
+
margin-bottom: 8px;
|
|
519
|
+
}
|
|
465
520
|
.forge-question .opt {
|
|
466
521
|
padding: 4px 10px;
|
|
467
522
|
border: 1px solid var(--border);
|
|
@@ -470,8 +525,14 @@
|
|
|
470
525
|
font-size: 12px;
|
|
471
526
|
background: var(--surface);
|
|
472
527
|
}
|
|
473
|
-
.forge-question .opt.recommended {
|
|
474
|
-
|
|
528
|
+
.forge-question .opt.recommended {
|
|
529
|
+
border-color: var(--accent);
|
|
530
|
+
}
|
|
531
|
+
.forge-question .opt.selected {
|
|
532
|
+
background: var(--accent);
|
|
533
|
+
color: white;
|
|
534
|
+
border-color: var(--accent);
|
|
535
|
+
}
|
|
475
536
|
.forge-input {
|
|
476
537
|
display: grid;
|
|
477
538
|
grid-template-columns: 1fr auto;
|
|
@@ -500,7 +561,10 @@
|
|
|
500
561
|
color: white;
|
|
501
562
|
cursor: pointer;
|
|
502
563
|
}
|
|
503
|
-
.forge-input button:disabled {
|
|
564
|
+
.forge-input button:disabled {
|
|
565
|
+
opacity: 0.4;
|
|
566
|
+
cursor: not-allowed;
|
|
567
|
+
}
|
|
504
568
|
.forge-spec {
|
|
505
569
|
border-left: 1px solid var(--border);
|
|
506
570
|
padding: 16px;
|
|
@@ -509,7 +573,9 @@
|
|
|
509
573
|
display: flex;
|
|
510
574
|
flex-direction: column;
|
|
511
575
|
}
|
|
512
|
-
.forge-spec h3 {
|
|
576
|
+
.forge-spec h3 {
|
|
577
|
+
margin: 0 0 8px 0;
|
|
578
|
+
}
|
|
513
579
|
.forge-spec textarea {
|
|
514
580
|
width: 100%;
|
|
515
581
|
font-family: ui-monospace, SFMono-Regular, monospace;
|
|
@@ -532,7 +598,10 @@
|
|
|
532
598
|
background: var(--surface);
|
|
533
599
|
cursor: pointer;
|
|
534
600
|
}
|
|
535
|
-
.forge-spec-msg {
|
|
601
|
+
.forge-spec-msg {
|
|
602
|
+
font-size: 11px;
|
|
603
|
+
color: var(--muted);
|
|
604
|
+
}
|
|
536
605
|
#forgeNew,
|
|
537
606
|
#councilNew,
|
|
538
607
|
#autoloopNew {
|
|
@@ -561,7 +630,9 @@
|
|
|
561
630
|
justify-content: center;
|
|
562
631
|
z-index: 100;
|
|
563
632
|
}
|
|
564
|
-
.modal-root[hidden] {
|
|
633
|
+
.modal-root[hidden] {
|
|
634
|
+
display: none;
|
|
635
|
+
}
|
|
565
636
|
.modal {
|
|
566
637
|
background: var(--surface);
|
|
567
638
|
border-radius: 8px;
|
|
@@ -576,7 +647,10 @@
|
|
|
576
647
|
padding: 14px 20px;
|
|
577
648
|
border-bottom: 1px solid var(--border);
|
|
578
649
|
}
|
|
579
|
-
.modal-header h2 {
|
|
650
|
+
.modal-header h2 {
|
|
651
|
+
margin: 0;
|
|
652
|
+
font-size: 15px;
|
|
653
|
+
}
|
|
580
654
|
.modal-header button {
|
|
581
655
|
background: transparent;
|
|
582
656
|
border: none;
|
|
@@ -637,8 +711,14 @@
|
|
|
637
711
|
margin: 80px auto 0;
|
|
638
712
|
max-width: 420px;
|
|
639
713
|
}
|
|
640
|
-
.empty-cta h2 {
|
|
641
|
-
|
|
714
|
+
.empty-cta h2 {
|
|
715
|
+
margin: 0 0 8px;
|
|
716
|
+
font-size: 16px;
|
|
717
|
+
}
|
|
718
|
+
.empty-cta p {
|
|
719
|
+
margin: 0 0 16px;
|
|
720
|
+
color: var(--muted);
|
|
721
|
+
}
|
|
642
722
|
.empty-cta button {
|
|
643
723
|
padding: 8px 16px;
|
|
644
724
|
border-radius: 4px;
|
|
@@ -658,10 +738,24 @@
|
|
|
658
738
|
cursor: pointer;
|
|
659
739
|
font-weight: 600;
|
|
660
740
|
}
|
|
661
|
-
#forgeStartBuild:disabled {
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
741
|
+
#forgeStartBuild:disabled {
|
|
742
|
+
opacity: 0.4;
|
|
743
|
+
cursor: not-allowed;
|
|
744
|
+
}
|
|
745
|
+
.forge-versions {
|
|
746
|
+
margin-top: 12px;
|
|
747
|
+
}
|
|
748
|
+
.forge-versions h4 {
|
|
749
|
+
margin: 0 0 6px 0;
|
|
750
|
+
font-size: 12px;
|
|
751
|
+
color: var(--muted);
|
|
752
|
+
text-transform: uppercase;
|
|
753
|
+
}
|
|
754
|
+
.version-list {
|
|
755
|
+
list-style: none;
|
|
756
|
+
padding: 0;
|
|
757
|
+
margin: 0;
|
|
758
|
+
}
|
|
665
759
|
.version-row {
|
|
666
760
|
display: flex;
|
|
667
761
|
gap: 8px;
|
|
@@ -677,8 +771,14 @@
|
|
|
677
771
|
font-family: monospace;
|
|
678
772
|
font-weight: 600;
|
|
679
773
|
}
|
|
680
|
-
.version-meta {
|
|
681
|
-
|
|
774
|
+
.version-meta {
|
|
775
|
+
color: var(--muted);
|
|
776
|
+
}
|
|
777
|
+
.version-row a {
|
|
778
|
+
color: var(--accent);
|
|
779
|
+
text-decoration: none;
|
|
780
|
+
word-break: break-all;
|
|
781
|
+
}
|
|
682
782
|
.version-row button {
|
|
683
783
|
margin-left: auto;
|
|
684
784
|
padding: 2px 8px;
|
|
@@ -688,7 +788,10 @@
|
|
|
688
788
|
border-radius: 3px;
|
|
689
789
|
cursor: pointer;
|
|
690
790
|
}
|
|
691
|
-
.version-row button:disabled {
|
|
791
|
+
.version-row button:disabled {
|
|
792
|
+
opacity: 0.4;
|
|
793
|
+
cursor: not-allowed;
|
|
794
|
+
}
|
|
692
795
|
</style>
|
|
693
796
|
</head>
|
|
694
797
|
<body>
|
|
@@ -733,19 +836,11 @@
|
|
|
733
836
|
<div data-for-tab="council" style="display: contents">
|
|
734
837
|
<label>
|
|
735
838
|
Task
|
|
736
|
-
<textarea
|
|
737
|
-
name="task"
|
|
738
|
-
rows="3"
|
|
739
|
-
placeholder="What should the council achieve?"
|
|
740
|
-
></textarea>
|
|
839
|
+
<textarea name="task" rows="3" placeholder="What should the council achieve?"></textarea>
|
|
741
840
|
</label>
|
|
742
841
|
<label>
|
|
743
842
|
Project directory
|
|
744
|
-
<input
|
|
745
|
-
name="projectDir"
|
|
746
|
-
type="text"
|
|
747
|
-
placeholder="/abs/path/to/workspace"
|
|
748
|
-
/>
|
|
843
|
+
<input name="projectDir" type="text" placeholder="/abs/path/to/workspace" />
|
|
749
844
|
</label>
|
|
750
845
|
<label>
|
|
751
846
|
Max rounds
|
|
@@ -756,11 +851,7 @@
|
|
|
756
851
|
<div data-for-tab="autoloop" style="display: contents">
|
|
757
852
|
<label>
|
|
758
853
|
Workspace
|
|
759
|
-
<input
|
|
760
|
-
name="workspace"
|
|
761
|
-
type="text"
|
|
762
|
-
placeholder="/abs/path/to/git/workspace"
|
|
763
|
-
/>
|
|
854
|
+
<input name="workspace" type="text" placeholder="/abs/path/to/git/workspace" />
|
|
764
855
|
</label>
|
|
765
856
|
<label>
|
|
766
857
|
Planner model (optional)
|
|
@@ -807,11 +898,11 @@
|
|
|
807
898
|
};
|
|
808
899
|
|
|
809
900
|
const $ = (id) => document.getElementById(id);
|
|
810
|
-
const fmtTime = (iso) =>
|
|
811
|
-
iso ? new Date(iso).toLocaleTimeString(undefined, { hour12: false }) : '';
|
|
901
|
+
const fmtTime = (iso) => (iso ? new Date(iso).toLocaleTimeString(undefined, { hour12: false }) : '');
|
|
812
902
|
const escapeHtml = (s) =>
|
|
813
|
-
String(s ?? '').replace(
|
|
814
|
-
|
|
903
|
+
String(s ?? '').replace(
|
|
904
|
+
/[&<>"']/g,
|
|
905
|
+
(c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c],
|
|
815
906
|
);
|
|
816
907
|
|
|
817
908
|
// ─── Tab switching ────────────────────────────────────────────
|
|
@@ -826,11 +917,7 @@
|
|
|
826
917
|
b.setAttribute('aria-selected', String(b.dataset.tab === tab));
|
|
827
918
|
});
|
|
828
919
|
$('sidebarTitle').textContent =
|
|
829
|
-
tab === 'autoloop'
|
|
830
|
-
? 'Autoloop runs'
|
|
831
|
-
: tab === 'council'
|
|
832
|
-
? 'Council sessions'
|
|
833
|
-
: 'Forge apps';
|
|
920
|
+
tab === 'autoloop' ? 'Autoloop runs' : tab === 'council' ? 'Council sessions' : 'Forge apps';
|
|
834
921
|
$('forgeNew').hidden = tab !== 'forge';
|
|
835
922
|
$('councilNew').hidden = tab !== 'council';
|
|
836
923
|
$('autoloopNew').hidden = tab !== 'autoloop';
|
|
@@ -879,11 +966,7 @@
|
|
|
879
966
|
const noun = labels[state.tab] || 'run';
|
|
880
967
|
if (state.runs.length === 0) {
|
|
881
968
|
const triggerId =
|
|
882
|
-
state.tab === 'autoloop'
|
|
883
|
-
? 'autoloopNew'
|
|
884
|
-
: state.tab === 'council'
|
|
885
|
-
? 'councilNew'
|
|
886
|
-
: 'forgeNew';
|
|
969
|
+
state.tab === 'autoloop' ? 'autoloopNew' : state.tab === 'council' ? 'councilNew' : 'forgeNew';
|
|
887
970
|
$('main').innerHTML = `
|
|
888
971
|
<div class="empty empty-cta">
|
|
889
972
|
<h2>No ${escapeHtml(noun)}s yet</h2>
|
|
@@ -892,8 +975,7 @@
|
|
|
892
975
|
</div>
|
|
893
976
|
`;
|
|
894
977
|
} else {
|
|
895
|
-
$('main').innerHTML =
|
|
896
|
-
'<div class="empty" style="margin-top:80px">Select a run to view.</div>';
|
|
978
|
+
$('main').innerHTML = '<div class="empty" style="margin-top:80px">Select a run to view.</div>';
|
|
897
979
|
}
|
|
898
980
|
}
|
|
899
981
|
|
|
@@ -901,20 +983,15 @@
|
|
|
901
983
|
async function refreshList() {
|
|
902
984
|
try {
|
|
903
985
|
const url =
|
|
904
|
-
state.tab === 'autoloop'
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
const opts = state.tab === 'forge' ? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' } : {};
|
|
986
|
+
state.tab === 'autoloop' ? '/autoloop/list' : state.tab === 'council' ? '/council/list' : '/ultraapp/list';
|
|
987
|
+
const opts =
|
|
988
|
+
state.tab === 'forge'
|
|
989
|
+
? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }
|
|
990
|
+
: {};
|
|
910
991
|
const r = await fetch(url, opts);
|
|
911
992
|
const j = await r.json();
|
|
912
993
|
state.runs =
|
|
913
|
-
state.tab === 'autoloop'
|
|
914
|
-
? j.runs || []
|
|
915
|
-
: state.tab === 'council'
|
|
916
|
-
? j.councils || []
|
|
917
|
-
: j.runs || [];
|
|
994
|
+
state.tab === 'autoloop' ? j.runs || [] : state.tab === 'council' ? j.councils || [] : j.runs || [];
|
|
918
995
|
renderList();
|
|
919
996
|
// Refresh the empty-state CTA — runs may have just appeared or
|
|
920
997
|
// disappeared, and renderEmptyMain's text depends on state.runs.length.
|
|
@@ -929,12 +1006,8 @@
|
|
|
929
1006
|
function renderList() {
|
|
930
1007
|
const ul = $('runlist');
|
|
931
1008
|
if (state.runs.length === 0) {
|
|
932
|
-
const noun =
|
|
933
|
-
|
|
934
|
-
ul.innerHTML =
|
|
935
|
-
'<li class="empty" style="padding:20px;list-style:none">No active ' +
|
|
936
|
-
noun +
|
|
937
|
-
'.</li>';
|
|
1009
|
+
const noun = state.tab === 'autoloop' ? 'runs' : state.tab === 'council' ? 'councils' : 'apps';
|
|
1010
|
+
ul.innerHTML = '<li class="empty" style="padding:20px;list-style:none">No active ' + noun + '.</li>';
|
|
938
1011
|
return;
|
|
939
1012
|
}
|
|
940
1013
|
const html = state.runs
|
|
@@ -998,12 +1071,26 @@
|
|
|
998
1071
|
const id = el.dataset.id;
|
|
999
1072
|
if (state.tab === 'autoloop') {
|
|
1000
1073
|
if (action !== 'delete') return;
|
|
1001
|
-
if (
|
|
1074
|
+
if (
|
|
1075
|
+
!confirm(
|
|
1076
|
+
`Delete autoloop run "${id}"? The registry entry is removed; the ledger directory under tasks/${id}/ is kept on disk.`,
|
|
1077
|
+
)
|
|
1078
|
+
)
|
|
1079
|
+
return;
|
|
1002
1080
|
try {
|
|
1003
|
-
const r = await fetch(`/autoloop/${encodeURIComponent(id)}/delete`, {
|
|
1081
|
+
const r = await fetch(`/autoloop/${encodeURIComponent(id)}/delete`, {
|
|
1082
|
+
method: 'POST',
|
|
1083
|
+
headers: { 'content-type': 'application/json' },
|
|
1084
|
+
body: '{}',
|
|
1085
|
+
});
|
|
1004
1086
|
if (!r.ok) {
|
|
1005
1087
|
let detail = `HTTP ${r.status}`;
|
|
1006
|
-
try {
|
|
1088
|
+
try {
|
|
1089
|
+
const j = await r.json();
|
|
1090
|
+
if (j && j.error) detail = j.error;
|
|
1091
|
+
} catch {
|
|
1092
|
+
/* ignore */
|
|
1093
|
+
}
|
|
1007
1094
|
alert(`Delete failed: ${detail}`);
|
|
1008
1095
|
return;
|
|
1009
1096
|
}
|
|
@@ -1020,7 +1107,11 @@
|
|
|
1020
1107
|
refreshList();
|
|
1021
1108
|
return;
|
|
1022
1109
|
}
|
|
1023
|
-
if (
|
|
1110
|
+
if (
|
|
1111
|
+
action === 'delete' &&
|
|
1112
|
+
!confirm('Delete this run? Container, image, and on-disk artifacts are removed.')
|
|
1113
|
+
)
|
|
1114
|
+
return;
|
|
1024
1115
|
await forgeLifecycleAction(id, action);
|
|
1025
1116
|
refreshList();
|
|
1026
1117
|
});
|
|
@@ -1272,7 +1363,11 @@
|
|
|
1272
1363
|
});
|
|
1273
1364
|
const j = await r.json();
|
|
1274
1365
|
if (!j.ok) {
|
|
1275
|
-
renderChatEntry({
|
|
1366
|
+
renderChatEntry({
|
|
1367
|
+
role: 'system',
|
|
1368
|
+
kind: 'error',
|
|
1369
|
+
text: '[error] ' + action + ': ' + (j.error || 'unknown'),
|
|
1370
|
+
});
|
|
1276
1371
|
}
|
|
1277
1372
|
if (action === 'delete' && j.ok && state.forge.runId === runId) {
|
|
1278
1373
|
// Run is gone — close it and refresh sidebar
|
|
@@ -1856,10 +1951,7 @@
|
|
|
1856
1951
|
const s = state.council.session;
|
|
1857
1952
|
if (!s) return;
|
|
1858
1953
|
$('c-status').innerHTML = `<span class="badge ${s.status}">${s.status}</span>`;
|
|
1859
|
-
const rounds =
|
|
1860
|
-
s.responses && s.responses.length > 0
|
|
1861
|
-
? Math.max(...s.responses.map((r) => r.round))
|
|
1862
|
-
: 0;
|
|
1954
|
+
const rounds = s.responses && s.responses.length > 0 ? Math.max(...s.responses.map((r) => r.round)) : 0;
|
|
1863
1955
|
$('c-round').textContent = rounds;
|
|
1864
1956
|
$('c-agents').textContent = (s.config?.agents || []).map((a) => a.name).join(', ');
|
|
1865
1957
|
$('c-task').textContent = s.task || '';
|
|
@@ -1944,8 +2036,7 @@
|
|
|
1944
2036
|
// both, switching visible fields by state.tab.
|
|
1945
2037
|
function openLauncherModal() {
|
|
1946
2038
|
const root = $('modalRoot');
|
|
1947
|
-
$('modalTitle').textContent =
|
|
1948
|
-
state.tab === 'council' ? 'New Council session' : 'New Autoloop run';
|
|
2039
|
+
$('modalTitle').textContent = state.tab === 'council' ? 'New Council session' : 'New Autoloop run';
|
|
1949
2040
|
root.querySelectorAll('[data-for-tab]').forEach((el) => {
|
|
1950
2041
|
const visible = el.dataset.forTab === state.tab;
|
|
1951
2042
|
el.style.display = visible ? 'contents' : 'none';
|
|
@@ -18,6 +18,46 @@ import { DEFAULT_SERVER_PORT, MAX_BODY_SIZE, RATE_LIMIT_MAX_REQUESTS, RATE_LIMIT
|
|
|
18
18
|
// Grace period for server.close() to drain connections before lingering
|
|
19
19
|
// SSE/keep-alive sockets are force-dropped (otherwise close() hangs forever).
|
|
20
20
|
const SERVER_CLOSE_GRACE_MS = 5000;
|
|
21
|
+
function autoloopErrorStatus(error) {
|
|
22
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
23
|
+
if (/^Autoloop run '.+' not found in registry$/.test(message))
|
|
24
|
+
return 404;
|
|
25
|
+
if (/^Autoloop with id '.+' (?:already exists|is being deleted|is still starting)$/.test(message))
|
|
26
|
+
return 409;
|
|
27
|
+
if (/^Autoloop session name '.+' is already in use$/.test(message))
|
|
28
|
+
return 409;
|
|
29
|
+
if (/^(?:Planner|Coder|Reviewer) engine '.+' is not supported$/.test(message))
|
|
30
|
+
return 400;
|
|
31
|
+
// Any custom-engine config complaint is caller error, not a server fault.
|
|
32
|
+
// The old form pinned a single dotted segment and the verb "must be", so
|
|
33
|
+
// `config.args.permissionMode must be a string` and `config.env must contain
|
|
34
|
+
// only string values` both fell through to 500 — the opposite of the split
|
|
35
|
+
// this helper exists to provide.
|
|
36
|
+
if (/^(?:Planner|Coder|Reviewer) custom engine config\b/.test(message))
|
|
37
|
+
return 400;
|
|
38
|
+
return 500;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Custom engines name an arbitrary executable (`bin`) plus argv and env. That
|
|
42
|
+
* is fine for a local, programmatic caller, but this HTTP surface is routinely
|
|
43
|
+
* reverse-tunnelled to a public hostname, and its auth token is a *monitoring*
|
|
44
|
+
* credential — it was never meant to confer "run any binary on the host".
|
|
45
|
+
* Accepting a custom-engine object from the request body would turn any
|
|
46
|
+
* dashboard session into remote code execution, so the network surface refuses
|
|
47
|
+
* it outright. Built-in engines (the actual ask in #72) stay fully selectable.
|
|
48
|
+
*/
|
|
49
|
+
const CUSTOM_ENGINE_BODY_KEYS = ['planner_custom_engine', 'coder_custom_engine', 'reviewer_custom_engine'];
|
|
50
|
+
function rejectCustomEngineOverHttp(body) {
|
|
51
|
+
if (typeof body !== 'object' || body === null)
|
|
52
|
+
return null;
|
|
53
|
+
const record = body;
|
|
54
|
+
for (const key of CUSTOM_ENGINE_BODY_KEYS) {
|
|
55
|
+
if (record[key] !== undefined && record[key] !== null) {
|
|
56
|
+
return `${key} is not accepted over HTTP: a custom engine names an executable to spawn, so it may only be configured by a local caller (MCP tool / SessionManager API). Use a built-in engine here.`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
21
61
|
export class EmbeddedServer {
|
|
22
62
|
server = null;
|
|
23
63
|
manager;
|
|
@@ -636,12 +676,18 @@ export class EmbeddedServer {
|
|
|
636
676
|
}
|
|
637
677
|
// ─── Autoloop — launch new (dashboard "+ New" button) ───────
|
|
638
678
|
//
|
|
639
|
-
//
|
|
679
|
+
// Contract: workspace/run_id plus optional per-role engine/model/custom-engine config.
|
|
640
680
|
// When run_id is omitted or malformed, the server generates
|
|
641
681
|
// `auto-{timestamp}-{4-byte-hex}`. Power users wanting a meaningful id
|
|
642
682
|
// (e.g. "ml-refactor-v2") can pass run_id explicitly.
|
|
643
683
|
if (path === '/autoloop/new') {
|
|
644
|
-
const
|
|
684
|
+
const customEngineRejection = rejectCustomEngineOverHttp(body);
|
|
685
|
+
if (customEngineRejection) {
|
|
686
|
+
json(400, { ok: false, error: customEngineRejection });
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const input = body;
|
|
690
|
+
const workspace = input.workspace;
|
|
645
691
|
if (typeof workspace !== 'string' || !workspace.trim()) {
|
|
646
692
|
json(400, { ok: false, error: 'workspace (string) required' });
|
|
647
693
|
return;
|
|
@@ -651,7 +697,7 @@ export class EmbeddedServer {
|
|
|
651
697
|
json(400, { ok: false, error: 'workspace failed sanitization' });
|
|
652
698
|
return;
|
|
653
699
|
}
|
|
654
|
-
const explicitId =
|
|
700
|
+
const explicitId = input.run_id;
|
|
655
701
|
const runId = typeof explicitId === 'string' && /^[a-zA-Z0-9_-]{1,64}$/.test(explicitId)
|
|
656
702
|
? explicitId
|
|
657
703
|
: `auto-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
@@ -659,8 +705,13 @@ export class EmbeddedServer {
|
|
|
659
705
|
const result = await this.manager.autoloopStart({
|
|
660
706
|
runId,
|
|
661
707
|
workspace: safeWorkspace,
|
|
662
|
-
|
|
663
|
-
|
|
708
|
+
plannerEngine: input.planner_engine,
|
|
709
|
+
plannerModel: input.planner_model,
|
|
710
|
+
coderEngine: input.coder_engine,
|
|
711
|
+
coderModel: input.coder_model,
|
|
712
|
+
reviewerEngine: input.reviewer_engine,
|
|
713
|
+
reviewerModel: input.reviewer_model,
|
|
714
|
+
sendTimeoutMs: input.send_timeout_ms,
|
|
664
715
|
});
|
|
665
716
|
json(200, {
|
|
666
717
|
ok: true,
|
|
@@ -669,7 +720,7 @@ export class EmbeddedServer {
|
|
|
669
720
|
});
|
|
670
721
|
}
|
|
671
722
|
catch (err) {
|
|
672
|
-
json(
|
|
723
|
+
json(autoloopErrorStatus(err), { ok: false, error: err.message });
|
|
673
724
|
}
|
|
674
725
|
return;
|
|
675
726
|
}
|
|
@@ -907,13 +958,20 @@ export class EmbeddedServer {
|
|
|
907
958
|
const v2ResumeMatch = path.match(/^\/autoloop\/([^/]+)\/resume$/);
|
|
908
959
|
if (v2ResumeMatch) {
|
|
909
960
|
const id = v2ResumeMatch[1];
|
|
961
|
+
const resumeRejection = rejectCustomEngineOverHttp(body);
|
|
962
|
+
if (resumeRejection) {
|
|
963
|
+
json(400, { ok: false, error: resumeRejection });
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
910
966
|
try {
|
|
911
|
-
|
|
967
|
+
// No custom-engine configs here by design (see rejectCustomEngineOverHttp).
|
|
968
|
+
// A run whose roles used custom engines therefore cannot be resumed over
|
|
969
|
+
// HTTP; autoloopResume reports that as a caller error, not a 500.
|
|
970
|
+
const state = await this.manager.autoloopResume(id, {});
|
|
912
971
|
json(200, { ok: true, state });
|
|
913
972
|
}
|
|
914
973
|
catch (err) {
|
|
915
|
-
|
|
916
|
-
json(/not found/i.test(msg) ? 404 : 500, { ok: false, error: msg });
|
|
974
|
+
json(autoloopErrorStatus(err), { ok: false, error: err.message });
|
|
917
975
|
}
|
|
918
976
|
return;
|
|
919
977
|
}
|