diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000000..2b58e54751 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,13 @@ +{ + "permissions": { + "allow": [ + "Bash(git push:*)", + "Bash(gh auth:*)", + "Bash(ssh:*)", + "Bash(echo:*)", + "Bash(git remote set-url:*)", + "Bash(git checkout:*)", + "Bash(git pull:*)" + ] + } +} diff --git a/README.md b/README.md index 115f673cb9..f4a4b2a1a0 100644 --- a/README.md +++ b/README.md @@ -46,9 +46,11 @@ CodexMonitor is a Tauri app for orchestrating multiple Codex agents across local ## Forge - Canonical Forge module docs: [docs/forge.md](docs/forge.md). -- Installs and uninstalls bundled Forge templates into workspace-scoped `.agent` artifacts. -- Returns a normalized template plan prompt for plan mode (including compatibility rewrites and `@plan` enforcement). -- Lists workspace plans and joins task status from matching `state-v2` files. +- Forge panel remains available from the sidebar header even when no project is selected. +- No-project mode is explicit and blocking: Forge shows `Select a project first to use Forge.` and disables template, plan, execution, and clean-progress actions. +- During execution, Forge renders exactly one running task row from live `runningInfo`; non-active stale `in_progress` rows are demoted to `completed` (all phases complete) or `pending` (otherwise). +- Execution is plan-scoped: Forge validates each backend `forge_get_next_phase_prompt` task id against the selected plan's visible task ids before `startThread`; unexpected tasks stop with a visible error and no extra session/thread is created. +- With a selected project, Forge installs/uninstalls bundled templates into workspace-scoped `.agent` artifacts, returns normalized plan prompts, and lists workspace plans with `state-v2` task status joins. ## Requirements diff --git a/coverage/base.css b/coverage/base.css new file mode 100644 index 0000000000..f418035b46 --- /dev/null +++ b/coverage/base.css @@ -0,0 +1,224 @@ +body, html { + margin:0; padding: 0; + height: 100%; +} +body { + font-family: Helvetica Neue, Helvetica, Arial; + font-size: 14px; + color:#333; +} +.small { font-size: 12px; } +*, *:after, *:before { + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + } +h1 { font-size: 20px; margin: 0;} +h2 { font-size: 14px; } +pre { + font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; + margin: 0; + padding: 0; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; +} +a { color:#0074D9; text-decoration:none; } +a:hover { text-decoration:underline; } +.strong { font-weight: bold; } +.space-top1 { padding: 10px 0 0 0; } +.pad2y { padding: 20px 0; } +.pad1y { padding: 10px 0; } +.pad2x { padding: 0 20px; } +.pad2 { padding: 20px; } +.pad1 { padding: 10px; } +.space-left2 { padding-left:55px; } +.space-right2 { padding-right:20px; } +.center { text-align:center; } +.clearfix { display:block; } +.clearfix:after { + content:''; + display:block; + height:0; + clear:both; + visibility:hidden; + } +.fl { float: left; } +@media only screen and (max-width:640px) { + .col3 { width:100%; max-width:100%; } + .hide-mobile { display:none!important; } +} + +.quiet { + color: #7f7f7f; + color: rgba(0,0,0,0.5); +} +.quiet a { opacity: 0.7; } + +.fraction { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font-size: 10px; + color: #555; + background: #E8E8E8; + padding: 4px 5px; + border-radius: 3px; + vertical-align: middle; +} + +div.path a:link, div.path a:visited { color: #333; } +table.coverage { + border-collapse: collapse; + margin: 10px 0 0 0; + padding: 0; +} + +table.coverage td { + margin: 0; + padding: 0; + vertical-align: top; +} +table.coverage td.line-count { + text-align: right; + padding: 0 5px 0 20px; +} +table.coverage td.line-coverage { + text-align: right; + padding-right: 10px; + min-width:20px; +} + +table.coverage td span.cline-any { + display: inline-block; + padding: 0 5px; + width: 100%; +} +.missing-if-branch { + display: inline-block; + margin-right: 5px; + border-radius: 3px; + position: relative; + padding: 0 4px; + background: #333; + color: yellow; +} + +.skip-if-branch { + display: none; + margin-right: 10px; + position: relative; + padding: 0 4px; + background: #ccc; + color: white; +} +.missing-if-branch .typ, .skip-if-branch .typ { + color: inherit !important; +} +.coverage-summary { + border-collapse: collapse; + width: 100%; +} +.coverage-summary tr { border-bottom: 1px solid #bbb; } +.keyline-all { border: 1px solid #ddd; } +.coverage-summary td, .coverage-summary th { padding: 10px; } +.coverage-summary tbody { border: 1px solid #bbb; } +.coverage-summary td { border-right: 1px solid #bbb; } +.coverage-summary td:last-child { border-right: none; } +.coverage-summary th { + text-align: left; + font-weight: normal; + white-space: nowrap; +} +.coverage-summary th.file { border-right: none !important; } +.coverage-summary th.pct { } +.coverage-summary th.pic, +.coverage-summary th.abs, +.coverage-summary td.pct, +.coverage-summary td.abs { text-align: right; } +.coverage-summary td.file { white-space: nowrap; } +.coverage-summary td.pic { min-width: 120px !important; } +.coverage-summary tfoot td { } + +.coverage-summary .sorter { + height: 10px; + width: 7px; + display: inline-block; + margin-left: 0.5em; + background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; +} +.coverage-summary .sorted .sorter { + background-position: 0 -20px; +} +.coverage-summary .sorted-desc .sorter { + background-position: 0 -10px; +} +.status-line { height: 10px; } +/* yellow */ +.cbranch-no { background: yellow !important; color: #111; } +/* dark red */ +.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } +.low .chart { border:1px solid #C21F39 } +.highlighted, +.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ + background: #C21F39 !important; +} +/* medium red */ +.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } +/* light red */ +.low, .cline-no { background:#FCE1E5 } +/* light green */ +.high, .cline-yes { background:rgb(230,245,208) } +/* medium green */ +.cstat-yes { background:rgb(161,215,106) } +/* dark green */ +.status-line.high, .high .cover-fill { background:rgb(77,146,33) } +.high .chart { border:1px solid rgb(77,146,33) } +/* dark yellow (gold) */ +.status-line.medium, .medium .cover-fill { background: #f9cd0b; } +.medium .chart { border:1px solid #f9cd0b; } +/* light yellow */ +.medium { background: #fff4c2; } + +.cstat-skip { background: #ddd; color: #111; } +.fstat-skip { background: #ddd; color: #111 !important; } +.cbranch-skip { background: #ddd !important; color: #111; } + +span.cline-neutral { background: #eaeaea; } + +.coverage-summary td.empty { + opacity: .5; + padding-top: 4px; + padding-bottom: 4px; + line-height: 1; + color: #888; +} + +.cover-fill, .cover-empty { + display:inline-block; + height: 12px; +} +.chart { + line-height: 0; +} +.cover-empty { + background: white; +} +.cover-full { + border-right: none !important; +} +pre.prettyprint { + border: none !important; + padding: 0 !important; + margin: 0 !important; +} +.com { color: #999 !important; } +.ignore-none { color: #999; font-weight: normal; } + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -48px; +} +.footer, .push { + height: 48px; +} diff --git a/coverage/block-navigation.js b/coverage/block-navigation.js new file mode 100644 index 0000000000..530d1ed2ba --- /dev/null +++ b/coverage/block-navigation.js @@ -0,0 +1,87 @@ +/* eslint-disable */ +var jumpToCode = (function init() { + // Classes of code we would like to highlight in the file view + var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; + + // Elements to highlight in the file listing view + var fileListingElements = ['td.pct.low']; + + // We don't want to select elements that are direct descendants of another match + var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` + + // Selector that finds elements on the page to which we can jump + var selector = + fileListingElements.join(', ') + + ', ' + + notSelector + + missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` + + // The NodeList of matching elements + var missingCoverageElements = document.querySelectorAll(selector); + + var currentIndex; + + function toggleClass(index) { + missingCoverageElements + .item(currentIndex) + .classList.remove('highlighted'); + missingCoverageElements.item(index).classList.add('highlighted'); + } + + function makeCurrent(index) { + toggleClass(index); + currentIndex = index; + missingCoverageElements.item(index).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); + } + + function goToPrevious() { + var nextIndex = 0; + if (typeof currentIndex !== 'number' || currentIndex === 0) { + nextIndex = missingCoverageElements.length - 1; + } else if (missingCoverageElements.length > 1) { + nextIndex = currentIndex - 1; + } + + makeCurrent(nextIndex); + } + + function goToNext() { + var nextIndex = 0; + + if ( + typeof currentIndex === 'number' && + currentIndex < missingCoverageElements.length - 1 + ) { + nextIndex = currentIndex + 1; + } + + makeCurrent(nextIndex); + } + + return function jump(event) { + if ( + document.getElementById('fileSearch') === document.activeElement && + document.activeElement != null + ) { + // if we're currently focused on the search input, we don't want to navigate + return; + } + + switch (event.which) { + case 78: // n + case 74: // j + goToNext(); + break; + case 66: // b + case 75: // k + case 80: // p + goToPrevious(); + break; + } + }; +})(); +window.addEventListener('keydown', jumpToCode); diff --git a/coverage/clover.xml b/coverage/clover.xml new file mode 100644 index 0000000000..fb5f41b6c9 --- /dev/null +++ b/coverage/clover.xml @@ -0,0 +1,1198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/coverage/components/Forge.tsx.html b/coverage/components/Forge.tsx.html new file mode 100644 index 0000000000..343717ab55 --- /dev/null +++ b/coverage/components/Forge.tsx.html @@ -0,0 +1,3250 @@ + + + + + + Code coverage report for components/Forge.tsx + + + + + + + + + +
+
+

All files / components Forge.tsx

+
+ +
+ 91.08% + Statements + 756/830 +
+ + +
+ 78.57% + Branches + 176/224 +
+ + +
+ 86.95% + Functions + 20/23 +
+ + +
+ 91.08% + Lines + 756/830 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +628 +629 +630 +631 +632 +633 +634 +635 +636 +637 +638 +639 +640 +641 +642 +643 +644 +645 +646 +647 +648 +649 +650 +651 +652 +653 +654 +655 +656 +657 +658 +659 +660 +661 +662 +663 +664 +665 +666 +667 +668 +669 +670 +671 +672 +673 +674 +675 +676 +677 +678 +679 +680 +681 +682 +683 +684 +685 +686 +687 +688 +689 +690 +691 +692 +693 +694 +695 +696 +697 +698 +699 +700 +701 +702 +703 +704 +705 +706 +707 +708 +709 +710 +711 +712 +713 +714 +715 +716 +717 +718 +719 +720 +721 +722 +723 +724 +725 +726 +727 +728 +729 +730 +731 +732 +733 +734 +735 +736 +737 +738 +739 +740 +741 +742 +743 +744 +745 +746 +747 +748 +749 +750 +751 +752 +753 +754 +755 +756 +757 +758 +759 +760 +761 +762 +763 +764 +765 +766 +767 +768 +769 +770 +771 +772 +773 +774 +775 +776 +777 +778 +779 +780 +781 +782 +783 +784 +785 +786 +787 +788 +789 +790 +791 +792 +793 +794 +795 +796 +797 +798 +799 +800 +801 +802 +803 +804 +805 +806 +807 +808 +809 +810 +811 +812 +813 +814 +815 +816 +817 +818 +819 +820 +821 +822 +823 +824 +825 +826 +827 +828 +829 +830 +831 +832 +833 +834 +835 +836 +837 +838 +839 +840 +841 +842 +843 +844 +845 +846 +847 +848 +849 +850 +851 +852 +853 +854 +855 +856 +857 +858 +859 +860 +861 +862 +863 +864 +865 +866 +867 +868 +869 +870 +871 +872 +873 +874 +875 +876 +877 +878 +879 +880 +881 +882 +883 +884 +885 +886 +887 +888 +889 +890 +891 +892 +893 +894 +895 +896 +897 +898 +899 +900 +901 +902 +903 +904 +905 +906 +907 +908 +909 +910 +911 +912 +913 +914 +915 +916 +917 +918 +919 +920 +921 +922 +923 +924 +925 +926 +927 +928 +929 +930 +931 +932 +933 +934 +935 +936 +937 +938 +939 +940 +941 +942 +943 +944 +945 +946 +947 +948 +949 +950 +951 +952 +953 +954 +955 +956 +957 +958 +959 +960 +961 +962 +963 +964 +965 +966 +967 +968 +969 +970 +971 +972 +973 +974 +975 +976 +977 +978 +979 +980 +981 +982 +983 +984 +985 +986 +987 +988 +989 +990 +991 +992 +993 +994 +995 +996 +997 +998 +999 +1000 +1001 +1002 +1003 +1004 +1005 +1006 +1007 +1008 +1009 +1010 +1011 +1012 +1013 +1014 +1015 +1016 +1017 +1018 +1019 +1020 +1021 +1022 +1023 +1024 +1025 +1026 +1027 +1028 +1029 +1030 +1031 +1032 +1033 +1034 +1035 +1036 +1037 +1038 +1039 +1040 +1041 +1042 +1043 +1044 +1045 +1046 +1047 +1048 +1049 +1050 +1051 +1052 +1053 +1054 +1055 +10561x +1x +1x +1x +1x +1x +1x +1x +1x +  +1x +1x +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +1x +1x +1x +  +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +1x +  +  +  +  +40x +40x +40x +40x +40x +40x +40x +  +  +  +  +40x +40x +  +  +40x +40x +  +75x +75x +75x +1x +1x +75x +15x +15x +59x +59x +  +344x +344x +344x +344x +344x +344x +344x +344x +14x +14x +344x +59x +59x +344x +105x +105x +166x +166x +  +110x +110x +110x +110x +110x +110x +110x +170x +170x +110x +110x +  +  +  +  +  +110x +  +330x +330x +330x +330x +330x +330x +224x +224x +330x +4x +4x +330x +59x +59x +43x +43x +  +349x +349x +349x +349x +349x +349x +349x +  +  +  +  +  +349x +349x +19x +19x +  +330x +349x +  +349x +4x +  +4x +4x +  +326x +326x +  +4x +4x +4x +4x +4x +4x +4x +4x +  +  +  +  +  +  +4x +4x +4x +1x +  +  +1x +1x +1x +4x +4x +4x +  +4x +3x +1x +1x +1x +1x +1x +4x +3x +1x +  +4x +4x +4x +4x +4x +  +4x +4x +  +4x +4x +4x +4x +3x +3x +3x +3x +3x +3x +3x +3x +3x +3x +3x +3x +3x +3x +  +  +  +3x +3x +3x +3x +3x +3x +1x +1x +1x +3x +3x +  +3x +3x +3x +  +1x +  +  +4x +5x +5x +5x +5x +5x +5x +5x +5x +5x +5x +5x +1x +1x +1x +5x +  +5x +5x +5x +5x +  +4x +4x +4x +4x +  +4x +4x +4x +  +4x +  +1x +323x +323x +323x +323x +323x +323x +323x +323x +323x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +  +323x +323x +89x +89x +  +  +89x +89x +89x +89x +88x +88x +88x +88x +  +89x +323x +323x +  +323x +323x +45x +43x +43x +45x +45x +  +  +45x +45x +  +  +  +  +2x +2x +45x +45x +45x +2x +2x +2x +45x +323x +323x +  +323x +323x +323x +323x +323x +  +323x +44x +44x +44x +44x +  +  +44x +44x +44x +  +44x +44x +44x +44x +323x +  +323x +44x +44x +3x +3x +3x +3x +3x +3x +41x +41x +41x +  +  +41x +41x +41x +  +41x +41x +41x +41x +323x +  +323x +323x +194x +  +  +194x +194x +194x +194x +194x +  +194x +194x +194x +194x +323x +323x +  +323x +44x +44x +3x +3x +3x +3x +3x +  +41x +193x +  +  +193x +193x +  +41x +41x +41x +41x +41x +41x +323x +  +323x +90x +40x +75x +75x +75x +75x +75x +75x +40x +  +40x +40x +40x +40x +40x +90x +323x +  +323x +323x +1x +  +  +1x +1x +1x +1x +1x +  +1x +1x +323x +323x +  +323x +1x +  +  +1x +1x +1x +1x +1x +  +1x +323x +  +323x +323x +180x +143x +323x +  +323x +323x +  +323x +  +323x +323x +323x +323x +323x +  +323x +44x +41x +41x +3x +323x +  +323x +127x +88x +88x +127x +  +  +323x +  +323x +85x +85x +48x +48x +48x +48x +48x +  +85x +85x +188x +188x +188x +188x +188x +188x +188x +  +  +188x +188x +  +  +  +  +  +  +188x +  +85x +85x +85x +37x +37x +37x +323x +323x +323x +323x +323x +323x +  +323x +44x +44x +44x +44x +43x +44x +323x +323x +323x +323x +323x +  +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +  +323x +  +81x +323x +  +323x +1x +  +  +  +1x +  +  +  +1x +1x +1x +1x +1x +  +1x +1x +  +1x +1x +1x +  +  +1x +1x +  +  +  +  +1x +  +1x +1x +  +  +  +  +1x +1x +1x +1x +1x +  +  +  +  +1x +  +  +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +  +323x +1x +1x +1x +  +  +  +1x +1x +1x +1x +  +  +  +1x +1x +1x +  +  +1x +1x +1x +  +1x +1x +1x +323x +323x +323x +323x +323x +323x +323x +323x +323x +  +323x +323x +323x +323x +323x +  +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +  +323x +323x +323x +323x +323x +323x +323x +39x +  +  +39x +39x +323x +323x +323x +  +323x +323x +323x +323x +323x +  +323x +42x +42x +38x +38x +38x +38x +38x +38x +37x +37x +37x +38x +  +38x +38x +42x +42x +42x +42x +  +  +  +42x +42x +  +42x +42x +42x +42x +42x +  +42x +42x +  +323x +323x +  +323x +323x +323x +323x +323x +323x +180x +19x +161x +143x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +9x +314x +134x +180x +53x +127x +  +323x +51x +  +  +51x +17x +17x +17x +34x +51x +  +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +9x +314x +134x +180x +  +323x +1x +1x +  +323x +323x +323x +323x +323x +323x +12x +12x +12x +311x +323x +9x +  +9x +314x +180x +180x +349x +349x +349x +349x +349x +349x +349x +349x +349x +349x +349x +349x +  +349x +349x +349x +349x +349x +6x +343x +104x +  +239x +  +349x +349x +349x +19x +19x +19x +330x +349x +349x +120x +120x +344x +344x +344x +344x +344x +344x +344x +344x +344x +344x +344x +344x +  +344x +344x +344x +344x +344x +  +120x +120x +229x +349x +  +180x +180x +  +134x +  +134x +  +323x +323x +  +323x +4x +4x +4x +4x +4x +4x +4x +4x +  +323x +  +323x + 
import CheckSquare2 from "lucide-react/dist/esm/icons/check-square-2";
+import ChevronDown from "lucide-react/dist/esm/icons/chevron-down";
+import Circle from "lucide-react/dist/esm/icons/circle";
+import Pencil from "lucide-react/dist/esm/icons/pencil";
+import RefreshCw from "lucide-react/dist/esm/icons/refresh-cw";
+import LayoutTemplate from "lucide-react/dist/esm/icons/layout-template";
+import Pause from "lucide-react/dist/esm/icons/pause";
+import Play from "lucide-react/dist/esm/icons/play";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import type { CollaborationModeOption, WorkspaceInfo } from "../../../types";
+import { ModalShell } from "../../design-system/components/modal/ModalShell";
+import {
+  PopoverMenuItem,
+  PopoverSurface,
+} from "../../design-system/components/popover/PopoverPrimitives";
+import { useDismissibleMenu } from "../../app/hooks/useDismissibleMenu";
+import {
+  forgeGetInstalledTemplate,
+  forgeInstallTemplate,
+  forgeLoadPhaseView,
+  forgeListBundledTemplates,
+  forgeUninstallTemplate,
+  type ForgeBundledTemplateInfo,
+  type ForgeNextPhasePrompt,
+  type ForgePhaseViewStatus,
+  type ForgePhaseView,
+  type ForgePhaseStatus,
+  type ForgeRunPhaseChecksResponse,
+  type ForgeTemplateLock,
+  type ForgeWorkspacePlan,
+} from "../../../services/tauri";
+import {
+  connectWorkspace,
+  forgeResetExecutionProgress,
+  forgeGetNextPhasePrompt,
+  forgeGetPlanPrompt,
+  forgeGetPhaseStatus,
+  forgeListPlans,
+  forgePrepareExecution,
+  forgeRunPhaseChecks,
+  interruptTurn,
+  sendUserMessage,
+  startThread,
+} from "../../../services/tauri";
+import { useForgeExecution } from "../hooks/useForgeExecution";
+import { getForgePhaseIconUrl } from "../../../utils/forgePhaseIcons";
+ 
+export type ForgeTemplatesClient = {
+  listBundledTemplates: () => Promise<ForgeBundledTemplateInfo[]>;
+  getInstalledTemplate: (workspaceId: string) => Promise<ForgeTemplateLock | null>;
+  installTemplate: (workspaceId: string, templateId: string) => Promise<ForgeTemplateLock>;
+  uninstallTemplate: (workspaceId: string) => Promise<void>;
+};
+ 
+export type ForgePlansClient = {
+  listPlans: (workspaceId: string) => Promise<ForgeWorkspacePlan[]>;
+  loadPhaseView?: (
+    workspaceId: string,
+    planId: string,
+    installedTemplateId: string | null,
+  ) => Promise<ForgePhaseView>;
+  getPlanPrompt: (workspaceId: string) => Promise<string>;
+  prepareExecution: (workspaceId: string, planId: string) => Promise<void>;
+  resetExecutionProgress: (workspaceId: string, planId: string) => Promise<void>;
+  getNextPhasePrompt: (
+    workspaceId: string,
+    planId: string,
+  ) => Promise<ForgeNextPhasePrompt | null>;
+  getPhaseStatus: (
+    workspaceId: string,
+    planId: string,
+    taskId: string,
+    phaseId: string,
+  ) => Promise<ForgePhaseStatus>;
+  runPhaseChecks: (
+    workspaceId: string,
+    planId: string,
+    taskId: string,
+    phaseId: string,
+  ) => Promise<ForgeRunPhaseChecksResponse>;
+  interruptTurn: (
+    workspaceId: string,
+    threadId: string,
+    turnId: string,
+  ) => Promise<unknown>;
+  connectWorkspace: (workspaceId: string) => Promise<void>;
+  startThread: (workspaceId: string) => Promise<any>;
+  sendUserMessage: (
+    workspaceId: string,
+    threadId: string,
+    text: string,
+    options?: {
+      collaborationMode?: Record<string, unknown> | null;
+    },
+  ) => Promise<any>;
+};
+ 
+const defaultForgeTemplatesClient: ForgeTemplatesClient = {
+  listBundledTemplates: forgeListBundledTemplates,
+  getInstalledTemplate: forgeGetInstalledTemplate,
+  installTemplate: forgeInstallTemplate,
+  uninstallTemplate: forgeUninstallTemplate,
+};
+ 
+const defaultForgePlansClient: ForgePlansClient = {
+  listPlans: forgeListPlans,
+  loadPhaseView: forgeLoadPhaseView,
+  getPlanPrompt: forgeGetPlanPrompt,
+  prepareExecution: forgePrepareExecution,
+  resetExecutionProgress: forgeResetExecutionProgress,
+  getNextPhasePrompt: forgeGetNextPhasePrompt,
+  getPhaseStatus: forgeGetPhaseStatus,
+  runPhaseChecks: forgeRunPhaseChecks,
+  interruptTurn,
+  connectWorkspace,
+  startThread,
+  sendUserMessage,
+};
+ 
+type ForgeItemStatus = "pending" | "inProgress" | "completed";
+ 
+type ForgePlanItem = {
+  id: string;
+  title: string;
+  status: ForgeItemStatus;
+};
+ 
+type ForgePlan = {
+  id: string;
+  name: string;
+  items: ForgePlanItem[];
+};
+ 
+const EMPTY_PHASE_VIEW: ForgePhaseView = {
+  phases: [],
+  taskPhaseStatusByTaskId: {},
+};
+ 
+type ForgePhaseChipState = "is-complete" | "is-current" | "is-pending";
+type ForgeRunningInfo = { taskId: string; phaseId: string } | null;
+ 
+function formatPlanLabel(plan: ForgeWorkspacePlan): string {
+  const title = plan.title?.trim() ?? "";
+  const goal = plan.goal?.trim() ?? "";
+  const id = plan.id?.trim() ?? "";
+  if (title && id) {
+    return `${title} (${id})`;
+  }
+  if (title) {
+    return title;
+  }
+  const shortGoal =
+    goal.length > 60 ? `${goal.slice(0, 57).trimEnd()}...` : goal;
+  if (shortGoal && id) {
+    return `${shortGoal} (${id})`;
+  }
+  return shortGoal || id || "Untitled plan";
+}
+ 
+function mapForgeItemStatus(status: string): ForgeItemStatus {
+  const normalized = status.trim().toLowerCase();
+  if (normalized === "completed") {
+    return "completed";
+  }
+  if (normalized === "in_progress") {
+    return "inProgress";
+  }
+  return "pending";
+}
+ 
+function mapForgePhaseChipState(
+  taskId: string,
+  phaseId: string,
+  phaseStatus: ForgePhaseViewStatus,
+  runningInfo: ForgeRunningInfo,
+): ForgePhaseChipState {
+  const normalizedStatus = phaseStatus.trim().toLowerCase();
+  if (runningInfo?.taskId === taskId && runningInfo.phaseId === phaseId) {
+    return "is-current";
+  }
+  if (normalizedStatus === "in_progress") {
+    return "is-current";
+  }
+  if (normalizedStatus === "completed") {
+    return "is-complete";
+  }
+  return "is-pending";
+}
+ 
+function allTaskPhasesCompleted(
+  taskPhaseStatuses: Record<string, ForgePhaseViewStatus>,
+  phases: ForgePhaseView["phases"],
+): boolean {
+  if (phases.length > 0) {
+    return phases.every(
+      (phase) =>
+        (taskPhaseStatuses[phase.id] ?? "pending").trim().toLowerCase() ===
+        "completed",
+    );
+  }
+  const phaseStatuses = Object.values(taskPhaseStatuses);
+  return (
+    phaseStatuses.length > 0 &&
+    phaseStatuses.every((status) => status.trim().toLowerCase() === "completed")
+  );
+}
+ 
+function deriveStatusFromTaskPhases(
+  taskPhaseStatuses: Record<string, ForgePhaseViewStatus>,
+  phases: ForgePhaseView["phases"],
+): ForgeItemStatus | null {
+  const phaseStatuses = Object.values(taskPhaseStatuses);
+  if (phaseStatuses.length === 0) {
+    return null;
+  }
+  if (allTaskPhasesCompleted(taskPhaseStatuses, phases)) {
+    return "completed";
+  }
+  if (phaseStatuses.some((status) => status.trim().toLowerCase() === "in_progress")) {
+    return "inProgress";
+  }
+  return "pending";
+}
+ 
+function resolveForgeTaskRowStatus({
+  taskId,
+  planTaskStatus,
+  taskPhaseStatuses,
+  phases,
+  runningInfo,
+}: {
+  taskId: string;
+  planTaskStatus: ForgeItemStatus;
+  taskPhaseStatuses: Record<string, ForgePhaseViewStatus>;
+  phases: ForgePhaseView["phases"];
+  runningInfo: ForgeRunningInfo;
+}): ForgeItemStatus {
+  if (runningInfo?.taskId === taskId) {
+    return "inProgress";
+  }
+ 
+  const phaseDerivedStatus = deriveStatusFromTaskPhases(taskPhaseStatuses, phases);
+  const resolvedStatus = phaseDerivedStatus ?? planTaskStatus;
+ 
+  if (runningInfo && resolvedStatus === "inProgress") {
+    return allTaskPhasesCompleted(taskPhaseStatuses, phases)
+      ? "completed"
+      : "pending";
+  }
+ 
+  return resolvedStatus;
+}
+ 
+function ForgeTemplatesModal({
+  templates,
+  installedTemplate,
+  canManage,
+  onInstallTemplate,
+  onUninstallTemplate,
+  onClose,
+}: {
+  templates: ForgeBundledTemplateInfo[];
+  installedTemplate: ForgeTemplateLock | null;
+  canManage: boolean;
+  onInstallTemplate: (templateId: string) => void;
+  onUninstallTemplate: () => void;
+  onClose: () => void;
+}) {
+  useEffect(() => {
+    const handleKeyDown = (event: KeyboardEvent) => {
+      if (event.key !== "Escape") {
+        return;
+      }
+      event.preventDefault();
+      onClose();
+    };
+    window.addEventListener("keydown", handleKeyDown);
+    return () => window.removeEventListener("keydown", handleKeyDown);
+  }, [onClose]);
+ 
+  const installed = installedTemplate
+    ? templates.find((t) => t.id === installedTemplate.installedTemplateId) ?? {
+        id: installedTemplate.installedTemplateId,
+        title: installedTemplate.installedTemplateId,
+        version: installedTemplate.installedTemplateVersion,
+      }
+    : null;
+  const others = installed
+    ? templates.filter((t) => t.id !== installed.id)
+    : templates;
+ 
+  return (
+    <ModalShell
+      className="forge-templates-modal"
+      onBackdropClick={onClose}
+      ariaLabel="Forge templates"
+    >
+      <div className="ds-modal-title forge-templates-title">Templates</div>
+      <div className="ds-modal-subtitle forge-templates-subtitle">
+        Choose a template to install or swap. Behaviors are stubbed for now.
+      </div>
+      <div className="ds-modal-divider" />
+      <div className="forge-templates-list" role="list">
+        {installed ? (
+          <div className="forge-template-row" role="listitem">
+            <div className="forge-template-main">
+              <div className="forge-template-name">{installed.title}</div>
+              <div className="forge-template-meta">
+                Installed{installed.version ? ` (${installed.version})` : ""}
+              </div>
+            </div>
+            <div className="forge-template-actions">
+              <button
+                type="button"
+                className="ghost forge-template-icon"
+                aria-label="Edit template"
+                title="Edit template"
+                onClick={() => {
+                  // TODO(Forge): wire template edit
+                }}
+              >
+                <Pencil aria-hidden />
+              </button>
+              <button
+                type="button"
+                className="ghost forge-template-action"
+                onClick={() => {
+                  onUninstallTemplate();
+                  onClose();
+                }}
+                disabled={!canManage}
+              >
+                Remove
+              </button>
+            </div>
+          </div>
+        ) : (
+          <div className="forge-template-empty">No template installed.</div>
+        )}
+ 
+        {others.map((template) => {
+          const actionLabel = installed ? "Swap" : "Install";
+          return (
+            <div key={template.id} className="forge-template-row" role="listitem">
+              <div className="forge-template-main">
+                <div className="forge-template-name">{template.title}</div>
+              </div>
+              <div className="forge-template-actions">
+                <button
+                  type="button"
+                  className="ghost forge-template-action"
+                  onClick={() => {
+                    onInstallTemplate(template.id);
+                    onClose();
+                  }}
+                  disabled={!canManage}
+                >
+                  {actionLabel}
+                </button>
+              </div>
+            </div>
+          );
+        })}
+      </div>
+      <div className="ds-modal-actions forge-templates-actions">
+        <button type="button" className="ghost ds-modal-button" onClick={onClose}>
+          Close
+        </button>
+      </div>
+    </ModalShell>
+  );
+}
+ 
+export function Forge({
+  activeWorkspaceId,
+  activeWorkspace = null,
+  sendUserMessageToThread,
+  templatesClient,
+  plansClient,
+  onSelectThread,
+  collaborationModes = [],
+  onSelectCollaborationMode,
+}: {
+  activeWorkspaceId: string | null;
+  activeWorkspace?: WorkspaceInfo | null;
+  sendUserMessageToThread?: (
+    workspace: WorkspaceInfo,
+    threadId: string,
+    message: string,
+    imageIds: string[],
+    options?: {
+      collaborationMode?: Record<string, unknown> | null;
+    },
+  ) => Promise<void>;
+  templatesClient?: ForgeTemplatesClient;
+  plansClient?: ForgePlansClient;
+  onSelectThread?: (workspaceId: string, threadId: string) => void;
+  collaborationModes?: CollaborationModeOption[];
+  onSelectCollaborationMode?: (id: string | null) => void;
+}) {
+  const client = templatesClient ?? defaultForgeTemplatesClient;
+  const plansApi = plansClient ?? defaultForgePlansClient;
+  const listBundledTemplates = client.listBundledTemplates;
+  const getInstalledTemplate = client.getInstalledTemplate;
+  const installTemplate = client.installTemplate;
+  const uninstallTemplate = client.uninstallTemplate;
+  const listPlans = plansApi.listPlans;
+  const loadPhaseView = plansApi.loadPhaseView ?? forgeLoadPhaseView;
+  const getPlanPrompt = plansApi.getPlanPrompt;
+  const prepareExecution = plansApi.prepareExecution;
+  const resetExecutionProgress = plansApi.resetExecutionProgress;
+  const getNextPhasePrompt = plansApi.getNextPhasePrompt;
+  const getPhaseStatus = plansApi.getPhaseStatus;
+  const runPhaseChecks = plansApi.runPhaseChecks;
+  const interruptPlanTurn = plansApi.interruptTurn;
+  const connectWorkspaceToRun = plansApi.connectWorkspace;
+  const startThreadForPlan = plansApi.startThread;
+  const sendUserMessageToPlanThread = plansApi.sendUserMessage;
+  const hasActiveWorkspace = Boolean(activeWorkspaceId?.trim());
+ 
+  const findCollaborationMode = useCallback(
+    (wanted: string) => {
+      const normalized = wanted.trim().toLowerCase();
+      if (!normalized) {
+        return null;
+      }
+      return (
+        collaborationModes.find(
+          (mode) => mode.id.trim().toLowerCase() === normalized,
+        ) ??
+        collaborationModes.find(
+          (mode) => (mode.mode || mode.id).trim().toLowerCase() === normalized,
+        ) ??
+        null
+      );
+    },
+    [collaborationModes],
+  );
+ 
+  const buildCollaborationModePayloadFor = useCallback(
+    (mode: CollaborationModeOption | null) => {
+      if (!mode) {
+        return null;
+      }
+      const modeValue = mode.mode || mode.id;
+      if (!modeValue) {
+        return null;
+      }
+      const modelValue = (mode.model ?? "").trim();
+      if (!modelValue) {
+        // CollaborationMode.settings.model is required by the app-server protocol.
+        // If we can't supply a model, don't send an override payload.
+        return null;
+      }
+      const settings: Record<string, unknown> = {
+        developer_instructions: mode.developerInstructions ?? null,
+        model: modelValue,
+      };
+      if (mode.reasoningEffort !== null) {
+        settings.reasoning_effort = mode.reasoningEffort;
+      }
+      return { mode: modeValue, settings };
+    },
+    [],
+  );
+ 
+  const [templates, setTemplates] = useState<ForgeBundledTemplateInfo[]>([]);
+  const [installedTemplate, setInstalledTemplate] = useState<ForgeTemplateLock | null>(null);
+  const [workspacePlans, setWorkspacePlans] = useState<ForgeWorkspacePlan[]>([]);
+  const [isResettingProgress, setIsResettingProgress] = useState(false);
+  const plansInFlightRef = useRef(false);
+ 
+  useEffect(() => {
+    let cancelled = false;
+    void listBundledTemplates()
+      .then((items) => {
+        if (cancelled) {
+          return;
+        }
+        setTemplates(items);
+      })
+      .catch((error) => {
+        console.warn("Failed to load Forge templates.", { error });
+      });
+    return () => {
+      cancelled = true;
+    };
+  }, [listBundledTemplates]);
+ 
+  useEffect(() => {
+    let cancelled = false;
+    if (!activeWorkspaceId) {
+      setInstalledTemplate(null);
+      setWorkspacePlans([]);
+      return () => {
+        cancelled = true;
+      };
+    }
+    void getInstalledTemplate(activeWorkspaceId)
+      .then((installed) => {
+        if (cancelled) {
+          return;
+        }
+        setInstalledTemplate(installed);
+      })
+      .catch((error) => {
+        console.warn("Failed to load installed Forge template.", { error });
+      });
+    return () => {
+      cancelled = true;
+    };
+  }, [activeWorkspaceId, getInstalledTemplate]);
+ 
+  const refreshPlans = useCallback(
+    async (workspaceId: string) => {
+      if (plansInFlightRef.current) {
+        return;
+      }
+      plansInFlightRef.current = true;
+      try {
+        const next = await listPlans(workspaceId);
+        setWorkspacePlans(next);
+      } catch (error) {
+        console.warn("Failed to load Forge plans.", { error });
+      } finally {
+        plansInFlightRef.current = false;
+      }
+    },
+    [listPlans],
+  );
+ 
+  useEffect(() => {
+    let cancelled = false;
+    if (!activeWorkspaceId) {
+      setWorkspacePlans([]);
+      return () => {
+        cancelled = true;
+      };
+    }
+ 
+    const loadPlans = async () => {
+      if (cancelled) {
+        return;
+      }
+      await refreshPlans(activeWorkspaceId);
+    };
+ 
+    void loadPlans();
+    const interval = window.setInterval(loadPlans, 2000);
+    return () => {
+      cancelled = true;
+      window.clearInterval(interval);
+    };
+  }, [activeWorkspaceId, refreshPlans]);
+ 
+  const plans: ForgePlan[] = useMemo(() => {
+    return workspacePlans.map((plan) => {
+      const items: ForgePlanItem[] = plan.tasks.map((task) => {
+        const status = mapForgeItemStatus(task.status);
+        return {
+          id: task.id,
+          title: task.name,
+          status,
+        };
+      });
+ 
+      return {
+        id: plan.id,
+        name: formatPlanLabel(plan),
+        items,
+      };
+    });
+  }, [workspacePlans]);
+ 
+  const handleInstallTemplate = useCallback(
+    (templateId: string) => {
+      if (!activeWorkspaceId) {
+        return;
+      }
+      void installTemplate(activeWorkspaceId, templateId)
+        .then((lock) => {
+          setInstalledTemplate(lock);
+        })
+        .catch((error) => {
+          console.warn("Failed to install Forge template.", { error });
+        });
+    },
+    [activeWorkspaceId, installTemplate],
+  );
+ 
+  const handleUninstallTemplate = useCallback(() => {
+    if (!activeWorkspaceId) {
+      return;
+    }
+    void uninstallTemplate(activeWorkspaceId)
+      .then(() => {
+        setInstalledTemplate(null);
+      })
+      .catch((error) => {
+        console.warn("Failed to uninstall Forge template.", { error });
+      });
+  }, [activeWorkspaceId, uninstallTemplate]);
+ 
+  const [selectedPlanId, setSelectedPlanId] = useState<string | null>(null);
+  const selectedPlan = selectedPlanId
+    ? plans.find((p) => p.id === selectedPlanId) ?? null
+    : null;
+  const [phaseView, setPhaseView] = useState<ForgePhaseView>(EMPTY_PHASE_VIEW);
+ 
+  const [planMenuOpen, setPlanMenuOpen] = useState(false);
+  const planMenuRef = useRef<HTMLDivElement | null>(null);
+ 
+  const [templatesOpen, setTemplatesOpen] = useState(false);
+ 
+  useDismissibleMenu({
+    isOpen: planMenuOpen,
+    containerRef: planMenuRef,
+    onClose: () => setPlanMenuOpen(false),
+  });
+ 
+  useEffect(() => {
+    if (hasActiveWorkspace) {
+      return;
+    }
+    setPlanMenuOpen(false);
+  }, [hasActiveWorkspace]);
+ 
+  useEffect(() => {
+    if (!selectedPlanId) {
+      return;
+    }
+    if (!plans.some((plan) => plan.id === selectedPlanId)) {
+      setSelectedPlanId(null);
+    }
+  }, [plans, selectedPlanId]);
+ 
+  useEffect(() => {
+    let cancelled = false;
+    if (!activeWorkspaceId || !selectedPlanId) {
+      setPhaseView(EMPTY_PHASE_VIEW);
+      return () => {
+        cancelled = true;
+      };
+    }
+ 
+    const installedTemplateId = installedTemplate?.installedTemplateId ?? null;
+    const refreshPhaseView = async () => {
+      try {
+        const next = await loadPhaseView(
+          activeWorkspaceId,
+          selectedPlanId,
+          installedTemplateId,
+        );
+        if (cancelled) {
+          return;
+        }
+        setPhaseView(next);
+      } catch (error) {
+        if (cancelled) {
+          return;
+        }
+        console.warn("Failed to load Forge phase view.", { error });
+        setPhaseView(EMPTY_PHASE_VIEW);
+      }
+    };
+ 
+    void refreshPhaseView();
+    const interval = window.setInterval(refreshPhaseView, 2000);
+    return () => {
+      cancelled = true;
+      window.clearInterval(interval);
+    };
+  }, [
+    activeWorkspaceId,
+    installedTemplate?.installedTemplateId,
+    loadPhaseView,
+    selectedPlanId,
+  ]);
+ 
+  const executionMode = useMemo(() => {
+    const mode =
+      findCollaborationMode("default") ??
+      findCollaborationMode("code") ??
+      collaborationModes[0] ??
+      null;
+    return buildCollaborationModePayloadFor(mode);
+  }, [buildCollaborationModePayloadFor, collaborationModes, findCollaborationMode]);
+  const selectedPlanTaskIds = useMemo(
+    () => selectedPlan?.items.map((item) => item.id) ?? null,
+    [selectedPlan],
+  );
+ 
+  const {
+    isExecuting,
+    runningInfo,
+    lastError,
+    startExecution,
+    pauseExecution,
+  } = useForgeExecution({
+    workspaceId: activeWorkspaceId,
+    knownTaskIds: selectedPlanTaskIds,
+    connectWorkspace: connectWorkspaceToRun,
+    prepareExecution,
+    getNextPhasePrompt,
+    getPhaseStatus,
+    runPhaseChecks,
+    interruptTurn: interruptPlanTurn,
+    startThread: startThreadForPlan,
+    sendUserMessage: sendUserMessageToPlanThread,
+    onSelectThread,
+    collaborationMode: executionMode,
+  });
+ 
+  useEffect(() => {
+    // Avoid executing the wrong plan if the user switches selection mid-run.
+    void pauseExecution();
+  }, [pauseExecution, selectedPlanId]);
+ 
+  const handleNewPlan = useCallback(async () => {
+    if (!activeWorkspaceId) {
+      return;
+    }
+ 
+    setPlanMenuOpen(false);
+ 
+    // Start in plan collaboration mode so the UI shows the structured plan turn item.
+    // Writing plan files is handled after planning (outside plan mode).
+    const planMode = findCollaborationMode("plan");
+    if (planMode?.id) {
+      onSelectCollaborationMode?.(planMode.id);
+    }
+    const collaborationMode = buildCollaborationModePayloadFor(planMode);
+ 
+    try {
+      await connectWorkspaceToRun(activeWorkspaceId);
+ 
+      const response = await startThreadForPlan(activeWorkspaceId);
+      const thread =
+        (response?.result?.thread ?? response?.thread ?? null) as
+          | Record<string, unknown>
+          | null;
+      const threadId = String(thread?.id ?? "").trim();
+      if (!threadId) {
+        console.warn("Forge new plan: missing thread id.", { response });
+        return;
+      }
+ 
+      onSelectThread?.(activeWorkspaceId, threadId);
+ 
+      const prompt = await getPlanPrompt(activeWorkspaceId);
+      if (!prompt.trim()) {
+        console.warn("Forge new plan: plan prompt is empty.");
+        return;
+      }
+ 
+      if (sendUserMessageToThread && activeWorkspace?.id === activeWorkspaceId) {
+        await sendUserMessageToThread(activeWorkspace, threadId, prompt, [], {
+          collaborationMode,
+        });
+      } else {
+        await sendUserMessageToPlanThread(activeWorkspaceId, threadId, prompt, {
+          collaborationMode,
+        });
+      }
+    } catch (error) {
+      console.warn("Forge new plan: failed to start plan thread.", { error });
+    }
+  }, [
+    activeWorkspaceId,
+    activeWorkspace,
+    buildCollaborationModePayloadFor,
+    connectWorkspaceToRun,
+    findCollaborationMode,
+    getPlanPrompt,
+    onSelectCollaborationMode,
+    onSelectThread,
+    sendUserMessageToPlanThread,
+    sendUserMessageToThread,
+    startThreadForPlan,
+  ]);
+ 
+  const handleCleanProgress = useCallback(async () => {
+    const workspaceId = activeWorkspaceId?.trim();
+    const planId = selectedPlanId?.trim();
+    if (!workspaceId || !planId || isResettingProgress) {
+      return;
+    }
+ 
+    const confirmed = window.confirm(
+      "Clean progress will reset this plan's state and derived execution files. Continue?",
+    );
+    if (!confirmed) {
+      return;
+    }
+ 
+    setIsResettingProgress(true);
+    try {
+      if (isExecuting) {
+        await pauseExecution();
+      }
+      await resetExecutionProgress(workspaceId, planId);
+      await refreshPlans(workspaceId);
+    } catch (error) {
+      console.warn("Forge clean progress failed.", { error });
+    } finally {
+      setIsResettingProgress(false);
+    }
+  }, [
+    activeWorkspaceId,
+    isExecuting,
+    isResettingProgress,
+    pauseExecution,
+    refreshPlans,
+    resetExecutionProgress,
+    selectedPlanId,
+  ]);
+ 
+  return (
+    <div className="forge-panel" data-testid="forge-panel">
+      <div className="forge-header">
+        <div className="forge-title">Forge</div>
+      </div>
+ 
+      <section className="forge-card" aria-label="Plan selection">
+        <div className="forge-card-header">
+          <div className="forge-card-title">Plan</div>
+        </div>
+        <div className="forge-plan-select" ref={planMenuRef}>
+          <button
+            type="button"
+            className="ghost forge-template-toggle"
+            aria-label="Open templates"
+            title="Templates"
+            data-tauri-drag-region="false"
+            onClick={() => setTemplatesOpen(true)}
+            disabled={!hasActiveWorkspace}
+          >
+            <LayoutTemplate aria-hidden />
+          </button>
+          <button
+            type="button"
+            className={`ghost forge-plan-toggle${planMenuOpen ? " is-open" : ""}`}
+            data-tauri-drag-region="false"
+            onClick={() => {
+              if (!hasActiveWorkspace) {
+                return;
+              }
+              setPlanMenuOpen((open) => !open);
+            }}
+            aria-haspopup="menu"
+            aria-expanded={planMenuOpen}
+            disabled={!hasActiveWorkspace}
+          >
+            <span className={selectedPlan ? "forge-plan-name" : "forge-plan-empty"}>
+              {selectedPlan ? selectedPlan.name : "Click to select"}
+            </span>
+            <ChevronDown aria-hidden />
+          </button>
+ 
+          {planMenuOpen && (
+            <PopoverSurface className="forge-plan-menu" role="menu">
+              {plans.map((plan) => (
+                <PopoverMenuItem
+                  key={plan.id}
+                  role="menuitemradio"
+                  aria-checked={selectedPlanId === plan.id}
+                  active={selectedPlanId === plan.id}
+                  onClick={() => {
+                    setSelectedPlanId(plan.id);
+                    setPlanMenuOpen(false);
+                  }}
+                  data-tauri-drag-region="false"
+                >
+                  {plan.name}
+                </PopoverMenuItem>
+              ))}
+              <div className="forge-plan-menu-divider" aria-hidden />
+              <PopoverMenuItem
+                onClick={() => {
+                  // TODO(Forge): wire file selection dialog
+                  setPlanMenuOpen(false);
+                }}
+                data-tauri-drag-region="false"
+              >
+                Other...
+              </PopoverMenuItem>
+              <PopoverMenuItem
+                onClick={handleNewPlan}
+                data-tauri-drag-region="false"
+              >
+                New plan...
+              </PopoverMenuItem>
+            </PopoverSurface>
+          )}
+        </div>
+      </section>
+ 
+      <section className="forge-card" aria-label="Plan execution">
+        <div className="forge-card-header">
+          <div className="forge-card-header-left">
+            <div className="forge-card-title">Execution</div>
+            <div className="forge-card-subtitle">
+              {selectedPlan
+                ? runningInfo
+                  ? `${selectedPlan.name} • ${runningInfo.taskId}/${runningInfo.phaseId}`
+                  : selectedPlan.name
+                : "No plan selected"}
+            </div>
+          </div>
+          <div className="forge-card-header-actions">
+            <button
+              type="button"
+              className="ghost forge-execute-toggle"
+              data-tauri-drag-region="false"
+              disabled={!hasActiveWorkspace || !selectedPlan}
+              aria-label={isExecuting ? "Pause plan" : "Resume plan"}
+              aria-pressed={isExecuting}
+              title={
+                !hasActiveWorkspace
+                  ? "Select a project to use Forge"
+                  : !selectedPlan
+                  ? "Select a plan to run"
+                  : isExecuting
+                    ? "Pause plan"
+                    : "Resume plan"
+              }
+              onClick={() => {
+                if (!hasActiveWorkspace || !selectedPlan) {
+                  return;
+                }
+                if (isExecuting) {
+                  void pauseExecution();
+                  return;
+                }
+                void startExecution(selectedPlan.id);
+              }}
+            >
+              {isExecuting ? <Pause aria-hidden /> : <Play aria-hidden />}
+            </button>
+            <button
+              type="button"
+              className="ghost forge-reset-progress"
+              data-tauri-drag-region="false"
+              disabled={!hasActiveWorkspace || !selectedPlan || isResettingProgress}
+              aria-label="Clean progress"
+              title={
+                !hasActiveWorkspace
+                  ? "Select a project to use Forge"
+                  : !selectedPlan
+                  ? "Select a plan to clean progress"
+                  : "Reset state and derived execution files"
+              }
+              onClick={() => {
+                void handleCleanProgress();
+              }}
+            >
+              {isResettingProgress ? "Resetting..." : "Clean progress"}
+            </button>
+          </div>
+        </div>
+        <div className="forge-execution">
+          {lastError ? (
+            <div className="forge-execution-error" role="alert">
+              {lastError}
+            </div>
+          ) : null}
+          {!hasActiveWorkspace ? (
+            <div className="forge-empty">
+              Select a project first to use Forge.
+            </div>
+          ) : selectedPlan ? (
+            <ol className="forge-items">
+              {selectedPlan.items.map((item, index) => {
+                const isActiveTask = runningInfo?.taskId === item.id;
+                const taskPhaseStatuses = phaseView.taskPhaseStatusByTaskId[item.id] ?? {};
+                const effectiveStatus = resolveForgeTaskRowStatus({
+                  taskId: item.id,
+                  planTaskStatus: item.status,
+                  taskPhaseStatuses,
+                  phases: phaseView.phases,
+                  runningInfo,
+                });
+                const isCompleted = effectiveStatus === "completed";
+                const isRunning = effectiveStatus === "inProgress";
+                const isActivePhase = isActiveTask && runningInfo?.phaseId;
+ 
+                return (
+                  <li key={`${item.id}-${item.title}-${index}`} className={`forge-item ${effectiveStatus}`}>
+                    <div className="forge-item-row">
+                      <span className="forge-item-icon" aria-hidden>
+                        {isCompleted ? (
+                          <CheckSquare2 aria-hidden />
+                        ) : isRunning ? (
+                          <RefreshCw className="forge-running-icon spinning" aria-hidden />
+                        ) : (
+                          <Circle aria-hidden />
+                        )}
+                      </span>
+                      <span className="forge-item-title">{item.title}</span>
+                      {isActivePhase ? (
+                        <span className="forge-item-meta">
+                          Phase {runningInfo.phaseId} in progress
+                        </span>
+                      ) : null}
+                    </div>
+                    {phaseView.phases.length > 0 ? (
+                      <div className="forge-phases">
+                        {phaseView.phases.map((phase) => {
+                          const chipState = mapForgePhaseChipState(
+                            item.id,
+                            phase.id,
+                            taskPhaseStatuses[phase.id] ?? "pending",
+                            runningInfo,
+                          );
+                          const iconUrl = getForgePhaseIconUrl(phase.iconId);
+                          return (
+                            <span
+                              key={`${item.id}-${phase.id}`}
+                              className={`forge-phase ${chipState}`}
+                              data-phase-id={phase.id}
+                            >
+                              <span className="forge-phase-icon" aria-hidden>
+                                <img src={iconUrl} alt="" aria-hidden />
+                              </span>
+                              <span className="forge-phase-name">{phase.title}</span>
+                            </span>
+                          );
+                        })}
+                      </div>
+                    ) : null}
+                  </li>
+                );
+              })}
+            </ol>
+          ) : (
+            <div className="forge-empty">
+              Select a plan to see execution progress.
+            </div>
+          )}
+        </div>
+      </section>
+ 
+      {templatesOpen && (
+        <ForgeTemplatesModal
+          templates={templates}
+          installedTemplate={installedTemplate}
+          canManage={Boolean(activeWorkspaceId)}
+          onInstallTemplate={handleInstallTemplate}
+          onUninstallTemplate={handleUninstallTemplate}
+          onClose={() => setTemplatesOpen(false)}
+        />
+      )}
+    </div>
+  );
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/components/index.html b/coverage/components/index.html new file mode 100644 index 0000000000..f58729ab94 --- /dev/null +++ b/coverage/components/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for components + + + + + + + + + +
+
+

All files components

+
+ +
+ 91.08% + Statements + 756/830 +
+ + +
+ 78.57% + Branches + 176/224 +
+ + +
+ 86.95% + Functions + 20/23 +
+ + +
+ 91.08% + Lines + 756/830 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
Forge.tsx +
+
91.08%756/83078.57%176/22486.95%20/2391.08%756/830
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/coverage-final.json b/coverage/coverage-final.json new file mode 100644 index 0000000000..74e4facf60 --- /dev/null +++ b/coverage/coverage-final.json @@ -0,0 +1,3 @@ +{"/Users/lstern/projects/forge/src/features/forge/components/Forge.tsx": {"path":"/Users/lstern/projects/forge/src/features/forge/components/Forge.tsx","all":false,"statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":70}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":67}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":56}},"3":{"start":{"line":4,"column":0},"end":{"line":4,"column":56}},"4":{"start":{"line":5,"column":0},"end":{"line":5,"column":63}},"5":{"start":{"line":6,"column":0},"end":{"line":6,"column":73}},"6":{"start":{"line":7,"column":0},"end":{"line":7,"column":54}},"7":{"start":{"line":8,"column":0},"end":{"line":8,"column":52}},"8":{"start":{"line":9,"column":0},"end":{"line":9,"column":74}},"10":{"start":{"line":11,"column":0},"end":{"line":11,"column":77}},"11":{"start":{"line":12,"column":0},"end":{"line":12,"column":8}},"15":{"start":{"line":16,"column":0},"end":{"line":16,"column":72}},"16":{"start":{"line":17,"column":0},"end":{"line":17,"column":8}},"31":{"start":{"line":32,"column":0},"end":{"line":32,"column":8}},"44":{"start":{"line":45,"column":0},"end":{"line":45,"column":63}},"45":{"start":{"line":46,"column":0},"end":{"line":46,"column":70}},"97":{"start":{"line":98,"column":0},"end":{"line":98,"column":59}},"98":{"start":{"line":99,"column":0},"end":{"line":99,"column":50}},"99":{"start":{"line":100,"column":0},"end":{"line":100,"column":50}},"100":{"start":{"line":101,"column":0},"end":{"line":101,"column":40}},"101":{"start":{"line":102,"column":0},"end":{"line":102,"column":44}},"102":{"start":{"line":103,"column":0},"end":{"line":103,"column":2}},"104":{"start":{"line":105,"column":0},"end":{"line":105,"column":51}},"105":{"start":{"line":106,"column":0},"end":{"line":106,"column":28}},"106":{"start":{"line":107,"column":0},"end":{"line":107,"column":36}},"107":{"start":{"line":108,"column":0},"end":{"line":108,"column":36}},"108":{"start":{"line":109,"column":0},"end":{"line":109,"column":42}},"109":{"start":{"line":110,"column":0},"end":{"line":110,"column":54}},"110":{"start":{"line":111,"column":0},"end":{"line":111,"column":46}},"111":{"start":{"line":112,"column":0},"end":{"line":112,"column":38}},"112":{"start":{"line":113,"column":0},"end":{"line":113,"column":38}},"113":{"start":{"line":114,"column":0},"end":{"line":114,"column":16}},"114":{"start":{"line":115,"column":0},"end":{"line":115,"column":19}},"115":{"start":{"line":116,"column":0},"end":{"line":116,"column":14}},"116":{"start":{"line":117,"column":0},"end":{"line":117,"column":18}},"117":{"start":{"line":118,"column":0},"end":{"line":118,"column":2}},"133":{"start":{"line":134,"column":0},"end":{"line":134,"column":42}},"134":{"start":{"line":135,"column":0},"end":{"line":135,"column":13}},"135":{"start":{"line":136,"column":0},"end":{"line":136,"column":30}},"136":{"start":{"line":137,"column":0},"end":{"line":137,"column":2}},"141":{"start":{"line":142,"column":0},"end":{"line":142,"column":60}},"142":{"start":{"line":143,"column":0},"end":{"line":143,"column":41}},"143":{"start":{"line":144,"column":0},"end":{"line":144,"column":39}},"144":{"start":{"line":145,"column":0},"end":{"line":145,"column":35}},"145":{"start":{"line":146,"column":0},"end":{"line":146,"column":20}},"146":{"start":{"line":147,"column":0},"end":{"line":147,"column":30}},"147":{"start":{"line":148,"column":0},"end":{"line":148,"column":3}},"148":{"start":{"line":149,"column":0},"end":{"line":149,"column":14}},"149":{"start":{"line":150,"column":0},"end":{"line":150,"column":17}},"150":{"start":{"line":151,"column":0},"end":{"line":151,"column":3}},"151":{"start":{"line":152,"column":0},"end":{"line":152,"column":19}},"152":{"start":{"line":153,"column":0},"end":{"line":153,"column":66}},"153":{"start":{"line":154,"column":0},"end":{"line":154,"column":24}},"154":{"start":{"line":155,"column":0},"end":{"line":155,"column":34}},"155":{"start":{"line":156,"column":0},"end":{"line":156,"column":3}},"156":{"start":{"line":157,"column":0},"end":{"line":157,"column":44}},"157":{"start":{"line":158,"column":0},"end":{"line":158,"column":1}},"159":{"start":{"line":160,"column":0},"end":{"line":160,"column":62}},"160":{"start":{"line":161,"column":0},"end":{"line":161,"column":49}},"161":{"start":{"line":162,"column":0},"end":{"line":162,"column":35}},"162":{"start":{"line":163,"column":0},"end":{"line":163,"column":23}},"163":{"start":{"line":164,"column":0},"end":{"line":164,"column":3}},"164":{"start":{"line":165,"column":0},"end":{"line":165,"column":37}},"165":{"start":{"line":166,"column":0},"end":{"line":166,"column":24}},"166":{"start":{"line":167,"column":0},"end":{"line":167,"column":3}},"167":{"start":{"line":168,"column":0},"end":{"line":168,"column":19}},"168":{"start":{"line":169,"column":0},"end":{"line":169,"column":1}},"170":{"start":{"line":171,"column":0},"end":{"line":171,"column":32}},"171":{"start":{"line":172,"column":0},"end":{"line":172,"column":17}},"172":{"start":{"line":173,"column":0},"end":{"line":173,"column":18}},"173":{"start":{"line":174,"column":0},"end":{"line":174,"column":36}},"174":{"start":{"line":175,"column":0},"end":{"line":175,"column":32}},"175":{"start":{"line":176,"column":0},"end":{"line":176,"column":24}},"176":{"start":{"line":177,"column":0},"end":{"line":177,"column":60}},"177":{"start":{"line":178,"column":0},"end":{"line":178,"column":74}},"178":{"start":{"line":179,"column":0},"end":{"line":179,"column":24}},"179":{"start":{"line":180,"column":0},"end":{"line":180,"column":3}},"180":{"start":{"line":181,"column":0},"end":{"line":181,"column":43}},"181":{"start":{"line":182,"column":0},"end":{"line":182,"column":24}},"182":{"start":{"line":183,"column":0},"end":{"line":183,"column":3}},"183":{"start":{"line":184,"column":0},"end":{"line":184,"column":41}},"184":{"start":{"line":185,"column":0},"end":{"line":185,"column":25}},"185":{"start":{"line":186,"column":0},"end":{"line":186,"column":3}},"186":{"start":{"line":187,"column":0},"end":{"line":187,"column":22}},"187":{"start":{"line":188,"column":0},"end":{"line":188,"column":1}},"189":{"start":{"line":190,"column":0},"end":{"line":190,"column":32}},"190":{"start":{"line":191,"column":0},"end":{"line":191,"column":58}},"191":{"start":{"line":192,"column":0},"end":{"line":192,"column":35}},"192":{"start":{"line":193,"column":0},"end":{"line":193,"column":12}},"193":{"start":{"line":194,"column":0},"end":{"line":194,"column":26}},"194":{"start":{"line":195,"column":0},"end":{"line":195,"column":24}},"195":{"start":{"line":196,"column":0},"end":{"line":196,"column":16}},"196":{"start":{"line":197,"column":0},"end":{"line":197,"column":75}},"197":{"start":{"line":198,"column":0},"end":{"line":198,"column":20}},"198":{"start":{"line":199,"column":0},"end":{"line":199,"column":6}},"199":{"start":{"line":200,"column":0},"end":{"line":200,"column":3}},"200":{"start":{"line":201,"column":0},"end":{"line":201,"column":57}},"201":{"start":{"line":202,"column":0},"end":{"line":202,"column":10}},"202":{"start":{"line":203,"column":0},"end":{"line":203,"column":31}},"203":{"start":{"line":204,"column":0},"end":{"line":204,"column":80}},"205":{"start":{"line":206,"column":0},"end":{"line":206,"column":1}},"207":{"start":{"line":208,"column":0},"end":{"line":208,"column":36}},"208":{"start":{"line":209,"column":0},"end":{"line":209,"column":58}},"209":{"start":{"line":210,"column":0},"end":{"line":210,"column":35}},"210":{"start":{"line":211,"column":0},"end":{"line":211,"column":27}},"211":{"start":{"line":212,"column":0},"end":{"line":212,"column":57}},"212":{"start":{"line":213,"column":0},"end":{"line":213,"column":35}},"213":{"start":{"line":214,"column":0},"end":{"line":214,"column":16}},"214":{"start":{"line":215,"column":0},"end":{"line":215,"column":3}},"215":{"start":{"line":216,"column":0},"end":{"line":216,"column":58}},"216":{"start":{"line":217,"column":0},"end":{"line":217,"column":23}},"217":{"start":{"line":218,"column":0},"end":{"line":218,"column":3}},"218":{"start":{"line":219,"column":0},"end":{"line":219,"column":86}},"219":{"start":{"line":220,"column":0},"end":{"line":220,"column":24}},"220":{"start":{"line":221,"column":0},"end":{"line":221,"column":3}},"221":{"start":{"line":222,"column":0},"end":{"line":222,"column":19}},"222":{"start":{"line":223,"column":0},"end":{"line":223,"column":1}},"224":{"start":{"line":225,"column":0},"end":{"line":225,"column":36}},"225":{"start":{"line":226,"column":0},"end":{"line":226,"column":9}},"226":{"start":{"line":227,"column":0},"end":{"line":227,"column":17}},"227":{"start":{"line":228,"column":0},"end":{"line":228,"column":20}},"228":{"start":{"line":229,"column":0},"end":{"line":229,"column":9}},"229":{"start":{"line":230,"column":0},"end":{"line":230,"column":14}},"230":{"start":{"line":231,"column":0},"end":{"line":231,"column":4}},"236":{"start":{"line":237,"column":0},"end":{"line":237,"column":21}},"237":{"start":{"line":238,"column":0},"end":{"line":238,"column":39}},"238":{"start":{"line":239,"column":0},"end":{"line":239,"column":24}},"239":{"start":{"line":240,"column":0},"end":{"line":240,"column":3}},"241":{"start":{"line":242,"column":0},"end":{"line":242,"column":83}},"242":{"start":{"line":243,"column":0},"end":{"line":243,"column":62}},"244":{"start":{"line":245,"column":0},"end":{"line":245,"column":55}},"245":{"start":{"line":246,"column":0},"end":{"line":246,"column":60}},"246":{"start":{"line":247,"column":0},"end":{"line":247,"column":19}},"247":{"start":{"line":248,"column":0},"end":{"line":248,"column":18}},"248":{"start":{"line":249,"column":0},"end":{"line":249,"column":3}},"250":{"start":{"line":251,"column":0},"end":{"line":251,"column":24}},"251":{"start":{"line":252,"column":0},"end":{"line":252,"column":1}},"253":{"start":{"line":254,"column":0},"end":{"line":254,"column":30}},"254":{"start":{"line":255,"column":0},"end":{"line":255,"column":12}},"255":{"start":{"line":256,"column":0},"end":{"line":256,"column":20}},"256":{"start":{"line":257,"column":0},"end":{"line":257,"column":12}},"257":{"start":{"line":258,"column":0},"end":{"line":258,"column":20}},"258":{"start":{"line":259,"column":0},"end":{"line":259,"column":22}},"259":{"start":{"line":260,"column":0},"end":{"line":260,"column":10}},"260":{"start":{"line":261,"column":0},"end":{"line":261,"column":4}},"267":{"start":{"line":268,"column":0},"end":{"line":268,"column":4}},"268":{"start":{"line":269,"column":0},"end":{"line":269,"column":19}},"269":{"start":{"line":270,"column":0},"end":{"line":270,"column":53}},"270":{"start":{"line":271,"column":0},"end":{"line":271,"column":35}},"271":{"start":{"line":272,"column":0},"end":{"line":272,"column":15}},"272":{"start":{"line":273,"column":0},"end":{"line":273,"column":7}},"273":{"start":{"line":274,"column":0},"end":{"line":274,"column":29}},"274":{"start":{"line":275,"column":0},"end":{"line":275,"column":16}},"275":{"start":{"line":276,"column":0},"end":{"line":276,"column":6}},"276":{"start":{"line":277,"column":0},"end":{"line":277,"column":54}},"277":{"start":{"line":278,"column":0},"end":{"line":278,"column":70}},"278":{"start":{"line":279,"column":0},"end":{"line":279,"column":16}},"280":{"start":{"line":281,"column":0},"end":{"line":281,"column":37}},"281":{"start":{"line":282,"column":0},"end":{"line":282,"column":80}},"282":{"start":{"line":283,"column":0},"end":{"line":283,"column":50}},"283":{"start":{"line":284,"column":0},"end":{"line":284,"column":53}},"284":{"start":{"line":285,"column":0},"end":{"line":285,"column":60}},"285":{"start":{"line":286,"column":0},"end":{"line":286,"column":7}},"286":{"start":{"line":287,"column":0},"end":{"line":287,"column":11}},"287":{"start":{"line":288,"column":0},"end":{"line":288,"column":26}},"288":{"start":{"line":289,"column":0},"end":{"line":289,"column":52}},"289":{"start":{"line":290,"column":0},"end":{"line":290,"column":16}},"291":{"start":{"line":292,"column":0},"end":{"line":292,"column":10}},"292":{"start":{"line":293,"column":0},"end":{"line":293,"column":15}},"293":{"start":{"line":294,"column":0},"end":{"line":294,"column":39}},"294":{"start":{"line":295,"column":0},"end":{"line":295,"column":31}},"295":{"start":{"line":296,"column":0},"end":{"line":296,"column":33}},"297":{"start":{"line":298,"column":0},"end":{"line":298,"column":75}},"298":{"start":{"line":299,"column":0},"end":{"line":299,"column":66}},"300":{"start":{"line":301,"column":0},"end":{"line":301,"column":12}},"301":{"start":{"line":302,"column":0},"end":{"line":302,"column":42}},"302":{"start":{"line":303,"column":0},"end":{"line":303,"column":56}},"303":{"start":{"line":304,"column":0},"end":{"line":304,"column":22}},"304":{"start":{"line":305,"column":0},"end":{"line":305,"column":62}},"305":{"start":{"line":306,"column":0},"end":{"line":306,"column":49}},"306":{"start":{"line":307,"column":0},"end":{"line":307,"column":74}},"307":{"start":{"line":308,"column":0},"end":{"line":308,"column":51}},"308":{"start":{"line":309,"column":0},"end":{"line":309,"column":77}},"309":{"start":{"line":310,"column":0},"end":{"line":310,"column":20}},"310":{"start":{"line":311,"column":0},"end":{"line":311,"column":18}},"311":{"start":{"line":312,"column":0},"end":{"line":312,"column":52}},"312":{"start":{"line":313,"column":0},"end":{"line":313,"column":21}},"313":{"start":{"line":314,"column":0},"end":{"line":314,"column":29}},"314":{"start":{"line":315,"column":0},"end":{"line":315,"column":53}},"315":{"start":{"line":316,"column":0},"end":{"line":316,"column":42}},"316":{"start":{"line":317,"column":0},"end":{"line":317,"column":37}},"317":{"start":{"line":318,"column":0},"end":{"line":318,"column":32}},"319":{"start":{"line":320,"column":0},"end":{"line":320,"column":18}},"321":{"start":{"line":322,"column":0},"end":{"line":322,"column":38}},"322":{"start":{"line":323,"column":0},"end":{"line":323,"column":23}},"323":{"start":{"line":324,"column":0},"end":{"line":324,"column":21}},"324":{"start":{"line":325,"column":0},"end":{"line":325,"column":29}},"325":{"start":{"line":326,"column":0},"end":{"line":326,"column":55}},"326":{"start":{"line":327,"column":0},"end":{"line":327,"column":32}},"327":{"start":{"line":328,"column":0},"end":{"line":328,"column":40}},"328":{"start":{"line":329,"column":0},"end":{"line":329,"column":28}},"329":{"start":{"line":330,"column":0},"end":{"line":330,"column":18}},"330":{"start":{"line":331,"column":0},"end":{"line":331,"column":37}},"331":{"start":{"line":332,"column":0},"end":{"line":332,"column":15}},"333":{"start":{"line":334,"column":0},"end":{"line":334,"column":23}},"334":{"start":{"line":335,"column":0},"end":{"line":335,"column":18}},"335":{"start":{"line":336,"column":0},"end":{"line":336,"column":16}},"337":{"start":{"line":338,"column":0},"end":{"line":338,"column":76}},"340":{"start":{"line":341,"column":0},"end":{"line":341,"column":35}},"341":{"start":{"line":342,"column":0},"end":{"line":342,"column":61}},"342":{"start":{"line":343,"column":0},"end":{"line":343,"column":18}},"343":{"start":{"line":344,"column":0},"end":{"line":344,"column":82}},"344":{"start":{"line":345,"column":0},"end":{"line":345,"column":51}},"345":{"start":{"line":346,"column":0},"end":{"line":346,"column":75}},"346":{"start":{"line":347,"column":0},"end":{"line":347,"column":20}},"347":{"start":{"line":348,"column":0},"end":{"line":348,"column":54}},"348":{"start":{"line":349,"column":0},"end":{"line":349,"column":23}},"349":{"start":{"line":350,"column":0},"end":{"line":350,"column":31}},"350":{"start":{"line":351,"column":0},"end":{"line":351,"column":57}},"351":{"start":{"line":352,"column":0},"end":{"line":352,"column":34}},"352":{"start":{"line":353,"column":0},"end":{"line":353,"column":51}},"353":{"start":{"line":354,"column":0},"end":{"line":354,"column":30}},"354":{"start":{"line":355,"column":0},"end":{"line":355,"column":20}},"355":{"start":{"line":356,"column":0},"end":{"line":356,"column":39}},"357":{"start":{"line":358,"column":0},"end":{"line":358,"column":31}},"358":{"start":{"line":359,"column":0},"end":{"line":359,"column":25}},"359":{"start":{"line":360,"column":0},"end":{"line":360,"column":20}},"360":{"start":{"line":361,"column":0},"end":{"line":361,"column":18}},"362":{"start":{"line":363,"column":0},"end":{"line":363,"column":11}},"363":{"start":{"line":364,"column":0},"end":{"line":364,"column":12}},"364":{"start":{"line":365,"column":0},"end":{"line":365,"column":64}},"365":{"start":{"line":366,"column":0},"end":{"line":366,"column":82}},"367":{"start":{"line":368,"column":0},"end":{"line":368,"column":17}},"368":{"start":{"line":369,"column":0},"end":{"line":369,"column":12}},"369":{"start":{"line":370,"column":0},"end":{"line":370,"column":17}},"371":{"start":{"line":372,"column":0},"end":{"line":372,"column":1}},"373":{"start":{"line":374,"column":0},"end":{"line":374,"column":23}},"374":{"start":{"line":375,"column":0},"end":{"line":375,"column":20}},"375":{"start":{"line":376,"column":0},"end":{"line":376,"column":25}},"376":{"start":{"line":377,"column":0},"end":{"line":377,"column":26}},"377":{"start":{"line":378,"column":0},"end":{"line":378,"column":18}},"378":{"start":{"line":379,"column":0},"end":{"line":379,"column":14}},"379":{"start":{"line":380,"column":0},"end":{"line":380,"column":17}},"380":{"start":{"line":381,"column":0},"end":{"line":381,"column":26}},"381":{"start":{"line":382,"column":0},"end":{"line":382,"column":28}},"382":{"start":{"line":383,"column":0},"end":{"line":383,"column":4}},"399":{"start":{"line":400,"column":0},"end":{"line":400,"column":4}},"400":{"start":{"line":401,"column":0},"end":{"line":401,"column":64}},"401":{"start":{"line":402,"column":0},"end":{"line":402,"column":58}},"402":{"start":{"line":403,"column":0},"end":{"line":403,"column":59}},"403":{"start":{"line":404,"column":0},"end":{"line":404,"column":59}},"404":{"start":{"line":405,"column":0},"end":{"line":405,"column":49}},"405":{"start":{"line":406,"column":0},"end":{"line":406,"column":53}},"406":{"start":{"line":407,"column":0},"end":{"line":407,"column":39}},"407":{"start":{"line":408,"column":0},"end":{"line":408,"column":69}},"408":{"start":{"line":409,"column":0},"end":{"line":409,"column":47}},"409":{"start":{"line":410,"column":0},"end":{"line":410,"column":53}},"410":{"start":{"line":411,"column":0},"end":{"line":411,"column":65}},"411":{"start":{"line":412,"column":0},"end":{"line":412,"column":57}},"412":{"start":{"line":413,"column":0},"end":{"line":413,"column":49}},"413":{"start":{"line":414,"column":0},"end":{"line":414,"column":49}},"414":{"start":{"line":415,"column":0},"end":{"line":415,"column":51}},"415":{"start":{"line":416,"column":0},"end":{"line":416,"column":58}},"416":{"start":{"line":417,"column":0},"end":{"line":417,"column":50}},"417":{"start":{"line":418,"column":0},"end":{"line":418,"column":63}},"418":{"start":{"line":419,"column":0},"end":{"line":419,"column":64}},"420":{"start":{"line":421,"column":0},"end":{"line":421,"column":44}},"421":{"start":{"line":422,"column":0},"end":{"line":422,"column":25}},"422":{"start":{"line":423,"column":0},"end":{"line":423,"column":53}},"423":{"start":{"line":424,"column":0},"end":{"line":424,"column":24}},"424":{"start":{"line":425,"column":0},"end":{"line":425,"column":20}},"425":{"start":{"line":426,"column":0},"end":{"line":426,"column":7}},"426":{"start":{"line":427,"column":0},"end":{"line":427,"column":14}},"427":{"start":{"line":428,"column":0},"end":{"line":428,"column":32}},"428":{"start":{"line":429,"column":0},"end":{"line":429,"column":64}},"429":{"start":{"line":430,"column":0},"end":{"line":430,"column":12}},"430":{"start":{"line":431,"column":0},"end":{"line":431,"column":32}},"431":{"start":{"line":432,"column":0},"end":{"line":432,"column":79}},"432":{"start":{"line":433,"column":0},"end":{"line":433,"column":12}},"433":{"start":{"line":434,"column":0},"end":{"line":434,"column":12}},"435":{"start":{"line":436,"column":0},"end":{"line":436,"column":6}},"436":{"start":{"line":437,"column":0},"end":{"line":437,"column":25}},"437":{"start":{"line":438,"column":0},"end":{"line":438,"column":4}},"439":{"start":{"line":440,"column":0},"end":{"line":440,"column":55}},"440":{"start":{"line":441,"column":0},"end":{"line":441,"column":47}},"441":{"start":{"line":442,"column":0},"end":{"line":442,"column":18}},"442":{"start":{"line":443,"column":0},"end":{"line":443,"column":20}},"443":{"start":{"line":444,"column":0},"end":{"line":444,"column":7}},"444":{"start":{"line":445,"column":0},"end":{"line":445,"column":45}},"445":{"start":{"line":446,"column":0},"end":{"line":446,"column":23}},"446":{"start":{"line":447,"column":0},"end":{"line":447,"column":20}},"447":{"start":{"line":448,"column":0},"end":{"line":448,"column":7}},"448":{"start":{"line":449,"column":0},"end":{"line":449,"column":51}},"449":{"start":{"line":450,"column":0},"end":{"line":450,"column":24}},"452":{"start":{"line":453,"column":0},"end":{"line":453,"column":20}},"453":{"start":{"line":454,"column":0},"end":{"line":454,"column":7}},"454":{"start":{"line":455,"column":0},"end":{"line":455,"column":49}},"455":{"start":{"line":456,"column":0},"end":{"line":456,"column":67}},"456":{"start":{"line":457,"column":0},"end":{"line":457,"column":26}},"457":{"start":{"line":458,"column":0},"end":{"line":458,"column":8}},"458":{"start":{"line":459,"column":0},"end":{"line":459,"column":42}},"459":{"start":{"line":460,"column":0},"end":{"line":460,"column":57}},"460":{"start":{"line":461,"column":0},"end":{"line":461,"column":7}},"461":{"start":{"line":462,"column":0},"end":{"line":462,"column":43}},"462":{"start":{"line":463,"column":0},"end":{"line":463,"column":6}},"463":{"start":{"line":464,"column":0},"end":{"line":464,"column":7}},"464":{"start":{"line":465,"column":0},"end":{"line":465,"column":4}},"466":{"start":{"line":467,"column":0},"end":{"line":467,"column":77}},"467":{"start":{"line":468,"column":0},"end":{"line":468,"column":93}},"468":{"start":{"line":469,"column":0},"end":{"line":469,"column":81}},"469":{"start":{"line":470,"column":0},"end":{"line":470,"column":72}},"470":{"start":{"line":471,"column":0},"end":{"line":471,"column":41}},"472":{"start":{"line":473,"column":0},"end":{"line":473,"column":19}},"473":{"start":{"line":474,"column":0},"end":{"line":474,"column":26}},"474":{"start":{"line":475,"column":0},"end":{"line":475,"column":31}},"475":{"start":{"line":476,"column":0},"end":{"line":476,"column":24}},"476":{"start":{"line":477,"column":0},"end":{"line":477,"column":24}},"477":{"start":{"line":478,"column":0},"end":{"line":478,"column":17}},"478":{"start":{"line":479,"column":0},"end":{"line":479,"column":9}},"479":{"start":{"line":480,"column":0},"end":{"line":480,"column":28}},"480":{"start":{"line":481,"column":0},"end":{"line":481,"column":8}},"481":{"start":{"line":482,"column":0},"end":{"line":482,"column":25}},"482":{"start":{"line":483,"column":0},"end":{"line":483,"column":67}},"483":{"start":{"line":484,"column":0},"end":{"line":484,"column":9}},"484":{"start":{"line":485,"column":0},"end":{"line":485,"column":18}},"485":{"start":{"line":486,"column":0},"end":{"line":486,"column":23}},"486":{"start":{"line":487,"column":0},"end":{"line":487,"column":6}},"487":{"start":{"line":488,"column":0},"end":{"line":488,"column":29}},"489":{"start":{"line":490,"column":0},"end":{"line":490,"column":19}},"490":{"start":{"line":491,"column":0},"end":{"line":491,"column":26}},"491":{"start":{"line":492,"column":0},"end":{"line":492,"column":29}},"492":{"start":{"line":493,"column":0},"end":{"line":493,"column":33}},"493":{"start":{"line":494,"column":0},"end":{"line":494,"column":28}},"494":{"start":{"line":495,"column":0},"end":{"line":495,"column":20}},"495":{"start":{"line":496,"column":0},"end":{"line":496,"column":25}},"496":{"start":{"line":497,"column":0},"end":{"line":497,"column":8}},"497":{"start":{"line":498,"column":0},"end":{"line":498,"column":5}},"498":{"start":{"line":499,"column":0},"end":{"line":499,"column":48}},"499":{"start":{"line":500,"column":0},"end":{"line":500,"column":28}},"500":{"start":{"line":501,"column":0},"end":{"line":501,"column":24}},"501":{"start":{"line":502,"column":0},"end":{"line":502,"column":17}},"502":{"start":{"line":503,"column":0},"end":{"line":503,"column":9}},"503":{"start":{"line":504,"column":0},"end":{"line":504,"column":40}},"504":{"start":{"line":505,"column":0},"end":{"line":505,"column":8}},"505":{"start":{"line":506,"column":0},"end":{"line":506,"column":25}},"506":{"start":{"line":507,"column":0},"end":{"line":507,"column":76}},"507":{"start":{"line":508,"column":0},"end":{"line":508,"column":9}},"508":{"start":{"line":509,"column":0},"end":{"line":509,"column":18}},"509":{"start":{"line":510,"column":0},"end":{"line":510,"column":23}},"510":{"start":{"line":511,"column":0},"end":{"line":511,"column":6}},"511":{"start":{"line":512,"column":0},"end":{"line":512,"column":48}},"513":{"start":{"line":514,"column":0},"end":{"line":514,"column":35}},"514":{"start":{"line":515,"column":0},"end":{"line":515,"column":36}},"515":{"start":{"line":516,"column":0},"end":{"line":516,"column":37}},"516":{"start":{"line":517,"column":0},"end":{"line":517,"column":15}},"517":{"start":{"line":518,"column":0},"end":{"line":518,"column":7}},"518":{"start":{"line":519,"column":0},"end":{"line":519,"column":38}},"519":{"start":{"line":520,"column":0},"end":{"line":520,"column":11}},"520":{"start":{"line":521,"column":0},"end":{"line":521,"column":50}},"521":{"start":{"line":522,"column":0},"end":{"line":522,"column":32}},"522":{"start":{"line":523,"column":0},"end":{"line":523,"column":23}},"523":{"start":{"line":524,"column":0},"end":{"line":524,"column":63}},"524":{"start":{"line":525,"column":0},"end":{"line":525,"column":17}},"525":{"start":{"line":526,"column":0},"end":{"line":526,"column":41}},"526":{"start":{"line":527,"column":0},"end":{"line":527,"column":7}},"527":{"start":{"line":528,"column":0},"end":{"line":528,"column":6}},"528":{"start":{"line":529,"column":0},"end":{"line":529,"column":16}},"529":{"start":{"line":530,"column":0},"end":{"line":530,"column":4}},"531":{"start":{"line":532,"column":0},"end":{"line":532,"column":19}},"532":{"start":{"line":533,"column":0},"end":{"line":533,"column":26}},"533":{"start":{"line":534,"column":0},"end":{"line":534,"column":29}},"534":{"start":{"line":535,"column":0},"end":{"line":535,"column":28}},"535":{"start":{"line":536,"column":0},"end":{"line":536,"column":20}},"536":{"start":{"line":537,"column":0},"end":{"line":537,"column":25}},"537":{"start":{"line":538,"column":0},"end":{"line":538,"column":8}},"538":{"start":{"line":539,"column":0},"end":{"line":539,"column":5}},"540":{"start":{"line":541,"column":0},"end":{"line":541,"column":35}},"541":{"start":{"line":542,"column":0},"end":{"line":542,"column":22}},"542":{"start":{"line":543,"column":0},"end":{"line":543,"column":15}},"543":{"start":{"line":544,"column":0},"end":{"line":544,"column":7}},"544":{"start":{"line":545,"column":0},"end":{"line":545,"column":44}},"545":{"start":{"line":546,"column":0},"end":{"line":546,"column":6}},"547":{"start":{"line":548,"column":0},"end":{"line":548,"column":21}},"548":{"start":{"line":549,"column":0},"end":{"line":549,"column":57}},"549":{"start":{"line":550,"column":0},"end":{"line":550,"column":18}},"550":{"start":{"line":551,"column":0},"end":{"line":551,"column":23}},"551":{"start":{"line":552,"column":0},"end":{"line":552,"column":37}},"552":{"start":{"line":553,"column":0},"end":{"line":553,"column":6}},"553":{"start":{"line":554,"column":0},"end":{"line":554,"column":40}},"555":{"start":{"line":556,"column":0},"end":{"line":556,"column":44}},"556":{"start":{"line":557,"column":0},"end":{"line":557,"column":41}},"557":{"start":{"line":558,"column":0},"end":{"line":558,"column":63}},"558":{"start":{"line":559,"column":0},"end":{"line":559,"column":55}},"559":{"start":{"line":560,"column":0},"end":{"line":560,"column":16}},"560":{"start":{"line":561,"column":0},"end":{"line":561,"column":22}},"561":{"start":{"line":562,"column":0},"end":{"line":562,"column":27}},"562":{"start":{"line":563,"column":0},"end":{"line":563,"column":17}},"563":{"start":{"line":564,"column":0},"end":{"line":564,"column":10}},"564":{"start":{"line":565,"column":0},"end":{"line":565,"column":9}},"566":{"start":{"line":567,"column":0},"end":{"line":567,"column":14}},"567":{"start":{"line":568,"column":0},"end":{"line":568,"column":20}},"568":{"start":{"line":569,"column":0},"end":{"line":569,"column":36}},"569":{"start":{"line":570,"column":0},"end":{"line":570,"column":14}},"570":{"start":{"line":571,"column":0},"end":{"line":571,"column":8}},"571":{"start":{"line":572,"column":0},"end":{"line":572,"column":7}},"572":{"start":{"line":573,"column":0},"end":{"line":573,"column":23}},"574":{"start":{"line":575,"column":0},"end":{"line":575,"column":44}},"575":{"start":{"line":576,"column":0},"end":{"line":576,"column":29}},"576":{"start":{"line":577,"column":0},"end":{"line":577,"column":31}},"577":{"start":{"line":578,"column":0},"end":{"line":578,"column":15}},"578":{"start":{"line":579,"column":0},"end":{"line":579,"column":7}},"579":{"start":{"line":580,"column":0},"end":{"line":580,"column":57}},"580":{"start":{"line":581,"column":0},"end":{"line":581,"column":25}},"581":{"start":{"line":582,"column":0},"end":{"line":582,"column":37}},"582":{"start":{"line":583,"column":0},"end":{"line":583,"column":10}},"583":{"start":{"line":584,"column":0},"end":{"line":584,"column":27}},"584":{"start":{"line":585,"column":0},"end":{"line":585,"column":71}},"585":{"start":{"line":586,"column":0},"end":{"line":586,"column":11}},"586":{"start":{"line":587,"column":0},"end":{"line":587,"column":6}},"587":{"start":{"line":588,"column":0},"end":{"line":588,"column":41}},"588":{"start":{"line":589,"column":0},"end":{"line":589,"column":4}},"590":{"start":{"line":591,"column":0},"end":{"line":591,"column":53}},"591":{"start":{"line":592,"column":0},"end":{"line":592,"column":29}},"592":{"start":{"line":593,"column":0},"end":{"line":593,"column":13}},"593":{"start":{"line":594,"column":0},"end":{"line":594,"column":5}},"594":{"start":{"line":595,"column":0},"end":{"line":595,"column":45}},"595":{"start":{"line":596,"column":0},"end":{"line":596,"column":19}},"596":{"start":{"line":597,"column":0},"end":{"line":597,"column":35}},"597":{"start":{"line":598,"column":0},"end":{"line":598,"column":8}},"598":{"start":{"line":599,"column":0},"end":{"line":599,"column":25}},"599":{"start":{"line":600,"column":0},"end":{"line":600,"column":71}},"600":{"start":{"line":601,"column":0},"end":{"line":601,"column":9}},"601":{"start":{"line":602,"column":0},"end":{"line":602,"column":45}},"603":{"start":{"line":604,"column":0},"end":{"line":604,"column":76}},"604":{"start":{"line":605,"column":0},"end":{"line":605,"column":37}},"605":{"start":{"line":606,"column":0},"end":{"line":606,"column":56}},"606":{"start":{"line":607,"column":0},"end":{"line":607,"column":11}},"607":{"start":{"line":608,"column":0},"end":{"line":608,"column":79}},"609":{"start":{"line":610,"column":0},"end":{"line":610,"column":58}},"610":{"start":{"line":611,"column":0},"end":{"line":611,"column":58}},"612":{"start":{"line":613,"column":0},"end":{"line":613,"column":60}},"614":{"start":{"line":615,"column":0},"end":{"line":615,"column":22}},"615":{"start":{"line":616,"column":0},"end":{"line":616,"column":25}},"616":{"start":{"line":617,"column":0},"end":{"line":617,"column":30}},"617":{"start":{"line":618,"column":0},"end":{"line":618,"column":42}},"618":{"start":{"line":619,"column":0},"end":{"line":619,"column":5}},"620":{"start":{"line":621,"column":0},"end":{"line":621,"column":19}},"621":{"start":{"line":622,"column":0},"end":{"line":622,"column":29}},"622":{"start":{"line":623,"column":0},"end":{"line":623,"column":13}},"623":{"start":{"line":624,"column":0},"end":{"line":624,"column":5}},"624":{"start":{"line":625,"column":0},"end":{"line":625,"column":27}},"625":{"start":{"line":626,"column":0},"end":{"line":626,"column":27}},"627":{"start":{"line":628,"column":0},"end":{"line":628,"column":19}},"628":{"start":{"line":629,"column":0},"end":{"line":629,"column":26}},"629":{"start":{"line":630,"column":0},"end":{"line":630,"column":13}},"630":{"start":{"line":631,"column":0},"end":{"line":631,"column":5}},"631":{"start":{"line":632,"column":0},"end":{"line":632,"column":60}},"632":{"start":{"line":633,"column":0},"end":{"line":633,"column":30}},"633":{"start":{"line":634,"column":0},"end":{"line":634,"column":5}},"634":{"start":{"line":635,"column":0},"end":{"line":635,"column":30}},"636":{"start":{"line":637,"column":0},"end":{"line":637,"column":19}},"637":{"start":{"line":638,"column":0},"end":{"line":638,"column":26}},"638":{"start":{"line":639,"column":0},"end":{"line":639,"column":48}},"639":{"start":{"line":640,"column":0},"end":{"line":640,"column":37}},"640":{"start":{"line":641,"column":0},"end":{"line":641,"column":20}},"641":{"start":{"line":642,"column":0},"end":{"line":642,"column":25}},"642":{"start":{"line":643,"column":0},"end":{"line":643,"column":8}},"643":{"start":{"line":644,"column":0},"end":{"line":644,"column":5}},"645":{"start":{"line":646,"column":0},"end":{"line":646,"column":79}},"646":{"start":{"line":647,"column":0},"end":{"line":647,"column":42}},"647":{"start":{"line":648,"column":0},"end":{"line":648,"column":11}},"648":{"start":{"line":649,"column":0},"end":{"line":649,"column":41}},"649":{"start":{"line":650,"column":0},"end":{"line":650,"column":28}},"650":{"start":{"line":651,"column":0},"end":{"line":651,"column":25}},"651":{"start":{"line":652,"column":0},"end":{"line":652,"column":30}},"652":{"start":{"line":653,"column":0},"end":{"line":653,"column":10}},"653":{"start":{"line":654,"column":0},"end":{"line":654,"column":24}},"654":{"start":{"line":655,"column":0},"end":{"line":655,"column":17}},"655":{"start":{"line":656,"column":0},"end":{"line":656,"column":9}},"656":{"start":{"line":657,"column":0},"end":{"line":657,"column":27}},"657":{"start":{"line":658,"column":0},"end":{"line":658,"column":23}},"658":{"start":{"line":659,"column":0},"end":{"line":659,"column":24}},"659":{"start":{"line":660,"column":0},"end":{"line":660,"column":17}},"660":{"start":{"line":661,"column":0},"end":{"line":661,"column":9}},"661":{"start":{"line":662,"column":0},"end":{"line":662,"column":68}},"662":{"start":{"line":663,"column":0},"end":{"line":663,"column":39}},"663":{"start":{"line":664,"column":0},"end":{"line":664,"column":7}},"664":{"start":{"line":665,"column":0},"end":{"line":665,"column":6}},"666":{"start":{"line":667,"column":0},"end":{"line":667,"column":28}},"667":{"start":{"line":668,"column":0},"end":{"line":668,"column":64}},"668":{"start":{"line":669,"column":0},"end":{"line":669,"column":18}},"669":{"start":{"line":670,"column":0},"end":{"line":670,"column":23}},"670":{"start":{"line":671,"column":0},"end":{"line":671,"column":37}},"671":{"start":{"line":672,"column":0},"end":{"line":672,"column":6}},"672":{"start":{"line":673,"column":0},"end":{"line":673,"column":6}},"673":{"start":{"line":674,"column":0},"end":{"line":674,"column":22}},"674":{"start":{"line":675,"column":0},"end":{"line":675,"column":43}},"675":{"start":{"line":676,"column":0},"end":{"line":676,"column":18}},"676":{"start":{"line":677,"column":0},"end":{"line":677,"column":19}},"677":{"start":{"line":678,"column":0},"end":{"line":678,"column":5}},"679":{"start":{"line":680,"column":0},"end":{"line":680,"column":39}},"680":{"start":{"line":681,"column":0},"end":{"line":681,"column":16}},"681":{"start":{"line":682,"column":0},"end":{"line":682,"column":41}},"682":{"start":{"line":683,"column":0},"end":{"line":683,"column":38}},"683":{"start":{"line":684,"column":0},"end":{"line":684,"column":30}},"684":{"start":{"line":685,"column":0},"end":{"line":685,"column":11}},"685":{"start":{"line":686,"column":0},"end":{"line":686,"column":50}},"686":{"start":{"line":687,"column":0},"end":{"line":687,"column":84}},"687":{"start":{"line":688,"column":0},"end":{"line":688,"column":38}},"688":{"start":{"line":689,"column":0},"end":{"line":689,"column":61}},"689":{"start":{"line":690,"column":0},"end":{"line":690,"column":19}},"690":{"start":{"line":691,"column":0},"end":{"line":691,"column":4}},"692":{"start":{"line":693,"column":0},"end":{"line":693,"column":9}},"693":{"start":{"line":694,"column":0},"end":{"line":694,"column":16}},"694":{"start":{"line":695,"column":0},"end":{"line":695,"column":16}},"695":{"start":{"line":696,"column":0},"end":{"line":696,"column":14}},"696":{"start":{"line":697,"column":0},"end":{"line":697,"column":19}},"697":{"start":{"line":698,"column":0},"end":{"line":698,"column":19}},"698":{"start":{"line":699,"column":0},"end":{"line":699,"column":25}},"699":{"start":{"line":700,"column":0},"end":{"line":700,"column":35}},"700":{"start":{"line":701,"column":0},"end":{"line":701,"column":38}},"701":{"start":{"line":702,"column":0},"end":{"line":702,"column":44}},"702":{"start":{"line":703,"column":0},"end":{"line":703,"column":21}},"703":{"start":{"line":704,"column":0},"end":{"line":704,"column":23}},"704":{"start":{"line":705,"column":0},"end":{"line":705,"column":19}},"705":{"start":{"line":706,"column":0},"end":{"line":706,"column":19}},"706":{"start":{"line":707,"column":0},"end":{"line":707,"column":37}},"707":{"start":{"line":708,"column":0},"end":{"line":708,"column":36}},"708":{"start":{"line":709,"column":0},"end":{"line":709,"column":49}},"709":{"start":{"line":710,"column":0},"end":{"line":710,"column":19}},"710":{"start":{"line":711,"column":0},"end":{"line":711,"column":37}},"711":{"start":{"line":712,"column":0},"end":{"line":712,"column":5}},"713":{"start":{"line":714,"column":0},"end":{"line":714,"column":19}},"715":{"start":{"line":716,"column":0},"end":{"line":716,"column":26}},"716":{"start":{"line":717,"column":0},"end":{"line":717,"column":39}},"718":{"start":{"line":719,"column":0},"end":{"line":719,"column":49}},"719":{"start":{"line":720,"column":0},"end":{"line":720,"column":29}},"720":{"start":{"line":721,"column":0},"end":{"line":721,"column":13}},"721":{"start":{"line":722,"column":0},"end":{"line":722,"column":5}},"723":{"start":{"line":724,"column":0},"end":{"line":724,"column":27}},"727":{"start":{"line":728,"column":0},"end":{"line":728,"column":51}},"728":{"start":{"line":729,"column":0},"end":{"line":729,"column":23}},"729":{"start":{"line":730,"column":0},"end":{"line":730,"column":47}},"730":{"start":{"line":731,"column":0},"end":{"line":731,"column":5}},"731":{"start":{"line":732,"column":0},"end":{"line":732,"column":73}},"733":{"start":{"line":734,"column":0},"end":{"line":734,"column":9}},"734":{"start":{"line":735,"column":0},"end":{"line":735,"column":53}},"736":{"start":{"line":737,"column":0},"end":{"line":737,"column":67}},"737":{"start":{"line":738,"column":0},"end":{"line":738,"column":20}},"738":{"start":{"line":739,"column":0},"end":{"line":739,"column":65}},"741":{"start":{"line":742,"column":0},"end":{"line":742,"column":55}},"742":{"start":{"line":743,"column":0},"end":{"line":743,"column":22}},"743":{"start":{"line":744,"column":0},"end":{"line":744,"column":73}},"744":{"start":{"line":745,"column":0},"end":{"line":745,"column":15}},"745":{"start":{"line":746,"column":0},"end":{"line":746,"column":7}},"747":{"start":{"line":748,"column":0},"end":{"line":748,"column":52}},"749":{"start":{"line":750,"column":0},"end":{"line":750,"column":60}},"750":{"start":{"line":751,"column":0},"end":{"line":751,"column":27}},"751":{"start":{"line":752,"column":0},"end":{"line":752,"column":62}},"752":{"start":{"line":753,"column":0},"end":{"line":753,"column":15}},"753":{"start":{"line":754,"column":0},"end":{"line":754,"column":7}},"755":{"start":{"line":756,"column":0},"end":{"line":756,"column":81}},"756":{"start":{"line":757,"column":0},"end":{"line":757,"column":78}},"757":{"start":{"line":758,"column":0},"end":{"line":758,"column":28}},"758":{"start":{"line":759,"column":0},"end":{"line":759,"column":11}},"759":{"start":{"line":760,"column":0},"end":{"line":760,"column":14}},"760":{"start":{"line":761,"column":0},"end":{"line":761,"column":80}},"761":{"start":{"line":762,"column":0},"end":{"line":762,"column":28}},"762":{"start":{"line":763,"column":0},"end":{"line":763,"column":11}},"763":{"start":{"line":764,"column":0},"end":{"line":764,"column":7}},"764":{"start":{"line":765,"column":0},"end":{"line":765,"column":21}},"765":{"start":{"line":766,"column":0},"end":{"line":766,"column":78}},"766":{"start":{"line":767,"column":0},"end":{"line":767,"column":5}},"767":{"start":{"line":768,"column":0},"end":{"line":768,"column":6}},"768":{"start":{"line":769,"column":0},"end":{"line":769,"column":22}},"769":{"start":{"line":770,"column":0},"end":{"line":770,"column":20}},"770":{"start":{"line":771,"column":0},"end":{"line":771,"column":37}},"771":{"start":{"line":772,"column":0},"end":{"line":772,"column":26}},"772":{"start":{"line":773,"column":0},"end":{"line":773,"column":26}},"773":{"start":{"line":774,"column":0},"end":{"line":774,"column":18}},"774":{"start":{"line":775,"column":0},"end":{"line":775,"column":30}},"775":{"start":{"line":776,"column":0},"end":{"line":776,"column":19}},"776":{"start":{"line":777,"column":0},"end":{"line":777,"column":32}},"777":{"start":{"line":778,"column":0},"end":{"line":778,"column":28}},"778":{"start":{"line":779,"column":0},"end":{"line":779,"column":23}},"779":{"start":{"line":780,"column":0},"end":{"line":780,"column":5}},"781":{"start":{"line":782,"column":0},"end":{"line":782,"column":55}},"782":{"start":{"line":783,"column":0},"end":{"line":783,"column":50}},"783":{"start":{"line":784,"column":0},"end":{"line":784,"column":42}},"784":{"start":{"line":785,"column":0},"end":{"line":785,"column":57}},"785":{"start":{"line":786,"column":0},"end":{"line":786,"column":13}},"786":{"start":{"line":787,"column":0},"end":{"line":787,"column":5}},"788":{"start":{"line":789,"column":0},"end":{"line":789,"column":37}},"789":{"start":{"line":790,"column":0},"end":{"line":790,"column":91}},"790":{"start":{"line":791,"column":0},"end":{"line":791,"column":6}},"791":{"start":{"line":792,"column":0},"end":{"line":792,"column":21}},"792":{"start":{"line":793,"column":0},"end":{"line":793,"column":13}},"793":{"start":{"line":794,"column":0},"end":{"line":794,"column":5}},"795":{"start":{"line":796,"column":0},"end":{"line":796,"column":33}},"796":{"start":{"line":797,"column":0},"end":{"line":797,"column":9}},"797":{"start":{"line":798,"column":0},"end":{"line":798,"column":24}},"798":{"start":{"line":799,"column":0},"end":{"line":799,"column":31}},"799":{"start":{"line":800,"column":0},"end":{"line":800,"column":7}},"800":{"start":{"line":801,"column":0},"end":{"line":801,"column":56}},"801":{"start":{"line":802,"column":0},"end":{"line":802,"column":38}},"802":{"start":{"line":803,"column":0},"end":{"line":803,"column":21}},"803":{"start":{"line":804,"column":0},"end":{"line":804,"column":62}},"804":{"start":{"line":805,"column":0},"end":{"line":805,"column":15}},"805":{"start":{"line":806,"column":0},"end":{"line":806,"column":36}},"806":{"start":{"line":807,"column":0},"end":{"line":807,"column":5}},"807":{"start":{"line":808,"column":0},"end":{"line":808,"column":6}},"808":{"start":{"line":809,"column":0},"end":{"line":809,"column":22}},"809":{"start":{"line":810,"column":0},"end":{"line":810,"column":16}},"810":{"start":{"line":811,"column":0},"end":{"line":811,"column":24}},"811":{"start":{"line":812,"column":0},"end":{"line":812,"column":19}},"812":{"start":{"line":813,"column":0},"end":{"line":813,"column":17}},"813":{"start":{"line":814,"column":0},"end":{"line":814,"column":27}},"814":{"start":{"line":815,"column":0},"end":{"line":815,"column":19}},"815":{"start":{"line":816,"column":0},"end":{"line":816,"column":5}},"817":{"start":{"line":818,"column":0},"end":{"line":818,"column":10}},"818":{"start":{"line":819,"column":0},"end":{"line":819,"column":59}},"819":{"start":{"line":820,"column":0},"end":{"line":820,"column":36}},"820":{"start":{"line":821,"column":0},"end":{"line":821,"column":48}},"821":{"start":{"line":822,"column":0},"end":{"line":822,"column":12}},"823":{"start":{"line":824,"column":0},"end":{"line":824,"column":66}},"824":{"start":{"line":825,"column":0},"end":{"line":825,"column":43}},"825":{"start":{"line":826,"column":0},"end":{"line":826,"column":54}},"826":{"start":{"line":827,"column":0},"end":{"line":827,"column":14}},"827":{"start":{"line":828,"column":0},"end":{"line":828,"column":61}},"828":{"start":{"line":829,"column":0},"end":{"line":829,"column":17}},"829":{"start":{"line":830,"column":0},"end":{"line":830,"column":25}},"830":{"start":{"line":831,"column":0},"end":{"line":831,"column":51}},"831":{"start":{"line":832,"column":0},"end":{"line":832,"column":39}},"832":{"start":{"line":833,"column":0},"end":{"line":833,"column":29}},"833":{"start":{"line":834,"column":0},"end":{"line":834,"column":42}},"834":{"start":{"line":835,"column":0},"end":{"line":835,"column":50}},"835":{"start":{"line":836,"column":0},"end":{"line":836,"column":42}},"837":{"start":{"line":838,"column":0},"end":{"line":838,"column":42}},"838":{"start":{"line":839,"column":0},"end":{"line":839,"column":19}},"839":{"start":{"line":840,"column":0},"end":{"line":840,"column":17}},"840":{"start":{"line":841,"column":0},"end":{"line":841,"column":25}},"841":{"start":{"line":842,"column":0},"end":{"line":842,"column":82}},"842":{"start":{"line":843,"column":0},"end":{"line":843,"column":42}},"843":{"start":{"line":844,"column":0},"end":{"line":844,"column":28}},"844":{"start":{"line":845,"column":0},"end":{"line":845,"column":40}},"845":{"start":{"line":846,"column":0},"end":{"line":846,"column":23}},"846":{"start":{"line":847,"column":0},"end":{"line":847,"column":15}},"847":{"start":{"line":848,"column":0},"end":{"line":848,"column":47}},"848":{"start":{"line":849,"column":0},"end":{"line":849,"column":14}},"849":{"start":{"line":850,"column":0},"end":{"line":850,"column":32}},"850":{"start":{"line":851,"column":0},"end":{"line":851,"column":40}},"851":{"start":{"line":852,"column":0},"end":{"line":852,"column":42}},"853":{"start":{"line":854,"column":0},"end":{"line":854,"column":84}},"854":{"start":{"line":855,"column":0},"end":{"line":855,"column":68}},"855":{"start":{"line":856,"column":0},"end":{"line":856,"column":19}},"856":{"start":{"line":857,"column":0},"end":{"line":857,"column":39}},"857":{"start":{"line":858,"column":0},"end":{"line":858,"column":19}},"859":{"start":{"line":860,"column":0},"end":{"line":860,"column":28}},"860":{"start":{"line":861,"column":0},"end":{"line":861,"column":68}},"861":{"start":{"line":862,"column":0},"end":{"line":862,"column":36}},"862":{"start":{"line":863,"column":0},"end":{"line":863,"column":32}},"863":{"start":{"line":864,"column":0},"end":{"line":864,"column":31}},"864":{"start":{"line":865,"column":0},"end":{"line":865,"column":38}},"865":{"start":{"line":866,"column":0},"end":{"line":866,"column":59}},"866":{"start":{"line":867,"column":0},"end":{"line":867,"column":53}},"867":{"start":{"line":868,"column":0},"end":{"line":868,"column":34}},"868":{"start":{"line":869,"column":0},"end":{"line":869,"column":47}},"869":{"start":{"line":870,"column":0},"end":{"line":870,"column":43}},"870":{"start":{"line":871,"column":0},"end":{"line":871,"column":20}},"871":{"start":{"line":872,"column":0},"end":{"line":872,"column":48}},"873":{"start":{"line":874,"column":0},"end":{"line":874,"column":29}},"874":{"start":{"line":875,"column":0},"end":{"line":875,"column":34}},"875":{"start":{"line":876,"column":0},"end":{"line":876,"column":17}},"876":{"start":{"line":877,"column":0},"end":{"line":877,"column":69}},"877":{"start":{"line":878,"column":0},"end":{"line":878,"column":30}},"878":{"start":{"line":879,"column":0},"end":{"line":879,"column":32}},"880":{"start":{"line":881,"column":0},"end":{"line":881,"column":41}},"881":{"start":{"line":882,"column":0},"end":{"line":882,"column":18}},"882":{"start":{"line":883,"column":0},"end":{"line":883,"column":46}},"883":{"start":{"line":884,"column":0},"end":{"line":884,"column":15}},"885":{"start":{"line":886,"column":0},"end":{"line":886,"column":32}},"886":{"start":{"line":887,"column":0},"end":{"line":887,"column":30}},"887":{"start":{"line":888,"column":0},"end":{"line":888,"column":39}},"888":{"start":{"line":889,"column":0},"end":{"line":889,"column":46}},"889":{"start":{"line":890,"column":0},"end":{"line":890,"column":15}},"891":{"start":{"line":892,"column":0},"end":{"line":892,"column":32}},"892":{"start":{"line":893,"column":0},"end":{"line":893,"column":29}},"894":{"start":{"line":895,"column":0},"end":{"line":895,"column":14}},"895":{"start":{"line":896,"column":0},"end":{"line":896,"column":16}},"897":{"start":{"line":898,"column":0},"end":{"line":898,"column":66}},"898":{"start":{"line":899,"column":0},"end":{"line":899,"column":43}},"899":{"start":{"line":900,"column":0},"end":{"line":900,"column":50}},"900":{"start":{"line":901,"column":0},"end":{"line":901,"column":61}},"901":{"start":{"line":902,"column":0},"end":{"line":902,"column":49}},"902":{"start":{"line":903,"column":0},"end":{"line":903,"column":27}},"903":{"start":{"line":904,"column":0},"end":{"line":904,"column":29}},"904":{"start":{"line":905,"column":0},"end":{"line":905,"column":89}},"905":{"start":{"line":906,"column":0},"end":{"line":906,"column":37}},"906":{"start":{"line":907,"column":0},"end":{"line":907,"column":37}},"907":{"start":{"line":908,"column":0},"end":{"line":908,"column":18}},"908":{"start":{"line":909,"column":0},"end":{"line":909,"column":16}},"909":{"start":{"line":910,"column":0},"end":{"line":910,"column":53}},"910":{"start":{"line":911,"column":0},"end":{"line":911,"column":19}},"911":{"start":{"line":912,"column":0},"end":{"line":912,"column":27}},"912":{"start":{"line":913,"column":0},"end":{"line":913,"column":52}},"913":{"start":{"line":914,"column":0},"end":{"line":914,"column":44}},"914":{"start":{"line":915,"column":0},"end":{"line":915,"column":61}},"915":{"start":{"line":916,"column":0},"end":{"line":916,"column":69}},"916":{"start":{"line":917,"column":0},"end":{"line":917,"column":40}},"917":{"start":{"line":918,"column":0},"end":{"line":918,"column":21}},"918":{"start":{"line":919,"column":0},"end":{"line":919,"column":35}},"919":{"start":{"line":920,"column":0},"end":{"line":920,"column":51}},"920":{"start":{"line":921,"column":0},"end":{"line":921,"column":33}},"921":{"start":{"line":922,"column":0},"end":{"line":922,"column":42}},"922":{"start":{"line":923,"column":0},"end":{"line":923,"column":31}},"923":{"start":{"line":924,"column":0},"end":{"line":924,"column":34}},"924":{"start":{"line":925,"column":0},"end":{"line":925,"column":35}},"926":{"start":{"line":927,"column":0},"end":{"line":927,"column":30}},"927":{"start":{"line":928,"column":0},"end":{"line":928,"column":59}},"928":{"start":{"line":929,"column":0},"end":{"line":929,"column":25}},"929":{"start":{"line":930,"column":0},"end":{"line":930,"column":17}},"930":{"start":{"line":931,"column":0},"end":{"line":931,"column":34}},"931":{"start":{"line":932,"column":0},"end":{"line":932,"column":40}},"932":{"start":{"line":933,"column":0},"end":{"line":933,"column":25}},"933":{"start":{"line":934,"column":0},"end":{"line":934,"column":17}},"934":{"start":{"line":935,"column":0},"end":{"line":935,"column":53}},"935":{"start":{"line":936,"column":0},"end":{"line":936,"column":16}},"937":{"start":{"line":938,"column":0},"end":{"line":938,"column":74}},"938":{"start":{"line":939,"column":0},"end":{"line":939,"column":21}},"939":{"start":{"line":940,"column":0},"end":{"line":940,"column":19}},"940":{"start":{"line":941,"column":0},"end":{"line":941,"column":27}},"941":{"start":{"line":942,"column":0},"end":{"line":942,"column":52}},"942":{"start":{"line":943,"column":0},"end":{"line":943,"column":44}},"943":{"start":{"line":944,"column":0},"end":{"line":944,"column":84}},"944":{"start":{"line":945,"column":0},"end":{"line":945,"column":41}},"945":{"start":{"line":946,"column":0},"end":{"line":946,"column":21}},"946":{"start":{"line":947,"column":0},"end":{"line":947,"column":35}},"947":{"start":{"line":948,"column":0},"end":{"line":948,"column":51}},"948":{"start":{"line":949,"column":0},"end":{"line":949,"column":33}},"949":{"start":{"line":950,"column":0},"end":{"line":950,"column":53}},"950":{"start":{"line":951,"column":0},"end":{"line":951,"column":61}},"952":{"start":{"line":953,"column":0},"end":{"line":953,"column":30}},"953":{"start":{"line":954,"column":0},"end":{"line":954,"column":43}},"954":{"start":{"line":955,"column":0},"end":{"line":955,"column":16}},"956":{"start":{"line":957,"column":0},"end":{"line":957,"column":71}},"957":{"start":{"line":958,"column":0},"end":{"line":958,"column":21}},"958":{"start":{"line":959,"column":0},"end":{"line":959,"column":16}},"959":{"start":{"line":960,"column":0},"end":{"line":960,"column":14}},"960":{"start":{"line":961,"column":0},"end":{"line":961,"column":41}},"961":{"start":{"line":962,"column":0},"end":{"line":962,"column":24}},"962":{"start":{"line":963,"column":0},"end":{"line":963,"column":64}},"963":{"start":{"line":964,"column":0},"end":{"line":964,"column":25}},"964":{"start":{"line":965,"column":0},"end":{"line":965,"column":18}},"965":{"start":{"line":966,"column":0},"end":{"line":966,"column":19}},"966":{"start":{"line":967,"column":0},"end":{"line":967,"column":34}},"967":{"start":{"line":968,"column":0},"end":{"line":968,"column":41}},"969":{"start":{"line":970,"column":0},"end":{"line":970,"column":18}},"970":{"start":{"line":971,"column":0},"end":{"line":971,"column":30}},"971":{"start":{"line":972,"column":0},"end":{"line":972,"column":40}},"972":{"start":{"line":973,"column":0},"end":{"line":973,"column":56}},"973":{"start":{"line":974,"column":0},"end":{"line":974,"column":69}},"974":{"start":{"line":975,"column":0},"end":{"line":975,"column":91}},"975":{"start":{"line":976,"column":0},"end":{"line":976,"column":67}},"976":{"start":{"line":977,"column":0},"end":{"line":977,"column":34}},"977":{"start":{"line":978,"column":0},"end":{"line":978,"column":46}},"978":{"start":{"line":979,"column":0},"end":{"line":979,"column":36}},"979":{"start":{"line":980,"column":0},"end":{"line":980,"column":43}},"980":{"start":{"line":981,"column":0},"end":{"line":981,"column":30}},"981":{"start":{"line":982,"column":0},"end":{"line":982,"column":19}},"982":{"start":{"line":983,"column":0},"end":{"line":983,"column":68}},"983":{"start":{"line":984,"column":0},"end":{"line":984,"column":67}},"984":{"start":{"line":985,"column":0},"end":{"line":985,"column":75}},"986":{"start":{"line":987,"column":0},"end":{"line":987,"column":24}},"987":{"start":{"line":988,"column":0},"end":{"line":988,"column":108}},"988":{"start":{"line":989,"column":0},"end":{"line":989,"column":52}},"989":{"start":{"line":990,"column":0},"end":{"line":990,"column":68}},"990":{"start":{"line":991,"column":0},"end":{"line":991,"column":40}},"991":{"start":{"line":992,"column":0},"end":{"line":992,"column":54}},"992":{"start":{"line":993,"column":0},"end":{"line":993,"column":41}},"993":{"start":{"line":994,"column":0},"end":{"line":994,"column":91}},"995":{"start":{"line":996,"column":0},"end":{"line":996,"column":48}},"997":{"start":{"line":998,"column":0},"end":{"line":998,"column":29}},"998":{"start":{"line":999,"column":0},"end":{"line":999,"column":76}},"999":{"start":{"line":1000,"column":0},"end":{"line":1000,"column":40}},"1000":{"start":{"line":1001,"column":0},"end":{"line":1001,"column":58}},"1001":{"start":{"line":1002,"column":0},"end":{"line":1002,"column":65}},"1002":{"start":{"line":1003,"column":0},"end":{"line":1003,"column":31}},"1003":{"start":{"line":1004,"column":0},"end":{"line":1004,"column":31}},"1004":{"start":{"line":1005,"column":0},"end":{"line":1005,"column":26}},"1005":{"start":{"line":1006,"column":0},"end":{"line":1006,"column":52}},"1006":{"start":{"line":1007,"column":0},"end":{"line":1007,"column":52}},"1007":{"start":{"line":1008,"column":0},"end":{"line":1008,"column":58}},"1008":{"start":{"line":1009,"column":0},"end":{"line":1009,"column":67}},"1009":{"start":{"line":1010,"column":0},"end":{"line":1010,"column":36}},"1010":{"start":{"line":1011,"column":0},"end":{"line":1011,"column":37}},"1011":{"start":{"line":1012,"column":0},"end":{"line":1012,"column":69}},"1012":{"start":{"line":1013,"column":0},"end":{"line":1013,"column":40}},"1013":{"start":{"line":1014,"column":0},"end":{"line":1014,"column":28}},"1014":{"start":{"line":1015,"column":0},"end":{"line":1015,"column":77}},"1015":{"start":{"line":1016,"column":0},"end":{"line":1016,"column":34}},"1016":{"start":{"line":1017,"column":0},"end":{"line":1017,"column":33}},"1017":{"start":{"line":1018,"column":0},"end":{"line":1018,"column":60}},"1018":{"start":{"line":1019,"column":0},"end":{"line":1019,"column":68}},"1019":{"start":{"line":1020,"column":0},"end":{"line":1020,"column":54}},"1021":{"start":{"line":1022,"column":0},"end":{"line":1022,"column":77}},"1022":{"start":{"line":1023,"column":0},"end":{"line":1023,"column":72}},"1023":{"start":{"line":1024,"column":0},"end":{"line":1024,"column":37}},"1024":{"start":{"line":1025,"column":0},"end":{"line":1025,"column":85}},"1025":{"start":{"line":1026,"column":0},"end":{"line":1026,"column":35}},"1027":{"start":{"line":1028,"column":0},"end":{"line":1028,"column":27}},"1028":{"start":{"line":1029,"column":0},"end":{"line":1029,"column":28}},"1029":{"start":{"line":1030,"column":0},"end":{"line":1030,"column":29}},"1030":{"start":{"line":1031,"column":0},"end":{"line":1031,"column":23}},"1032":{"start":{"line":1033,"column":0},"end":{"line":1033,"column":17}},"1033":{"start":{"line":1034,"column":0},"end":{"line":1034,"column":17}},"1035":{"start":{"line":1036,"column":0},"end":{"line":1036,"column":41}},"1037":{"start":{"line":1038,"column":0},"end":{"line":1038,"column":18}},"1039":{"start":{"line":1040,"column":0},"end":{"line":1040,"column":14}},"1040":{"start":{"line":1041,"column":0},"end":{"line":1041,"column":16}},"1042":{"start":{"line":1043,"column":0},"end":{"line":1043,"column":25}},"1043":{"start":{"line":1044,"column":0},"end":{"line":1044,"column":28}},"1044":{"start":{"line":1045,"column":0},"end":{"line":1045,"column":31}},"1045":{"start":{"line":1046,"column":0},"end":{"line":1046,"column":47}},"1046":{"start":{"line":1047,"column":0},"end":{"line":1047,"column":48}},"1047":{"start":{"line":1048,"column":0},"end":{"line":1048,"column":51}},"1048":{"start":{"line":1049,"column":0},"end":{"line":1049,"column":55}},"1049":{"start":{"line":1050,"column":0},"end":{"line":1050,"column":49}},"1050":{"start":{"line":1051,"column":0},"end":{"line":1051,"column":10}},"1052":{"start":{"line":1053,"column":0},"end":{"line":1053,"column":10}},"1054":{"start":{"line":1055,"column":0},"end":{"line":1055,"column":1}}},"s":{"0":1,"1":1,"2":1,"3":1,"4":1,"5":1,"6":1,"7":1,"8":1,"10":1,"11":1,"15":1,"16":1,"31":1,"44":1,"45":1,"97":1,"98":1,"99":1,"100":1,"101":1,"102":1,"104":1,"105":1,"106":1,"107":1,"108":1,"109":1,"110":1,"111":1,"112":1,"113":1,"114":1,"115":1,"116":1,"117":1,"133":1,"134":1,"135":1,"136":1,"141":40,"142":40,"143":40,"144":40,"145":40,"146":40,"147":40,"148":0,"149":0,"150":0,"151":0,"152":40,"153":40,"154":0,"155":0,"156":40,"157":40,"159":75,"160":75,"161":75,"162":1,"163":1,"164":75,"165":15,"166":15,"167":59,"168":59,"170":344,"171":344,"172":344,"173":344,"174":344,"175":344,"176":344,"177":344,"178":14,"179":14,"180":344,"181":59,"182":59,"183":344,"184":105,"185":105,"186":166,"187":166,"189":110,"190":110,"191":110,"192":110,"193":110,"194":110,"195":110,"196":170,"197":170,"198":110,"199":110,"200":0,"201":0,"202":0,"203":0,"205":110,"207":330,"208":330,"209":330,"210":330,"211":330,"212":330,"213":224,"214":224,"215":330,"216":4,"217":4,"218":330,"219":59,"220":59,"221":43,"222":43,"224":349,"225":349,"226":349,"227":349,"228":349,"229":349,"230":349,"236":349,"237":349,"238":19,"239":19,"241":330,"242":349,"244":349,"245":4,"246":0,"247":4,"248":4,"250":326,"251":326,"253":4,"254":4,"255":4,"256":4,"257":4,"258":4,"259":4,"260":4,"267":4,"268":4,"269":4,"270":1,"271":0,"272":0,"273":1,"274":1,"275":1,"276":4,"277":4,"278":4,"280":4,"281":3,"282":1,"283":1,"284":1,"285":1,"286":1,"287":4,"288":3,"289":1,"291":4,"292":4,"293":4,"294":4,"295":4,"297":4,"298":4,"300":4,"301":4,"302":4,"303":4,"304":3,"305":3,"306":3,"307":3,"308":3,"309":3,"310":3,"311":3,"312":3,"313":3,"314":3,"315":3,"316":3,"317":3,"319":0,"321":3,"322":3,"323":3,"324":3,"325":3,"326":3,"327":1,"328":1,"329":1,"330":3,"331":3,"333":3,"334":3,"335":3,"337":1,"340":4,"341":5,"342":5,"343":5,"344":5,"345":5,"346":5,"347":5,"348":5,"349":5,"350":5,"351":5,"352":1,"353":1,"354":1,"355":5,"357":5,"358":5,"359":5,"360":5,"362":4,"363":4,"364":4,"365":4,"367":4,"368":4,"369":4,"371":4,"373":1,"374":323,"375":323,"376":323,"377":323,"378":323,"379":323,"380":323,"381":323,"382":323,"399":323,"400":323,"401":323,"402":323,"403":323,"404":323,"405":323,"406":323,"407":323,"408":323,"409":323,"410":323,"411":323,"412":323,"413":323,"414":323,"415":323,"416":323,"417":323,"418":323,"420":323,"421":323,"422":89,"423":89,"424":0,"425":0,"426":89,"427":89,"428":89,"429":89,"430":88,"431":88,"432":88,"433":88,"435":89,"436":323,"437":323,"439":323,"440":323,"441":45,"442":43,"443":43,"444":45,"445":45,"446":0,"447":0,"448":45,"449":45,"452":0,"453":0,"454":2,"455":2,"456":45,"457":45,"458":45,"459":2,"460":2,"461":2,"462":45,"463":323,"464":323,"466":323,"467":323,"468":323,"469":323,"470":323,"472":323,"473":44,"474":44,"475":44,"476":44,"477":0,"478":0,"479":44,"480":44,"481":44,"482":0,"483":44,"484":44,"485":44,"486":44,"487":323,"489":323,"490":44,"491":44,"492":3,"493":3,"494":3,"495":3,"496":3,"497":3,"498":41,"499":41,"500":41,"501":0,"502":0,"503":41,"504":41,"505":41,"506":0,"507":41,"508":41,"509":41,"510":41,"511":323,"513":323,"514":323,"515":194,"516":0,"517":0,"518":194,"519":194,"520":194,"521":194,"522":194,"523":0,"524":194,"525":194,"526":194,"527":194,"528":323,"529":323,"531":323,"532":44,"533":44,"534":3,"535":3,"536":3,"537":3,"538":3,"540":41,"541":193,"542":0,"543":0,"544":193,"545":193,"547":41,"548":41,"549":41,"550":41,"551":41,"552":41,"553":323,"555":323,"556":90,"557":40,"558":75,"559":75,"560":75,"561":75,"562":75,"563":75,"564":40,"566":40,"567":40,"568":40,"569":40,"570":40,"571":90,"572":323,"574":323,"575":323,"576":1,"577":0,"578":0,"579":1,"580":1,"581":1,"582":1,"583":1,"584":0,"585":1,"586":1,"587":323,"588":323,"590":323,"591":1,"592":0,"593":0,"594":1,"595":1,"596":1,"597":1,"598":1,"599":0,"600":1,"601":323,"603":323,"604":323,"605":180,"606":143,"607":323,"609":323,"610":323,"612":323,"614":323,"615":323,"616":323,"617":323,"618":323,"620":323,"621":44,"622":41,"623":41,"624":3,"625":323,"627":323,"628":127,"629":88,"630":88,"631":127,"632":0,"633":0,"634":323,"636":323,"637":85,"638":85,"639":48,"640":48,"641":48,"642":48,"643":48,"645":85,"646":85,"647":188,"648":188,"649":188,"650":188,"651":188,"652":188,"653":188,"654":0,"655":0,"656":188,"657":188,"658":0,"659":0,"660":0,"661":0,"662":0,"663":0,"664":188,"666":85,"667":85,"668":85,"669":37,"670":37,"671":37,"672":323,"673":323,"674":323,"675":323,"676":323,"677":323,"679":323,"680":44,"681":44,"682":44,"683":44,"684":43,"685":44,"686":323,"687":323,"688":323,"689":323,"690":323,"692":323,"693":323,"694":323,"695":323,"696":323,"697":323,"698":323,"699":323,"700":323,"701":323,"702":323,"703":323,"704":323,"705":323,"706":323,"707":323,"708":323,"709":323,"710":323,"711":323,"713":323,"715":81,"716":323,"718":323,"719":1,"720":0,"721":0,"723":1,"727":1,"728":1,"729":1,"730":1,"731":1,"733":1,"734":1,"736":1,"737":1,"738":1,"741":1,"742":1,"743":0,"744":0,"745":0,"747":1,"749":1,"750":1,"751":0,"752":0,"753":0,"755":1,"756":1,"757":1,"758":1,"759":1,"760":0,"761":0,"762":0,"763":0,"764":1,"765":0,"766":0,"767":323,"768":323,"769":323,"770":323,"771":323,"772":323,"773":323,"774":323,"775":323,"776":323,"777":323,"778":323,"779":323,"781":323,"782":1,"783":1,"784":1,"785":0,"786":0,"788":1,"789":1,"790":1,"791":1,"792":0,"793":0,"795":1,"796":1,"797":1,"798":0,"799":0,"800":1,"801":1,"802":1,"803":0,"804":1,"805":1,"806":1,"807":323,"808":323,"809":323,"810":323,"811":323,"812":323,"813":323,"814":323,"815":323,"817":323,"818":323,"819":323,"820":323,"821":323,"823":323,"824":323,"825":323,"826":323,"827":323,"828":323,"829":323,"830":323,"831":323,"832":323,"833":323,"834":323,"835":323,"837":323,"838":323,"839":323,"840":323,"841":323,"842":323,"843":323,"844":39,"845":0,"846":0,"847":39,"848":39,"849":323,"850":323,"851":323,"853":323,"854":323,"855":323,"856":323,"857":323,"859":323,"860":42,"861":42,"862":38,"863":38,"864":38,"865":38,"866":38,"867":38,"868":37,"869":37,"870":37,"871":38,"873":38,"874":38,"875":42,"876":42,"877":42,"878":42,"880":0,"881":0,"882":42,"883":42,"885":42,"886":42,"887":42,"888":42,"889":42,"891":42,"892":42,"894":323,"895":323,"897":323,"898":323,"899":323,"900":323,"901":323,"902":323,"903":180,"904":19,"905":161,"906":143,"907":323,"908":323,"909":323,"910":323,"911":323,"912":323,"913":323,"914":323,"915":323,"916":323,"917":323,"918":323,"919":9,"920":314,"921":134,"922":180,"923":53,"924":127,"926":323,"927":51,"928":0,"929":0,"930":51,"931":17,"932":17,"933":17,"934":34,"935":51,"937":323,"938":323,"939":323,"940":323,"941":323,"942":323,"943":323,"944":323,"945":323,"946":323,"947":9,"948":314,"949":134,"950":180,"952":323,"953":1,"954":1,"956":323,"957":323,"958":323,"959":323,"960":323,"961":323,"962":12,"963":12,"964":12,"965":311,"966":323,"967":9,"969":9,"970":314,"971":180,"972":180,"973":349,"974":349,"975":349,"976":349,"977":349,"978":349,"979":349,"980":349,"981":349,"982":349,"983":349,"984":349,"986":349,"987":349,"988":349,"989":349,"990":349,"991":6,"992":343,"993":104,"995":239,"997":349,"998":349,"999":349,"1000":19,"1001":19,"1002":19,"1003":330,"1004":349,"1005":349,"1006":120,"1007":120,"1008":344,"1009":344,"1010":344,"1011":344,"1012":344,"1013":344,"1014":344,"1015":344,"1016":344,"1017":344,"1018":344,"1019":344,"1021":344,"1022":344,"1023":344,"1024":344,"1025":344,"1027":120,"1028":120,"1029":229,"1030":349,"1032":180,"1033":180,"1035":134,"1037":134,"1039":323,"1040":323,"1042":323,"1043":4,"1044":4,"1045":4,"1046":4,"1047":4,"1048":4,"1049":4,"1050":4,"1052":323,"1054":323},"branchMap":{"0":{"type":"branch","line":142,"loc":{"start":{"line":142,"column":0},"end":{"line":158,"column":1}},"locations":[{"start":{"line":142,"column":0},"end":{"line":158,"column":1}}]},"1":{"type":"branch","line":143,"loc":{"start":{"line":143,"column":33},"end":{"line":143,"column":41}},"locations":[{"start":{"line":143,"column":33},"end":{"line":143,"column":41}}]},"2":{"type":"branch","line":144,"loc":{"start":{"line":144,"column":31},"end":{"line":144,"column":39}},"locations":[{"start":{"line":144,"column":31},"end":{"line":144,"column":39}}]},"3":{"type":"branch","line":145,"loc":{"start":{"line":145,"column":27},"end":{"line":145,"column":35}},"locations":[{"start":{"line":145,"column":27},"end":{"line":145,"column":35}}]},"4":{"type":"branch","line":148,"loc":{"start":{"line":148,"column":2},"end":{"line":153,"column":61}},"locations":[{"start":{"line":148,"column":2},"end":{"line":153,"column":61}}]},"5":{"type":"branch","line":153,"loc":{"start":{"line":153,"column":53},"end":{"line":153,"column":66}},"locations":[{"start":{"line":153,"column":53},"end":{"line":153,"column":66}}]},"6":{"type":"branch","line":154,"loc":{"start":{"line":154,"column":6},"end":{"line":154,"column":23}},"locations":[{"start":{"line":154,"column":6},"end":{"line":154,"column":23}}]},"7":{"type":"branch","line":154,"loc":{"start":{"line":154,"column":23},"end":{"line":157,"column":28}},"locations":[{"start":{"line":154,"column":23},"end":{"line":157,"column":28}}]},"8":{"type":"branch","line":157,"loc":{"start":{"line":157,"column":22},"end":{"line":157,"column":44}},"locations":[{"start":{"line":157,"column":22},"end":{"line":157,"column":44}}]},"9":{"type":"branch","line":160,"loc":{"start":{"line":160,"column":0},"end":{"line":169,"column":1}},"locations":[{"start":{"line":160,"column":0},"end":{"line":169,"column":1}}]},"10":{"type":"branch","line":162,"loc":{"start":{"line":162,"column":34},"end":{"line":164,"column":3}},"locations":[{"start":{"line":162,"column":34},"end":{"line":164,"column":3}}]},"11":{"type":"branch","line":164,"loc":{"start":{"line":164,"column":2},"end":{"line":165,"column":36}},"locations":[{"start":{"line":164,"column":2},"end":{"line":165,"column":36}}]},"12":{"type":"branch","line":165,"loc":{"start":{"line":165,"column":36},"end":{"line":167,"column":3}},"locations":[{"start":{"line":165,"column":36},"end":{"line":167,"column":3}}]},"13":{"type":"branch","line":167,"loc":{"start":{"line":167,"column":2},"end":{"line":169,"column":1}},"locations":[{"start":{"line":167,"column":2},"end":{"line":169,"column":1}}]},"14":{"type":"branch","line":171,"loc":{"start":{"line":171,"column":0},"end":{"line":188,"column":1}},"locations":[{"start":{"line":171,"column":0},"end":{"line":188,"column":1}}]},"15":{"type":"branch","line":178,"loc":{"start":{"line":178,"column":6},"end":{"line":178,"column":30}},"locations":[{"start":{"line":178,"column":6},"end":{"line":178,"column":30}}]},"16":{"type":"branch","line":178,"loc":{"start":{"line":178,"column":30},"end":{"line":178,"column":73}},"locations":[{"start":{"line":178,"column":30},"end":{"line":178,"column":73}}]},"17":{"type":"branch","line":178,"loc":{"start":{"line":178,"column":73},"end":{"line":180,"column":3}},"locations":[{"start":{"line":178,"column":73},"end":{"line":180,"column":3}}]},"18":{"type":"branch","line":180,"loc":{"start":{"line":180,"column":2},"end":{"line":181,"column":42}},"locations":[{"start":{"line":180,"column":2},"end":{"line":181,"column":42}}]},"19":{"type":"branch","line":181,"loc":{"start":{"line":181,"column":42},"end":{"line":183,"column":3}},"locations":[{"start":{"line":181,"column":42},"end":{"line":183,"column":3}}]},"20":{"type":"branch","line":183,"loc":{"start":{"line":183,"column":2},"end":{"line":184,"column":40}},"locations":[{"start":{"line":183,"column":2},"end":{"line":184,"column":40}}]},"21":{"type":"branch","line":184,"loc":{"start":{"line":184,"column":40},"end":{"line":186,"column":3}},"locations":[{"start":{"line":184,"column":40},"end":{"line":186,"column":3}}]},"22":{"type":"branch","line":186,"loc":{"start":{"line":186,"column":2},"end":{"line":188,"column":1}},"locations":[{"start":{"line":186,"column":2},"end":{"line":188,"column":1}}]},"23":{"type":"branch","line":190,"loc":{"start":{"line":190,"column":0},"end":{"line":206,"column":1}},"locations":[{"start":{"line":190,"column":0},"end":{"line":206,"column":1}}]},"24":{"type":"branch","line":200,"loc":{"start":{"line":200,"column":2},"end":{"line":204,"column":80}},"locations":[{"start":{"line":200,"column":2},"end":{"line":204,"column":80}}]},"25":{"type":"branch","line":196,"loc":{"start":{"line":196,"column":6},"end":{"line":198,"column":20}},"locations":[{"start":{"line":196,"column":6},"end":{"line":198,"column":20}}]},"26":{"type":"branch","line":197,"loc":{"start":{"line":197,"column":35},"end":{"line":197,"column":51}},"locations":[{"start":{"line":197,"column":35},"end":{"line":197,"column":51}}]},"27":{"type":"branch","line":208,"loc":{"start":{"line":208,"column":0},"end":{"line":223,"column":1}},"locations":[{"start":{"line":208,"column":0},"end":{"line":223,"column":1}}]},"28":{"type":"branch","line":213,"loc":{"start":{"line":213,"column":34},"end":{"line":215,"column":3}},"locations":[{"start":{"line":213,"column":34},"end":{"line":215,"column":3}}]},"29":{"type":"branch","line":215,"loc":{"start":{"line":215,"column":2},"end":{"line":216,"column":57}},"locations":[{"start":{"line":215,"column":2},"end":{"line":216,"column":57}}]},"30":{"type":"branch","line":216,"loc":{"start":{"line":216,"column":57},"end":{"line":218,"column":3}},"locations":[{"start":{"line":216,"column":57},"end":{"line":218,"column":3}}]},"31":{"type":"branch","line":218,"loc":{"start":{"line":218,"column":2},"end":{"line":219,"column":85}},"locations":[{"start":{"line":218,"column":2},"end":{"line":219,"column":85}}]},"32":{"type":"branch","line":219,"loc":{"start":{"line":219,"column":85},"end":{"line":221,"column":3}},"locations":[{"start":{"line":219,"column":85},"end":{"line":221,"column":3}}]},"33":{"type":"branch","line":221,"loc":{"start":{"line":221,"column":2},"end":{"line":223,"column":1}},"locations":[{"start":{"line":221,"column":2},"end":{"line":223,"column":1}}]},"34":{"type":"branch","line":219,"loc":{"start":{"line":219,"column":25},"end":{"line":219,"column":82}},"locations":[{"start":{"line":219,"column":25},"end":{"line":219,"column":82}}]},"35":{"type":"branch","line":225,"loc":{"start":{"line":225,"column":0},"end":{"line":252,"column":1}},"locations":[{"start":{"line":225,"column":0},"end":{"line":252,"column":1}}]},"36":{"type":"branch","line":238,"loc":{"start":{"line":238,"column":6},"end":{"line":238,"column":30}},"locations":[{"start":{"line":238,"column":6},"end":{"line":238,"column":30}}]},"37":{"type":"branch","line":238,"loc":{"start":{"line":238,"column":38},"end":{"line":240,"column":3}},"locations":[{"start":{"line":238,"column":38},"end":{"line":240,"column":3}}]},"38":{"type":"branch","line":240,"loc":{"start":{"line":240,"column":2},"end":{"line":243,"column":47}},"locations":[{"start":{"line":240,"column":2},"end":{"line":243,"column":47}}]},"39":{"type":"branch","line":243,"loc":{"start":{"line":243,"column":25},"end":{"line":243,"column":62}},"locations":[{"start":{"line":243,"column":25},"end":{"line":243,"column":62}}]},"40":{"type":"branch","line":245,"loc":{"start":{"line":245,"column":6},"end":{"line":245,"column":54}},"locations":[{"start":{"line":245,"column":6},"end":{"line":245,"column":54}}]},"41":{"type":"branch","line":245,"loc":{"start":{"line":245,"column":54},"end":{"line":249,"column":3}},"locations":[{"start":{"line":245,"column":54},"end":{"line":249,"column":3}}]},"42":{"type":"branch","line":246,"loc":{"start":{"line":246,"column":59},"end":{"line":247,"column":19}},"locations":[{"start":{"line":246,"column":59},"end":{"line":247,"column":19}}]},"43":{"type":"branch","line":249,"loc":{"start":{"line":249,"column":2},"end":{"line":252,"column":1}},"locations":[{"start":{"line":249,"column":2},"end":{"line":252,"column":1}}]},"44":{"type":"branch","line":254,"loc":{"start":{"line":254,"column":0},"end":{"line":372,"column":1}},"locations":[{"start":{"line":254,"column":0},"end":{"line":372,"column":1}}]},"45":{"type":"branch","line":281,"loc":{"start":{"line":281,"column":20},"end":{"line":286,"column":7}},"locations":[{"start":{"line":281,"column":20},"end":{"line":286,"column":7}}]},"46":{"type":"branch","line":282,"loc":{"start":{"line":282,"column":74},"end":{"line":286,"column":7}},"locations":[{"start":{"line":282,"column":74},"end":{"line":286,"column":7}}]},"47":{"type":"branch","line":286,"loc":{"start":{"line":286,"column":6},"end":{"line":287,"column":11}},"locations":[{"start":{"line":286,"column":6},"end":{"line":287,"column":11}}]},"48":{"type":"branch","line":288,"loc":{"start":{"line":288,"column":17},"end":{"line":289,"column":52}},"locations":[{"start":{"line":288,"column":17},"end":{"line":289,"column":52}}]},"49":{"type":"branch","line":289,"loc":{"start":{"line":289,"column":51},"end":{"line":290,"column":16}},"locations":[{"start":{"line":289,"column":51},"end":{"line":290,"column":16}}]},"50":{"type":"branch","line":304,"loc":{"start":{"line":304,"column":9},"end":{"line":336,"column":16}},"locations":[{"start":{"line":304,"column":9},"end":{"line":336,"column":16}}]},"51":{"type":"branch","line":309,"loc":{"start":{"line":309,"column":68},"end":{"line":309,"column":77}},"locations":[{"start":{"line":309,"column":68},"end":{"line":309,"column":77}}]},"52":{"type":"branch","line":336,"loc":{"start":{"line":336,"column":10},"end":{"line":338,"column":76}},"locations":[{"start":{"line":336,"column":10},"end":{"line":338,"column":76}}]},"53":{"type":"branch","line":269,"loc":{"start":{"line":269,"column":12},"end":{"line":279,"column":5}},"locations":[{"start":{"line":269,"column":12},"end":{"line":279,"column":5}}]},"54":{"type":"branch","line":270,"loc":{"start":{"line":270,"column":26},"end":{"line":276,"column":6}},"locations":[{"start":{"line":270,"column":26},"end":{"line":276,"column":6}}]},"55":{"type":"branch","line":271,"loc":{"start":{"line":271,"column":34},"end":{"line":273,"column":7}},"locations":[{"start":{"line":271,"column":34},"end":{"line":273,"column":7}}]},"56":{"type":"branch","line":278,"loc":{"start":{"line":278,"column":11},"end":{"line":278,"column":70}},"locations":[{"start":{"line":278,"column":11},"end":{"line":278,"column":70}}]},"57":{"type":"branch","line":282,"loc":{"start":{"line":282,"column":21},"end":{"line":282,"column":74}},"locations":[{"start":{"line":282,"column":21},"end":{"line":282,"column":74}}]},"58":{"type":"branch","line":289,"loc":{"start":{"line":289,"column":23},"end":{"line":289,"column":51}},"locations":[{"start":{"line":289,"column":23},"end":{"line":289,"column":51}}]},"59":{"type":"branch","line":327,"loc":{"start":{"line":327,"column":25},"end":{"line":330,"column":18}},"locations":[{"start":{"line":327,"column":25},"end":{"line":330,"column":18}}]},"60":{"type":"branch","line":341,"loc":{"start":{"line":341,"column":20},"end":{"line":363,"column":9}},"locations":[{"start":{"line":341,"column":20},"end":{"line":363,"column":9}}]},"61":{"type":"branch","line":342,"loc":{"start":{"line":342,"column":30},"end":{"line":342,"column":51}},"locations":[{"start":{"line":342,"column":30},"end":{"line":342,"column":51}}]},"62":{"type":"branch","line":342,"loc":{"start":{"line":342,"column":42},"end":{"line":342,"column":61}},"locations":[{"start":{"line":342,"column":42},"end":{"line":342,"column":61}}]},"63":{"type":"branch","line":352,"loc":{"start":{"line":352,"column":27},"end":{"line":355,"column":20}},"locations":[{"start":{"line":352,"column":27},"end":{"line":355,"column":20}}]},"64":{"type":"branch","line":374,"loc":{"start":{"line":374,"column":7},"end":{"line":1055,"column":1}},"locations":[{"start":{"line":374,"column":7},"end":{"line":1055,"column":1}}]},"65":{"type":"branch","line":401,"loc":{"start":{"line":401,"column":17},"end":{"line":401,"column":64}},"locations":[{"start":{"line":401,"column":17},"end":{"line":401,"column":64}}]},"66":{"type":"branch","line":402,"loc":{"start":{"line":402,"column":19},"end":{"line":402,"column":58}},"locations":[{"start":{"line":402,"column":19},"end":{"line":402,"column":58}}]},"67":{"type":"branch","line":408,"loc":{"start":{"line":408,"column":33},"end":{"line":408,"column":69}},"locations":[{"start":{"line":408,"column":33},"end":{"line":408,"column":69}}]},"68":{"type":"branch","line":419,"loc":{"start":{"line":419,"column":37},"end":{"line":419,"column":61}},"locations":[{"start":{"line":419,"column":37},"end":{"line":419,"column":61}}]},"69":{"type":"branch","line":605,"loc":{"start":{"line":605,"column":23},"end":{"line":606,"column":56}},"locations":[{"start":{"line":605,"column":23},"end":{"line":606,"column":56}}]},"70":{"type":"branch","line":606,"loc":{"start":{"line":606,"column":47},"end":{"line":606,"column":56}},"locations":[{"start":{"line":606,"column":47},"end":{"line":606,"column":56}}]},"71":{"type":"branch","line":606,"loc":{"start":{"line":606,"column":52},"end":{"line":607,"column":11}},"locations":[{"start":{"line":606,"column":52},"end":{"line":607,"column":11}}]},"72":{"type":"branch","line":675,"loc":{"start":{"line":675,"column":4},"end":{"line":675,"column":43}},"locations":[{"start":{"line":675,"column":4},"end":{"line":675,"column":43}}]},"73":{"type":"branch","line":842,"loc":{"start":{"line":842,"column":49},"end":{"line":842,"column":77}},"locations":[{"start":{"line":842,"column":49},"end":{"line":842,"column":77}}]},"74":{"type":"branch","line":842,"loc":{"start":{"line":842,"column":64},"end":{"line":842,"column":79}},"locations":[{"start":{"line":842,"column":64},"end":{"line":842,"column":79}}]},"75":{"type":"branch","line":854,"loc":{"start":{"line":854,"column":29},"end":{"line":854,"column":64}},"locations":[{"start":{"line":854,"column":29},"end":{"line":854,"column":64}}]},"76":{"type":"branch","line":854,"loc":{"start":{"line":854,"column":44},"end":{"line":854,"column":84}},"locations":[{"start":{"line":854,"column":44},"end":{"line":854,"column":84}}]},"77":{"type":"branch","line":855,"loc":{"start":{"line":855,"column":15},"end":{"line":855,"column":50}},"locations":[{"start":{"line":855,"column":15},"end":{"line":855,"column":50}}]},"78":{"type":"branch","line":855,"loc":{"start":{"line":855,"column":43},"end":{"line":855,"column":68}},"locations":[{"start":{"line":855,"column":43},"end":{"line":855,"column":68}}]},"79":{"type":"branch","line":860,"loc":{"start":{"line":860,"column":11},"end":{"line":893,"column":29}},"locations":[{"start":{"line":860,"column":11},"end":{"line":893,"column":29}}]},"80":{"type":"branch","line":903,"loc":{"start":{"line":903,"column":15},"end":{"line":906,"column":37}},"locations":[{"start":{"line":903,"column":15},"end":{"line":906,"column":37}}]},"81":{"type":"branch","line":904,"loc":{"start":{"line":904,"column":18},"end":{"line":905,"column":89}},"locations":[{"start":{"line":904,"column":18},"end":{"line":905,"column":89}}]},"82":{"type":"branch","line":905,"loc":{"start":{"line":905,"column":87},"end":{"line":906,"column":37}},"locations":[{"start":{"line":905,"column":87},"end":{"line":906,"column":37}}]},"83":{"type":"branch","line":906,"loc":{"start":{"line":906,"column":33},"end":{"line":907,"column":37}},"locations":[{"start":{"line":906,"column":33},"end":{"line":907,"column":37}}]},"84":{"type":"branch","line":915,"loc":{"start":{"line":915,"column":25},"end":{"line":915,"column":61}},"locations":[{"start":{"line":915,"column":25},"end":{"line":915,"column":61}}]},"85":{"type":"branch","line":916,"loc":{"start":{"line":916,"column":26},"end":{"line":916,"column":55}},"locations":[{"start":{"line":916,"column":26},"end":{"line":916,"column":55}}]},"86":{"type":"branch","line":916,"loc":{"start":{"line":916,"column":40},"end":{"line":916,"column":69}},"locations":[{"start":{"line":916,"column":40},"end":{"line":916,"column":69}}]},"87":{"type":"branch","line":919,"loc":{"start":{"line":919,"column":17},"end":{"line":920,"column":51}},"locations":[{"start":{"line":919,"column":17},"end":{"line":920,"column":51}}]},"88":{"type":"branch","line":920,"loc":{"start":{"line":920,"column":20},"end":{"line":925,"column":35}},"locations":[{"start":{"line":920,"column":20},"end":{"line":925,"column":35}}]},"89":{"type":"branch","line":921,"loc":{"start":{"line":921,"column":21},"end":{"line":922,"column":42}},"locations":[{"start":{"line":921,"column":21},"end":{"line":922,"column":42}}]},"90":{"type":"branch","line":922,"loc":{"start":{"line":922,"column":20},"end":{"line":925,"column":35}},"locations":[{"start":{"line":922,"column":20},"end":{"line":925,"column":35}}]},"91":{"type":"branch","line":923,"loc":{"start":{"line":923,"column":20},"end":{"line":924,"column":34}},"locations":[{"start":{"line":923,"column":20},"end":{"line":924,"column":34}}]},"92":{"type":"branch","line":924,"loc":{"start":{"line":924,"column":22},"end":{"line":925,"column":35}},"locations":[{"start":{"line":924,"column":22},"end":{"line":925,"column":35}}]},"93":{"type":"branch","line":938,"loc":{"start":{"line":938,"column":15},"end":{"line":938,"column":53}},"locations":[{"start":{"line":938,"column":15},"end":{"line":938,"column":53}}]},"94":{"type":"branch","line":938,"loc":{"start":{"line":938,"column":48},"end":{"line":938,"column":74}},"locations":[{"start":{"line":938,"column":48},"end":{"line":938,"column":74}}]},"95":{"type":"branch","line":944,"loc":{"start":{"line":944,"column":25},"end":{"line":944,"column":64}},"locations":[{"start":{"line":944,"column":25},"end":{"line":944,"column":64}}]},"96":{"type":"branch","line":944,"loc":{"start":{"line":944,"column":48},"end":{"line":944,"column":84}},"locations":[{"start":{"line":944,"column":48},"end":{"line":944,"column":84}}]},"97":{"type":"branch","line":947,"loc":{"start":{"line":947,"column":17},"end":{"line":948,"column":51}},"locations":[{"start":{"line":947,"column":17},"end":{"line":948,"column":51}}]},"98":{"type":"branch","line":948,"loc":{"start":{"line":948,"column":20},"end":{"line":951,"column":61}},"locations":[{"start":{"line":948,"column":20},"end":{"line":951,"column":61}}]},"99":{"type":"branch","line":949,"loc":{"start":{"line":949,"column":21},"end":{"line":950,"column":53}},"locations":[{"start":{"line":949,"column":21},"end":{"line":950,"column":53}}]},"100":{"type":"branch","line":950,"loc":{"start":{"line":950,"column":20},"end":{"line":951,"column":61}},"locations":[{"start":{"line":950,"column":20},"end":{"line":951,"column":61}}]},"101":{"type":"branch","line":957,"loc":{"start":{"line":957,"column":15},"end":{"line":957,"column":54}},"locations":[{"start":{"line":957,"column":15},"end":{"line":957,"column":54}}]},"102":{"type":"branch","line":957,"loc":{"start":{"line":957,"column":37},"end":{"line":957,"column":71}},"locations":[{"start":{"line":957,"column":37},"end":{"line":957,"column":71}}]},"103":{"type":"branch","line":962,"loc":{"start":{"line":962,"column":11},"end":{"line":965,"column":18}},"locations":[{"start":{"line":962,"column":11},"end":{"line":965,"column":18}}]},"104":{"type":"branch","line":965,"loc":{"start":{"line":965,"column":12},"end":{"line":966,"column":19}},"locations":[{"start":{"line":965,"column":12},"end":{"line":966,"column":19}}]},"105":{"type":"branch","line":967,"loc":{"start":{"line":967,"column":12},"end":{"line":970,"column":18}},"locations":[{"start":{"line":967,"column":12},"end":{"line":970,"column":18}}]},"106":{"type":"branch","line":970,"loc":{"start":{"line":970,"column":12},"end":{"line":1038,"column":18}},"locations":[{"start":{"line":970,"column":12},"end":{"line":1038,"column":18}}]},"107":{"type":"branch","line":971,"loc":{"start":{"line":971,"column":14},"end":{"line":1034,"column":17}},"locations":[{"start":{"line":971,"column":14},"end":{"line":1034,"column":17}}]},"108":{"type":"branch","line":1034,"loc":{"start":{"line":1034,"column":12},"end":{"line":1038,"column":18}},"locations":[{"start":{"line":1034,"column":12},"end":{"line":1038,"column":18}}]},"109":{"type":"branch","line":1043,"loc":{"start":{"line":1043,"column":7},"end":{"line":1051,"column":10}},"locations":[{"start":{"line":1043,"column":7},"end":{"line":1051,"column":10}}]},"110":{"type":"branch","line":422,"loc":{"start":{"line":422,"column":4},"end":{"line":436,"column":6}},"locations":[{"start":{"line":422,"column":4},"end":{"line":436,"column":6}}]},"111":{"type":"branch","line":424,"loc":{"start":{"line":424,"column":23},"end":{"line":426,"column":7}},"locations":[{"start":{"line":424,"column":23},"end":{"line":426,"column":7}}]},"112":{"type":"branch","line":430,"loc":{"start":{"line":430,"column":8},"end":{"line":433,"column":12}},"locations":[{"start":{"line":430,"column":8},"end":{"line":433,"column":12}}]},"113":{"type":"branch","line":433,"loc":{"start":{"line":433,"column":8},"end":{"line":434,"column":12}},"locations":[{"start":{"line":433,"column":8},"end":{"line":434,"column":12}}]},"114":{"type":"branch","line":429,"loc":{"start":{"line":429,"column":10},"end":{"line":429,"column":64}},"locations":[{"start":{"line":429,"column":10},"end":{"line":429,"column":64}}]},"115":{"type":"branch","line":432,"loc":{"start":{"line":432,"column":10},"end":{"line":432,"column":79}},"locations":[{"start":{"line":432,"column":10},"end":{"line":432,"column":79}}]},"116":{"type":"branch","line":432,"loc":{"start":{"line":432,"column":26},"end":{"line":432,"column":43}},"locations":[{"start":{"line":432,"column":26},"end":{"line":432,"column":43}}]},"117":{"type":"branch","line":441,"loc":{"start":{"line":441,"column":4},"end":{"line":463,"column":6}},"locations":[{"start":{"line":441,"column":4},"end":{"line":463,"column":6}}]},"118":{"type":"branch","line":442,"loc":{"start":{"line":442,"column":17},"end":{"line":444,"column":7}},"locations":[{"start":{"line":442,"column":17},"end":{"line":444,"column":7}}]},"119":{"type":"branch","line":444,"loc":{"start":{"line":444,"column":6},"end":{"line":445,"column":37}},"locations":[{"start":{"line":444,"column":6},"end":{"line":445,"column":37}}]},"120":{"type":"branch","line":445,"loc":{"start":{"line":445,"column":29},"end":{"line":445,"column":45}},"locations":[{"start":{"line":445,"column":29},"end":{"line":445,"column":45}}]},"121":{"type":"branch","line":446,"loc":{"start":{"line":446,"column":22},"end":{"line":448,"column":7}},"locations":[{"start":{"line":446,"column":22},"end":{"line":448,"column":7}}]},"122":{"type":"branch","line":448,"loc":{"start":{"line":448,"column":6},"end":{"line":449,"column":40}},"locations":[{"start":{"line":448,"column":6},"end":{"line":449,"column":40}}]},"123":{"type":"branch","line":449,"loc":{"start":{"line":449,"column":31},"end":{"line":449,"column":44}},"locations":[{"start":{"line":449,"column":31},"end":{"line":449,"column":44}}]},"124":{"type":"branch","line":450,"loc":{"start":{"line":450,"column":23},"end":{"line":454,"column":7}},"locations":[{"start":{"line":450,"column":23},"end":{"line":454,"column":7}}]},"125":{"type":"branch","line":454,"loc":{"start":{"line":454,"column":6},"end":{"line":456,"column":67}},"locations":[{"start":{"line":454,"column":6},"end":{"line":456,"column":67}}]},"126":{"type":"branch","line":459,"loc":{"start":{"line":459,"column":41},"end":{"line":462,"column":43}},"locations":[{"start":{"line":459,"column":41},"end":{"line":462,"column":43}}]},"127":{"type":"branch","line":473,"loc":{"start":{"line":473,"column":12},"end":{"line":488,"column":5}},"locations":[{"start":{"line":473,"column":12},"end":{"line":488,"column":5}}]},"128":{"type":"branch","line":476,"loc":{"start":{"line":476,"column":12},"end":{"line":481,"column":7}},"locations":[{"start":{"line":476,"column":12},"end":{"line":481,"column":7}}]},"129":{"type":"branch","line":477,"loc":{"start":{"line":477,"column":23},"end":{"line":479,"column":9}},"locations":[{"start":{"line":477,"column":23},"end":{"line":479,"column":9}}]},"130":{"type":"branch","line":485,"loc":{"start":{"line":485,"column":11},"end":{"line":487,"column":6}},"locations":[{"start":{"line":485,"column":11},"end":{"line":487,"column":6}}]},"131":{"type":"branch","line":490,"loc":{"start":{"line":490,"column":12},"end":{"line":512,"column":5}},"locations":[{"start":{"line":490,"column":12},"end":{"line":512,"column":5}}]},"132":{"type":"branch","line":492,"loc":{"start":{"line":492,"column":28},"end":{"line":498,"column":5}},"locations":[{"start":{"line":492,"column":28},"end":{"line":498,"column":5}}]},"133":{"type":"branch","line":498,"loc":{"start":{"line":498,"column":4},"end":{"line":511,"column":6}},"locations":[{"start":{"line":498,"column":4},"end":{"line":511,"column":6}}]},"134":{"type":"branch","line":495,"loc":{"start":{"line":495,"column":13},"end":{"line":497,"column":8}},"locations":[{"start":{"line":495,"column":13},"end":{"line":497,"column":8}}]},"135":{"type":"branch","line":500,"loc":{"start":{"line":500,"column":12},"end":{"line":505,"column":7}},"locations":[{"start":{"line":500,"column":12},"end":{"line":505,"column":7}}]},"136":{"type":"branch","line":501,"loc":{"start":{"line":501,"column":23},"end":{"line":503,"column":9}},"locations":[{"start":{"line":501,"column":23},"end":{"line":503,"column":9}}]},"137":{"type":"branch","line":509,"loc":{"start":{"line":509,"column":11},"end":{"line":511,"column":6}},"locations":[{"start":{"line":509,"column":11},"end":{"line":511,"column":6}}]},"138":{"type":"branch","line":515,"loc":{"start":{"line":515,"column":4},"end":{"line":528,"column":6}},"locations":[{"start":{"line":515,"column":4},"end":{"line":528,"column":6}}]},"139":{"type":"branch","line":516,"loc":{"start":{"line":516,"column":36},"end":{"line":518,"column":7}},"locations":[{"start":{"line":516,"column":36},"end":{"line":518,"column":7}}]},"140":{"type":"branch","line":523,"loc":{"start":{"line":523,"column":6},"end":{"line":525,"column":8}},"locations":[{"start":{"line":523,"column":6},"end":{"line":525,"column":8}}]},"141":{"type":"branch","line":532,"loc":{"start":{"line":532,"column":12},"end":{"line":554,"column":5}},"locations":[{"start":{"line":532,"column":12},"end":{"line":554,"column":5}}]},"142":{"type":"branch","line":534,"loc":{"start":{"line":534,"column":28},"end":{"line":539,"column":5}},"locations":[{"start":{"line":534,"column":28},"end":{"line":539,"column":5}}]},"143":{"type":"branch","line":539,"loc":{"start":{"line":539,"column":4},"end":{"line":553,"column":6}},"locations":[{"start":{"line":539,"column":4},"end":{"line":553,"column":6}}]},"144":{"type":"branch","line":536,"loc":{"start":{"line":536,"column":13},"end":{"line":538,"column":8}},"locations":[{"start":{"line":536,"column":13},"end":{"line":538,"column":8}}]},"145":{"type":"branch","line":541,"loc":{"start":{"line":541,"column":22},"end":{"line":546,"column":6}},"locations":[{"start":{"line":541,"column":22},"end":{"line":546,"column":6}}]},"146":{"type":"branch","line":542,"loc":{"start":{"line":542,"column":21},"end":{"line":544,"column":7}},"locations":[{"start":{"line":542,"column":21},"end":{"line":544,"column":7}}]},"147":{"type":"branch","line":550,"loc":{"start":{"line":550,"column":11},"end":{"line":553,"column":6}},"locations":[{"start":{"line":550,"column":11},"end":{"line":553,"column":6}}]},"148":{"type":"branch","line":556,"loc":{"start":{"line":556,"column":37},"end":{"line":573,"column":5}},"locations":[{"start":{"line":556,"column":37},"end":{"line":573,"column":5}}]},"149":{"type":"branch","line":557,"loc":{"start":{"line":557,"column":30},"end":{"line":572,"column":5}},"locations":[{"start":{"line":557,"column":30},"end":{"line":572,"column":5}}]},"150":{"type":"branch","line":558,"loc":{"start":{"line":558,"column":52},"end":{"line":565,"column":7}},"locations":[{"start":{"line":558,"column":52},"end":{"line":565,"column":7}}]},"151":{"type":"branch","line":576,"loc":{"start":{"line":576,"column":4},"end":{"line":587,"column":6}},"locations":[{"start":{"line":576,"column":4},"end":{"line":587,"column":6}}]},"152":{"type":"branch","line":577,"loc":{"start":{"line":577,"column":30},"end":{"line":579,"column":7}},"locations":[{"start":{"line":577,"column":30},"end":{"line":579,"column":7}}]},"153":{"type":"branch","line":581,"loc":{"start":{"line":581,"column":14},"end":{"line":583,"column":9}},"locations":[{"start":{"line":581,"column":14},"end":{"line":583,"column":9}}]},"154":{"type":"branch","line":591,"loc":{"start":{"line":591,"column":46},"end":{"line":602,"column":5}},"locations":[{"start":{"line":591,"column":46},"end":{"line":602,"column":5}}]},"155":{"type":"branch","line":592,"loc":{"start":{"line":592,"column":28},"end":{"line":594,"column":5}},"locations":[{"start":{"line":592,"column":28},"end":{"line":594,"column":5}}]},"156":{"type":"branch","line":596,"loc":{"start":{"line":596,"column":12},"end":{"line":598,"column":7}},"locations":[{"start":{"line":596,"column":12},"end":{"line":598,"column":7}}]},"157":{"type":"branch","line":606,"loc":{"start":{"line":606,"column":17},"end":{"line":606,"column":47}},"locations":[{"start":{"line":606,"column":17},"end":{"line":606,"column":47}}]},"158":{"type":"branch","line":621,"loc":{"start":{"line":621,"column":12},"end":{"line":626,"column":5}},"locations":[{"start":{"line":621,"column":12},"end":{"line":626,"column":5}}]},"159":{"type":"branch","line":622,"loc":{"start":{"line":622,"column":28},"end":{"line":624,"column":5}},"locations":[{"start":{"line":622,"column":28},"end":{"line":624,"column":5}}]},"160":{"type":"branch","line":624,"loc":{"start":{"line":624,"column":4},"end":{"line":625,"column":27}},"locations":[{"start":{"line":624,"column":4},"end":{"line":625,"column":27}}]},"161":{"type":"branch","line":628,"loc":{"start":{"line":628,"column":12},"end":{"line":635,"column":5}},"locations":[{"start":{"line":628,"column":12},"end":{"line":635,"column":5}}]},"162":{"type":"branch","line":629,"loc":{"start":{"line":629,"column":25},"end":{"line":631,"column":5}},"locations":[{"start":{"line":629,"column":25},"end":{"line":631,"column":5}}]},"163":{"type":"branch","line":631,"loc":{"start":{"line":631,"column":4},"end":{"line":632,"column":59}},"locations":[{"start":{"line":631,"column":4},"end":{"line":632,"column":59}}]},"164":{"type":"branch","line":632,"loc":{"start":{"line":632,"column":59},"end":{"line":634,"column":5}},"locations":[{"start":{"line":632,"column":59},"end":{"line":634,"column":5}}]},"165":{"type":"branch","line":632,"loc":{"start":{"line":632,"column":20},"end":{"line":632,"column":56}},"locations":[{"start":{"line":632,"column":20},"end":{"line":632,"column":56}}]},"166":{"type":"branch","line":637,"loc":{"start":{"line":637,"column":12},"end":{"line":673,"column":5}},"locations":[{"start":{"line":637,"column":12},"end":{"line":673,"column":5}}]},"167":{"type":"branch","line":639,"loc":{"start":{"line":639,"column":9},"end":{"line":639,"column":47}},"locations":[{"start":{"line":639,"column":9},"end":{"line":639,"column":47}}]},"168":{"type":"branch","line":639,"loc":{"start":{"line":639,"column":47},"end":{"line":644,"column":5}},"locations":[{"start":{"line":639,"column":47},"end":{"line":644,"column":5}}]},"169":{"type":"branch","line":644,"loc":{"start":{"line":644,"column":4},"end":{"line":646,"column":51}},"locations":[{"start":{"line":644,"column":4},"end":{"line":646,"column":51}}]},"170":{"type":"branch","line":646,"loc":{"start":{"line":646,"column":32},"end":{"line":646,"column":74}},"locations":[{"start":{"line":646,"column":32},"end":{"line":646,"column":74}}]},"171":{"type":"branch","line":646,"loc":{"start":{"line":646,"column":51},"end":{"line":646,"column":79}},"locations":[{"start":{"line":646,"column":51},"end":{"line":646,"column":79}}]},"172":{"type":"branch","line":641,"loc":{"start":{"line":641,"column":13},"end":{"line":643,"column":8}},"locations":[{"start":{"line":641,"column":13},"end":{"line":643,"column":8}}]},"173":{"type":"branch","line":647,"loc":{"start":{"line":647,"column":29},"end":{"line":665,"column":6}},"locations":[{"start":{"line":647,"column":29},"end":{"line":665,"column":6}}]},"174":{"type":"branch","line":654,"loc":{"start":{"line":654,"column":23},"end":{"line":656,"column":9}},"locations":[{"start":{"line":654,"column":23},"end":{"line":656,"column":9}}]},"175":{"type":"branch","line":658,"loc":{"start":{"line":658,"column":6},"end":{"line":664,"column":7}},"locations":[{"start":{"line":658,"column":6},"end":{"line":664,"column":7}}]},"176":{"type":"branch","line":669,"loc":{"start":{"line":669,"column":11},"end":{"line":672,"column":6}},"locations":[{"start":{"line":669,"column":11},"end":{"line":672,"column":6}}]},"177":{"type":"branch","line":680,"loc":{"start":{"line":680,"column":32},"end":{"line":687,"column":5}},"locations":[{"start":{"line":680,"column":32},"end":{"line":687,"column":5}}]},"178":{"type":"branch","line":684,"loc":{"start":{"line":684,"column":26},"end":{"line":685,"column":11}},"locations":[{"start":{"line":684,"column":26},"end":{"line":685,"column":11}}]},"179":{"type":"branch","line":689,"loc":{"start":{"line":689,"column":4},"end":{"line":689,"column":61}},"locations":[{"start":{"line":689,"column":4},"end":{"line":689,"column":61}}]},"180":{"type":"branch","line":689,"loc":{"start":{"line":689,"column":10},"end":{"line":689,"column":30}},"locations":[{"start":{"line":689,"column":10},"end":{"line":689,"column":30}}]},"181":{"type":"branch","line":689,"loc":{"start":{"line":689,"column":51},"end":{"line":689,"column":61}},"locations":[{"start":{"line":689,"column":51},"end":{"line":689,"column":61}}]},"182":{"type":"branch","line":689,"loc":{"start":{"line":689,"column":34},"end":{"line":689,"column":51}},"locations":[{"start":{"line":689,"column":34},"end":{"line":689,"column":51}}]},"183":{"type":"branch","line":714,"loc":{"start":{"line":714,"column":12},"end":{"line":717,"column":5}},"locations":[{"start":{"line":714,"column":12},"end":{"line":717,"column":5}}]},"184":{"type":"branch","line":719,"loc":{"start":{"line":719,"column":36},"end":{"line":768,"column":5}},"locations":[{"start":{"line":719,"column":36},"end":{"line":768,"column":5}}]},"185":{"type":"branch","line":720,"loc":{"start":{"line":720,"column":28},"end":{"line":722,"column":5}},"locations":[{"start":{"line":720,"column":28},"end":{"line":722,"column":5}}]},"186":{"type":"branch","line":739,"loc":{"start":{"line":739,"column":27},"end":{"line":739,"column":57}},"locations":[{"start":{"line":739,"column":27},"end":{"line":739,"column":57}}]},"187":{"type":"branch","line":739,"loc":{"start":{"line":739,"column":47},"end":{"line":739,"column":65}},"locations":[{"start":{"line":739,"column":47},"end":{"line":739,"column":65}}]},"188":{"type":"branch","line":742,"loc":{"start":{"line":742,"column":38},"end":{"line":742,"column":46}},"locations":[{"start":{"line":742,"column":38},"end":{"line":742,"column":46}}]},"189":{"type":"branch","line":743,"loc":{"start":{"line":743,"column":21},"end":{"line":746,"column":7}},"locations":[{"start":{"line":743,"column":21},"end":{"line":746,"column":7}}]},"190":{"type":"branch","line":751,"loc":{"start":{"line":751,"column":26},"end":{"line":754,"column":7}},"locations":[{"start":{"line":751,"column":26},"end":{"line":754,"column":7}}]},"191":{"type":"branch","line":760,"loc":{"start":{"line":760,"column":6},"end":{"line":764,"column":7}},"locations":[{"start":{"line":760,"column":6},"end":{"line":764,"column":7}}]},"192":{"type":"branch","line":765,"loc":{"start":{"line":765,"column":4},"end":{"line":767,"column":5}},"locations":[{"start":{"line":765,"column":4},"end":{"line":767,"column":5}}]},"193":{"type":"branch","line":782,"loc":{"start":{"line":782,"column":42},"end":{"line":808,"column":5}},"locations":[{"start":{"line":782,"column":42},"end":{"line":808,"column":5}}]},"194":{"type":"branch","line":785,"loc":{"start":{"line":785,"column":56},"end":{"line":787,"column":5}},"locations":[{"start":{"line":785,"column":56},"end":{"line":787,"column":5}}]},"195":{"type":"branch","line":792,"loc":{"start":{"line":792,"column":20},"end":{"line":794,"column":5}},"locations":[{"start":{"line":792,"column":20},"end":{"line":794,"column":5}}]},"196":{"type":"branch","line":798,"loc":{"start":{"line":798,"column":23},"end":{"line":800,"column":7}},"locations":[{"start":{"line":798,"column":23},"end":{"line":800,"column":7}}]},"197":{"type":"branch","line":803,"loc":{"start":{"line":803,"column":4},"end":{"line":805,"column":6}},"locations":[{"start":{"line":803,"column":4},"end":{"line":805,"column":6}}]},"198":{"type":"branch","line":835,"loc":{"start":{"line":835,"column":21},"end":{"line":835,"column":50}},"locations":[{"start":{"line":835,"column":21},"end":{"line":835,"column":50}}]},"199":{"type":"branch","line":844,"loc":{"start":{"line":844,"column":21},"end":{"line":849,"column":14}},"locations":[{"start":{"line":844,"column":21},"end":{"line":849,"column":14}}]},"200":{"type":"branch","line":845,"loc":{"start":{"line":845,"column":39},"end":{"line":847,"column":15}},"locations":[{"start":{"line":845,"column":39},"end":{"line":847,"column":15}}]},"201":{"type":"branch","line":848,"loc":{"start":{"line":848,"column":30},"end":{"line":848,"column":45}},"locations":[{"start":{"line":848,"column":30},"end":{"line":848,"column":45}}]},"202":{"type":"branch","line":862,"loc":{"start":{"line":862,"column":25},"end":{"line":875,"column":34}},"locations":[{"start":{"line":862,"column":25},"end":{"line":875,"column":34}}]},"203":{"type":"branch","line":868,"loc":{"start":{"line":868,"column":27},"end":{"line":871,"column":20}},"locations":[{"start":{"line":868,"column":27},"end":{"line":871,"column":20}}]},"204":{"type":"branch","line":927,"loc":{"start":{"line":927,"column":23},"end":{"line":936,"column":16}},"locations":[{"start":{"line":927,"column":23},"end":{"line":936,"column":16}}]},"205":{"type":"branch","line":928,"loc":{"start":{"line":928,"column":58},"end":{"line":930,"column":17}},"locations":[{"start":{"line":928,"column":58},"end":{"line":930,"column":17}}]},"206":{"type":"branch","line":931,"loc":{"start":{"line":931,"column":33},"end":{"line":934,"column":17}},"locations":[{"start":{"line":931,"column":33},"end":{"line":934,"column":17}}]},"207":{"type":"branch","line":934,"loc":{"start":{"line":934,"column":16},"end":{"line":935,"column":53}},"locations":[{"start":{"line":934,"column":16},"end":{"line":935,"column":53}}]},"208":{"type":"branch","line":953,"loc":{"start":{"line":953,"column":23},"end":{"line":955,"column":16}},"locations":[{"start":{"line":953,"column":23},"end":{"line":955,"column":16}}]},"209":{"type":"branch","line":973,"loc":{"start":{"line":973,"column":38},"end":{"line":1033,"column":15}},"locations":[{"start":{"line":973,"column":38},"end":{"line":1033,"column":15}}]},"210":{"type":"branch","line":974,"loc":{"start":{"line":974,"column":37},"end":{"line":974,"column":61}},"locations":[{"start":{"line":974,"column":37},"end":{"line":974,"column":61}}]},"211":{"type":"branch","line":975,"loc":{"start":{"line":975,"column":83},"end":{"line":975,"column":91}},"locations":[{"start":{"line":975,"column":83},"end":{"line":975,"column":91}}]},"212":{"type":"branch","line":985,"loc":{"start":{"line":985,"column":38},"end":{"line":985,"column":75}},"locations":[{"start":{"line":985,"column":38},"end":{"line":985,"column":75}}]},"213":{"type":"branch","line":991,"loc":{"start":{"line":991,"column":25},"end":{"line":992,"column":54}},"locations":[{"start":{"line":991,"column":25},"end":{"line":992,"column":54}}]},"214":{"type":"branch","line":992,"loc":{"start":{"line":992,"column":52},"end":{"line":996,"column":48}},"locations":[{"start":{"line":992,"column":52},"end":{"line":996,"column":48}}]},"215":{"type":"branch","line":993,"loc":{"start":{"line":993,"column":28},"end":{"line":994,"column":91}},"locations":[{"start":{"line":993,"column":28},"end":{"line":994,"column":91}}]},"216":{"type":"branch","line":994,"loc":{"start":{"line":994,"column":89},"end":{"line":996,"column":48}},"locations":[{"start":{"line":994,"column":89},"end":{"line":996,"column":48}}]},"217":{"type":"branch","line":1000,"loc":{"start":{"line":1000,"column":23},"end":{"line":1003,"column":31}},"locations":[{"start":{"line":1000,"column":23},"end":{"line":1003,"column":31}}]},"218":{"type":"branch","line":1003,"loc":{"start":{"line":1003,"column":24},"end":{"line":1004,"column":31}},"locations":[{"start":{"line":1003,"column":24},"end":{"line":1004,"column":31}}]},"219":{"type":"branch","line":1006,"loc":{"start":{"line":1006,"column":47},"end":{"line":1029,"column":28}},"locations":[{"start":{"line":1006,"column":47},"end":{"line":1029,"column":28}}]},"220":{"type":"branch","line":1029,"loc":{"start":{"line":1029,"column":22},"end":{"line":1030,"column":29}},"locations":[{"start":{"line":1029,"column":22},"end":{"line":1030,"column":29}}]},"221":{"type":"branch","line":1008,"loc":{"start":{"line":1008,"column":46},"end":{"line":1028,"column":25}},"locations":[{"start":{"line":1008,"column":46},"end":{"line":1028,"column":25}}]},"222":{"type":"branch","line":1012,"loc":{"start":{"line":1012,"column":54},"end":{"line":1012,"column":69}},"locations":[{"start":{"line":1012,"column":54},"end":{"line":1012,"column":69}}]},"223":{"type":"branch","line":1050,"loc":{"start":{"line":1050,"column":19},"end":{"line":1050,"column":49}},"locations":[{"start":{"line":1050,"column":19},"end":{"line":1050,"column":49}}]}},"b":{"0":[40],"1":[0],"2":[0],"3":[0],"4":[0],"5":[0],"6":[0],"7":[0],"8":[0],"9":[75],"10":[1],"11":[74],"12":[15],"13":[59],"14":[344],"15":[80],"16":[40],"17":[14],"18":[330],"19":[59],"20":[271],"21":[105],"22":[166],"23":[110],"24":[0],"25":[170],"26":[0],"27":[330],"28":[224],"29":[106],"30":[4],"31":[102],"32":[59],"33":[43],"34":[244],"35":[349],"36":[33],"37":[19],"38":[330],"39":[224],"40":[14],"41":[4],"42":[0],"43":[326],"44":[4],"45":[3],"46":[1],"47":[1],"48":[3],"49":[1],"50":[3],"51":[0],"52":[1],"53":[4],"54":[1],"55":[0],"56":[4],"57":[4],"58":[5],"59":[1],"60":[5],"61":[3],"62":[2],"63":[1],"64":[323],"65":[0],"66":[0],"67":[177],"68":[314],"69":[180],"70":[0],"71":[143],"72":[9],"73":[42],"74":[281],"75":[180],"76":[143],"77":[180],"78":[143],"79":[42],"80":[180],"81":[19],"82":[161],"83":[143],"84":[314],"85":[53],"86":[270],"87":[9],"88":[314],"89":[134],"90":[180],"91":[53],"92":[127],"93":[53],"94":[270],"95":[314],"96":[180],"97":[9],"98":[314],"99":[134],"100":[180],"101":[1],"102":[322],"103":[12],"104":[311],"105":[9],"106":[314],"107":[180],"108":[134],"109":[4],"110":[89],"111":[0],"112":[88],"113":[88],"114":[3],"115":[2],"116":[0],"117":[45],"118":[43],"119":[2],"120":[0],"121":[0],"122":[2],"123":[0],"124":[0],"125":[2],"126":[2],"127":[44],"128":[44],"129":[0],"130":[44],"131":[44],"132":[3],"133":[41],"134":[3],"135":[41],"136":[0],"137":[41],"138":[194],"139":[0],"140":[0],"141":[44],"142":[3],"143":[41],"144":[3],"145":[193],"146":[0],"147":[41],"148":[90],"149":[40],"150":[75],"151":[1],"152":[0],"153":[1],"154":[1],"155":[0],"156":[1],"157":[180],"158":[44],"159":[41],"160":[3],"161":[127],"162":[88],"163":[39],"164":[0],"165":[39],"166":[85],"167":[82],"168":[48],"169":[37],"170":[0],"171":[37],"172":[48],"173":[188],"174":[0],"175":[0],"176":[37],"177":[44],"178":[43],"179":[83],"180":[39],"181":[44],"182":[74],"183":[81],"184":[1],"185":[0],"186":[0],"187":[0],"188":[0],"189":[0],"190":[0],"191":[0],"192":[0],"193":[1],"194":[0],"195":[0],"196":[0],"197":[0],"198":[4],"199":[39],"200":[0],"201":[39],"202":[38],"203":[37],"204":[51],"205":[0],"206":[17],"207":[34],"208":[1],"209":[349],"210":[33],"211":[229],"212":[19],"213":[6],"214":[343],"215":[104],"216":[239],"217":[19],"218":[330],"219":[120],"220":[229],"221":[344],"222":[0],"223":[3]},"fnMap":{"0":{"name":"formatPlanLabel","decl":{"start":{"line":142,"column":0},"end":{"line":158,"column":1}},"loc":{"start":{"line":142,"column":0},"end":{"line":158,"column":1}},"line":142},"1":{"name":"mapForgeItemStatus","decl":{"start":{"line":160,"column":0},"end":{"line":169,"column":1}},"loc":{"start":{"line":160,"column":0},"end":{"line":169,"column":1}},"line":160},"2":{"name":"mapForgePhaseChipState","decl":{"start":{"line":171,"column":0},"end":{"line":188,"column":1}},"loc":{"start":{"line":171,"column":0},"end":{"line":188,"column":1}},"line":171},"3":{"name":"allTaskPhasesCompleted","decl":{"start":{"line":190,"column":0},"end":{"line":206,"column":1}},"loc":{"start":{"line":190,"column":0},"end":{"line":206,"column":1}},"line":190},"4":{"name":"deriveStatusFromTaskPhases","decl":{"start":{"line":208,"column":0},"end":{"line":223,"column":1}},"loc":{"start":{"line":208,"column":0},"end":{"line":223,"column":1}},"line":208},"5":{"name":"resolveForgeTaskRowStatus","decl":{"start":{"line":225,"column":0},"end":{"line":252,"column":1}},"loc":{"start":{"line":225,"column":0},"end":{"line":252,"column":1}},"line":225},"6":{"name":"ForgeTemplatesModal","decl":{"start":{"line":254,"column":0},"end":{"line":372,"column":1}},"loc":{"start":{"line":254,"column":0},"end":{"line":372,"column":1}},"line":254},"7":{"name":"handleKeyDown","decl":{"start":{"line":270,"column":26},"end":{"line":276,"column":6}},"loc":{"start":{"line":270,"column":26},"end":{"line":276,"column":6}},"line":270},"8":{"name":"onClick","decl":{"start":{"line":318,"column":25},"end":{"line":320,"column":18}},"loc":{"start":{"line":318,"column":25},"end":{"line":320,"column":18}},"line":318},"9":{"name":"onClick","decl":{"start":{"line":327,"column":25},"end":{"line":330,"column":18}},"loc":{"start":{"line":327,"column":25},"end":{"line":330,"column":18}},"line":327},"10":{"name":"onClick","decl":{"start":{"line":352,"column":27},"end":{"line":355,"column":20}},"loc":{"start":{"line":352,"column":27},"end":{"line":355,"column":20}},"line":352},"11":{"name":"Forge","decl":{"start":{"line":374,"column":7},"end":{"line":1055,"column":1}},"loc":{"start":{"line":374,"column":7},"end":{"line":1055,"column":1}},"line":374},"12":{"name":"loadPlans","decl":{"start":{"line":541,"column":22},"end":{"line":546,"column":6}},"loc":{"start":{"line":541,"column":22},"end":{"line":546,"column":6}},"line":541},"13":{"name":"onClose","decl":{"start":{"line":618,"column":13},"end":{"line":618,"column":42}},"loc":{"start":{"line":618,"column":13},"end":{"line":618,"column":42}},"line":618},"14":{"name":"installedTemplateId","decl":{"start":{"line":641,"column":13},"end":{"line":643,"column":8}},"loc":{"start":{"line":641,"column":13},"end":{"line":643,"column":8}},"line":641},"15":{"name":"refreshPhaseView","decl":{"start":{"line":647,"column":29},"end":{"line":665,"column":6}},"loc":{"start":{"line":647,"column":29},"end":{"line":665,"column":6}},"line":647},"16":{"name":"onClick","decl":{"start":{"line":835,"column":21},"end":{"line":835,"column":50}},"loc":{"start":{"line":835,"column":21},"end":{"line":835,"column":50}},"line":835},"17":{"name":"onClick","decl":{"start":{"line":844,"column":21},"end":{"line":849,"column":14}},"loc":{"start":{"line":844,"column":21},"end":{"line":849,"column":14}},"line":844},"18":{"name":"onClick","decl":{"start":{"line":868,"column":27},"end":{"line":871,"column":20}},"loc":{"start":{"line":868,"column":27},"end":{"line":871,"column":20}},"line":868},"19":{"name":"onClick","decl":{"start":{"line":879,"column":25},"end":{"line":882,"column":18}},"loc":{"start":{"line":879,"column":25},"end":{"line":882,"column":18}},"line":879},"20":{"name":"onClick","decl":{"start":{"line":927,"column":23},"end":{"line":936,"column":16}},"loc":{"start":{"line":927,"column":23},"end":{"line":936,"column":16}},"line":927},"21":{"name":"onClick","decl":{"start":{"line":953,"column":23},"end":{"line":955,"column":16}},"loc":{"start":{"line":953,"column":23},"end":{"line":955,"column":16}},"line":953},"22":{"name":"onClose","decl":{"start":{"line":1050,"column":19},"end":{"line":1050,"column":49}},"loc":{"start":{"line":1050,"column":19},"end":{"line":1050,"column":49}},"line":1050}},"f":{"0":40,"1":75,"2":344,"3":110,"4":330,"5":349,"6":4,"7":1,"8":0,"9":1,"10":1,"11":323,"12":193,"13":0,"14":48,"15":188,"16":4,"17":39,"18":37,"19":0,"20":51,"21":1,"22":3}} +,"/Users/lstern/projects/forge/src/features/forge/hooks/useForgeExecution.ts": {"path":"/Users/lstern/projects/forge/src/features/forge/hooks/useForgeExecution.ts","all":false,"statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":74}},"67":{"start":{"line":68,"column":0},"end":{"line":68,"column":30}},"69":{"start":{"line":70,"column":0},"end":{"line":70,"column":72}},"70":{"start":{"line":71,"column":0},"end":{"line":71,"column":49}},"71":{"start":{"line":72,"column":0},"end":{"line":72,"column":10}},"72":{"start":{"line":73,"column":0},"end":{"line":73,"column":33}},"73":{"start":{"line":74,"column":0},"end":{"line":74,"column":32}},"74":{"start":{"line":75,"column":0},"end":{"line":75,"column":28}},"75":{"start":{"line":76,"column":0},"end":{"line":76,"column":31}},"76":{"start":{"line":77,"column":0},"end":{"line":77,"column":33}},"77":{"start":{"line":78,"column":0},"end":{"line":78,"column":30}},"78":{"start":{"line":79,"column":0},"end":{"line":79,"column":31}},"79":{"start":{"line":80,"column":0},"end":{"line":80,"column":29}},"80":{"start":{"line":81,"column":0},"end":{"line":81,"column":32}},"81":{"start":{"line":82,"column":0},"end":{"line":82,"column":30}},"83":{"start":{"line":84,"column":0},"end":{"line":84,"column":1}},"85":{"start":{"line":86,"column":0},"end":{"line":86,"column":76}},"86":{"start":{"line":87,"column":0},"end":{"line":87,"column":49}},"87":{"start":{"line":88,"column":0},"end":{"line":88,"column":10}},"88":{"start":{"line":89,"column":0},"end":{"line":89,"column":33}},"89":{"start":{"line":90,"column":0},"end":{"line":90,"column":32}},"90":{"start":{"line":91,"column":0},"end":{"line":91,"column":28}},"91":{"start":{"line":92,"column":0},"end":{"line":92,"column":31}},"92":{"start":{"line":93,"column":0},"end":{"line":93,"column":30}},"94":{"start":{"line":95,"column":0},"end":{"line":95,"column":1}},"96":{"start":{"line":97,"column":0},"end":{"line":97,"column":60}},"97":{"start":{"line":98,"column":0},"end":{"line":98,"column":50}},"98":{"start":{"line":99,"column":0},"end":{"line":99,"column":16}},"99":{"start":{"line":100,"column":0},"end":{"line":100,"column":3}},"101":{"start":{"line":102,"column":0},"end":{"line":102,"column":74}},"102":{"start":{"line":103,"column":0},"end":{"line":103,"column":95}},"103":{"start":{"line":104,"column":0},"end":{"line":104,"column":58}},"104":{"start":{"line":105,"column":0},"end":{"line":105,"column":31}},"105":{"start":{"line":106,"column":0},"end":{"line":106,"column":16}},"106":{"start":{"line":107,"column":0},"end":{"line":107,"column":3}},"108":{"start":{"line":109,"column":0},"end":{"line":109,"column":28}},"109":{"start":{"line":110,"column":0},"end":{"line":110,"column":45}},"110":{"start":{"line":111,"column":0},"end":{"line":111,"column":1}},"112":{"start":{"line":113,"column":0},"end":{"line":113,"column":58}},"113":{"start":{"line":114,"column":0},"end":{"line":114,"column":50}},"114":{"start":{"line":115,"column":0},"end":{"line":115,"column":16}},"115":{"start":{"line":116,"column":0},"end":{"line":116,"column":3}},"117":{"start":{"line":118,"column":0},"end":{"line":118,"column":68}},"118":{"start":{"line":119,"column":0},"end":{"line":119,"column":89}},"119":{"start":{"line":120,"column":0},"end":{"line":120,"column":54}},"120":{"start":{"line":121,"column":0},"end":{"line":121,"column":31}},"121":{"start":{"line":122,"column":0},"end":{"line":122,"column":16}},"122":{"start":{"line":123,"column":0},"end":{"line":123,"column":3}},"124":{"start":{"line":125,"column":0},"end":{"line":125,"column":28}},"125":{"start":{"line":126,"column":0},"end":{"line":126,"column":45}},"126":{"start":{"line":127,"column":0},"end":{"line":127,"column":1}},"128":{"start":{"line":129,"column":0},"end":{"line":129,"column":66}},"129":{"start":{"line":130,"column":0},"end":{"line":130,"column":25}},"130":{"start":{"line":131,"column":0},"end":{"line":131,"column":28}},"131":{"start":{"line":132,"column":0},"end":{"line":132,"column":3}},"132":{"start":{"line":133,"column":0},"end":{"line":133,"column":37}},"133":{"start":{"line":134,"column":0},"end":{"line":134,"column":27}},"134":{"start":{"line":135,"column":0},"end":{"line":135,"column":3}},"135":{"start":{"line":136,"column":0},"end":{"line":136,"column":7}},"136":{"start":{"line":137,"column":0},"end":{"line":137,"column":48}},"137":{"start":{"line":138,"column":0},"end":{"line":138,"column":22}},"138":{"start":{"line":139,"column":0},"end":{"line":139,"column":28}},"139":{"start":{"line":140,"column":0},"end":{"line":140,"column":5}},"140":{"start":{"line":141,"column":0},"end":{"line":141,"column":83}},"141":{"start":{"line":142,"column":0},"end":{"line":142,"column":11}},"142":{"start":{"line":143,"column":0},"end":{"line":143,"column":35}},"143":{"start":{"line":144,"column":0},"end":{"line":144,"column":3}},"144":{"start":{"line":145,"column":0},"end":{"line":145,"column":1}},"146":{"start":{"line":147,"column":0},"end":{"line":147,"column":42}},"147":{"start":{"line":148,"column":0},"end":{"line":148,"column":35}},"148":{"start":{"line":149,"column":0},"end":{"line":149,"column":35}},"149":{"start":{"line":150,"column":0},"end":{"line":150,"column":5}},"150":{"start":{"line":151,"column":0},"end":{"line":151,"column":1}},"152":{"start":{"line":153,"column":0},"end":{"line":153,"column":35}},"153":{"start":{"line":154,"column":0},"end":{"line":154,"column":14}},"154":{"start":{"line":155,"column":0},"end":{"line":155,"column":22}},"155":{"start":{"line":156,"column":0},"end":{"line":156,"column":19}},"156":{"start":{"line":157,"column":0},"end":{"line":157,"column":19}},"157":{"start":{"line":158,"column":0},"end":{"line":158,"column":21}},"158":{"start":{"line":159,"column":0},"end":{"line":159,"column":17}},"159":{"start":{"line":160,"column":0},"end":{"line":160,"column":17}},"160":{"start":{"line":161,"column":0},"end":{"line":161,"column":16}},"161":{"start":{"line":162,"column":0},"end":{"line":162,"column":14}},"162":{"start":{"line":163,"column":0},"end":{"line":163,"column":18}},"163":{"start":{"line":164,"column":0},"end":{"line":164,"column":17}},"164":{"start":{"line":165,"column":0},"end":{"line":165,"column":27}},"165":{"start":{"line":166,"column":0},"end":{"line":166,"column":45}},"166":{"start":{"line":167,"column":0},"end":{"line":167,"column":56}},"167":{"start":{"line":168,"column":0},"end":{"line":168,"column":80}},"168":{"start":{"line":169,"column":0},"end":{"line":169,"column":66}},"169":{"start":{"line":170,"column":0},"end":{"line":170,"column":32}},"170":{"start":{"line":171,"column":0},"end":{"line":171,"column":51}},"171":{"start":{"line":172,"column":0},"end":{"line":172,"column":32}},"175":{"start":{"line":176,"column":0},"end":{"line":176,"column":18}},"176":{"start":{"line":177,"column":0},"end":{"line":177,"column":48}},"177":{"start":{"line":178,"column":0},"end":{"line":178,"column":24}},"178":{"start":{"line":179,"column":0},"end":{"line":179,"column":18}},"179":{"start":{"line":180,"column":0},"end":{"line":180,"column":5}},"180":{"start":{"line":181,"column":0},"end":{"line":181,"column":35}},"181":{"start":{"line":182,"column":0},"end":{"line":182,"column":40}},"182":{"start":{"line":183,"column":0},"end":{"line":183,"column":36}},"183":{"start":{"line":184,"column":0},"end":{"line":184,"column":20}},"184":{"start":{"line":185,"column":0},"end":{"line":185,"column":26}},"185":{"start":{"line":186,"column":0},"end":{"line":186,"column":7}},"186":{"start":{"line":187,"column":0},"end":{"line":187,"column":5}},"187":{"start":{"line":188,"column":0},"end":{"line":188,"column":16}},"188":{"start":{"line":189,"column":0},"end":{"line":189,"column":21}},"190":{"start":{"line":191,"column":0},"end":{"line":191,"column":49}},"191":{"start":{"line":192,"column":0},"end":{"line":192,"column":26}},"192":{"start":{"line":193,"column":0},"end":{"line":193,"column":25}},"193":{"start":{"line":194,"column":0},"end":{"line":194,"column":9}},"195":{"start":{"line":196,"column":0},"end":{"line":196,"column":50}},"196":{"start":{"line":197,"column":0},"end":{"line":197,"column":45}},"197":{"start":{"line":198,"column":0},"end":{"line":198,"column":33}},"198":{"start":{"line":199,"column":0},"end":{"line":199,"column":29}},"199":{"start":{"line":200,"column":0},"end":{"line":200,"column":32}},"200":{"start":{"line":201,"column":0},"end":{"line":201,"column":26}},"201":{"start":{"line":202,"column":0},"end":{"line":202,"column":22}},"202":{"start":{"line":203,"column":0},"end":{"line":203,"column":13}},"203":{"start":{"line":204,"column":0},"end":{"line":204,"column":5}},"205":{"start":{"line":206,"column":0},"end":{"line":206,"column":9}},"206":{"start":{"line":207,"column":0},"end":{"line":207,"column":26}},"207":{"start":{"line":208,"column":0},"end":{"line":208,"column":31}},"208":{"start":{"line":209,"column":0},"end":{"line":209,"column":28}},"209":{"start":{"line":210,"column":0},"end":{"line":210,"column":39}},"210":{"start":{"line":211,"column":0},"end":{"line":211,"column":8}},"211":{"start":{"line":212,"column":0},"end":{"line":212,"column":21}},"212":{"start":{"line":213,"column":0},"end":{"line":213,"column":73}},"213":{"start":{"line":214,"column":0},"end":{"line":214,"column":5}},"214":{"start":{"line":215,"column":0},"end":{"line":215,"column":43}},"216":{"start":{"line":217,"column":0},"end":{"line":217,"column":19}},"217":{"start":{"line":218,"column":0},"end":{"line":218,"column":18}},"218":{"start":{"line":219,"column":0},"end":{"line":219,"column":35}},"219":{"start":{"line":220,"column":0},"end":{"line":220,"column":31}},"220":{"start":{"line":221,"column":0},"end":{"line":221,"column":34}},"221":{"start":{"line":222,"column":0},"end":{"line":222,"column":6}},"222":{"start":{"line":223,"column":0},"end":{"line":223,"column":9}},"224":{"start":{"line":225,"column":0},"end":{"line":225,"column":37}},"225":{"start":{"line":226,"column":0},"end":{"line":226,"column":31}},"226":{"start":{"line":227,"column":0},"end":{"line":227,"column":44}},"227":{"start":{"line":228,"column":0},"end":{"line":228,"column":45}},"228":{"start":{"line":229,"column":0},"end":{"line":229,"column":77}},"229":{"start":{"line":230,"column":0},"end":{"line":230,"column":15}},"230":{"start":{"line":231,"column":0},"end":{"line":231,"column":7}},"232":{"start":{"line":233,"column":0},"end":{"line":233,"column":44}},"233":{"start":{"line":234,"column":0},"end":{"line":234,"column":34}},"234":{"start":{"line":235,"column":0},"end":{"line":235,"column":35}},"235":{"start":{"line":236,"column":0},"end":{"line":236,"column":35}},"236":{"start":{"line":237,"column":0},"end":{"line":237,"column":27}},"237":{"start":{"line":238,"column":0},"end":{"line":238,"column":27}},"238":{"start":{"line":239,"column":0},"end":{"line":239,"column":25}},"240":{"start":{"line":241,"column":0},"end":{"line":241,"column":93}},"242":{"start":{"line":243,"column":0},"end":{"line":243,"column":45}},"243":{"start":{"line":244,"column":0},"end":{"line":244,"column":23}},"244":{"start":{"line":245,"column":0},"end":{"line":245,"column":24}},"245":{"start":{"line":246,"column":0},"end":{"line":246,"column":54}},"246":{"start":{"line":247,"column":0},"end":{"line":247,"column":28}},"247":{"start":{"line":248,"column":0},"end":{"line":248,"column":51}},"248":{"start":{"line":249,"column":0},"end":{"line":249,"column":22}},"249":{"start":{"line":250,"column":0},"end":{"line":250,"column":29}},"250":{"start":{"line":251,"column":0},"end":{"line":251,"column":19}},"251":{"start":{"line":252,"column":0},"end":{"line":252,"column":20}},"252":{"start":{"line":253,"column":0},"end":{"line":253,"column":12}},"253":{"start":{"line":254,"column":0},"end":{"line":254,"column":28}},"254":{"start":{"line":255,"column":0},"end":{"line":255,"column":24}},"255":{"start":{"line":256,"column":0},"end":{"line":256,"column":11}},"256":{"start":{"line":257,"column":0},"end":{"line":257,"column":55}},"257":{"start":{"line":258,"column":0},"end":{"line":258,"column":38}},"258":{"start":{"line":259,"column":0},"end":{"line":259,"column":11}},"259":{"start":{"line":260,"column":0},"end":{"line":260,"column":39}},"260":{"start":{"line":261,"column":0},"end":{"line":261,"column":9}},"261":{"start":{"line":262,"column":0},"end":{"line":262,"column":20}},"262":{"start":{"line":263,"column":0},"end":{"line":263,"column":8}},"264":{"start":{"line":265,"column":0},"end":{"line":265,"column":11}},"265":{"start":{"line":266,"column":0},"end":{"line":266,"column":42}},"266":{"start":{"line":267,"column":0},"end":{"line":267,"column":26}},"267":{"start":{"line":268,"column":0},"end":{"line":268,"column":17}},"268":{"start":{"line":269,"column":0},"end":{"line":269,"column":9}},"270":{"start":{"line":271,"column":0},"end":{"line":271,"column":60}},"271":{"start":{"line":272,"column":0},"end":{"line":272,"column":26}},"272":{"start":{"line":273,"column":0},"end":{"line":273,"column":17}},"273":{"start":{"line":274,"column":0},"end":{"line":274,"column":9}},"275":{"start":{"line":276,"column":0},"end":{"line":276,"column":49}},"276":{"start":{"line":277,"column":0},"end":{"line":277,"column":53}},"277":{"start":{"line":278,"column":0},"end":{"line":278,"column":57}},"278":{"start":{"line":279,"column":0},"end":{"line":279,"column":58}},"279":{"start":{"line":280,"column":0},"end":{"line":280,"column":43}},"280":{"start":{"line":281,"column":0},"end":{"line":281,"column":17}},"282":{"start":{"line":283,"column":0},"end":{"line":283,"column":28}},"283":{"start":{"line":284,"column":0},"end":{"line":284,"column":78}},"284":{"start":{"line":285,"column":0},"end":{"line":285,"column":28}},"285":{"start":{"line":286,"column":0},"end":{"line":286,"column":19}},"286":{"start":{"line":287,"column":0},"end":{"line":287,"column":11}},"287":{"start":{"line":288,"column":0},"end":{"line":288,"column":23}},"288":{"start":{"line":289,"column":0},"end":{"line":289,"column":19}},"289":{"start":{"line":290,"column":0},"end":{"line":290,"column":11}},"290":{"start":{"line":291,"column":0},"end":{"line":291,"column":45}},"291":{"start":{"line":292,"column":0},"end":{"line":292,"column":24}},"292":{"start":{"line":293,"column":0},"end":{"line":293,"column":82}},"293":{"start":{"line":294,"column":0},"end":{"line":294,"column":11}},"294":{"start":{"line":295,"column":0},"end":{"line":295,"column":47}},"295":{"start":{"line":296,"column":0},"end":{"line":296,"column":25}},"296":{"start":{"line":297,"column":0},"end":{"line":297,"column":83}},"297":{"start":{"line":298,"column":0},"end":{"line":298,"column":11}},"299":{"start":{"line":300,"column":0},"end":{"line":300,"column":78}},"300":{"start":{"line":301,"column":0},"end":{"line":301,"column":28}},"301":{"start":{"line":302,"column":0},"end":{"line":302,"column":106}},"302":{"start":{"line":303,"column":0},"end":{"line":303,"column":14}},"303":{"start":{"line":304,"column":0},"end":{"line":304,"column":11}},"305":{"start":{"line":306,"column":0},"end":{"line":306,"column":68}},"306":{"start":{"line":307,"column":0},"end":{"line":307,"column":14}},"307":{"start":{"line":308,"column":0},"end":{"line":308,"column":30}},"308":{"start":{"line":309,"column":0},"end":{"line":309,"column":37}},"309":{"start":{"line":310,"column":0},"end":{"line":310,"column":46}},"310":{"start":{"line":311,"column":0},"end":{"line":311,"column":34}},"311":{"start":{"line":312,"column":0},"end":{"line":312,"column":42}},"312":{"start":{"line":313,"column":0},"end":{"line":313,"column":13}},"313":{"start":{"line":314,"column":0},"end":{"line":314,"column":28}},"314":{"start":{"line":315,"column":0},"end":{"line":315,"column":109}},"315":{"start":{"line":316,"column":0},"end":{"line":316,"column":14}},"316":{"start":{"line":317,"column":0},"end":{"line":317,"column":11}},"317":{"start":{"line":318,"column":0},"end":{"line":318,"column":46}},"318":{"start":{"line":319,"column":0},"end":{"line":319,"column":32}},"319":{"start":{"line":320,"column":0},"end":{"line":320,"column":54}},"320":{"start":{"line":321,"column":0},"end":{"line":321,"column":52}},"321":{"start":{"line":322,"column":0},"end":{"line":322,"column":41}},"322":{"start":{"line":323,"column":0},"end":{"line":323,"column":64}},"323":{"start":{"line":324,"column":0},"end":{"line":324,"column":30}},"324":{"start":{"line":325,"column":0},"end":{"line":325,"column":21}},"325":{"start":{"line":326,"column":0},"end":{"line":326,"column":13}},"327":{"start":{"line":328,"column":0},"end":{"line":328,"column":61}},"328":{"start":{"line":329,"column":0},"end":{"line":329,"column":28}},"329":{"start":{"line":330,"column":0},"end":{"line":330,"column":30}},"330":{"start":{"line":331,"column":0},"end":{"line":331,"column":114}},"331":{"start":{"line":332,"column":0},"end":{"line":332,"column":33}},"332":{"start":{"line":333,"column":0},"end":{"line":333,"column":22}},"333":{"start":{"line":334,"column":0},"end":{"line":334,"column":16}},"334":{"start":{"line":335,"column":0},"end":{"line":335,"column":13}},"336":{"start":{"line":337,"column":0},"end":{"line":337,"column":49}},"337":{"start":{"line":338,"column":0},"end":{"line":338,"column":38}},"338":{"start":{"line":339,"column":0},"end":{"line":339,"column":40}},"339":{"start":{"line":340,"column":0},"end":{"line":340,"column":16}},"340":{"start":{"line":341,"column":0},"end":{"line":341,"column":31}},"341":{"start":{"line":342,"column":0},"end":{"line":342,"column":42}},"342":{"start":{"line":343,"column":0},"end":{"line":343,"column":33}},"343":{"start":{"line":344,"column":0},"end":{"line":344,"column":43}},"344":{"start":{"line":345,"column":0},"end":{"line":345,"column":15}},"345":{"start":{"line":346,"column":0},"end":{"line":346,"column":83}},"346":{"start":{"line":347,"column":0},"end":{"line":347,"column":31}},"347":{"start":{"line":348,"column":0},"end":{"line":348,"column":33}},"348":{"start":{"line":349,"column":0},"end":{"line":349,"column":23}},"349":{"start":{"line":350,"column":0},"end":{"line":350,"column":25}},"350":{"start":{"line":351,"column":0},"end":{"line":351,"column":17}},"351":{"start":{"line":352,"column":0},"end":{"line":352,"column":13}},"352":{"start":{"line":353,"column":0},"end":{"line":353,"column":50}},"353":{"start":{"line":354,"column":0},"end":{"line":354,"column":18}},"354":{"start":{"line":355,"column":0},"end":{"line":355,"column":44}},"355":{"start":{"line":356,"column":0},"end":{"line":356,"column":40}},"356":{"start":{"line":357,"column":0},"end":{"line":357,"column":11}},"358":{"start":{"line":359,"column":0},"end":{"line":359,"column":46}},"360":{"start":{"line":361,"column":0},"end":{"line":361,"column":42}},"361":{"start":{"line":362,"column":0},"end":{"line":362,"column":26}},"362":{"start":{"line":363,"column":0},"end":{"line":363,"column":69}},"363":{"start":{"line":364,"column":0},"end":{"line":364,"column":11}},"364":{"start":{"line":365,"column":0},"end":{"line":365,"column":35}},"365":{"start":{"line":366,"column":0},"end":{"line":366,"column":35}},"366":{"start":{"line":367,"column":0},"end":{"line":367,"column":21}},"367":{"start":{"line":368,"column":0},"end":{"line":368,"column":30}},"368":{"start":{"line":369,"column":0},"end":{"line":369,"column":12}},"369":{"start":{"line":370,"column":0},"end":{"line":370,"column":93}},"370":{"start":{"line":371,"column":0},"end":{"line":371,"column":30}},"371":{"start":{"line":372,"column":0},"end":{"line":372,"column":13}},"372":{"start":{"line":373,"column":0},"end":{"line":373,"column":28}},"373":{"start":{"line":374,"column":0},"end":{"line":374,"column":19}},"374":{"start":{"line":375,"column":0},"end":{"line":375,"column":11}},"375":{"start":{"line":376,"column":0},"end":{"line":376,"column":53}},"376":{"start":{"line":377,"column":0},"end":{"line":377,"column":71}},"377":{"start":{"line":378,"column":0},"end":{"line":378,"column":37}},"378":{"start":{"line":379,"column":0},"end":{"line":379,"column":37}},"379":{"start":{"line":380,"column":0},"end":{"line":380,"column":23}},"380":{"start":{"line":381,"column":0},"end":{"line":381,"column":21}},"381":{"start":{"line":382,"column":0},"end":{"line":382,"column":14}},"382":{"start":{"line":383,"column":0},"end":{"line":383,"column":11}},"384":{"start":{"line":385,"column":0},"end":{"line":385,"column":81}},"386":{"start":{"line":387,"column":0},"end":{"line":387,"column":28}},"387":{"start":{"line":388,"column":0},"end":{"line":388,"column":19}},"388":{"start":{"line":389,"column":0},"end":{"line":389,"column":11}},"389":{"start":{"line":390,"column":0},"end":{"line":390,"column":39}},"391":{"start":{"line":392,"column":0},"end":{"line":392,"column":41}},"392":{"start":{"line":393,"column":0},"end":{"line":393,"column":19}},"393":{"start":{"line":394,"column":0},"end":{"line":394,"column":11}},"395":{"start":{"line":396,"column":0},"end":{"line":396,"column":57}},"396":{"start":{"line":397,"column":0},"end":{"line":397,"column":28}},"397":{"start":{"line":398,"column":0},"end":{"line":398,"column":107}},"398":{"start":{"line":399,"column":0},"end":{"line":399,"column":14}},"399":{"start":{"line":400,"column":0},"end":{"line":400,"column":11}},"401":{"start":{"line":402,"column":0},"end":{"line":402,"column":46}},"402":{"start":{"line":403,"column":0},"end":{"line":403,"column":22}},"403":{"start":{"line":404,"column":0},"end":{"line":404,"column":29}},"404":{"start":{"line":405,"column":0},"end":{"line":405,"column":19}},"405":{"start":{"line":406,"column":0},"end":{"line":406,"column":20}},"406":{"start":{"line":407,"column":0},"end":{"line":407,"column":12}},"407":{"start":{"line":408,"column":0},"end":{"line":408,"column":27}},"408":{"start":{"line":409,"column":0},"end":{"line":409,"column":41}},"409":{"start":{"line":410,"column":0},"end":{"line":410,"column":21}},"410":{"start":{"line":411,"column":0},"end":{"line":411,"column":11}},"411":{"start":{"line":412,"column":0},"end":{"line":412,"column":9}},"412":{"start":{"line":413,"column":0},"end":{"line":413,"column":23}},"413":{"start":{"line":414,"column":0},"end":{"line":414,"column":23}},"414":{"start":{"line":415,"column":0},"end":{"line":415,"column":67}},"415":{"start":{"line":416,"column":0},"end":{"line":416,"column":34}},"416":{"start":{"line":417,"column":0},"end":{"line":417,"column":28}},"417":{"start":{"line":418,"column":0},"end":{"line":418,"column":59}},"418":{"start":{"line":419,"column":0},"end":{"line":419,"column":59}},"419":{"start":{"line":420,"column":0},"end":{"line":420,"column":17}},"420":{"start":{"line":421,"column":0},"end":{"line":421,"column":45}},"421":{"start":{"line":422,"column":0},"end":{"line":422,"column":39}},"422":{"start":{"line":423,"column":0},"end":{"line":423,"column":9}},"423":{"start":{"line":424,"column":0},"end":{"line":424,"column":45}},"424":{"start":{"line":425,"column":0},"end":{"line":425,"column":38}},"425":{"start":{"line":426,"column":0},"end":{"line":426,"column":32}},"426":{"start":{"line":427,"column":0},"end":{"line":427,"column":9}},"427":{"start":{"line":428,"column":0},"end":{"line":428,"column":7}},"428":{"start":{"line":429,"column":0},"end":{"line":429,"column":6}},"429":{"start":{"line":430,"column":0},"end":{"line":430,"column":5}},"430":{"start":{"line":431,"column":0},"end":{"line":431,"column":26}},"431":{"start":{"line":432,"column":0},"end":{"line":432,"column":24}},"432":{"start":{"line":433,"column":0},"end":{"line":433,"column":23}},"433":{"start":{"line":434,"column":0},"end":{"line":434,"column":25}},"434":{"start":{"line":435,"column":0},"end":{"line":435,"column":21}},"435":{"start":{"line":436,"column":0},"end":{"line":436,"column":21}},"436":{"start":{"line":437,"column":0},"end":{"line":437,"column":23}},"437":{"start":{"line":438,"column":0},"end":{"line":438,"column":21}},"438":{"start":{"line":439,"column":0},"end":{"line":439,"column":22}},"439":{"start":{"line":440,"column":0},"end":{"line":440,"column":18}},"440":{"start":{"line":441,"column":0},"end":{"line":441,"column":29}},"441":{"start":{"line":442,"column":0},"end":{"line":442,"column":18}},"442":{"start":{"line":443,"column":0},"end":{"line":443,"column":6}},"443":{"start":{"line":444,"column":0},"end":{"line":444,"column":4}},"445":{"start":{"line":446,"column":0},"end":{"line":446,"column":17}},"446":{"start":{"line":447,"column":0},"end":{"line":447,"column":12}},"447":{"start":{"line":448,"column":0},"end":{"line":448,"column":18}},"448":{"start":{"line":449,"column":0},"end":{"line":449,"column":18}},"449":{"start":{"line":450,"column":0},"end":{"line":450,"column":16}},"450":{"start":{"line":451,"column":0},"end":{"line":451,"column":21}},"451":{"start":{"line":452,"column":0},"end":{"line":452,"column":21}},"452":{"start":{"line":453,"column":0},"end":{"line":453,"column":7}},"453":{"start":{"line":454,"column":0},"end":{"line":454,"column":74}},"454":{"start":{"line":455,"column":0},"end":{"line":455,"column":4}},"455":{"start":{"line":456,"column":0},"end":{"line":456,"column":1}}},"s":{"0":1,"67":1,"69":281,"70":281,"71":281,"72":281,"73":269,"74":269,"75":269,"76":269,"77":269,"78":268,"79":268,"80":268,"81":268,"83":281,"85":13,"86":13,"87":13,"88":13,"89":1,"90":1,"91":1,"92":1,"94":13,"96":29,"97":29,"98":0,"99":0,"101":29,"102":29,"103":29,"104":29,"105":0,"106":0,"108":29,"109":29,"110":29,"112":30,"113":30,"114":0,"115":0,"117":30,"118":30,"119":30,"120":30,"121":29,"122":29,"124":1,"125":30,"126":30,"128":0,"129":0,"130":0,"131":0,"132":0,"133":0,"134":0,"135":0,"136":0,"137":0,"138":0,"139":0,"140":0,"141":0,"142":0,"143":0,"144":0,"146":269,"147":269,"148":269,"149":269,"150":269,"152":1,"153":323,"154":323,"155":323,"156":323,"157":323,"158":323,"159":323,"160":323,"161":323,"162":323,"163":323,"164":323,"165":323,"166":323,"167":323,"168":323,"169":323,"170":323,"171":323,"175":323,"176":323,"177":83,"178":44,"179":44,"180":39,"181":83,"182":74,"183":74,"184":74,"185":74,"186":74,"187":39,"188":323,"190":323,"191":115,"192":115,"193":323,"195":323,"196":98,"197":98,"198":98,"199":98,"200":98,"201":98,"202":81,"203":81,"205":17,"206":17,"207":17,"208":17,"209":98,"210":98,"211":98,"212":0,"213":0,"214":323,"216":323,"217":44,"218":44,"219":44,"220":44,"221":44,"222":323,"224":323,"225":323,"226":34,"227":34,"228":34,"229":0,"230":0,"232":34,"233":34,"234":34,"235":34,"236":34,"237":34,"238":34,"240":34,"242":34,"243":30,"244":30,"245":30,"246":30,"247":281,"248":281,"249":281,"250":281,"251":281,"252":281,"253":281,"254":0,"255":0,"256":281,"257":13,"258":13,"259":268,"260":251,"261":0,"262":30,"264":34,"265":34,"266":34,"267":0,"268":0,"270":34,"271":34,"272":0,"273":0,"275":34,"276":34,"277":34,"278":34,"279":34,"280":0,"282":34,"283":46,"284":46,"285":0,"286":0,"287":46,"288":5,"289":5,"290":41,"291":46,"292":0,"293":0,"294":41,"295":46,"296":0,"297":0,"299":46,"300":11,"301":11,"302":11,"303":11,"305":46,"306":46,"307":46,"308":29,"309":29,"310":29,"311":29,"312":46,"313":0,"314":0,"315":0,"316":0,"317":46,"318":46,"319":29,"320":29,"321":29,"322":29,"323":29,"324":0,"325":0,"327":29,"328":29,"329":0,"330":0,"331":0,"332":0,"333":0,"334":0,"336":29,"337":29,"338":29,"339":29,"340":29,"341":4,"342":4,"343":4,"344":29,"345":0,"346":0,"347":0,"348":0,"349":0,"350":0,"351":0,"352":29,"353":46,"354":1,"355":1,"356":1,"358":30,"360":30,"361":46,"362":0,"363":0,"364":30,"365":30,"366":30,"367":30,"368":30,"369":30,"370":30,"371":30,"372":46,"373":0,"374":0,"375":30,"376":46,"377":1,"378":1,"379":1,"380":1,"381":1,"382":1,"384":30,"386":46,"387":0,"388":0,"389":13,"391":46,"392":0,"393":0,"395":46,"396":1,"397":1,"398":1,"399":1,"401":12,"402":12,"403":12,"404":12,"405":12,"406":12,"407":46,"408":1,"409":1,"410":1,"411":46,"412":34,"413":12,"414":12,"415":12,"416":0,"417":12,"418":12,"419":34,"420":17,"421":17,"422":17,"423":17,"424":17,"425":17,"426":17,"427":17,"428":34,"429":323,"430":323,"431":323,"432":323,"433":323,"434":323,"435":323,"436":323,"437":323,"438":323,"439":323,"440":323,"441":323,"442":323,"443":323,"445":323,"446":323,"447":168,"448":168,"449":168,"450":168,"451":168,"452":168,"453":323,"454":323,"455":323},"branchMap":{"0":{"type":"branch","line":70,"loc":{"start":{"line":70,"column":0},"end":{"line":84,"column":1}},"locations":[{"start":{"line":70,"column":0},"end":{"line":84,"column":1}}]},"1":{"type":"branch","line":73,"loc":{"start":{"line":73,"column":19},"end":{"line":74,"column":32}},"locations":[{"start":{"line":73,"column":19},"end":{"line":74,"column":32}}]},"2":{"type":"branch","line":74,"loc":{"start":{"line":74,"column":19},"end":{"line":75,"column":28}},"locations":[{"start":{"line":74,"column":19},"end":{"line":75,"column":28}}]},"3":{"type":"branch","line":75,"loc":{"start":{"line":75,"column":19},"end":{"line":76,"column":31}},"locations":[{"start":{"line":75,"column":19},"end":{"line":76,"column":31}}]},"4":{"type":"branch","line":76,"loc":{"start":{"line":76,"column":19},"end":{"line":77,"column":33}},"locations":[{"start":{"line":76,"column":19},"end":{"line":77,"column":33}}]},"5":{"type":"branch","line":77,"loc":{"start":{"line":77,"column":19},"end":{"line":78,"column":30}},"locations":[{"start":{"line":77,"column":19},"end":{"line":78,"column":30}}]},"6":{"type":"branch","line":78,"loc":{"start":{"line":78,"column":19},"end":{"line":79,"column":31}},"locations":[{"start":{"line":78,"column":19},"end":{"line":79,"column":31}}]},"7":{"type":"branch","line":79,"loc":{"start":{"line":79,"column":19},"end":{"line":80,"column":29}},"locations":[{"start":{"line":79,"column":19},"end":{"line":80,"column":29}}]},"8":{"type":"branch","line":80,"loc":{"start":{"line":80,"column":19},"end":{"line":81,"column":32}},"locations":[{"start":{"line":80,"column":19},"end":{"line":81,"column":32}}]},"9":{"type":"branch","line":81,"loc":{"start":{"line":81,"column":19},"end":{"line":82,"column":30}},"locations":[{"start":{"line":81,"column":19},"end":{"line":82,"column":30}}]},"10":{"type":"branch","line":86,"loc":{"start":{"line":86,"column":0},"end":{"line":95,"column":1}},"locations":[{"start":{"line":86,"column":0},"end":{"line":95,"column":1}}]},"11":{"type":"branch","line":89,"loc":{"start":{"line":89,"column":19},"end":{"line":90,"column":32}},"locations":[{"start":{"line":89,"column":19},"end":{"line":90,"column":32}}]},"12":{"type":"branch","line":90,"loc":{"start":{"line":90,"column":19},"end":{"line":91,"column":28}},"locations":[{"start":{"line":90,"column":19},"end":{"line":91,"column":28}}]},"13":{"type":"branch","line":91,"loc":{"start":{"line":91,"column":19},"end":{"line":92,"column":31}},"locations":[{"start":{"line":91,"column":19},"end":{"line":92,"column":31}}]},"14":{"type":"branch","line":92,"loc":{"start":{"line":92,"column":19},"end":{"line":93,"column":30}},"locations":[{"start":{"line":92,"column":19},"end":{"line":93,"column":30}}]},"15":{"type":"branch","line":97,"loc":{"start":{"line":97,"column":0},"end":{"line":111,"column":1}},"locations":[{"start":{"line":97,"column":0},"end":{"line":111,"column":1}}]},"16":{"type":"branch","line":98,"loc":{"start":{"line":98,"column":49},"end":{"line":100,"column":3}},"locations":[{"start":{"line":98,"column":49},"end":{"line":100,"column":3}}]},"17":{"type":"branch","line":104,"loc":{"start":{"line":104,"column":13},"end":{"line":104,"column":33}},"locations":[{"start":{"line":104,"column":13},"end":{"line":104,"column":33}}]},"18":{"type":"branch","line":104,"loc":{"start":{"line":104,"column":47},"end":{"line":104,"column":58}},"locations":[{"start":{"line":104,"column":47},"end":{"line":104,"column":58}}]},"19":{"type":"branch","line":105,"loc":{"start":{"line":105,"column":30},"end":{"line":107,"column":3}},"locations":[{"start":{"line":105,"column":30},"end":{"line":107,"column":3}}]},"20":{"type":"branch","line":110,"loc":{"start":{"line":110,"column":30},"end":{"line":110,"column":45}},"locations":[{"start":{"line":110,"column":30},"end":{"line":110,"column":45}}]},"21":{"type":"branch","line":113,"loc":{"start":{"line":113,"column":0},"end":{"line":127,"column":1}},"locations":[{"start":{"line":113,"column":0},"end":{"line":127,"column":1}}]},"22":{"type":"branch","line":114,"loc":{"start":{"line":114,"column":49},"end":{"line":116,"column":3}},"locations":[{"start":{"line":114,"column":49},"end":{"line":116,"column":3}}]},"23":{"type":"branch","line":119,"loc":{"start":{"line":119,"column":76},"end":{"line":119,"column":89}},"locations":[{"start":{"line":119,"column":76},"end":{"line":119,"column":89}}]},"24":{"type":"branch","line":120,"loc":{"start":{"line":120,"column":13},"end":{"line":120,"column":31}},"locations":[{"start":{"line":120,"column":13},"end":{"line":120,"column":31}}]},"25":{"type":"branch","line":120,"loc":{"start":{"line":120,"column":31},"end":{"line":120,"column":49}},"locations":[{"start":{"line":120,"column":31},"end":{"line":120,"column":49}}]},"26":{"type":"branch","line":120,"loc":{"start":{"line":120,"column":43},"end":{"line":120,"column":54}},"locations":[{"start":{"line":120,"column":43},"end":{"line":120,"column":54}}]},"27":{"type":"branch","line":121,"loc":{"start":{"line":121,"column":30},"end":{"line":123,"column":3}},"locations":[{"start":{"line":121,"column":30},"end":{"line":123,"column":3}}]},"28":{"type":"branch","line":123,"loc":{"start":{"line":123,"column":2},"end":{"line":126,"column":40}},"locations":[{"start":{"line":123,"column":2},"end":{"line":126,"column":40}}]},"29":{"type":"branch","line":126,"loc":{"start":{"line":126,"column":30},"end":{"line":126,"column":45}},"locations":[{"start":{"line":126,"column":30},"end":{"line":126,"column":45}}]},"30":{"type":"branch","line":147,"loc":{"start":{"line":147,"column":0},"end":{"line":151,"column":1}},"locations":[{"start":{"line":147,"column":0},"end":{"line":151,"column":1}}]},"31":{"type":"branch","line":148,"loc":{"start":{"line":148,"column":21},"end":{"line":150,"column":3}},"locations":[{"start":{"line":148,"column":21},"end":{"line":150,"column":3}}]},"32":{"type":"branch","line":153,"loc":{"start":{"line":153,"column":7},"end":{"line":456,"column":1}},"locations":[{"start":{"line":153,"column":7},"end":{"line":456,"column":1}}]},"33":{"type":"branch","line":177,"loc":{"start":{"line":177,"column":41},"end":{"line":189,"column":5}},"locations":[{"start":{"line":177,"column":41},"end":{"line":189,"column":5}}]},"34":{"type":"branch","line":178,"loc":{"start":{"line":178,"column":23},"end":{"line":180,"column":5}},"locations":[{"start":{"line":178,"column":23},"end":{"line":180,"column":5}}]},"35":{"type":"branch","line":180,"loc":{"start":{"line":180,"column":4},"end":{"line":182,"column":39}},"locations":[{"start":{"line":180,"column":4},"end":{"line":182,"column":39}}]},"36":{"type":"branch","line":182,"loc":{"start":{"line":182,"column":39},"end":{"line":187,"column":5}},"locations":[{"start":{"line":182,"column":39},"end":{"line":187,"column":5}}]},"37":{"type":"branch","line":187,"loc":{"start":{"line":187,"column":4},"end":{"line":188,"column":16}},"locations":[{"start":{"line":187,"column":4},"end":{"line":188,"column":16}}]},"38":{"type":"branch","line":191,"loc":{"start":{"line":191,"column":42},"end":{"line":194,"column":5}},"locations":[{"start":{"line":191,"column":42},"end":{"line":194,"column":5}}]},"39":{"type":"branch","line":196,"loc":{"start":{"line":196,"column":37},"end":{"line":215,"column":5}},"locations":[{"start":{"line":196,"column":37},"end":{"line":215,"column":5}}]},"40":{"type":"branch","line":202,"loc":{"start":{"line":202,"column":21},"end":{"line":204,"column":5}},"locations":[{"start":{"line":202,"column":21},"end":{"line":204,"column":5}}]},"41":{"type":"branch","line":204,"loc":{"start":{"line":204,"column":4},"end":{"line":210,"column":29}},"locations":[{"start":{"line":204,"column":4},"end":{"line":210,"column":29}}]},"42":{"type":"branch","line":210,"loc":{"start":{"line":210,"column":19},"end":{"line":210,"column":39}},"locations":[{"start":{"line":210,"column":19},"end":{"line":210,"column":39}}]},"43":{"type":"branch","line":211,"loc":{"start":{"line":211,"column":6},"end":{"line":212,"column":13}},"locations":[{"start":{"line":211,"column":6},"end":{"line":212,"column":13}}]},"44":{"type":"branch","line":212,"loc":{"start":{"line":212,"column":4},"end":{"line":214,"column":5}},"locations":[{"start":{"line":212,"column":4},"end":{"line":214,"column":5}}]},"45":{"type":"branch","line":217,"loc":{"start":{"line":217,"column":12},"end":{"line":223,"column":5}},"locations":[{"start":{"line":217,"column":12},"end":{"line":223,"column":5}}]},"46":{"type":"branch","line":218,"loc":{"start":{"line":218,"column":11},"end":{"line":222,"column":6}},"locations":[{"start":{"line":218,"column":11},"end":{"line":222,"column":6}}]},"47":{"type":"branch","line":226,"loc":{"start":{"line":226,"column":4},"end":{"line":429,"column":6}},"locations":[{"start":{"line":226,"column":4},"end":{"line":429,"column":6}}]},"48":{"type":"branch","line":229,"loc":{"start":{"line":229,"column":76},"end":{"line":231,"column":7}},"locations":[{"start":{"line":229,"column":76},"end":{"line":231,"column":7}}]},"49":{"type":"branch","line":267,"loc":{"start":{"line":267,"column":25},"end":{"line":269,"column":9}},"locations":[{"start":{"line":267,"column":25},"end":{"line":269,"column":9}}]},"50":{"type":"branch","line":272,"loc":{"start":{"line":272,"column":25},"end":{"line":274,"column":9}},"locations":[{"start":{"line":272,"column":25},"end":{"line":274,"column":9}}]},"51":{"type":"branch","line":280,"loc":{"start":{"line":280,"column":42},"end":{"line":281,"column":17}},"locations":[{"start":{"line":280,"column":42},"end":{"line":281,"column":17}}]},"52":{"type":"branch","line":283,"loc":{"start":{"line":283,"column":27},"end":{"line":412,"column":9}},"locations":[{"start":{"line":283,"column":27},"end":{"line":412,"column":9}}]},"53":{"type":"branch","line":285,"loc":{"start":{"line":285,"column":27},"end":{"line":287,"column":11}},"locations":[{"start":{"line":285,"column":27},"end":{"line":287,"column":11}}]},"54":{"type":"branch","line":288,"loc":{"start":{"line":288,"column":22},"end":{"line":290,"column":11}},"locations":[{"start":{"line":288,"column":22},"end":{"line":290,"column":11}}]},"55":{"type":"branch","line":290,"loc":{"start":{"line":290,"column":10},"end":{"line":292,"column":23}},"locations":[{"start":{"line":290,"column":10},"end":{"line":292,"column":23}}]},"56":{"type":"branch","line":292,"loc":{"start":{"line":292,"column":23},"end":{"line":294,"column":11}},"locations":[{"start":{"line":292,"column":23},"end":{"line":294,"column":11}}]},"57":{"type":"branch","line":294,"loc":{"start":{"line":294,"column":10},"end":{"line":296,"column":24}},"locations":[{"start":{"line":294,"column":10},"end":{"line":296,"column":24}}]},"58":{"type":"branch","line":296,"loc":{"start":{"line":296,"column":24},"end":{"line":298,"column":11}},"locations":[{"start":{"line":296,"column":24},"end":{"line":298,"column":11}}]},"59":{"type":"branch","line":298,"loc":{"start":{"line":298,"column":10},"end":{"line":300,"column":77}},"locations":[{"start":{"line":298,"column":10},"end":{"line":300,"column":77}}]},"60":{"type":"branch","line":300,"loc":{"start":{"line":300,"column":77},"end":{"line":304,"column":11}},"locations":[{"start":{"line":300,"column":77},"end":{"line":304,"column":11}}]},"61":{"type":"branch","line":304,"loc":{"start":{"line":304,"column":10},"end":{"line":306,"column":63}},"locations":[{"start":{"line":304,"column":10},"end":{"line":306,"column":63}}]},"62":{"type":"branch","line":306,"loc":{"start":{"line":306,"column":58},"end":{"line":306,"column":68}},"locations":[{"start":{"line":306,"column":58},"end":{"line":306,"column":68}}]},"63":{"type":"branch","line":308,"loc":{"start":{"line":308,"column":13},"end":{"line":309,"column":37}},"locations":[{"start":{"line":308,"column":13},"end":{"line":309,"column":37}}]},"64":{"type":"branch","line":309,"loc":{"start":{"line":309,"column":12},"end":{"line":310,"column":46}},"locations":[{"start":{"line":309,"column":12},"end":{"line":310,"column":46}}]},"65":{"type":"branch","line":310,"loc":{"start":{"line":310,"column":42},"end":{"line":311,"column":34}},"locations":[{"start":{"line":310,"column":42},"end":{"line":311,"column":34}}]},"66":{"type":"branch","line":311,"loc":{"start":{"line":311,"column":12},"end":{"line":312,"column":42}},"locations":[{"start":{"line":311,"column":12},"end":{"line":312,"column":42}}]},"67":{"type":"branch","line":313,"loc":{"start":{"line":313,"column":12},"end":{"line":317,"column":11}},"locations":[{"start":{"line":313,"column":12},"end":{"line":317,"column":11}}]},"68":{"type":"branch","line":317,"loc":{"start":{"line":317,"column":10},"end":{"line":318,"column":38}},"locations":[{"start":{"line":317,"column":10},"end":{"line":318,"column":38}}]},"69":{"type":"branch","line":319,"loc":{"start":{"line":319,"column":31},"end":{"line":354,"column":17}},"locations":[{"start":{"line":319,"column":31},"end":{"line":354,"column":17}}]},"70":{"type":"branch","line":324,"loc":{"start":{"line":324,"column":29},"end":{"line":326,"column":13}},"locations":[{"start":{"line":324,"column":29},"end":{"line":326,"column":13}}]},"71":{"type":"branch","line":329,"loc":{"start":{"line":329,"column":27},"end":{"line":335,"column":13}},"locations":[{"start":{"line":329,"column":27},"end":{"line":335,"column":13}}]},"72":{"type":"branch","line":341,"loc":{"start":{"line":341,"column":14},"end":{"line":342,"column":42}},"locations":[{"start":{"line":341,"column":14},"end":{"line":342,"column":42}}]},"73":{"type":"branch","line":342,"loc":{"start":{"line":342,"column":33},"end":{"line":343,"column":33}},"locations":[{"start":{"line":342,"column":33},"end":{"line":343,"column":33}}]},"74":{"type":"branch","line":343,"loc":{"start":{"line":343,"column":14},"end":{"line":344,"column":43}},"locations":[{"start":{"line":343,"column":14},"end":{"line":344,"column":43}}]},"75":{"type":"branch","line":345,"loc":{"start":{"line":345,"column":14},"end":{"line":352,"column":13}},"locations":[{"start":{"line":345,"column":14},"end":{"line":352,"column":13}}]},"76":{"type":"branch","line":353,"loc":{"start":{"line":353,"column":12},"end":{"line":353,"column":50}},"locations":[{"start":{"line":353,"column":12},"end":{"line":353,"column":50}}]},"77":{"type":"branch","line":354,"loc":{"start":{"line":354,"column":10},"end":{"line":357,"column":11}},"locations":[{"start":{"line":354,"column":10},"end":{"line":357,"column":11}}]},"78":{"type":"branch","line":357,"loc":{"start":{"line":357,"column":10},"end":{"line":362,"column":25}},"locations":[{"start":{"line":357,"column":10},"end":{"line":362,"column":25}}]},"79":{"type":"branch","line":362,"loc":{"start":{"line":362,"column":25},"end":{"line":364,"column":11}},"locations":[{"start":{"line":362,"column":25},"end":{"line":364,"column":11}}]},"80":{"type":"branch","line":364,"loc":{"start":{"line":364,"column":10},"end":{"line":373,"column":27}},"locations":[{"start":{"line":364,"column":10},"end":{"line":373,"column":27}}]},"81":{"type":"branch","line":373,"loc":{"start":{"line":373,"column":27},"end":{"line":375,"column":11}},"locations":[{"start":{"line":373,"column":27},"end":{"line":375,"column":11}}]},"82":{"type":"branch","line":375,"loc":{"start":{"line":375,"column":10},"end":{"line":377,"column":24}},"locations":[{"start":{"line":375,"column":10},"end":{"line":377,"column":24}}]},"83":{"type":"branch","line":377,"loc":{"start":{"line":377,"column":14},"end":{"line":377,"column":70}},"locations":[{"start":{"line":377,"column":14},"end":{"line":377,"column":70}}]},"84":{"type":"branch","line":377,"loc":{"start":{"line":377,"column":70},"end":{"line":383,"column":11}},"locations":[{"start":{"line":377,"column":70},"end":{"line":383,"column":11}}]},"85":{"type":"branch","line":383,"loc":{"start":{"line":383,"column":10},"end":{"line":385,"column":81}},"locations":[{"start":{"line":383,"column":10},"end":{"line":385,"column":81}}]},"86":{"type":"branch","line":385,"loc":{"start":{"line":385,"column":79},"end":{"line":387,"column":27}},"locations":[{"start":{"line":385,"column":79},"end":{"line":387,"column":27}}]},"87":{"type":"branch","line":387,"loc":{"start":{"line":387,"column":27},"end":{"line":389,"column":11}},"locations":[{"start":{"line":387,"column":27},"end":{"line":389,"column":11}}]},"88":{"type":"branch","line":389,"loc":{"start":{"line":389,"column":10},"end":{"line":392,"column":40}},"locations":[{"start":{"line":389,"column":10},"end":{"line":392,"column":40}}]},"89":{"type":"branch","line":392,"loc":{"start":{"line":392,"column":40},"end":{"line":394,"column":11}},"locations":[{"start":{"line":392,"column":40},"end":{"line":394,"column":11}}]},"90":{"type":"branch","line":394,"loc":{"start":{"line":394,"column":10},"end":{"line":396,"column":56}},"locations":[{"start":{"line":394,"column":10},"end":{"line":396,"column":56}}]},"91":{"type":"branch","line":396,"loc":{"start":{"line":396,"column":56},"end":{"line":400,"column":11}},"locations":[{"start":{"line":396,"column":56},"end":{"line":400,"column":11}}]},"92":{"type":"branch","line":400,"loc":{"start":{"line":400,"column":10},"end":{"line":408,"column":26}},"locations":[{"start":{"line":400,"column":10},"end":{"line":408,"column":26}}]},"93":{"type":"branch","line":408,"loc":{"start":{"line":408,"column":26},"end":{"line":411,"column":11}},"locations":[{"start":{"line":408,"column":26},"end":{"line":411,"column":11}}]},"94":{"type":"branch","line":413,"loc":{"start":{"line":413,"column":6},"end":{"line":420,"column":8}},"locations":[{"start":{"line":413,"column":6},"end":{"line":420,"column":8}}]},"95":{"type":"branch","line":416,"loc":{"start":{"line":416,"column":33},"end":{"line":417,"column":28}},"locations":[{"start":{"line":416,"column":33},"end":{"line":417,"column":28}}]},"96":{"type":"branch","line":420,"loc":{"start":{"line":420,"column":6},"end":{"line":428,"column":7}},"locations":[{"start":{"line":420,"column":6},"end":{"line":428,"column":7}}]},"97":{"type":"branch","line":241,"loc":{"start":{"line":241,"column":23},"end":{"line":241,"column":93}},"locations":[{"start":{"line":241,"column":23},"end":{"line":241,"column":93}}]},"98":{"type":"branch","line":243,"loc":{"start":{"line":243,"column":38},"end":{"line":263,"column":8}},"locations":[{"start":{"line":243,"column":38},"end":{"line":263,"column":8}}]},"99":{"type":"branch","line":247,"loc":{"start":{"line":247,"column":27},"end":{"line":261,"column":9}},"locations":[{"start":{"line":247,"column":27},"end":{"line":261,"column":9}}]},"100":{"type":"branch","line":254,"loc":{"start":{"line":254,"column":27},"end":{"line":256,"column":11}},"locations":[{"start":{"line":254,"column":27},"end":{"line":256,"column":11}}]},"101":{"type":"branch","line":257,"loc":{"start":{"line":257,"column":54},"end":{"line":259,"column":11}},"locations":[{"start":{"line":257,"column":54},"end":{"line":259,"column":11}}]},"102":{"type":"branch","line":259,"loc":{"start":{"line":259,"column":10},"end":{"line":260,"column":39}},"locations":[{"start":{"line":259,"column":10},"end":{"line":260,"column":39}}]},"103":{"type":"branch","line":260,"loc":{"start":{"line":260,"column":37},"end":{"line":261,"column":9}},"locations":[{"start":{"line":260,"column":37},"end":{"line":261,"column":9}}]},"104":{"type":"branch","line":261,"loc":{"start":{"line":261,"column":8},"end":{"line":262,"column":20}},"locations":[{"start":{"line":261,"column":8},"end":{"line":262,"column":20}}]},"105":{"type":"branch","line":447,"loc":{"start":{"line":447,"column":4},"end":{"line":453,"column":7}},"locations":[{"start":{"line":447,"column":4},"end":{"line":453,"column":7}}]}},"b":{"0":[281],"1":[269],"2":[269],"3":[269],"4":[269],"5":[269],"6":[268],"7":[268],"8":[268],"9":[268],"10":[13],"11":[1],"12":[1],"13":[1],"14":[1],"15":[29],"16":[0],"17":[0],"18":[0],"19":[0],"20":[0],"21":[30],"22":[0],"23":[1],"24":[0],"25":[1],"26":[29],"27":[29],"28":[1],"29":[0],"30":[269],"31":[269],"32":[323],"33":[83],"34":[44],"35":[39],"36":[74],"37":[39],"38":[115],"39":[98],"40":[81],"41":[17],"42":[0],"43":[17],"44":[0],"45":[44],"46":[44],"47":[34],"48":[0],"49":[0],"50":[0],"51":[0],"52":[46],"53":[0],"54":[5],"55":[41],"56":[0],"57":[41],"58":[0],"59":[41],"60":[11],"61":[30],"62":[29],"63":[29],"64":[29],"65":[29],"66":[29],"67":[0],"68":[30],"69":[29],"70":[0],"71":[0],"72":[4],"73":[4],"74":[4],"75":[0],"76":[4],"77":[1],"78":[30],"79":[0],"80":[30],"81":[0],"82":[30],"83":[1],"84":[1],"85":[30],"86":[13],"87":[0],"88":[13],"89":[0],"90":[13],"91":[1],"92":[12],"93":[1],"94":[12],"95":[0],"96":[17],"97":[794],"98":[30],"99":[281],"100":[0],"101":[13],"102":[268],"103":[251],"104":[0],"105":[168]},"fnMap":{"0":{"name":"isFinalPhaseStatus","decl":{"start":{"line":70,"column":0},"end":{"line":84,"column":1}},"loc":{"start":{"line":70,"column":0},"end":{"line":84,"column":1}},"line":70},"1":{"name":"isCompletedPhaseStatus","decl":{"start":{"line":86,"column":0},"end":{"line":95,"column":1}},"loc":{"start":{"line":86,"column":0},"end":{"line":95,"column":1}},"line":86},"2":{"name":"extractThreadId","decl":{"start":{"line":97,"column":0},"end":{"line":111,"column":1}},"loc":{"start":{"line":97,"column":0},"end":{"line":111,"column":1}},"line":97},"3":{"name":"extractTurnId","decl":{"start":{"line":113,"column":0},"end":{"line":127,"column":1}},"loc":{"start":{"line":113,"column":0},"end":{"line":127,"column":1}},"line":113},"4":{"name":"summarizeStartThreadResponse","decl":{"start":{"line":129,"column":0},"end":{"line":145,"column":1}},"loc":{"start":{"line":129,"column":0},"end":{"line":145,"column":1}},"line":129},"5":{"name":"wait","decl":{"start":{"line":147,"column":0},"end":{"line":151,"column":1}},"loc":{"start":{"line":147,"column":0},"end":{"line":151,"column":1}},"line":147},"6":{"name":"useForgeExecution","decl":{"start":{"line":153,"column":7},"end":{"line":456,"column":1}},"loc":{"start":{"line":153,"column":7},"end":{"line":456,"column":1}},"line":153},"7":{"name":"isActive","decl":{"start":{"line":241,"column":23},"end":{"line":241,"column":93}},"loc":{"start":{"line":241,"column":23},"end":{"line":241,"column":93}},"line":241},"8":{"name":"waitForPhaseFinalStatus","decl":{"start":{"line":243,"column":38},"end":{"line":263,"column":8}},"loc":{"start":{"line":243,"column":38},"end":{"line":263,"column":8}},"line":243}},"f":{"0":281,"1":13,"2":29,"3":30,"4":0,"5":269,"6":323,"7":794,"8":30}} +} diff --git a/coverage/favicon.png b/coverage/favicon.png new file mode 100644 index 0000000000..c1525b811a Binary files /dev/null and b/coverage/favicon.png differ diff --git a/coverage/hooks/index.html b/coverage/hooks/index.html new file mode 100644 index 0000000000..c9d9aac659 --- /dev/null +++ b/coverage/hooks/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for hooks + + + + + + + + + +
+
+

All files hooks

+
+ +
+ 80.28% + Statements + 281/350 +
+ + +
+ 73.58% + Branches + 78/106 +
+ + +
+ 88.88% + Functions + 8/9 +
+ + +
+ 80.28% + Lines + 281/350 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
useForgeExecution.ts +
+
80.28%281/35073.58%78/10688.88%8/980.28%281/350
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/hooks/useForgeExecution.ts.html b/coverage/hooks/useForgeExecution.ts.html new file mode 100644 index 0000000000..c26eba18ae --- /dev/null +++ b/coverage/hooks/useForgeExecution.ts.html @@ -0,0 +1,1453 @@ + + + + + + Code coverage report for hooks/useForgeExecution.ts + + + + + + + + + +
+
+

All files / hooks useForgeExecution.ts

+
+ +
+ 80.28% + Statements + 281/350 +
+ + +
+ 73.58% + Branches + 78/106 +
+ + +
+ 88.88% + Functions + 8/9 +
+ + +
+ 80.28% + Lines + 281/350 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +4571x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +281x +281x +281x +281x +269x +269x +269x +269x +269x +268x +268x +268x +268x +  +281x +  +13x +13x +13x +13x +1x +1x +1x +1x +  +13x +  +29x +29x +  +  +  +29x +29x +29x +29x +  +  +  +29x +29x +29x +  +30x +30x +  +  +  +30x +30x +30x +30x +29x +29x +  +1x +30x +30x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +269x +269x +269x +269x +269x +  +1x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +  +  +  +323x +323x +83x +44x +44x +39x +83x +74x +74x +74x +74x +74x +39x +323x +  +323x +115x +115x +323x +  +323x +98x +98x +98x +98x +98x +98x +81x +81x +  +17x +17x +17x +17x +98x +98x +98x +  +  +323x +  +323x +44x +44x +44x +44x +44x +323x +  +323x +323x +34x +34x +34x +  +  +  +34x +34x +34x +34x +34x +34x +34x +  +34x +  +34x +30x +30x +30x +30x +281x +281x +281x +281x +281x +281x +281x +  +  +281x +13x +13x +268x +251x +  +30x +  +34x +34x +34x +  +  +  +34x +34x +  +  +  +34x +34x +34x +34x +34x +  +  +34x +46x +46x +  +  +46x +5x +5x +41x +46x +  +  +41x +46x +  +  +  +46x +11x +11x +11x +11x +  +46x +46x +46x +29x +29x +29x +29x +46x +  +  +  +  +46x +46x +29x +29x +29x +29x +29x +  +  +  +29x +29x +  +  +  +  +  +  +  +29x +29x +29x +29x +29x +4x +4x +4x +29x +  +  +  +  +  +  +  +29x +46x +1x +1x +1x +  +30x +  +30x +46x +  +  +30x +30x +30x +30x +30x +30x +30x +30x +46x +  +  +30x +46x +1x +1x +1x +1x +1x +1x +  +30x +  +46x +  +  +13x +  +46x +  +  +  +46x +1x +1x +1x +1x +  +12x +12x +12x +12x +12x +12x +46x +1x +1x +1x +46x +34x +12x +12x +12x +  +12x +12x +34x +17x +17x +17x +17x +17x +17x +17x +17x +34x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +323x +  +323x +323x +168x +168x +168x +168x +168x +168x +323x +323x +323x + 
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import type {
+  ForgeNextPhasePrompt,
+  ForgePhaseStatus,
+  ForgeRunPhaseChecksResponse,
+} from "../../../services/tauri";
+ 
+type ForgeExecutionStatusLike =
+  | "pending"
+  | "in_progress"
+  | "completed"
+  | "failed"
+  | "canceled"
+  | string;
+ 
+type ForgeExecutionArgs = {
+  workspaceId: string | null;
+  knownTaskIds?: string[] | null;
+  connectWorkspace: (workspaceId: string) => Promise<void>;
+  prepareExecution: (workspaceId: string, planId: string) => Promise<void>;
+  getNextPhasePrompt: (
+    workspaceId: string,
+    planId: string,
+  ) => Promise<ForgeNextPhasePrompt | null>;
+  getPhaseStatus: (
+    workspaceId: string,
+    planId: string,
+    taskId: string,
+    phaseId: string,
+  ) => Promise<ForgePhaseStatus>;
+  runPhaseChecks: (
+    workspaceId: string,
+    planId: string,
+    taskId: string,
+    phaseId: string,
+  ) => Promise<ForgeRunPhaseChecksResponse>;
+  interruptTurn: (
+    workspaceId: string,
+    threadId: string,
+    turnId: string,
+  ) => Promise<unknown>;
+  startThread: (workspaceId: string) => Promise<unknown>;
+  sendUserMessage: (
+    workspaceId: string,
+    threadId: string,
+    text: string,
+    options?: {
+      collaborationMode?: Record<string, unknown> | null;
+    },
+  ) => Promise<unknown>;
+  onSelectThread?: (workspaceId: string, threadId: string) => void;
+  collaborationMode?: Record<string, unknown> | null;
+};
+ 
+type ForgeRunningInfo = {
+  taskId: string;
+  phaseId: string;
+};
+ 
+type ForgeExecutionState = {
+  isExecuting: boolean;
+  runningInfo: ForgeRunningInfo | null;
+  lastError: string | null;
+  startExecution: (planId: string) => Promise<void>;
+  pauseExecution: () => Promise<void>;
+};
+ 
+const POLL_INTERVAL_MS = 1200;
+ 
+function isFinalPhaseStatus(status: ForgeExecutionStatusLike): boolean {
+  const normalized = status.trim().toLowerCase();
+  return (
+    normalized === "completed" ||
+    normalized === "complete" ||
+    normalized === "done" ||
+    normalized === "success" ||
+    normalized === "succeeded" ||
+    normalized === "failed" ||
+    normalized === "blocked" ||
+    normalized === "error" ||
+    normalized === "canceled" ||
+    normalized === "cancelled"
+  );
+}
+ 
+function isCompletedPhaseStatus(status: ForgeExecutionStatusLike): boolean {
+  const normalized = status.trim().toLowerCase();
+  return (
+    normalized === "completed" ||
+    normalized === "complete" ||
+    normalized === "done" ||
+    normalized === "success" ||
+    normalized === "succeeded"
+  );
+}
+ 
+function extractThreadId(response: unknown): string | null {
+  if (!response || typeof response !== "object") {
+    return null;
+  }
+ 
+  const directThread = (response as { thread?: { id?: unknown } }).thread;
+  const nestedThread = (response as { result?: { thread?: { id?: unknown } } }).result?.thread;
+  const id = directThread?.id ?? nestedThread?.id ?? null;
+  if (typeof id !== "string") {
+    return null;
+  }
+ 
+  const trimmed = id.trim();
+  return trimmed.length > 0 ? trimmed : null;
+}
+ 
+function extractTurnId(response: unknown): string | null {
+  if (!response || typeof response !== "object") {
+    return null;
+  }
+ 
+  const directTurn = (response as { turn?: { id?: unknown } }).turn;
+  const nestedTurn = (response as { result?: { turn?: { id?: unknown } } }).result?.turn;
+  const id = directTurn?.id ?? nestedTurn?.id ?? null;
+  if (typeof id !== "string") {
+    return null;
+  }
+ 
+  const trimmed = id.trim();
+  return trimmed.length > 0 ? trimmed : null;
+}
+ 
+function summarizeStartThreadResponse(response: unknown): string {
+  if (response == null) {
+    return String(response);
+  }
+  if (typeof response !== "object") {
+    return typeof response;
+  }
+  try {
+    const serialized = JSON.stringify(response);
+    if (!serialized) {
+      return "empty object";
+    }
+    return serialized.length > 240 ? `${serialized.slice(0, 237)}...` : serialized;
+  } catch {
+    return "unserializable object";
+  }
+}
+ 
+function wait(ms: number): Promise<void> {
+  return new Promise((resolve) => {
+    window.setTimeout(resolve, ms);
+  });
+}
+ 
+export function useForgeExecution({
+  workspaceId,
+  knownTaskIds = null,
+  connectWorkspace,
+  prepareExecution,
+  getNextPhasePrompt,
+  getPhaseStatus,
+  runPhaseChecks,
+  interruptTurn,
+  startThread,
+  sendUserMessage,
+  onSelectThread,
+  collaborationMode = null,
+}: ForgeExecutionArgs): ForgeExecutionState {
+  const [isExecuting, setIsExecuting] = useState(false);
+  const [runningInfo, setRunningInfo] = useState<ForgeRunningInfo | null>(null);
+  const [lastError, setLastError] = useState<string | null>(null);
+  const runTokenRef = useRef(0);
+  const activeRunRef = useRef<number | null>(null);
+  const activeTurnRef = useRef<{
+    workspaceId: string;
+    threadId: string;
+    turnId: string;
+  } | null>(null);
+  const normalizedKnownTaskIds = useMemo(() => {
+    if (!knownTaskIds) {
+      return null;
+    }
+    const next = new Set<string>();
+    for (const taskId of knownTaskIds) {
+      const trimmed = taskId.trim();
+      if (trimmed) {
+        next.add(trimmed);
+      }
+    }
+    return next;
+  }, [knownTaskIds]);
+ 
+  const clearExecutionState = useCallback(() => {
+    setIsExecuting(false);
+    setRunningInfo(null);
+  }, []);
+ 
+  const pauseExecution = useCallback(async () => {
+    const activeTurn = activeTurnRef.current;
+    activeTurnRef.current = null;
+    runTokenRef.current += 1;
+    activeRunRef.current = null;
+    clearExecutionState();
+    if (!activeTurn) {
+      return;
+    }
+ 
+    try {
+      await interruptTurn(
+        activeTurn.workspaceId,
+        activeTurn.threadId,
+        activeTurn.turnId || "pending",
+      );
+    } catch (error) {
+      console.warn("Forge execution pause interrupt failed.", { error });
+    }
+  }, [clearExecutionState, interruptTurn]);
+ 
+  useEffect(() => {
+    return () => {
+      activeTurnRef.current = null;
+      runTokenRef.current += 1;
+      activeRunRef.current = null;
+    };
+  }, []);
+ 
+  const startExecution = useCallback(
+    async (planId: string) => {
+      const workspace = workspaceId?.trim();
+      const normalizedPlanId = planId.trim();
+      if (!workspace || !normalizedPlanId || activeRunRef.current !== null) {
+        return;
+      }
+ 
+      const token = runTokenRef.current + 1;
+      runTokenRef.current = token;
+      activeRunRef.current = token;
+      activeTurnRef.current = null;
+      setIsExecuting(true);
+      setRunningInfo(null);
+      setLastError(null);
+ 
+      const isActive = () => activeRunRef.current === token && runTokenRef.current === token;
+ 
+      const waitForPhaseFinalStatus = async (
+        taskId: string,
+        phaseId: string,
+      ): Promise<ForgeExecutionStatusLike | null> => {
+        while (isActive()) {
+          const phaseStatus = await getPhaseStatus(
+            workspace,
+            normalizedPlanId,
+            taskId,
+            phaseId,
+          );
+          if (!isActive()) {
+            return null;
+          }
+          if (isFinalPhaseStatus(phaseStatus.status)) {
+            return phaseStatus.status;
+          }
+          await wait(POLL_INTERVAL_MS);
+        }
+        return null;
+      };
+ 
+      try {
+        await connectWorkspace(workspace);
+        if (!isActive()) {
+          return;
+        }
+ 
+        await prepareExecution(workspace, normalizedPlanId);
+        if (!isActive()) {
+          return;
+        }
+ 
+        let activeThreadId: string | null = null;
+        let activeThreadTaskId: string | null = null;
+        const threadByTaskId = new Map<string, string>();
+        const pendingKnownTaskIds = normalizedKnownTaskIds
+          ? new Set(normalizedKnownTaskIds)
+          : null;
+ 
+        while (isActive()) {
+          const phase = await getNextPhasePrompt(workspace, normalizedPlanId);
+          if (!isActive()) {
+            return;
+          }
+          if (!phase) {
+            return;
+          }
+          const taskId = phase.taskId.trim();
+          if (!taskId) {
+            throw new Error("Forge next phase prompt returned an empty task id.");
+          }
+          const phaseId = phase.phaseId.trim();
+          if (!phaseId) {
+            throw new Error("Forge next phase prompt returned an empty phase id.");
+          }
+ 
+          if (normalizedKnownTaskIds && !normalizedKnownTaskIds.has(taskId)) {
+            throw new Error(
+              `Forge execution received unexpected task id "${taskId}" not present in the selected plan.`,
+            );
+          }
+ 
+          const mappedThreadId = threadByTaskId.get(taskId) ?? null;
+          if (
+            !mappedThreadId &&
+            normalizedKnownTaskIds &&
+            normalizedKnownTaskIds.size > 0 &&
+            pendingKnownTaskIds &&
+            pendingKnownTaskIds.size === 0
+          ) {
+            throw new Error(
+              `Forge execution received unexpected task id "${taskId}" after exhausting visible plan tasks.`,
+            );
+          }
+          pendingKnownTaskIds?.delete(taskId);
+          if (!mappedThreadId) {
+            const previousTaskId = activeThreadTaskId;
+            const previousThreadId = activeThreadId;
+            activeTurnRef.current = null;
+            const threadResponse = await startThread(workspace);
+            if (!isActive()) {
+              return;
+            }
+ 
+            const threadId = extractThreadId(threadResponse);
+            if (!threadId) {
+              throw new Error(
+                `Forge execution start thread response missing thread id (response=${summarizeStartThreadResponse(
+                  threadResponse,
+                )}).`,
+              );
+            }
+ 
+            threadByTaskId.set(taskId, threadId);
+            activeThreadId = threadId;
+            activeThreadTaskId = taskId;
+            if (
+              previousTaskId &&
+              previousTaskId !== taskId &&
+              previousThreadId &&
+              previousThreadId === threadId
+            ) {
+              console.warn("Forge execution task transition reused a thread id.", {
+                previousTaskId,
+                previousThreadId,
+                taskId,
+                threadId,
+              });
+            }
+            onSelectThread?.(workspace, threadId);
+          } else {
+            activeThreadId = mappedThreadId;
+            activeThreadTaskId = taskId;
+          }
+ 
+          setRunningInfo({ taskId, phaseId });
+ 
+          const threadId = activeThreadId;
+          if (!threadId) {
+            throw new Error("Forge execution thread id is missing.");
+          }
+          activeTurnRef.current = {
+            workspaceId: workspace,
+            threadId,
+            turnId: "pending",
+          };
+          const turnResponse = await sendUserMessage(workspace, threadId, phase.promptText, {
+            collaborationMode,
+          });
+          if (!isActive()) {
+            return;
+          }
+          const turnId = extractTurnId(turnResponse);
+          if (turnId && activeTurnRef.current?.threadId === threadId) {
+            activeTurnRef.current = {
+              workspaceId: workspace,
+              threadId,
+              turnId,
+            };
+          }
+ 
+          const lastPhaseStatus = await waitForPhaseFinalStatus(taskId, phaseId);
+ 
+          if (!isActive()) {
+            return;
+          }
+          activeTurnRef.current = null;
+ 
+          if (lastPhaseStatus === null) {
+            return;
+          }
+ 
+          if (!isCompletedPhaseStatus(lastPhaseStatus)) {
+            throw new Error(
+              `Phase ${taskId}/${phaseId} reached terminal status "${lastPhaseStatus}" before completion.`,
+            );
+          }
+ 
+          const checks = await runPhaseChecks(
+            workspace,
+            normalizedPlanId,
+            taskId,
+            phaseId,
+          );
+          if (!checks.ok) {
+            await wait(POLL_INTERVAL_MS);
+            continue;
+          }
+        }
+      } catch (error) {
+        const message =
+          error instanceof Error && error.message.trim().length > 0
+            ? error.message.trim()
+            : String(error);
+        setLastError(`Forge execution failed: ${message}`);
+        console.warn("Forge execution failed.", { error });
+      } finally {
+        if (activeRunRef.current === token) {
+          activeTurnRef.current = null;
+        }
+        if (activeRunRef.current === token) {
+          activeRunRef.current = null;
+          clearExecutionState();
+        }
+      }
+    },
+    [
+      clearExecutionState,
+      collaborationMode,
+      connectWorkspace,
+      getNextPhasePrompt,
+      getPhaseStatus,
+      onSelectThread,
+      prepareExecution,
+      runPhaseChecks,
+      sendUserMessage,
+      startThread,
+      normalizedKnownTaskIds,
+      workspaceId,
+    ],
+  );
+ 
+  return useMemo(
+    () => ({
+      isExecuting,
+      runningInfo,
+      lastError,
+      startExecution,
+      pauseExecution,
+    }),
+    [isExecuting, lastError, pauseExecution, runningInfo, startExecution],
+  );
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/index.html b/coverage/index.html new file mode 100644 index 0000000000..bd3534d459 --- /dev/null +++ b/coverage/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for All files + + + + + + + + + +
+
+

All files

+
+ +
+ 87.88% + Statements + 1037/1180 +
+ + +
+ 76.96% + Branches + 254/330 +
+ + +
+ 87.5% + Functions + 28/32 +
+ + +
+ 87.88% + Lines + 1037/1180 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
components +
+
91.08%756/83078.57%176/22486.95%20/2391.08%756/830
hooks +
+
80.28%281/35073.58%78/10688.88%8/980.28%281/350
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/prettify.css b/coverage/prettify.css new file mode 100644 index 0000000000..b317a7cda3 --- /dev/null +++ b/coverage/prettify.css @@ -0,0 +1 @@ +.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/coverage/prettify.js b/coverage/prettify.js new file mode 100644 index 0000000000..b3225238f2 --- /dev/null +++ b/coverage/prettify.js @@ -0,0 +1,2 @@ +/* eslint-disable */ +window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/coverage/sort-arrow-sprite.png b/coverage/sort-arrow-sprite.png new file mode 100644 index 0000000000..6ed68316eb Binary files /dev/null and b/coverage/sort-arrow-sprite.png differ diff --git a/coverage/sorter.js b/coverage/sorter.js new file mode 100644 index 0000000000..4ed70ae5ac --- /dev/null +++ b/coverage/sorter.js @@ -0,0 +1,210 @@ +/* eslint-disable */ +var addSorting = (function() { + 'use strict'; + var cols, + currentSort = { + index: 0, + desc: false + }; + + // returns the summary table element + function getTable() { + return document.querySelector('.coverage-summary'); + } + // returns the thead element of the summary table + function getTableHeader() { + return getTable().querySelector('thead tr'); + } + // returns the tbody element of the summary table + function getTableBody() { + return getTable().querySelector('tbody'); + } + // returns the th element for nth column + function getNthColumn(n) { + return getTableHeader().querySelectorAll('th')[n]; + } + + function onFilterInput() { + const searchValue = document.getElementById('fileSearch').value; + const rows = document.getElementsByTagName('tbody')[0].children; + + // Try to create a RegExp from the searchValue. If it fails (invalid regex), + // it will be treated as a plain text search + let searchRegex; + try { + searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive + } catch (error) { + searchRegex = null; + } + + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + let isMatch = false; + + if (searchRegex) { + // If a valid regex was created, use it for matching + isMatch = searchRegex.test(row.textContent); + } else { + // Otherwise, fall back to the original plain text search + isMatch = row.textContent + .toLowerCase() + .includes(searchValue.toLowerCase()); + } + + row.style.display = isMatch ? '' : 'none'; + } + } + + // loads the search box + function addSearchBox() { + var template = document.getElementById('filterTemplate'); + var templateClone = template.content.cloneNode(true); + templateClone.getElementById('fileSearch').oninput = onFilterInput; + template.parentElement.appendChild(templateClone); + } + + // loads all columns + function loadColumns() { + var colNodes = getTableHeader().querySelectorAll('th'), + colNode, + cols = [], + col, + i; + + for (i = 0; i < colNodes.length; i += 1) { + colNode = colNodes[i]; + col = { + key: colNode.getAttribute('data-col'), + sortable: !colNode.getAttribute('data-nosort'), + type: colNode.getAttribute('data-type') || 'string' + }; + cols.push(col); + if (col.sortable) { + col.defaultDescSort = col.type === 'number'; + colNode.innerHTML = + colNode.innerHTML + ''; + } + } + return cols; + } + // attaches a data attribute to every tr element with an object + // of data values keyed by column name + function loadRowData(tableRow) { + var tableCols = tableRow.querySelectorAll('td'), + colNode, + col, + data = {}, + i, + val; + for (i = 0; i < tableCols.length; i += 1) { + colNode = tableCols[i]; + col = cols[i]; + val = colNode.getAttribute('data-value'); + if (col.type === 'number') { + val = Number(val); + } + data[col.key] = val; + } + return data; + } + // loads all row data + function loadData() { + var rows = getTableBody().querySelectorAll('tr'), + i; + + for (i = 0; i < rows.length; i += 1) { + rows[i].data = loadRowData(rows[i]); + } + } + // sorts the table using the data for the ith column + function sortByIndex(index, desc) { + var key = cols[index].key, + sorter = function(a, b) { + a = a.data[key]; + b = b.data[key]; + return a < b ? -1 : a > b ? 1 : 0; + }, + finalSorter = sorter, + tableBody = document.querySelector('.coverage-summary tbody'), + rowNodes = tableBody.querySelectorAll('tr'), + rows = [], + i; + + if (desc) { + finalSorter = function(a, b) { + return -1 * sorter(a, b); + }; + } + + for (i = 0; i < rowNodes.length; i += 1) { + rows.push(rowNodes[i]); + tableBody.removeChild(rowNodes[i]); + } + + rows.sort(finalSorter); + + for (i = 0; i < rows.length; i += 1) { + tableBody.appendChild(rows[i]); + } + } + // removes sort indicators for current column being sorted + function removeSortIndicators() { + var col = getNthColumn(currentSort.index), + cls = col.className; + + cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); + col.className = cls; + } + // adds sort indicators for current column being sorted + function addSortIndicators() { + getNthColumn(currentSort.index).className += currentSort.desc + ? ' sorted-desc' + : ' sorted'; + } + // adds event listeners for all sorter widgets + function enableUI() { + var i, + el, + ithSorter = function ithSorter(i) { + var col = cols[i]; + + return function() { + var desc = col.defaultDescSort; + + if (currentSort.index === i) { + desc = !currentSort.desc; + } + sortByIndex(i, desc); + removeSortIndicators(); + currentSort.index = i; + currentSort.desc = desc; + addSortIndicators(); + }; + }; + for (i = 0; i < cols.length; i += 1) { + if (cols[i].sortable) { + // add the click event handler on the th so users + // dont have to click on those tiny arrows + el = getNthColumn(i).querySelector('.sorter').parentElement; + if (el.addEventListener) { + el.addEventListener('click', ithSorter(i)); + } else { + el.attachEvent('onclick', ithSorter(i)); + } + } + } + } + // adds sorting functionality to the UI + return function() { + if (!getTable()) { + return; + } + cols = loadColumns(); + loadData(); + addSearchBox(); + addSortIndicators(); + enableUI(); + }; +})(); + +window.addEventListener('load', addSorting); diff --git a/docs/codebase-map.md b/docs/codebase-map.md index c3a92a03e7..dbc900bed8 100644 --- a/docs/codebase-map.md +++ b/docs/codebase-map.md @@ -21,6 +21,7 @@ If a behavior must work in both app and daemon, implement it in `src-tauri/src/s | Need | Primary files to edit | | --- | --- | | App-level UI composition/layout wiring | `src/App.tsx`, `src/features/app/components/AppLayout.tsx`, `src/features/app/bootstrap/*`, `src/features/app/orchestration/*`, `src/features/app/hooks/*` | +| Change Forge sidebar panel behavior (project gate, task-row running status, plan actions, execution controls, selected-plan task-id execution guards) | `src/features/forge/components/Forge.tsx`, `src/features/app/components/Sidebar.tsx`, `src/features/forge/hooks/useForgeExecution.ts`, `src/features/forge/components/Forge.plans.test.tsx`, `src/features/app/components/Sidebar.test.tsx`, `docs/forge.md` | | Add/change Tauri IPC methods used by frontend | `src/services/tauri.ts`, `src-tauri/src/lib.rs`, matching backend adapter module | | Add/change app-server event handling in UI | `src/services/events.ts`, `src/features/app/hooks/useAppServerEvents.ts`, `src/utils/appServerEvents.ts`, `src/features/threads/utils/threadNormalize.ts` | | Change thread state transitions | `src/features/threads/hooks/useThreadsReducer.ts`, `src/features/threads/hooks/threadReducer/*`, `src/features/threads/hooks/useThreads.ts`, focused thread hooks under `src/features/threads/hooks/*` | @@ -84,6 +85,13 @@ Use TS/Vite aliases for refactor-safe imports: - Prompt UI and workflow: `src/features/prompts/components/PromptPanel.tsx`, `src/features/prompts/hooks/useCustomPrompts.ts` +### Forge + +- Forge panel UI and controls: `src/features/forge/components/Forge.tsx` +- Forge execution loop wiring: `src/features/forge/hooks/useForgeExecution.ts` +- Sidebar Forge panel toggle wiring: `src/features/app/components/Sidebar.tsx` +- Forge behavior tests: `src/features/forge/components/Forge.plans.test.tsx`, `src/features/app/components/Sidebar.test.tsx` + ## Backend App (Tauri) Navigation - Command registry (what frontend can invoke): `src-tauri/src/lib.rs` diff --git a/docs/forge.md b/docs/forge.md index 1b83842c84..ed66c1267f 100644 --- a/docs/forge.md +++ b/docs/forge.md @@ -40,6 +40,57 @@ Notes: - In remote mode, each app handler calls `remote_backend::call_remote`; daemon `rpc.rs` parses the same camelCase keys with `parse_string`. - `remote_backend::can_retry_after_disconnect` includes all Forge methods above, so disconnect errors are retried once after reconnect. +## UI Phase Metadata and Icon Resolution + +Forge execution chips use a frontend-composed phase-view model rather than a dedicated `forge_*` backend command: + +- Loader: `src/services/tauri.ts::forgeLoadPhaseView` +- File reads (via `read_workspace_file`): + - `plans//state.json` + - `.agent/templates//phases.json` (when a template is installed) + +`forgeLoadPhaseView` behavior: + +- Missing, malformed, or truncated files are treated as non-fatal and return empty phase metadata/status maps. +- State phase statuses are normalized to: + - `pending` + - `in_progress` + - `completed` + - `blocked` + - `failed` +- Unknown statuses normalize to `pending`. +- Template phase metadata is sorted by `order` (fallback: array index + 1). +- Missing/blank `iconId` metadata falls back to `check_circle` during metadata parsing. + +Execution UI rendering behavior: + +- `src/features/forge/components/Forge.tsx::mapForgePhaseChipState` maps states as: + - running phase or `in_progress` -> `is-current` + - `completed` -> `is-complete` + - `pending` / `blocked` / `failed` -> `is-pending` +- `src/features/forge/components/Forge.tsx::resolveForgeTaskRowStatus` computes task-row state with deterministic precedence: + - active `runningInfo.taskId` -> row `inProgress` + - otherwise phase-state fallback from `phaseView.taskPhaseStatusByTaskId[taskId]` + - otherwise plan task status from `forge_list_plans` / `state-v2` +- Stale non-active `in_progress` rows are demoted while another task is actively running: + - demote to `completed` if all phases for that task are `completed` + - otherwise demote to `pending` +- If `runningInfo.taskId` does not match any visible plan task row, no row renders a running spinner. +- `src/features/forge/hooks/useForgeExecution.ts` enforces selected-plan task-id execution guards: + - `Forge.tsx` passes `knownTaskIds` from `selectedPlan.items[*].id` into `useForgeExecution`. + - Every `forge_get_next_phase_prompt` task id is validated before `startThread`. + - If task id is unknown to the selected plan, execution stops with `lastError`/alert and does not start a thread for that task. + - Overflow guard: once all visible task ids have been seen in the run, a new task id cannot start a new session/thread. + - If backend returns `null` after final visible task, execution exits cleanly with no extra session/thread. +- Forge sidebar panel project gate (`src/features/forge/components/Forge.tsx` + `src/features/app/components/Sidebar.tsx`): + - Forge panel can be opened from the sidebar header even when no workspace is active. + - Forge actions require an explicitly selected project (`activeWorkspaceId` non-empty); opening the panel alone is not enough to run Forge operations. + - If `activeWorkspaceId` is null/blank, Forge renders blocker copy `Select a project first to use Forge.`. + - In no-project mode, these controls are disabled and blocked from backend side effects: `Open templates`, plan selector, `Resume plan`, and `Clean progress`. + - Plan menu actions (`New plan...`, `Other...`) are not reachable while blocked, and no-project control clicks do not call `connectWorkspace`, `startThread`, or `sendUserMessage`. +- `src/utils/forgePhaseIcons.ts::getForgePhaseIconUrl` validates icon ids with `isMaterialIconName` and resolves URLs with `getIconUrlByName(id, "/assets/material-icons")`. +- Invalid/empty icon ids fall back to safe icon id `file` (`/assets/material-icons/file.svg`). + ## Local vs Remote Command Routing Each app handler in `src-tauri/src/forge/mod.rs` follows the same branch: @@ -81,9 +132,10 @@ Bundled templates are stored under: Each template folder must contain `template.json` (read by `read_manifest` in `src-tauri/src/shared/forge_templates_core.rs`). `forge_list_bundled_templates` ignores folders that are missing or fail to parse `template.json`. -Current bundled example: +Current bundled templates: - `src-tauri/resources/forge/templates/ralph-loop/template.json` +- `src-tauri/resources/forge/templates/test-first-loop/template.json` `template.json` fields (schema `forge-template-v1`): @@ -102,7 +154,53 @@ Current bundled example: - `entrypoints.hooks.preExecute`: hook script run by `forge_prepare_execution`/`forge_reset_execution_progress` before execution. - `entrypoints.hooks.postStep`: hook script run by `forge_get_next_phase_prompt` and `forge_run_phase_checks` to regenerate `plans//execute-prompt.md`. -`template.json` should also be included in `files` so installed templates at `.agent/templates//` remain self-describing (the bundled `ralph-loop` template does this). +`template.json` should also be included in `files` so installed templates at `.agent/templates//` remain self-describing (both bundled templates do this). + +## `test-first-loop` Phase Contract (6 Phases) + +`src-tauri/resources/forge/templates/test-first-loop/phases.json` defines six ordered phases: + +| Order | Phase id | Title | iconId | Completion contract highlights | +| --- | --- | --- | --- | --- | +| 1 | `test-case-mapping` | `Test Case Mapping` | `taskfile` | Requirement-to-test mapping, edge/failure paths, risk prioritization, assumptions documented | +| 2 | `behavioral-tests` | `Behavioral Tests` | `cucumber` | Behavior-focused executable tests, fail-before/pass-after flow, outcome assertions, stabilized determinism | +| 3 | `implementation` | `Implementation` | `console` | Minimal scoped code changes with passing targeted tests and recorded decisions | +| 4 | `coverage-hardening` | `Coverage Hardening` | `codecov` | Fixed coverage floor of **80%**, explicit remaining risk gaps, property-based tests for high-risk gaps | +| 5 | `documentation` | `Documentation` | `readme` | Canonical docs/runbooks updated to live behavior, obsolete guidance removed | +| 6 | `ai-review` | `AI Review Gate` | `folder-review` | Zero-findings gate: any remaining finding keeps the phase non-completed (`failed` or `blocked`) and execution must stop | + +The `ai-review` phase is terminal and is treated as a hard gate for task completion. +`forge_execute_core` only records task completion commit SHA on successful final-phase checks. When terminal `ai-review` checks fail, the phase remains non-completed and progression does not advance. + +## Regression Coverage Snapshot + +Current regression coverage for this behavior spans frontend, template hooks, and shared Rust execution core: + +- Frontend project-gate behavior: + - `src/features/app/components/Sidebar.test.tsx` + - `src/features/forge/components/Forge.plans.test.tsx` + - Covers no-project blocker copy, disabled Forge controls, blocked menu reachability, no backend calls in blocked mode, and unchanged sidebar Forge open/close toggling. +- Frontend execution chip rendering: + - `src/features/forge/components/Forge.plans.test.tsx` + - Covers template-order chip rendering, class-state mapping, deterministic single-running-row behavior, stale non-active `in_progress` demotion (`pending` vs `completed`), non-visible running-task guard, selected-plan unknown-task stop behavior, overflow no-extra-thread guard, deterministic seeded prompt-sequence guard invariants, failed-check re-poll without extra session creation, icon URL fallback, and terminal `ai-review` staying non-completed/visibly blocking. +- Frontend icon resolver: + - `src/utils/forgePhaseIcons.test.ts` + - Covers valid icon URL resolution, invalid-id fallback to `file`, and whitespace-trimmed valid ids. +- Frontend phase-view loader: + - `src/services/tauri.test.ts` + - Covers `forgeLoadPhaseView` parsing/fallbacks plus normalization/preservation of `blocked` and `failed` statuses. +- Template hook behavior: + - `src/features/forge/scripts/testFirstLoopScripts.test.ts` + - Covers six-phase initialization and post-step selection of first non-completed phase, including `ai-review`. +- Shared execution core: + - `src-tauri/src/shared/forge_execute_core.rs` tests under `forge_execute_core::tests` + - Covers partial progression, non-final no-commit success, final `ai-review` failure blocking completion, and final `ai-review` success commit gating. + +Task-local validation commands for Forge panel/runtime behavior: + +- `npm run test -- src/features/forge/components/Forge.plans.test.tsx src/features/app/components/Sidebar.test.tsx` +- `npm run test -- --coverage --coverage.include=src/features/forge/hooks/useForgeExecution.ts --coverage.include=src/features/forge/components/Forge.tsx src/features/forge/components/Forge.plans.test.tsx` +- `npm run typecheck` ## Workspace Artifacts Created by Install diff --git a/src-tauri/resources/forge/templates/test-first-loop/phases.json b/src-tauri/resources/forge/templates/test-first-loop/phases.json new file mode 100644 index 0000000000..b380669493 --- /dev/null +++ b/src-tauri/resources/forge/templates/test-first-loop/phases.json @@ -0,0 +1,59 @@ +{ + "schema": "forge-phases-v1", + "phases": [ + { + "id": "test-case-mapping", + "title": "Test Case Mapping", + "iconId": "taskfile", + "order": 1, + "goal": "Translate requirements into a concrete, risk-prioritized test map before writing implementation code.", + "description": "Create an explicit test inventory that maps each acceptance criterion and risk to planned automated coverage. Completion checks: (1) every requirement is mapped to at least one planned test, (2) edge cases and failure paths are called out, (3) high-risk behaviors are prioritized first, and (4) assumptions and unresolved questions are documented.", + "checks": [] + }, + { + "id": "behavioral-tests", + "title": "Behavioral Tests", + "iconId": "cucumber", + "order": 2, + "goal": "Implement behavioral tests that codify intended external behavior before core implementation changes.", + "description": "Add or update behavior-focused tests (for example integration/BDD/contract style) that fail for the right reasons before implementation and pass only when behavior is correct. Completion checks: (1) new behavior is covered by executable tests, (2) tests fail before implementation changes and pass afterward, (3) assertions verify outcomes rather than internals, and (4) flaky or nondeterministic cases are removed or stabilized.", + "checks": [] + }, + { + "id": "implementation", + "title": "Implementation", + "iconId": "console", + "order": 3, + "goal": "Implement the minimum production changes needed to satisfy mapped requirements and behavioral tests.", + "description": "Apply focused code changes that satisfy tests without broad refactors unless required for correctness. Completion checks: (1) all phase-targeted tests pass locally, (2) changes are limited to necessary scope, (3) state/progress notes capture key decisions, and (4) no known regressions are introduced.", + "checks": [] + }, + { + "id": "coverage-hardening", + "title": "Coverage Hardening", + "iconId": "codecov", + "order": 4, + "goal": "Raise confidence by hardening regression coverage and closing high-risk testing gaps.", + "description": "Expand regression tests and quality checks until coverage and risk goals are met. Completion checks: (1) measured coverage is at least 80%, (2) uncovered high-risk behaviors are identified explicitly, (3) property-based tests are added for remaining high-risk gaps, and (4) resulting test evidence is documented in task notes.", + "checks": [] + }, + { + "id": "documentation", + "title": "Documentation", + "iconId": "readme", + "order": 5, + "goal": "Update operator-facing and developer-facing documentation to reflect the delivered behavior.", + "description": "Revise docs, runbooks, and inline guidance so future work uses current live behavior. Completion checks: (1) changed behavior is documented in canonical docs, (2) setup/run/validation instructions are current, (3) removed or obsolete guidance is deleted, and (4) documentation paths are referenced in notes.", + "checks": [] + }, + { + "id": "ai-review", + "title": "AI Review Gate", + "iconId": "folder-review", + "order": 6, + "goal": "Run an AI quality review pass and enforce a strict zero-findings completion gate.", + "description": "Execute an AI review over the final change set and treat any finding as a release blocker for this phase. Completion checks: (1) AI review is run against the final diff, (2) all findings are resolved and rechecked, and (3) the phase is marked completed only with zero findings. If any finding remains, phase status must stay non-completed (`failed` or `blocked`) and execution must stop until fixes are applied and the review is rerun clean.", + "checks": [] + } + ] +} diff --git a/src-tauri/resources/forge/templates/test-first-loop/prompts/execute.md b/src-tauri/resources/forge/templates/test-first-loop/prompts/execute.md new file mode 100644 index 0000000000..13806a6406 --- /dev/null +++ b/src-tauri/resources/forge/templates/test-first-loop/prompts/execute.md @@ -0,0 +1,108 @@ +# Mode: execute + +You are executing a single task from a development plan. Your context is cleared +between tasks - everything you need is below. + +## Runtime contract (read first) + +`Forge` is the Forge backend orchestrator driving this loop. + +- Forge selects the current task/phase from `plans/{{plan_id}}/state.json`. +- Forge starts fresh task-scoped threads, runs backend checks, and manages final task commits. +- Your job is to implement the requested phase and keep `state.json`/`progress.md` accurate. + +--- + +## Plan + +**Goal:** {{goal}} +**Tech Stack:** {{tech_stack}} +**Constraints:** +{{constraints}} + +## Progress (iteration {{iteration}}) + +{{summary}} + +## All tasks + +{{task_list}} + +## Learnings from previous iterations + +{{progress_notes}} + +--- + +## YOUR TASK: {{current_task_id}} - {{current_task_name}} + +{{current_task_description}} + +## Current phase: {{current_phase_id}} - {{current_phase_title}} + +**Phase goal:** {{current_phase_goal}} + +{{current_phase_description}} + +Treat the phase description above as the required completion checklist for this phase. +Before handoff, explicitly verify each completion check in your notes. +If any completion check is unmet, do not mark the phase `completed`; use `in_progress`, `blocked`, or `failed` as appropriate. + +**Files:** {{current_task_files}} + +**Verification:** +{{current_task_verification}} + +**Attempts so far:** {{current_task_attempts}} +{{current_task_previous_notes}} + +**What dependencies produced:** +{{dependency_notes}} + +--- + +## Phase protocol (important) + +1. Implement the current phase for the current task. +2. Verify all completion checks in the current phase description are satisfied before setting phase status. +3. Update `plans/{{plan_id}}/state.json`: + - Set this phase `status` to `completed` only when every completion check is satisfied. + - If any check is unmet (including any AI-review finding), keep this phase non-completed as `blocked` or `failed` so execution stops until fixed. + - Increment this phase `attempts`. + - Append concise notes (paths/decisions). + - Keep task `status` as `in_progress` while handing off to Forge checks. +4. Forge will run backend checks after your phase is marked complete. + - Forge finalizes phase/task completion statuses after checks. + - If checks fail, Forge reopens the phase and you retry. + - If checks pass on the last phase, Forge creates the task commit and records `commit_sha`. +5. Do NOT run `git` commands yourself in execute mode. + - Do NOT run `git add`, `git commit`, `git commit --amend`, or `git push`. + - Do NOT set `commit_sha` in `state.json`; Forge manages it. +6. End your message with this exact single line marker: + +```text +[[cm_forge:done plan={{plan_id}} task={{current_task_id}} phase={{current_phase_id}}]] +``` + - Use the exact `plan/task/phase` values shown above. Do not reuse marker values from a previous task or phase. + +--- + +## After implementation - update state + +Update `plans/{{plan_id}}/state.json` following these rules: + +1. Keep your task's `status` as `in_progress` while implementing (or `failed` if truly stuck after multiple attempts) +2. Write `notes` explaining what you did - file paths, decisions, config values. The next + iteration has no memory of you; these notes are its only link to your work. +3. Update `summary` - orient a newcomer: what's done, what's next (max 300 chars) +4. Increment your task's `attempts` count +5. Do NOT change any other task's status +6. Do NOT modify `plan.json` - it is immutable +7. Do NOT set `commit_sha`; Forge writes it after final-phase checks pass. + +If you learned something useful beyond this task (a gotcha, a project convention, +a tool quirk), append a one-liner to `plans/{{plan_id}}/progress.md`: + +```text +- {{date}} iter {{iteration}}: +``` diff --git a/src-tauri/resources/forge/templates/test-first-loop/prompts/plan.md b/src-tauri/resources/forge/templates/test-first-loop/prompts/plan.md new file mode 100644 index 0000000000..54ad942085 --- /dev/null +++ b/src-tauri/resources/forge/templates/test-first-loop/prompts/plan.md @@ -0,0 +1,36 @@ +@plan + +# Mode: plan + +You are generating a development plan. + +## Two-step flow (important) + +This message is only the planning template injection. Do NOT generate a plan yet. + +1. First, reply only with: `Ready for your request.` +2. Then wait for the user to describe what they want to build in their next message. +3. Only after you receive the user's request, follow the instructions below to generate the plan. + +## Instructions + +Read the @plan skill and its reference schemas before generating anything: + +1. `.agents/skills/plan/SKILL.md` - rules, field guidelines, sizing advice +2. `.agents/skills/plan/references/plan-schema.md` - full plan.json JSON Schema + example + +## Output + +- Choose a `plan_id` slug matching `^[a-z0-9][a-z0-9-]*[a-z0-9]$` (max 64 chars). +- Output the plan as the exact `plan-v1` JSON object inside a single `...` block. +- Do NOT write any files yet. + +Inside the JSON, include: + +- `"$schema": "plan-v1"` +- `"id": ""` +- `"title": ""` + +## Hard requirement + +Do NOT implement the plan. Stop after outputting the `` block. diff --git a/src-tauri/resources/forge/templates/test-first-loop/schemas/plan.schema.json b/src-tauri/resources/forge/templates/test-first-loop/schemas/plan.schema.json new file mode 100644 index 0000000000..462256aaa4 --- /dev/null +++ b/src-tauri/resources/forge/templates/test-first-loop/schemas/plan.schema.json @@ -0,0 +1,117 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Plan", + "description": "A structured development plan for agentic coding orchestrators", + "type": "object", + "required": ["$schema", "id", "goal", "context", "tasks"], + "additionalProperties": false, + "properties": { + "$schema": { + "type": "string", + "const": "plan-v1", + "description": "Schema version identifier" + }, + "id": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9-]*[a-z0-9]$", + "maxLength": 64, + "description": "URL-safe slug identifying this plan" + }, + "title": { + "type": "string", + "minLength": 3, + "maxLength": 80, + "description": "Short friendly label for UI/menus (keep under ~60 chars)" + }, + "goal": { + "type": "string", + "minLength": 10, + "maxLength": 500, + "description": "One sentence describing the desired end state" + }, + "context": { + "type": "object", + "required": ["tech_stack", "constraints"], + "additionalProperties": false, + "properties": { + "tech_stack": { + "type": "array", + "items": { "type": "string" }, + "minItems": 1, + "description": "Technologies to use" + }, + "constraints": { + "type": "array", + "items": { "type": "string" }, + "description": "Hard rules and boundaries" + }, + "references": { + "type": "array", + "items": { + "type": "object", + "required": ["path", "description"], + "additionalProperties": false, + "properties": { + "path": { "type": "string" }, + "description": { "type": "string" } + } + }, + "description": "Files the LLM should read for context" + } + } + }, + "tasks": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "id", + "name", + "description", + "depends_on", + "files", + "verification" + ], + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "pattern": "^task-[0-9]+$", + "description": "Format: task-{sequence}" + }, + "name": { + "type": "string", + "maxLength": 80, + "description": "Short display name for UI" + }, + "description": { + "type": "string", + "minLength": 20, + "description": "Detailed implementation instructions" + }, + "depends_on": { + "type": "array", + "items": { + "type": "string", + "pattern": "^task-[0-9]+$" + }, + "description": "Task IDs that must complete before this task can start" + }, + "files": { + "type": "array", + "items": { "type": "string" }, + "minItems": 1, + "description": "File paths this task will create or modify" + }, + "verification": { + "type": "array", + "items": { "type": "string" }, + "minItems": 1, + "description": "Concrete, testable assertions for completion" + } + } + } + } + } +} diff --git a/src-tauri/resources/forge/templates/test-first-loop/schemas/state.schema.json b/src-tauri/resources/forge/templates/test-first-loop/schemas/state.schema.json new file mode 100644 index 0000000000..582e5c9caf --- /dev/null +++ b/src-tauri/resources/forge/templates/test-first-loop/schemas/state.schema.json @@ -0,0 +1,66 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "State", + "description": "Execution progress tracking for a development plan", + "type": "object", + "required": ["$schema", "plan_id", "iteration", "summary", "tasks"], + "additionalProperties": false, + "properties": { + "$schema": { + "type": "string", + "const": "state-v2" + }, + "plan_id": { + "type": "string", + "description": "Must match the id field in plan.json" + }, + "iteration": { + "type": "integer", + "minimum": 0, + "description": "Incremented by the agent before each phase completion" + }, + "summary": { + "type": "string", + "maxLength": 300, + "description": "Cumulative progress summary for context continuity" + }, + "tasks": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["id", "status", "attempts", "notes", "commit_sha", "phases"], + "additionalProperties": false, + "properties": { + "id": { "type": "string", "pattern": "^task-[0-9]+$" }, + "status": { + "type": "string", + "enum": ["pending", "in_progress", "completed", "blocked", "failed"] + }, + "attempts": { "type": "integer", "minimum": 0 }, + "notes": { "type": "string" }, + "commit_sha": { "type": ["string", "null"] }, + "phases": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["id", "status", "attempts", "notes"], + "additionalProperties": false, + "properties": { + "id": { "type": "string" }, + "status": { + "type": "string", + "enum": ["pending", "in_progress", "completed", "blocked", "failed"] + }, + "attempts": { "type": "integer", "minimum": 0 }, + "notes": { "type": "string" } + } + } + } + } + } + } + } +} + diff --git a/src-tauri/resources/forge/templates/test-first-loop/scripts/lib/args.mjs b/src-tauri/resources/forge/templates/test-first-loop/scripts/lib/args.mjs new file mode 100644 index 0000000000..4773dcc1b8 --- /dev/null +++ b/src-tauri/resources/forge/templates/test-first-loop/scripts/lib/args.mjs @@ -0,0 +1,23 @@ +export function parseFlagValue(argv, flagName) { + const exact = `--${flagName}`; + const prefix = `--${flagName}=`; + for (let i = 0; i < argv.length; i++) { + const arg = argv[i]; + if (arg === exact) { + return argv[i + 1] ?? null; + } + if (arg.startsWith(prefix)) { + return arg.slice(prefix.length); + } + } + return null; +} + +export function requireFlagValue(argv, flagName) { + const value = parseFlagValue(argv, flagName); + if (!value || !String(value).trim()) { + throw new Error(`Missing required flag: --${flagName} `); + } + return value; +} + diff --git a/src-tauri/resources/forge/templates/test-first-loop/scripts/lib/context.mjs b/src-tauri/resources/forge/templates/test-first-loop/scripts/lib/context.mjs new file mode 100644 index 0000000000..7d5ed44f4c --- /dev/null +++ b/src-tauri/resources/forge/templates/test-first-loop/scripts/lib/context.mjs @@ -0,0 +1,39 @@ +import fs from "node:fs/promises"; + +const REQUIRED_STRING_FIELDS = [ + "workspaceRoot", + "templateRoot", + "planId", + "planDir", + "planPath", + "statePath", + "progressPath", + "generatedPlanMdPath", + "generatedExecutePromptPath", + "todayIso", +]; + +export async function readContext(contextPath) { + const raw = await fs.readFile(contextPath, "utf8"); + let parsed; + try { + parsed = JSON.parse(raw); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + throw new Error(`Invalid JSON in context file: ${contextPath} (${message})`); + } + + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + throw new Error(`Context must be a JSON object: ${contextPath}`); + } + + for (const field of REQUIRED_STRING_FIELDS) { + const value = parsed[field]; + if (typeof value !== "string" || value.trim() === "") { + throw new Error(`Context missing required string field: ${field}`); + } + } + + return parsed; +} + diff --git a/src-tauri/resources/forge/templates/test-first-loop/scripts/lib/execute.mjs b/src-tauri/resources/forge/templates/test-first-loop/scripts/lib/execute.mjs new file mode 100644 index 0000000000..9afa7c7b2c --- /dev/null +++ b/src-tauri/resources/forge/templates/test-first-loop/scripts/lib/execute.mjs @@ -0,0 +1,206 @@ +import { formatBulletList, renderTemplate } from "./render.mjs"; + +function statusMark(status) { + switch (status) { + case "completed": + return "[x]"; + case "in_progress": + return "[*]"; + case "blocked": + return "[!]"; + case "failed": + return "[~]"; + case "pending": + default: + return "[ ]"; + } +} + +function normalizeNotes(notes) { + if (typeof notes !== "string") { + return ""; + } + return notes.trim(); +} + +function buildTaskList(plan, state) { + const planTasks = Array.isArray(plan?.tasks) ? plan.tasks : []; + const stateTasks = Array.isArray(state?.tasks) ? state.tasks : []; + const byId = new Map(stateTasks.map((t) => [t.id, t])); + + const lines = []; + for (const task of planTasks) { + const st = byId.get(task.id) ?? { status: "pending", notes: "" }; + const notes = normalizeNotes(st.notes); + const noteSuffix = notes ? ` - ${notes.slice(0, 120)}` : ""; + lines.push(`${statusMark(st.status)} ${task.id} ${task.name}${noteSuffix}`); + } + return lines.join("\n"); +} + +function mapStateTasks(state) { + const stateTasks = Array.isArray(state?.tasks) ? state.tasks : []; + return new Map(stateTasks.map((t) => [t.id, t])); +} + +function isCompletedStatus(status) { + return typeof status === "string" && status.trim() === "completed"; +} + +function isTaskCompleted(stateTask) { + const phases = Array.isArray(stateTask?.phases) ? stateTask.phases : []; + return phases.length > 0 && phases.every((phase) => isCompletedStatus(phase.status)); +} + +function isDepsSatisfied(task, stateById) { + const deps = Array.isArray(task?.depends_on) ? task.depends_on : []; + for (const dep of deps) { + const st = stateById.get(dep); + if (!st || !isTaskCompleted(st)) { + return false; + } + } + return true; +} + +export function findNextRunnableTask(plan, state) { + const planTasks = Array.isArray(plan?.tasks) ? plan.tasks : []; + const stateById = mapStateTasks(state); + + for (const task of planTasks) { + const st = stateById.get(task.id); + if (!st) { + continue; + } + if (isTaskCompleted(st)) { + continue; + } + if (isDepsSatisfied(task, stateById)) { + return task; + } + } + return null; +} + +function findNextRunnablePhase(stateTask) { + const phases = Array.isArray(stateTask?.phases) ? stateTask.phases : []; + for (const phase of phases) { + if (!isCompletedStatus(phase.status)) { + return phase; + } + } + return null; +} + +function mapTemplatePhases(templatePhases) { + const phases = Array.isArray(templatePhases) ? templatePhases : []; + return new Map(phases.map((p) => [p.id, p])); +} + +function dependencyNotesForTask(task, stateById) { + const deps = Array.isArray(task?.depends_on) ? task.depends_on : []; + if (deps.length === 0) { + return "(none)"; + } + const lines = []; + for (const dep of deps) { + const st = stateById.get(dep); + const notes = normalizeNotes(st?.notes ?? ""); + lines.push(`- ${dep}: ${notes || "(no notes)"}`); + } + return lines.join("\n"); +} + +export function renderExecutePrompt({ + templateText, + plan, + state, + templatePhases, + progressNotes, + todayIso, +}) { + const techStack = Array.isArray(plan?.context?.tech_stack) + ? plan.context.tech_stack.join(", ") + : ""; + const constraints = Array.isArray(plan?.context?.constraints) + ? formatBulletList(plan.context.constraints) + : ""; + + const stateById = mapStateTasks(state); + const current = findNextRunnableTask(plan, state); + const templatePhaseById = mapTemplatePhases(templatePhases); + + if (!current) { + const values = { + plan_id: plan.id ?? "", + goal: plan.goal ?? "", + tech_stack: techStack, + constraints, + iteration: String(state?.iteration ?? 0), + summary: state?.summary ?? "", + task_list: buildTaskList(plan, state), + progress_notes: progressNotes ?? "", + current_task_id: "(none)", + current_task_name: "All tasks completed", + current_task_description: "No runnable pending task found. The plan may be complete.", + current_phase_id: "(none)", + current_phase_title: "", + current_phase_goal: "", + current_phase_description: "", + current_task_files: "", + current_task_verification: "", + current_task_attempts: "0", + current_task_previous_notes: "", + dependency_notes: "", + date: todayIso, + }; + return renderTemplate(templateText, values); + } + + const st = stateById.get(current.id) ?? { + status: "pending", + attempts: 0, + notes: "", + phases: [], + }; + + const phase = findNextRunnablePhase(st) ?? { id: "implementation", status: "pending", attempts: 0, notes: "" }; + const phaseMeta = templatePhaseById.get(phase.id) ?? { id: phase.id, title: phase.id, goal: "", description: "" }; + + const previousNotes = normalizeNotes(st.notes); + const phaseNotes = normalizeNotes(phase.notes); + const notesBlocks = []; + if (previousNotes) { + notesBlocks.push(`Task notes:\n${previousNotes}`); + } + if (phaseNotes) { + notesBlocks.push(`Phase notes:\n${phaseNotes}`); + } + const currentTaskPreviousNotes = notesBlocks.length > 0 ? `\nPrevious notes:\n${notesBlocks.join("\n\n")}` : ""; + + const values = { + plan_id: plan.id ?? "", + goal: plan.goal ?? "", + tech_stack: techStack, + constraints, + iteration: String(state?.iteration ?? 0), + summary: state?.summary ?? "", + task_list: buildTaskList(plan, state), + progress_notes: progressNotes ?? "", + current_task_id: current.id, + current_task_name: current.name ?? "", + current_task_description: current.description ?? "", + current_phase_id: phase.id ?? "", + current_phase_title: phaseMeta.title ?? phaseMeta.id ?? "", + current_phase_goal: phaseMeta.goal ?? "", + current_phase_description: phaseMeta.description ?? "", + current_task_files: `\n${formatBulletList(current.files ?? [])}`, + current_task_verification: formatBulletList(current.verification ?? []), + current_task_attempts: String(phase.attempts ?? 0), + current_task_previous_notes: currentTaskPreviousNotes, + dependency_notes: dependencyNotesForTask(current, stateById), + date: todayIso, + }; + + return renderTemplate(templateText, values); +} diff --git a/src-tauri/resources/forge/templates/test-first-loop/scripts/lib/markdown.mjs b/src-tauri/resources/forge/templates/test-first-loop/scripts/lib/markdown.mjs new file mode 100644 index 0000000000..2e13f0bb31 --- /dev/null +++ b/src-tauri/resources/forge/templates/test-first-loop/scripts/lib/markdown.mjs @@ -0,0 +1,117 @@ +import { formatBulletList } from "./render.mjs"; + +export function planToMarkdown(plan) { + const techStack = Array.isArray(plan?.context?.tech_stack) + ? plan.context.tech_stack.join(", ") + : ""; + const constraints = Array.isArray(plan?.context?.constraints) + ? formatBulletList(plan.context.constraints) + : ""; + + const tasks = Array.isArray(plan?.tasks) ? plan.tasks : []; + + const lines = []; + lines.push(`# Plan: ${plan.id}`); + lines.push(""); + lines.push(`## Goal`); + lines.push(""); + lines.push(plan.goal ?? ""); + lines.push(""); + lines.push("## Context"); + lines.push(""); + lines.push(`- Tech stack: ${techStack}`); + lines.push(""); + if (constraints) { + lines.push("### Constraints"); + lines.push(""); + lines.push(constraints); + lines.push(""); + } + + lines.push("## Tasks"); + lines.push(""); + for (const task of tasks) { + lines.push(`### ${task.id}: ${task.name}`); + lines.push(""); + lines.push(`- Depends on: ${Array.isArray(task.depends_on) && task.depends_on.length > 0 ? task.depends_on.join(", ") : "(none)"}`); + lines.push(""); + lines.push(task.description ?? ""); + lines.push(""); + if (Array.isArray(task.files) && task.files.length > 0) { + lines.push("Files:"); + lines.push(formatBulletList(task.files)); + lines.push(""); + } + if (Array.isArray(task.verification) && task.verification.length > 0) { + lines.push("Verification:"); + lines.push(formatBulletList(task.verification)); + lines.push(""); + } + } + + return `${lines.join("\n").trimEnd()}\n`; +} + +function statusMark(status) { + switch (String(status ?? "").trim()) { + case "completed": + return "[x]"; + case "in_progress": + return "[*]"; + case "blocked": + return "[!]"; + case "failed": + return "[~]"; + case "pending": + default: + return "[ ]"; + } +} + +export function planStateToMarkdown(plan, state, templatePhases) { + const planTasks = Array.isArray(plan?.tasks) ? plan.tasks : []; + const stateTasks = Array.isArray(state?.tasks) ? state.tasks : []; + const stateById = new Map(stateTasks.map((t) => [t.id, t])); + const phases = Array.isArray(templatePhases) ? templatePhases : []; + + const lines = []; + lines.push(`# Plan: ${plan?.id ?? ""}`); + lines.push(""); + lines.push(`## Goal`); + lines.push(""); + lines.push(String(plan?.goal ?? "")); + lines.push(""); + lines.push("## Execution State"); + lines.push(""); + lines.push(`- Iteration: ${String(state?.iteration ?? 0)}`); + lines.push(`- Summary: ${String(state?.summary ?? "").trim()}`); + lines.push(""); + lines.push("## Tasks"); + lines.push(""); + + for (const task of planTasks) { + const st = stateById.get(task.id) ?? { status: "pending", notes: "", phases: [] }; + lines.push(`### ${statusMark(st.status)} ${task.id}: ${task.name}`); + lines.push(""); + const notes = String(st.notes ?? "").trim(); + if (notes) { + lines.push("Notes:"); + lines.push(formatBulletList(notes.split("\n").map((l) => l.trim()).filter(Boolean))); + lines.push(""); + } + + const stPhases = Array.isArray(st.phases) ? st.phases : []; + const phaseById = new Map(stPhases.map((p) => [p.id, p])); + if (phases.length > 0) { + lines.push("Phases:"); + for (const phase of phases) { + const p = phaseById.get(phase.id) ?? { status: "pending", notes: "" }; + const title = phase.title ?? phase.id; + lines.push(`- ${statusMark(p.status)} ${phase.id}: ${title}`); + } + lines.push(""); + } + } + + return `${lines.join("\n").trimEnd()}\n`; +} diff --git a/src-tauri/resources/forge/templates/test-first-loop/scripts/lib/plan.mjs b/src-tauri/resources/forge/templates/test-first-loop/scripts/lib/plan.mjs new file mode 100644 index 0000000000..8749d86157 --- /dev/null +++ b/src-tauri/resources/forge/templates/test-first-loop/scripts/lib/plan.mjs @@ -0,0 +1,408 @@ +function isPlainObject(value) { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +function pushError(errors, path, message) { + errors.push(`${path}: ${message}`); +} + +function expectNoExtraKeys(errors, obj, allowedKeys, path) { + if (!isPlainObject(obj)) { + return; + } + for (const key of Object.keys(obj)) { + if (!allowedKeys.includes(key)) { + pushError(errors, path, `Unexpected property: ${key}`); + } + } +} + +function expectString(errors, value, path, { minLength, maxLength, pattern } = {}) { + if (typeof value !== "string") { + pushError(errors, path, "Expected string"); + return; + } + if (minLength != null && value.length < minLength) { + pushError(errors, path, `Too short (minLength ${minLength})`); + } + if (maxLength != null && value.length > maxLength) { + pushError(errors, path, `Too long (maxLength ${maxLength})`); + } + if (pattern && !pattern.test(value)) { + pushError(errors, path, `Does not match pattern ${pattern}`); + } +} + +function expectArray(errors, value, path, { minItems } = {}) { + if (!Array.isArray(value)) { + pushError(errors, path, "Expected array"); + return; + } + if (minItems != null && value.length < minItems) { + pushError(errors, path, `Too few items (minItems ${minItems})`); + } +} + +function expectArrayOfStrings(errors, value, path, { minItems } = {}) { + expectArray(errors, value, path, { minItems }); + if (!Array.isArray(value)) { + return; + } + for (let i = 0; i < value.length; i++) { + if (typeof value[i] !== "string") { + pushError(errors, `${path}[${i}]`, "Expected string"); + } + } +} + +const PLAN_ID_RE = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/; +const TASK_ID_RE = /^task-[0-9]+$/; +const NOTES_TRUNCATED_SUFFIX = " ... [truncated]"; + +export const STATE_SUMMARY_MAX_LENGTH = 300; +export const TASK_NOTES_MAX_LENGTH = 2000; +export const PHASE_NOTES_MAX_LENGTH = 800; + +function truncateNotes(value, maxLength) { + if (typeof value !== "string" || value.length <= maxLength) { + return [value, false]; + } + const suffix = maxLength > NOTES_TRUNCATED_SUFFIX.length ? NOTES_TRUNCATED_SUFFIX : ""; + const sliceLength = Math.max(maxLength - suffix.length, 0); + const truncated = `${value.slice(0, sliceLength)}${suffix}`; + return [truncated, true]; +} + +function parseTaskNumber(taskId) { + const match = /^task-([0-9]+)$/.exec(taskId); + if (!match) { + return null; + } + const value = Number(match[1]); + if (!Number.isInteger(value) || value < 1) { + return null; + } + return value; +} + +function validateDag(errors, tasksById) { + const visiting = new Set(); + const visited = new Set(); + + function dfs(id) { + if (visited.has(id)) { + return; + } + if (visiting.has(id)) { + pushError(errors, "tasks", `Cycle detected at ${id}`); + return; + } + visiting.add(id); + const task = tasksById.get(id); + const deps = Array.isArray(task?.depends_on) ? task.depends_on : []; + for (const dep of deps) { + if (tasksById.has(dep)) { + dfs(dep); + } + } + visiting.delete(id); + visited.add(id); + } + + for (const id of tasksById.keys()) { + dfs(id); + } +} + +export function validatePlan(plan) { + const errors = []; + + if (!isPlainObject(plan)) { + pushError(errors, "plan", "Expected object"); + throw new Error(errors.join("\n")); + } + + expectNoExtraKeys( + errors, + plan, + ["$schema", "id", "title", "goal", "context", "tasks"], + "plan", + ); + if (plan.$schema !== "plan-v1") { + pushError(errors, "plan.$schema", 'Expected "plan-v1"'); + } + expectString(errors, plan.id, "plan.id", { + maxLength: 64, + pattern: PLAN_ID_RE, + }); + if (plan.title != null) { + expectString(errors, plan.title, "plan.title", { minLength: 3, maxLength: 80 }); + } + expectString(errors, plan.goal, "plan.goal", { minLength: 10, maxLength: 500 }); + + if (!isPlainObject(plan.context)) { + pushError(errors, "plan.context", "Expected object"); + } else { + expectNoExtraKeys(errors, plan.context, ["tech_stack", "constraints", "references"], "plan.context"); + expectArrayOfStrings(errors, plan.context.tech_stack, "plan.context.tech_stack", { minItems: 1 }); + expectArrayOfStrings(errors, plan.context.constraints, "plan.context.constraints"); + if (plan.context.references != null) { + expectArray(errors, plan.context.references, "plan.context.references"); + if (Array.isArray(plan.context.references)) { + for (let i = 0; i < plan.context.references.length; i++) { + const ref = plan.context.references[i]; + const refPath = `plan.context.references[${i}]`; + if (!isPlainObject(ref)) { + pushError(errors, refPath, "Expected object"); + continue; + } + expectNoExtraKeys(errors, ref, ["path", "description"], refPath); + expectString(errors, ref.path, `${refPath}.path`); + expectString(errors, ref.description, `${refPath}.description`); + } + } + } + } + + expectArray(errors, plan.tasks, "plan.tasks", { minItems: 1 }); + const tasksById = new Map(); + let hasEntryPoint = false; + + if (Array.isArray(plan.tasks)) { + for (let i = 0; i < plan.tasks.length; i++) { + const task = plan.tasks[i]; + const taskPath = `plan.tasks[${i}]`; + if (!isPlainObject(task)) { + pushError(errors, taskPath, "Expected object"); + continue; + } + expectNoExtraKeys( + errors, + task, + ["id", "name", "description", "depends_on", "files", "verification"], + taskPath, + ); + + expectString(errors, task.id, `${taskPath}.id`, { pattern: TASK_ID_RE }); + expectString(errors, task.name, `${taskPath}.name`, { maxLength: 80 }); + expectString(errors, task.description, `${taskPath}.description`, { minLength: 20 }); + expectArrayOfStrings(errors, task.depends_on, `${taskPath}.depends_on`); + expectArrayOfStrings(errors, task.files, `${taskPath}.files`, { minItems: 1 }); + expectArrayOfStrings(errors, task.verification, `${taskPath}.verification`, { minItems: 1 }); + + if (Array.isArray(task.depends_on) && task.depends_on.length === 0) { + hasEntryPoint = true; + } + + if (typeof task.id === "string") { + if (tasksById.has(task.id)) { + pushError(errors, `${taskPath}.id`, `Duplicate task id: ${task.id}`); + } else { + tasksById.set(task.id, task); + } + const taskNumber = parseTaskNumber(task.id); + const expectedId = `task-${i + 1}`; + if (taskNumber == null) { + pushError(errors, `${taskPath}.id`, "Task id must be task- with n >= 1"); + } else if (task.id !== expectedId) { + pushError(errors, `${taskPath}.id`, `Task ids must match array order (expected ${expectedId})`); + } + } + } + } + + if (!hasEntryPoint) { + pushError(errors, "plan.tasks", "At least one task must have depends_on: []"); + } + + // depends_on referential integrity + self-deps + for (const [id, task] of tasksById.entries()) { + const deps = Array.isArray(task.depends_on) ? task.depends_on : []; + for (const dep of deps) { + if (dep === id) { + pushError(errors, `task:${id}.depends_on`, "Task cannot depend on itself"); + } else if (!tasksById.has(dep)) { + pushError(errors, `task:${id}.depends_on`, `Unknown dependency: ${dep}`); + } + } + } + + validateDag(errors, tasksById); + + if (errors.length > 0) { + throw new Error(`plan.json is invalid:\n${errors.join("\n")}`); + } +} + +function validatePhaseList(errors, phases, path) { + expectArray(errors, phases, path, { minItems: 1 }); + if (!Array.isArray(phases)) { + return; + } + for (let i = 0; i < phases.length; i++) { + const phase = phases[i]; + const phasePath = `${path}[${i}]`; + if (!isPlainObject(phase)) { + pushError(errors, phasePath, "Expected object"); + continue; + } + expectNoExtraKeys( + errors, + phase, + ["id", "title", "order", "iconId", "goal", "description", "checks"], + phasePath, + ); + expectString(errors, phase.id, `${phasePath}.id`, { minLength: 1, maxLength: 64 }); + expectString(errors, phase.title, `${phasePath}.title`, { minLength: 1, maxLength: 80 }); + } +} + +export function buildInitialState(plan, templatePhases) { + const tasks = Array.isArray(plan?.tasks) ? plan.tasks : []; + const phases = Array.isArray(templatePhases) ? templatePhases : []; + return { + $schema: "state-v2", + plan_id: plan.id, + iteration: 0, + summary: "", + tasks: tasks.map((task) => ({ + id: task.id, + status: "pending", + attempts: 0, + notes: "", + commit_sha: null, + phases: phases.map((phase) => ({ + id: phase.id, + status: "pending", + attempts: 0, + notes: "", + })), + })), + }; +} + +export function normalizeStateNotes(state) { + let changed = false; + + if (!isPlainObject(state) || !Array.isArray(state.tasks)) { + return { state, changed }; + } + + for (const task of state.tasks) { + if (!isPlainObject(task)) { + continue; + } + const [taskNotes, taskChanged] = truncateNotes(task.notes, TASK_NOTES_MAX_LENGTH); + if (taskChanged) { + task.notes = taskNotes; + changed = true; + } + + if (!Array.isArray(task.phases)) { + continue; + } + for (const phase of task.phases) { + if (!isPlainObject(phase)) { + continue; + } + const [phaseNotes, phaseChanged] = truncateNotes(phase.notes, PHASE_NOTES_MAX_LENGTH); + if (phaseChanged) { + phase.notes = phaseNotes; + changed = true; + } + } + } + + return { state, changed }; +} + +export function validateStateAgainstPlan(state, plan, templatePhases) { + const errors = []; + + if (!isPlainObject(state)) { + pushError(errors, "state", "Expected object"); + return errors; + } + + expectNoExtraKeys(errors, state, ["$schema", "plan_id", "iteration", "summary", "tasks"], "state"); + if (state.$schema !== "state-v2") { + pushError(errors, "state.$schema", 'Expected "state-v2"'); + } + if (state.plan_id !== plan.id) { + pushError(errors, "state.plan_id", `Expected ${plan.id}`); + } + if (!Number.isInteger(state.iteration) || state.iteration < 0) { + pushError(errors, "state.iteration", "Expected integer >= 0"); + } + expectString(errors, state.summary, "state.summary", { maxLength: STATE_SUMMARY_MAX_LENGTH }); + + expectArray(errors, state.tasks, "state.tasks", { minItems: 1 }); + + const planTasks = Array.isArray(plan?.tasks) ? plan.tasks : []; + if (Array.isArray(state.tasks) && state.tasks.length !== planTasks.length) { + pushError(errors, "state.tasks", "Must match plan.tasks length"); + } + + const allowedStatus = new Set(["pending", "in_progress", "completed", "blocked", "failed"]); + validatePhaseList(errors, templatePhases, "templatePhases"); + const expectedPhaseIds = Array.isArray(templatePhases) + ? templatePhases.map((p) => String(p.id ?? "")).filter(Boolean) + : []; + + if (Array.isArray(state.tasks)) { + for (let i = 0; i < state.tasks.length; i++) { + const entry = state.tasks[i]; + const entryPath = `state.tasks[${i}]`; + if (!isPlainObject(entry)) { + pushError(errors, entryPath, "Expected object"); + continue; + } + expectNoExtraKeys(errors, entry, ["id", "status", "attempts", "notes", "commit_sha", "phases"], entryPath); + expectString(errors, entry.id, `${entryPath}.id`, { pattern: TASK_ID_RE }); + if (typeof entry.status !== "string" || !allowedStatus.has(entry.status)) { + pushError(errors, `${entryPath}.status`, "Invalid status"); + } + if (!Number.isInteger(entry.attempts) || entry.attempts < 0) { + pushError(errors, `${entryPath}.attempts`, "Expected integer >= 0"); + } + expectString(errors, entry.notes, `${entryPath}.notes`, { maxLength: TASK_NOTES_MAX_LENGTH }); + + if (entry.commit_sha != null && typeof entry.commit_sha !== "string") { + pushError(errors, `${entryPath}.commit_sha`, "Expected string or null"); + } + + if (planTasks[i]?.id && entry.id !== planTasks[i].id) { + pushError(errors, entryPath, `Task id mismatch at index ${i} (expected ${planTasks[i].id})`); + } + + expectArray(errors, entry.phases, `${entryPath}.phases`, { minItems: 1 }); + if (Array.isArray(entry.phases) && expectedPhaseIds.length > 0 && entry.phases.length !== expectedPhaseIds.length) { + pushError(errors, `${entryPath}.phases`, "Must match templatePhases length"); + } + if (Array.isArray(entry.phases)) { + for (let j = 0; j < entry.phases.length; j++) { + const phase = entry.phases[j]; + const phasePath = `${entryPath}.phases[${j}]`; + if (!isPlainObject(phase)) { + pushError(errors, phasePath, "Expected object"); + continue; + } + expectNoExtraKeys(errors, phase, ["id", "status", "attempts", "notes"], phasePath); + expectString(errors, phase.id, `${phasePath}.id`, { minLength: 1, maxLength: 64 }); + if (expectedPhaseIds[j] && phase.id !== expectedPhaseIds[j]) { + pushError(errors, `${phasePath}.id`, `Phase id mismatch at index ${j} (expected ${expectedPhaseIds[j]})`); + } + if (typeof phase.status !== "string" || !allowedStatus.has(phase.status)) { + pushError(errors, `${phasePath}.status`, "Invalid status"); + } + if (!Number.isInteger(phase.attempts) || phase.attempts < 0) { + pushError(errors, `${phasePath}.attempts`, "Expected integer >= 0"); + } + expectString(errors, phase.notes, `${phasePath}.notes`, { maxLength: PHASE_NOTES_MAX_LENGTH }); + } + } + } + } + + return errors; +} diff --git a/src-tauri/resources/forge/templates/test-first-loop/scripts/lib/render.mjs b/src-tauri/resources/forge/templates/test-first-loop/scripts/lib/render.mjs new file mode 100644 index 0000000000..477583fff6 --- /dev/null +++ b/src-tauri/resources/forge/templates/test-first-loop/scripts/lib/render.mjs @@ -0,0 +1,22 @@ +function escapeRegExp(text) { + return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +export function renderTemplate(templateText, values) { + let output = templateText; + for (const [key, rawValue] of Object.entries(values)) { + const value = rawValue == null ? "" : String(rawValue); + const pattern = new RegExp(`\\{\\{${escapeRegExp(key)}\\}\\}`, "g"); + output = output.replace(pattern, value); + } + return output; +} + +export function formatBulletList(items) { + const list = Array.isArray(items) ? items : []; + if (list.length === 0) { + return ""; + } + return list.map((item) => `- ${item}`).join("\n"); +} + diff --git a/src-tauri/resources/forge/templates/test-first-loop/scripts/lib/state.mjs b/src-tauri/resources/forge/templates/test-first-loop/scripts/lib/state.mjs new file mode 100644 index 0000000000..8e68063045 --- /dev/null +++ b/src-tauri/resources/forge/templates/test-first-loop/scripts/lib/state.mjs @@ -0,0 +1,27 @@ +import fs from "node:fs/promises"; +import path from "node:path"; + +export async function readJsonFile(filePath) { + const raw = await fs.readFile(filePath, "utf8"); + try { + return JSON.parse(raw); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + throw new Error(`Invalid JSON: ${filePath} (${message})`); + } +} + +export async function writeJsonFile(filePath, value) { + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8"); +} + +export async function ensureFileExists(filePath, initialContent = "") { + try { + await fs.access(filePath); + } catch { + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, initialContent, "utf8"); + } +} + diff --git a/src-tauri/resources/forge/templates/test-first-loop/scripts/post-plan.mjs b/src-tauri/resources/forge/templates/test-first-loop/scripts/post-plan.mjs new file mode 100644 index 0000000000..9becd81953 --- /dev/null +++ b/src-tauri/resources/forge/templates/test-first-loop/scripts/post-plan.mjs @@ -0,0 +1,57 @@ +import fs from "node:fs/promises"; +import path from "node:path"; + +import { requireFlagValue } from "./lib/args.mjs"; +import { readContext } from "./lib/context.mjs"; +import { renderExecutePrompt } from "./lib/execute.mjs"; +import { planStateToMarkdown } from "./lib/markdown.mjs"; +import { buildInitialState, validatePlan } from "./lib/plan.mjs"; +import { ensureFileExists, readJsonFile, writeJsonFile } from "./lib/state.mjs"; + +async function main() { + const contextPath = requireFlagValue(process.argv.slice(2), "context"); + const ctx = await readContext(contextPath); + + const plan = await readJsonFile(ctx.planPath); + validatePlan(plan); + + const phases = await readJsonFile(path.join(ctx.templateRoot, "phases.json")); + const templatePhases = Array.isArray(phases?.phases) ? phases.phases : []; + + // Initialize plans//state.json + const state = buildInitialState(plan, templatePhases); + await writeJsonFile(ctx.statePath, state); + + // Write plans//plan.md (derived, includes state) + await fs.mkdir(path.dirname(ctx.generatedPlanMdPath), { recursive: true }); + await fs.writeFile( + ctx.generatedPlanMdPath, + planStateToMarkdown(plan, state, templatePhases), + "utf8", + ); + + // Ensure plans//progress.md exists. + await ensureFileExists(ctx.progressPath, ""); + + // Render initial execute prompt for the first runnable task. + const templateText = await fs.readFile( + path.join(ctx.templateRoot, "prompts", "execute.md"), + "utf8", + ); + const progressNotes = await fs.readFile(ctx.progressPath, "utf8").catch(() => ""); + const prompt = renderExecutePrompt({ + templateText, + plan, + state, + templatePhases, + progressNotes, + todayIso: ctx.todayIso, + }); + await fs.mkdir(path.dirname(ctx.generatedExecutePromptPath), { recursive: true }); + await fs.writeFile(ctx.generatedExecutePromptPath, prompt, "utf8"); +} + +main().catch((err) => { + console.error(err instanceof Error ? err.message : String(err)); + process.exit(1); +}); diff --git a/src-tauri/resources/forge/templates/test-first-loop/scripts/post-step.mjs b/src-tauri/resources/forge/templates/test-first-loop/scripts/post-step.mjs new file mode 100644 index 0000000000..157a27a371 --- /dev/null +++ b/src-tauri/resources/forge/templates/test-first-loop/scripts/post-step.mjs @@ -0,0 +1,60 @@ +import fs from "node:fs/promises"; +import path from "node:path"; + +import { requireFlagValue } from "./lib/args.mjs"; +import { readContext } from "./lib/context.mjs"; +import { renderExecutePrompt } from "./lib/execute.mjs"; +import { planStateToMarkdown } from "./lib/markdown.mjs"; +import { normalizeStateNotes, validatePlan, validateStateAgainstPlan } from "./lib/plan.mjs"; +import { readJsonFile, writeJsonFile } from "./lib/state.mjs"; + +async function main() { + const contextPath = requireFlagValue(process.argv.slice(2), "context"); + const ctx = await readContext(contextPath); + + const plan = await readJsonFile(ctx.planPath); + validatePlan(plan); + + const phases = await readJsonFile(path.join(ctx.templateRoot, "phases.json")); + const templatePhases = Array.isArray(phases?.phases) ? phases.phases : []; + + const rawState = await readJsonFile(ctx.statePath); + const { state, changed: notesWereTruncated } = normalizeStateNotes(rawState); + if (notesWereTruncated) { + await writeJsonFile(ctx.statePath, state); + } + const stateErrors = validateStateAgainstPlan(state, plan, templatePhases); + if (stateErrors.length > 0) { + throw new Error(`state.json is invalid:\n${stateErrors.join("\n")}`); + } + + await fs.mkdir(path.dirname(ctx.generatedPlanMdPath), { recursive: true }); + await fs.writeFile( + ctx.generatedPlanMdPath, + planStateToMarkdown(plan, state, templatePhases), + "utf8", + ); + + const templateText = await fs.readFile( + path.join(ctx.templateRoot, "prompts", "execute.md"), + "utf8", + ); + const progressNotes = await fs.readFile(ctx.progressPath, "utf8").catch(() => ""); + + const prompt = renderExecutePrompt({ + templateText, + plan, + state, + templatePhases, + progressNotes, + todayIso: ctx.todayIso, + }); + + await fs.mkdir(path.dirname(ctx.generatedExecutePromptPath), { recursive: true }); + await fs.writeFile(ctx.generatedExecutePromptPath, prompt, "utf8"); +} + +main().catch((err) => { + console.error(err instanceof Error ? err.message : String(err)); + process.exit(1); +}); diff --git a/src-tauri/resources/forge/templates/test-first-loop/scripts/pre-execute.mjs b/src-tauri/resources/forge/templates/test-first-loop/scripts/pre-execute.mjs new file mode 100644 index 0000000000..249997e760 --- /dev/null +++ b/src-tauri/resources/forge/templates/test-first-loop/scripts/pre-execute.mjs @@ -0,0 +1,51 @@ +import fs from "node:fs/promises"; +import path from "node:path"; + +import { requireFlagValue } from "./lib/args.mjs"; +import { readContext } from "./lib/context.mjs"; +import { readJsonFile } from "./lib/state.mjs"; + +async function fileExists(filePath) { + try { + const stat = await fs.stat(filePath); + return stat.isFile(); + } catch { + return false; + } +} + +async function main() { + const contextPath = requireFlagValue(process.argv.slice(2), "context"); + const ctx = await readContext(contextPath); + + // Validate template completeness (manifest-driven). + const manifestPath = path.join(ctx.templateRoot, "template.json"); + const manifest = await readJsonFile(manifestPath); + const files = Array.isArray(manifest?.files) ? manifest.files : []; + for (const rel of files) { + const abs = path.join(ctx.templateRoot, rel); + if (!(await fileExists(abs))) { + throw new Error(`Template file missing: ${rel}`); + } + } + + // Validate required skill is installed into the workspace. + const skillPath = path.join(ctx.workspaceRoot, ".agent", "skills", "plan", "SKILL.md"); + if (!(await fileExists(skillPath))) { + throw new Error(`Missing required workspace skill: ${skillPath}`); + } + + // Validate that plan + state exist. + if (!(await fileExists(ctx.planPath))) { + throw new Error(`Missing plan.json: ${ctx.planPath}`); + } + if (!(await fileExists(ctx.statePath))) { + throw new Error(`Missing state.json: ${ctx.statePath}`); + } +} + +main().catch((err) => { + console.error(err instanceof Error ? err.message : String(err)); + process.exit(1); +}); + diff --git a/src-tauri/resources/forge/templates/test-first-loop/skills/plan/SKILL.md b/src-tauri/resources/forge/templates/test-first-loop/skills/plan/SKILL.md new file mode 100644 index 0000000000..bbc395ff57 --- /dev/null +++ b/src-tauri/resources/forge/templates/test-first-loop/skills/plan/SKILL.md @@ -0,0 +1,102 @@ +--- +name: plan +description: | + How to create structured development plans for agentic coding orchestrators. + Use this skill whenever asked to create a plan, break down a feature, generate + a task list, or prepare work for an AI agent loop. Triggers: "plan this", + "break this down", "create tasks for", or any reference to spec-driven + development, ralph loop, or agentic task execution. +--- + +# @plan -- Create a Development Plan + +Generate a structured `plan.json` that an orchestrator will feed to stateless +LLM iterations, one task at a time. + +## What you produce + +This skill supports two outputs depending on collaboration mode: + +- In **Plan** collaboration mode: output the plan as the exact `plan-v1` JSON object inside a single `...` block. Do not write files. +- In **Default** (or any non-Plan) mode: write the plan file to `plans//plan.json` (create the folder if needed). + +The orchestrator initializes state/progress files programmatically. + +## Read the schema first + +Before generating anything, read: +- `references/plan-schema.md` -- Full plan.json JSON Schema + complete example + +## plan.json structure (summary) + +```json +{ + "$schema": "plan-v1", + "id": "", + "title": "Short UI title (<= 60 chars)", + "goal": "One sentence - the desired end state", + "context": { + "tech_stack": ["Node.js", "Express"], + "constraints": ["Must use existing schema in db/schema.sql"], + "references": [{ "path": "db/schema.sql", "description": "Existing DB schema" }] + }, + "tasks": [ + { + "id": "task-1", + "name": "Project setup", + "description": "Detailed implementation instructions...", + "depends_on": [], + "files": ["src/index.ts"], + "verification": ["GET /health returns 200"] + } + ] +} +``` + +## Key rules + +### IDs +- Task IDs: `task-{n}` -- e.g. `task-1`, `task-2`, ... +- Plan ID: URL-safe slug -- e.g. `auth-system`, `notification-service` + +### Title +- Add a short, friendly `title` for UI/menus (aim for <= 60 chars). +- Keep `goal` longer and more descriptive (the desired end state). + +### Tasks +- Each completable in one LLM iteration (~5-30 min agent work) +- Too big: >5 files, >200 lines new code, mixes unrelated concerns +- Too small: single line change, no meaningful verification + +### Dependencies +- `depends_on` lists direct dependencies only (task IDs) +- At least one task with `"depends_on": []` (entry point) +- No circular dependencies +- Minimize chain length -- long chains serialize execution + +### Descriptions -- be extremely specific + +Bad: +``` +"Add authentication to the API" +``` + +Good: +``` +"Implement POST /auth/login accepting { email, password }. Validate against users +table using bcrypt.compare(). On success, return { accessToken, refreshToken } as +httpOnly cookies. Access token: JWT with { userId, email } payload, 15min expiry, +signed with ACCESS_TOKEN_SECRET. On failure, return 401." +``` + +### Verification -- concrete, testable assertions + +Bad: `"Auth works correctly"` +Good: `"POST /auth/login with valid credentials returns 200 with Set-Cookie headers"` + +### Constraints -- include what NOT to build + +Good constraints: +- "Do NOT add a frontend -- API only" +- "Do NOT modify existing tables, extend only" +- "Max 3 external dependencies" diff --git a/src-tauri/resources/forge/templates/test-first-loop/skills/plan/references/plan-schema.md b/src-tauri/resources/forge/templates/test-first-loop/skills/plan/references/plan-schema.md new file mode 100644 index 0000000000..970ede8856 --- /dev/null +++ b/src-tauri/resources/forge/templates/test-first-loop/skills/plan/references/plan-schema.md @@ -0,0 +1,282 @@ +# Plan Schema Reference + +Full JSON Schema for `plan.json` files. Read this before generating a plan. + +## Complete JSON Schema + +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Plan", + "description": "A structured development plan for agentic coding orchestrators", + "type": "object", + "required": ["$schema", "id", "goal", "context", "tasks"], + "additionalProperties": false, + "properties": { + "$schema": { + "type": "string", + "const": "plan-v1", + "description": "Schema version identifier" + }, + "id": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9-]*[a-z0-9]$", + "maxLength": 64, + "description": "URL-safe slug identifying this plan" + }, + "title": { + "type": "string", + "minLength": 3, + "maxLength": 80, + "description": "Short friendly label for UI/menus (keep under ~60 chars)" + }, + "goal": { + "type": "string", + "minLength": 10, + "maxLength": 500, + "description": "One sentence describing the desired end state" + }, + "context": { + "type": "object", + "required": ["tech_stack", "constraints"], + "additionalProperties": false, + "properties": { + "tech_stack": { + "type": "array", + "items": { "type": "string" }, + "minItems": 1, + "description": "Technologies to use" + }, + "constraints": { + "type": "array", + "items": { "type": "string" }, + "description": "Hard rules and boundaries" + }, + "references": { + "type": "array", + "items": { + "type": "object", + "required": ["path", "description"], + "additionalProperties": false, + "properties": { + "path": { "type": "string" }, + "description": { "type": "string" } + } + }, + "description": "Files the LLM should read for context" + } + } + }, + "tasks": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["id", "name", "description", "depends_on", "files", "verification"], + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "pattern": "^task-[0-9]+$", + "description": "Format: task-{sequence}" + }, + "name": { + "type": "string", + "maxLength": 80, + "description": "Short display name for UI" + }, + "description": { + "type": "string", + "minLength": 20, + "description": "Detailed implementation instructions" + }, + "depends_on": { + "type": "array", + "items": { + "type": "string", + "pattern": "^task-[0-9]+$" + }, + "description": "Task IDs that must complete before this task can start" + }, + "files": { + "type": "array", + "items": { "type": "string" }, + "minItems": 1, + "description": "File paths this task will create or modify" + }, + "verification": { + "type": "array", + "items": { "type": "string" }, + "minItems": 1, + "description": "Concrete, testable assertions for completion" + } + } + } + } + } +} +``` + +## Validation rules (beyond JSON Schema) + +The orchestrator should enforce these rules programmatically after the LLM generates a plan: + +### Referential integrity +- Every entry in `depends_on` must match an existing task `id` +- No task can depend on itself + +### Dependency graph +- The dependency graph must be a DAG (no cycles) +- At least one task must have `"depends_on": []` (an entry point) + +### ID consistency +- Task IDs must use the format `task-{sequence}` +- Task IDs must be sequential starting at 1 with no gaps: `task-1`, `task-2`, ..., `task-n` +- Task IDs must match the array order: `plan.tasks[0].id` is `task-1`, etc. + +### Content quality checks +- `description` should contain concrete specifics (endpoint paths, field names, config values) +- `verification` items should be testable (contain expected status codes, return values, observable behaviors) +- `files` should not be empty - every task must touch at least one file + +## Complete example + +```json +{ + "$schema": "plan-v1", + "id": "task-api", + "title": "Tasks API", + "goal": "A REST API with JWT auth, task CRUD with pagination, and WebSocket notifications, fully tested with OpenAPI docs", + "context": { + "tech_stack": ["Node.js", "TypeScript", "Express", "PostgreSQL", "Socket.io", "JWT"], + "constraints": [ + "Must use existing PostgreSQL schema in db/schema.sql", + "Auth must support email/password and OAuth2 Google login", + "All endpoints must have OpenAPI documentation", + "Minimum 80% test coverage on business logic", + "Do NOT add a frontend - API only" + ], + "references": [ + { "path": "db/schema.sql", "description": "Existing database schema - do not modify, extend only" }, + { "path": "src/middleware/auth.ts", "description": "Existing auth middleware skeleton to build upon" }, + { "path": "docs/api-contract.md", "description": "Agreed API contract with frontend team" } + ] + }, + "tasks": [ + { + "id": "task-1", + "name": "Project setup and database connection", + "description": "Initialize Express+TypeScript app with tsconfig (strict mode). Configure dotenv for DATABASE_URL, PORT, ACCESS_TOKEN_SECRET, REFRESH_TOKEN_SECRET env vars. Set up PostgreSQL connection pool using pg library with max 20 connections. Create GET /health endpoint returning { status: 'ok', db: boolean } where db reflects a successful SELECT 1 query.", + "depends_on": [], + "files": ["src/index.ts", "src/db.ts", "src/routes/health.ts", "tsconfig.json", ".env.example"], + "verification": [ + "npm run build compiles without errors", + "GET /health returns 200 with { status: 'ok', db: true } when DB is connected", + "GET /health returns 200 with { status: 'ok', db: false } when DB is unreachable", + "App reads PORT from environment variable, defaults to 3000" + ] + }, + { + "id": "task-2", + "name": "Error handling middleware", + "description": "Create centralized error handling middleware. Define AppError class extending Error with statusCode and isOperational fields. Global error handler catches all errors, logs stack traces for 5xx, returns { error: string, code: string } to client. Add request ID middleware using uuid v4, attach to req and include in error responses.", + "depends_on": ["task-1"], + "files": ["src/middleware/error-handler.ts", "src/errors/app-error.ts", "src/middleware/request-id.ts"], + "verification": [ + "Throwing AppError(404, 'Not found') returns { error: 'Not found', code: 'NOT_FOUND', requestId: '...' }", + "Unhandled errors return 500 with generic message (no stack leak)", + "Every response includes x-request-id header" + ] + }, + { + "id": "task-3", + "name": "JWT authentication endpoints", + "description": "Implement POST /auth/register accepting { email, password, name }. Validate email format and password length >= 8. Hash password with bcrypt cost 12. Store in users table. Return { accessToken, refreshToken } as httpOnly cookies. Implement POST /auth/login with same response format. Access tokens: JWT with { userId, email } payload, 15min expiry signed with ACCESS_TOKEN_SECRET. Refresh tokens: opaque UUID stored in refresh_tokens table, 7-day expiry. POST /auth/refresh rotates refresh token (invalidate old, issue new pair). Rate limit login: 5 attempts per email per 15 minutes.", + "depends_on": ["task-1", "task-2"], + "files": ["src/routes/auth.ts", "src/services/auth.service.ts", "src/types/auth.ts"], + "verification": [ + "POST /auth/register with valid data returns 201 with Set-Cookie headers", + "POST /auth/register with duplicate email returns 409", + "POST /auth/register with password < 8 chars returns 400", + "POST /auth/login with valid credentials returns 200 with tokens", + "POST /auth/login with wrong password returns 401", + "POST /auth/refresh with valid refresh token returns new token pair", + "POST /auth/refresh invalidates the old refresh token", + "6th login attempt for same email within 15 minutes returns 429" + ] + }, + { + "id": "task-4", + "name": "Google OAuth2 integration", + "description": "Add Google OAuth2 using passport-google-oauth20. Configure with GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET env vars. GET /auth/google redirects to Google consent screen requesting email and profile scopes. GET /auth/google/callback handles the response: if a user with matching email exists, link the Google ID to their account; if not, create a new user. Return same token format as /auth/login. Store google_id in users table (nullable column).", + "depends_on": ["task-3"], + "files": ["src/routes/auth-google.ts", "src/services/oauth.service.ts", "src/config/passport.ts"], + "verification": [ + "GET /auth/google returns 302 redirect to accounts.google.com", + "Callback with new Google user creates user record and returns tokens", + "Callback with existing email links Google ID without creating duplicate", + "GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in .env.example" + ] + }, + { + "id": "task-5", + "name": "Task CRUD endpoints", + "description": "Implement authenticated CRUD for /tasks. POST /tasks creates a task with { title, description, status, assigneeId }. GET /tasks returns paginated list (default 20 per page) with ?page, ?status, ?assignee query filters, sorted by createdAt desc. GET /tasks/:id returns single task. PUT /tasks/:id updates task fields. DELETE /tasks/:id soft-deletes (sets deletedAt). All routes require valid access token via auth middleware. Users can only see/modify tasks they created or are assigned to.", + "depends_on": ["task-3"], + "files": ["src/routes/tasks.ts", "src/services/task.service.ts", "src/types/task.ts"], + "verification": [ + "POST /tasks without auth returns 401", + "POST /tasks with auth creates task and returns 201", + "GET /tasks returns paginated array with total count in response", + "GET /tasks?status=done filters by status", + "GET /tasks?page=2 returns second page", + "PUT /tasks/:id updates fields and returns 200", + "DELETE /tasks/:id sets deletedAt, subsequent GET returns 404", + "User A cannot GET/PUT/DELETE tasks owned by User B" + ] + }, + { + "id": "task-6", + "name": "WebSocket notifications", + "description": "Set up Socket.io server attached to the Express http server. Authenticate WebSocket connections by extracting JWT from the handshake auth header. On task.created, task.updated, and task.assigned events, broadcast to relevant users (creator and assignee). Event payload: { type, taskId, taskTitle, actorId, timestamp }. Store notification in notifications table with userId, type, payload, readAt (nullable). Add GET /notifications for authenticated user with ?unread=true filter.", + "depends_on": ["task-5"], + "files": ["src/websocket/server.ts", "src/websocket/handlers.ts", "src/services/notification.service.ts", "src/routes/notifications.ts"], + "verification": [ + "WebSocket connection without valid JWT is rejected", + "Creating a task emits task.created to the creator's socket", + "Assigning a task emits task.assigned to the assignee's socket", + "Notifications are persisted in the notifications table", + "GET /notifications returns user's notifications", + "GET /notifications?unread=true filters to readAt IS NULL" + ] + }, + { + "id": "task-7", + "name": "Integration tests", + "description": "Write Jest integration tests using supertest. Set up test database with docker-compose.test.yml running PostgreSQL. Before each test suite: run migrations, seed test data. After each suite: truncate all tables. Cover: full auth flow (register -> login -> refresh -> access protected route), task CRUD with auth, pagination edge cases (empty results, last page), WebSocket connection and event delivery. Aim for 80%+ coverage on src/services/ and src/routes/.", + "depends_on": ["task-4", "task-6"], + "files": ["tests/auth.test.ts", "tests/tasks.test.ts", "tests/notifications.test.ts", "tests/setup.ts", "docker-compose.test.yml", "jest.config.ts"], + "verification": [ + "npm test runs all tests and exits 0", + "Auth tests cover register, login, refresh, and token expiry", + "Task tests cover CRUD, pagination, filtering, and authorization", + "Coverage report shows >= 80% on src/services/ and src/routes/" + ] + }, + { + "id": "task-8", + "name": "OpenAPI documentation", + "description": "Add swagger-jsdoc and swagger-ui-express. Write JSDoc annotations on all route handlers with @openapi tags. Include request/response schemas, auth requirements (bearerAuth), error responses, and query parameter descriptions. Serve Swagger UI at GET /docs. Generate openapi.json at build time via a script in package.json.", + "depends_on": ["task-6"], + "files": ["src/config/swagger.ts", "src/routes/*.ts", "scripts/generate-openapi.ts"], + "verification": [ + "GET /docs serves Swagger UI page", + "Every endpoint appears in the documentation", + "npm run generate:openapi produces a valid openapi.json", + "Auth endpoints show request/response body schemas", + "Protected endpoints show bearerAuth requirement" + ] + } + ] +} +``` diff --git a/src-tauri/resources/forge/templates/test-first-loop/template.json b/src-tauri/resources/forge/templates/test-first-loop/template.json new file mode 100644 index 0000000000..650cb182d2 --- /dev/null +++ b/src-tauri/resources/forge/templates/test-first-loop/template.json @@ -0,0 +1,39 @@ +{ + "schema": "forge-template-v1", + "id": "test-first-loop", + "title": "Test-First Loop", + "version": "0.1.0", + "files": [ + "template.json", + "phases.json", + "prompts/plan.md", + "prompts/execute.md", + "skills/plan/SKILL.md", + "skills/plan/references/plan-schema.md", + "schemas/plan.schema.json", + "schemas/state.schema.json", + "scripts/post-plan.mjs", + "scripts/pre-execute.mjs", + "scripts/post-step.mjs", + "scripts/lib/args.mjs", + "scripts/lib/context.mjs", + "scripts/lib/execute.mjs", + "scripts/lib/markdown.mjs", + "scripts/lib/plan.mjs", + "scripts/lib/render.mjs", + "scripts/lib/state.mjs" + ], + "entrypoints": { + "phases": "phases.json", + "planPrompt": "prompts/plan.md", + "executePrompt": "prompts/execute.md", + "planSchema": "schemas/plan.schema.json", + "stateSchema": "schemas/state.schema.json", + "requiredSkills": ["plan"], + "hooks": { + "postPlan": "scripts/post-plan.mjs", + "preExecute": "scripts/pre-execute.mjs", + "postStep": "scripts/post-step.mjs" + } + } +} diff --git a/src-tauri/src/shared/forge_execute_core.rs b/src-tauri/src/shared/forge_execute_core.rs index 1ce45431aa..c5ee1081a4 100644 --- a/src-tauri/src/shared/forge_execute_core.rs +++ b/src-tauri/src/shared/forge_execute_core.rs @@ -1067,6 +1067,27 @@ pub(crate) async fn forge_run_phase_checks_core( mod tests { use super::*; use serde_json::json; + use std::future::Future; + use std::process::Command; + + const SIX_PHASE_IDS: [&str; 6] = [ + "test-case-mapping", + "behavioral-tests", + "implementation", + "coverage-hardening", + "documentation", + "ai-review", + ]; + + struct TestWorkspace { + root: PathBuf, + } + + impl Drop for TestWorkspace { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.root); + } + } fn temp_workspace_root() -> PathBuf { std::env::temp_dir().join(format!("codex-monitor-forge-execute-test-{}", Uuid::new_v4())) @@ -1083,26 +1104,72 @@ mod tests { write_text(path, &format!("{}\n", serde_json::to_string_pretty(&value).expect("json encode"))); } - #[test] - fn get_next_phase_prompt_regenerates_even_when_cached_prompt_exists() { + fn run_async_test(future: F) + where + F: Future, + { let rt = tokio::runtime::Builder::new_current_thread() .enable_all() .build() .expect("build runtime"); - rt.block_on(async { + rt.block_on(future); + } + + fn run_git(workspace: &Path, args: &[&str]) { + let output = Command::new("git") + .args(args) + .current_dir(workspace) + .output() + .expect("run git"); + assert!( + output.status.success(), + "git {:?} failed: stdout={} stderr={}", + args, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } + + fn init_git_repo(workspace: &Path) { + run_git(workspace, &["init"]); + run_git(workspace, &["config", "user.email", "forge-tests@example.com"]); + run_git(workspace, &["config", "user.name", "Forge Tests"]); + } + + fn build_task_phase_state(statuses: [&str; 6]) -> Vec { + SIX_PHASE_IDS + .iter() + .enumerate() + .map(|(index, phase_id)| { + json!({ + "id": phase_id, + "status": statuses[index], + "attempts": 0, + "notes": "" + }) + }) + .collect() + } + + fn setup_six_phase_workspace( + task_one_status: &str, + task_one_phase_statuses: [&str; 6], + ai_review_exit_code: i32, + ) -> TestWorkspace { let workspace = temp_workspace_root(); std::fs::create_dir_all(&workspace).expect("create workspace"); - let template_root = workspace.join(".agent").join("templates").join("ralph-loop"); + let template_id = "test-first-loop"; + let template_root = workspace.join(".agent").join("templates").join(template_id); std::fs::create_dir_all(template_root.join("scripts")).expect("create scripts dir"); write_json( &workspace.join(".agent").join("template-lock.json"), json!({ "schema": "forge-template-lock-v1", - "installedTemplateId": "ralph-loop", - "installedTemplateVersion": "0.2.1", - "installedAtIso": "2026-02-11T00:00:00Z", + "installedTemplateId": template_id, + "installedTemplateVersion": "0.1.0", + "installedAtIso": "2026-02-12T00:00:00Z", "installedFiles": ["template.json"] }), ); @@ -1111,9 +1178,9 @@ mod tests { &template_root.join("template.json"), json!({ "schema": "forge-template-v1", - "id": "ralph-loop", - "title": "Ralph Loop", - "version": "0.2.1", + "id": template_id, + "title": "Test-First Loop", + "version": "0.1.0", "files": [ "template.json", "phases.json", @@ -1140,13 +1207,26 @@ mod tests { } }), ); + + let phases = SIX_PHASE_IDS + .iter() + .map(|phase_id| { + let exit_code = if *phase_id == "ai-review" { + ai_review_exit_code + } else { + 0 + }; + json!({ + "id": phase_id, + "checks": [format!("node -e process.exitCode={exit_code}")] + }) + }) + .collect::>(); write_json( &template_root.join("phases.json"), json!({ "schema": "forge-phases-v1", - "phases": [ - { "id": "implementation", "checks": [] } - ] + "phases": phases }), ); write_text(&template_root.join("prompts").join("plan.md"), "# Mode: plan\n"); @@ -1169,7 +1249,7 @@ const i = process.argv.indexOf('--context');\n\ if (i < 0 || !process.argv[i + 1]) process.exit(2);\n\ const ctx = JSON.parse(await fs.readFile(process.argv[i + 1], 'utf8'));\n\ await fs.mkdir(path.dirname(ctx.generatedExecutePromptPath), { recursive: true });\n\ -await fs.writeFile(ctx.generatedExecutePromptPath, 'fresh prompt from post-step\\n', 'utf8');\n", +await fs.writeFile(ctx.generatedExecutePromptPath, 'generated prompt from post-step\\n', 'utf8');\n", ); let plan_dir = workspace.join("plans").join("alpha"); @@ -1196,11 +1276,11 @@ await fs.writeFile(ctx.generatedExecutePromptPath, 'fresh prompt from post-step\ "tasks": [ { "id": "task-1", - "status": "completed", + "status": task_one_status, "attempts": 0, "notes": "", - "commit_sha": "abc123", - "phases": [{ "id": "implementation", "status": "completed", "attempts": 0, "notes": "" }] + "commit_sha": null, + "phases": build_task_phase_state(task_one_phase_statuses) }, { "id": "task-2", @@ -1208,25 +1288,326 @@ await fs.writeFile(ctx.generatedExecutePromptPath, 'fresh prompt from post-step\ "attempts": 0, "notes": "", "commit_sha": null, - "phases": [{ "id": "implementation", "status": "pending", "attempts": 0, "notes": "" }] + "phases": [ + { "id": "implementation", "status": "pending", "attempts": 0, "notes": "" } + ] } ] }), ); - write_text( - &plan_dir.join("execute-prompt.md"), - "stale prompt that should not be returned\n", - ); - let next = forge_get_next_phase_prompt_core(&workspace, "alpha") - .await - .expect("get next phase prompt") - .expect("expected runnable phase"); - assert_eq!(next.task_id, "task-2"); - assert_eq!(next.phase_id, "implementation"); - assert_eq!(next.prompt_text.trim(), "fresh prompt from post-step"); + TestWorkspace { root: workspace } + } + + fn load_state_task(workspace: &Path, plan_id: &str, task_id: &str) -> StateTaskV2 { + let state_path = workspace.join("plans").join(plan_id).join("state.json"); + let state = read_json_file::(&state_path).expect("load state"); + state + .tasks + .into_iter() + .find(|task| task.id == task_id) + .expect("task in state") + } - let _ = std::fs::remove_dir_all(&workspace); + fn phase_status(task: &StateTaskV2, phase_id: &str) -> String { + task.phases + .iter() + .find(|phase| phase.id == phase_id) + .map(|phase| phase.status.clone()) + .expect("phase in task") + } + + #[test] + fn get_next_phase_prompt_regenerates_even_when_cached_prompt_exists() { + run_async_test(async { + let workspace = temp_workspace_root(); + std::fs::create_dir_all(&workspace).expect("create workspace"); + + let template_root = workspace.join(".agent").join("templates").join("ralph-loop"); + std::fs::create_dir_all(template_root.join("scripts")).expect("create scripts dir"); + + write_json( + &workspace.join(".agent").join("template-lock.json"), + json!({ + "schema": "forge-template-lock-v1", + "installedTemplateId": "ralph-loop", + "installedTemplateVersion": "0.2.1", + "installedAtIso": "2026-02-11T00:00:00Z", + "installedFiles": ["template.json"] + }), + ); + + write_json( + &template_root.join("template.json"), + json!({ + "schema": "forge-template-v1", + "id": "ralph-loop", + "title": "Ralph Loop", + "version": "0.2.1", + "files": [ + "template.json", + "phases.json", + "prompts/plan.md", + "prompts/execute.md", + "schemas/plan.schema.json", + "schemas/state.schema.json", + "scripts/post-plan.mjs", + "scripts/pre-execute.mjs", + "scripts/post-step.mjs" + ], + "entrypoints": { + "phases": "phases.json", + "planPrompt": "prompts/plan.md", + "executePrompt": "prompts/execute.md", + "planSchema": "schemas/plan.schema.json", + "stateSchema": "schemas/state.schema.json", + "requiredSkills": [], + "hooks": { + "postPlan": "scripts/post-plan.mjs", + "preExecute": "scripts/pre-execute.mjs", + "postStep": "scripts/post-step.mjs" + } + } + }), + ); + write_json( + &template_root.join("phases.json"), + json!({ + "schema": "forge-phases-v1", + "phases": [ + { "id": "implementation", "checks": [] } + ] + }), + ); + write_text(&template_root.join("prompts").join("plan.md"), "# Mode: plan\n"); + write_text(&template_root.join("prompts").join("execute.md"), "# Mode: execute\n"); + write_text(&template_root.join("schemas").join("plan.schema.json"), "{}\n"); + write_text(&template_root.join("schemas").join("state.schema.json"), "{}\n"); + write_text( + &template_root.join("scripts").join("post-plan.mjs"), + "process.exit(0);\n", + ); + write_text( + &template_root.join("scripts").join("pre-execute.mjs"), + "process.exit(0);\n", + ); + write_text( + &template_root.join("scripts").join("post-step.mjs"), + "import fs from 'node:fs/promises';\n\ +import path from 'node:path';\n\ +const i = process.argv.indexOf('--context');\n\ +if (i < 0 || !process.argv[i + 1]) process.exit(2);\n\ +const ctx = JSON.parse(await fs.readFile(process.argv[i + 1], 'utf8'));\n\ +await fs.mkdir(path.dirname(ctx.generatedExecutePromptPath), { recursive: true });\n\ +await fs.writeFile(ctx.generatedExecutePromptPath, 'fresh prompt from post-step\\n', 'utf8');\n", + ); + + let plan_dir = workspace.join("plans").join("alpha"); + std::fs::create_dir_all(&plan_dir).expect("create plan dir"); + write_json( + &plan_dir.join("plan.json"), + json!({ + "$schema": "plan-v1", + "id": "alpha", + "goal": "Test goal", + "tasks": [ + { "id": "task-1", "name": "Task 1", "depends_on": [] }, + { "id": "task-2", "name": "Task 2", "depends_on": ["task-1"] } + ] + }), + ); + write_json( + &plan_dir.join("state.json"), + json!({ + "$schema": "state-v2", + "plan_id": "alpha", + "iteration": 0, + "summary": "", + "tasks": [ + { + "id": "task-1", + "status": "completed", + "attempts": 0, + "notes": "", + "commit_sha": "abc123", + "phases": [{ "id": "implementation", "status": "completed", "attempts": 0, "notes": "" }] + }, + { + "id": "task-2", + "status": "pending", + "attempts": 0, + "notes": "", + "commit_sha": null, + "phases": [{ "id": "implementation", "status": "pending", "attempts": 0, "notes": "" }] + } + ] + }), + ); + write_text( + &plan_dir.join("execute-prompt.md"), + "stale prompt that should not be returned\n", + ); + + let next = forge_get_next_phase_prompt_core(&workspace, "alpha") + .await + .expect("get next phase prompt") + .expect("expected runnable phase"); + assert_eq!(next.task_id, "task-2"); + assert_eq!(next.phase_id, "implementation"); + assert_eq!(next.prompt_text.trim(), "fresh prompt from post-step"); + + let _ = std::fs::remove_dir_all(&workspace); + }); + } + + #[test] + fn get_next_phase_prompt_selects_first_incomplete_phase_for_partially_completed_task() { + run_async_test(async { + let fixture = setup_six_phase_workspace( + "in_progress", + [ + "completed", + "completed", + "completed", + "in_progress", + "pending", + "pending", + ], + 0, + ); + + let next = forge_get_next_phase_prompt_core(&fixture.root, "alpha") + .await + .expect("get next phase") + .expect("expected next phase"); + assert_eq!(next.task_id, "task-1"); + assert_eq!(next.phase_id, "coverage-hardening"); + assert!(!next.is_last_phase); + assert_eq!(next.prompt_text.trim(), "generated prompt from post-step"); + }); + } + + #[test] + fn run_phase_checks_non_final_success_completes_only_current_phase_without_commit() { + run_async_test(async { + let fixture = setup_six_phase_workspace( + "pending", + [ + "pending", + "pending", + "pending", + "pending", + "pending", + "pending", + ], + 0, + ); + + let result = + forge_run_phase_checks_core(&fixture.root, "alpha", "task-1", "test-case-mapping") + .await + .expect("run phase checks"); + assert!(result.ok); + assert!(!result.results.iter().any(|check| check.id == "forge-commit")); + + let task = load_state_task(&fixture.root, "alpha", "task-1"); + assert_eq!(task.status, "in_progress"); + assert_eq!(task.commit_sha, None); + assert_eq!(phase_status(&task, "test-case-mapping"), "completed"); + assert_eq!(phase_status(&task, "behavioral-tests"), "pending"); + assert_eq!(phase_status(&task, "ai-review"), "pending"); + + let next = forge_get_next_phase_prompt_core(&fixture.root, "alpha") + .await + .expect("get next phase") + .expect("expected next phase"); + assert_eq!(next.task_id, "task-1"); + assert_eq!(next.phase_id, "behavioral-tests"); + assert!(!next.is_last_phase); + }); + } + + #[test] + fn run_phase_checks_final_ai_review_failure_blocks_completion_and_progression() { + run_async_test(async { + let fixture = setup_six_phase_workspace( + "in_progress", + [ + "completed", + "completed", + "completed", + "completed", + "completed", + "pending", + ], + 1, + ); + + let result = forge_run_phase_checks_core(&fixture.root, "alpha", "task-1", "ai-review") + .await + .expect("run phase checks"); + assert!(!result.ok); + assert!(result.results.iter().any(|check| check.exit_code != 0)); + assert!(!result.results.iter().any(|check| check.id == "forge-commit")); + + let task = load_state_task(&fixture.root, "alpha", "task-1"); + assert_eq!(task.status, "in_progress"); + assert_eq!(task.commit_sha, None); + assert_eq!(phase_status(&task, "ai-review"), "in_progress"); + + let next = forge_get_next_phase_prompt_core(&fixture.root, "alpha") + .await + .expect("get next phase") + .expect("expected next phase"); + assert_eq!(next.task_id, "task-1"); + assert_eq!(next.phase_id, "ai-review"); + assert!(next.is_last_phase); + }); + } + + #[test] + fn run_phase_checks_final_ai_review_success_completes_task_and_records_commit() { + run_async_test(async { + let fixture = setup_six_phase_workspace( + "in_progress", + [ + "completed", + "completed", + "completed", + "completed", + "completed", + "pending", + ], + 0, + ); + init_git_repo(&fixture.root); + + let result = forge_run_phase_checks_core(&fixture.root, "alpha", "task-1", "ai-review") + .await + .expect("run phase checks"); + assert!(result.ok); + assert!(result + .results + .iter() + .any(|check| check.id == "forge-commit" && check.exit_code == 0)); + + let task = load_state_task(&fixture.root, "alpha", "task-1"); + assert_eq!(task.status, "completed"); + assert_eq!(phase_status(&task, "ai-review"), "completed"); + assert!( + task.commit_sha + .as_ref() + .map(|sha| !sha.trim().is_empty()) + .unwrap_or(false) + ); + + let next = forge_get_next_phase_prompt_core(&fixture.root, "alpha") + .await + .expect("get next phase") + .expect("expected next phase"); + assert_eq!(next.task_id, "task-2"); + assert_eq!(next.phase_id, "implementation"); + assert!(next.is_last_phase); }); } } diff --git a/src/features/app/components/Sidebar.test.tsx b/src/features/app/components/Sidebar.test.tsx index eb655c1311..18611b1a9c 100644 --- a/src/features/app/components/Sidebar.test.tsx +++ b/src/features/app/components/Sidebar.test.tsx @@ -72,7 +72,7 @@ const baseProps = { }; describe("Sidebar", () => { - it("toggles Forge panel from the header anvil icon", () => { + it("toggles Forge panel from the header anvil icon and shows no-project blocker state", () => { render(); expect(screen.queryByText("Forge")).toBeNull(); @@ -81,11 +81,21 @@ describe("Sidebar", () => { fireEvent.click(screen.getByRole("button", { name: "Toggle Forge" })); expect(screen.getByText("Forge")).toBeTruthy(); - expect(screen.getByText("Click to select")).toBeTruthy(); + expect(screen.getByText(/Select a project first to use Forge/i)).toBeTruthy(); expect(screen.queryByText("Add a workspace to start.")).toBeNull(); + const templateButton = screen.getByRole("button", { name: "Open templates" }); + expect(templateButton.hasAttribute("disabled")).toBe(true); + const planSelectButton = screen.getByRole("button", { name: /Click to select/i }); + expect(planSelectButton.hasAttribute("disabled")).toBe(true); const runButton = screen.getByRole("button", { name: "Resume plan" }); expect(runButton.hasAttribute("disabled")).toBe(true); + const cleanProgressButton = screen.getByRole("button", { name: "Clean progress" }); + expect(cleanProgressButton.hasAttribute("disabled")).toBe(true); + + fireEvent.click(screen.getByRole("button", { name: "Toggle Forge" })); + expect(screen.queryByText("Forge")).toBeNull(); + expect(screen.getByText("Add a workspace to start.")).toBeTruthy(); }); it("closes Forge when the search toggle is activated", () => { @@ -99,15 +109,15 @@ describe("Sidebar", () => { expect(screen.getByLabelText("Search projects")).toBeTruthy(); }); - it("shows Forge plan menu actions", () => { + it("does not show Forge plan menu actions when no project is selected", () => { render(); fireEvent.click(screen.getByRole("button", { name: "Toggle Forge" })); fireEvent.click(screen.getByRole("button", { name: /Click to select/i })); - expect(screen.getByText("Other...")).toBeTruthy(); - expect(screen.getByText("New plan...")).toBeTruthy(); + expect(screen.queryByText("Other...")).toBeNull(); + expect(screen.queryByText("New plan...")).toBeNull(); }); it("toggles the search bar from the header icon", () => { diff --git a/src/features/forge/components/Forge.plans.test.tsx b/src/features/forge/components/Forge.plans.test.tsx index 9cdee85641..0b8f29ee0b 100644 --- a/src/features/forge/components/Forge.plans.test.tsx +++ b/src/features/forge/components/Forge.plans.test.tsx @@ -22,6 +22,272 @@ const templatesClient: ForgeTemplatesClient = { }; describe("Forge plans", () => { + it("opens templates modal, supports swap/remove actions, and closes on escape", async () => { + const installTemplate = vi.fn(async () => ({ + schema: "forge-template-lock-v1", + installedTemplateId: "tpl-beta", + installedTemplateVersion: "2.0.0", + installedAtIso: "2026-02-12T00:00:00.000Z", + installedFiles: [], + })); + const uninstallTemplate = vi.fn(async () => {}); + const templatesClientWithInstall: ForgeTemplatesClient = { + listBundledTemplates: async () => [ + { id: "tpl-alpha", title: "Alpha Template", version: "1.0.0" }, + { id: "tpl-beta", title: "Beta Template", version: "2.0.0" }, + ], + getInstalledTemplate: async () => ({ + schema: "forge-template-lock-v1", + installedTemplateId: "tpl-alpha", + installedTemplateVersion: "1.0.0", + installedAtIso: "2026-02-12T00:00:00.000Z", + installedFiles: [], + }), + installTemplate, + uninstallTemplate, + }; + + const plansClient: ForgePlansClient = { + listPlans: async () => [], + getPlanPrompt: async () => "", + prepareExecution: async () => {}, + resetExecutionProgress: async () => {}, + getNextPhasePrompt: async () => null, + getPhaseStatus: async () => ({ status: "pending", commitSha: null }), + runPhaseChecks: async () => ({ ok: true, results: [] }), + interruptTurn: async () => ({}), + connectWorkspace: async () => {}, + startThread: async () => ({ result: { thread: { id: "thread-1" } } }), + sendUserMessage: async () => ({}), + }; + + render( + , + ); + + fireEvent.click(await screen.findByRole("button", { name: "Open templates" })); + expect(await screen.findByText("Templates")).toBeTruthy(); + expect(screen.getByText("Alpha Template")).toBeTruthy(); + fireEvent.click(screen.getByRole("button", { name: "Swap" })); + + await waitFor(() => expect(installTemplate).toHaveBeenCalledWith("ws-1", "tpl-beta")); + await waitFor(() => expect(screen.queryByText("Templates")).toBeNull()); + + fireEvent.click(screen.getByRole("button", { name: "Open templates" })); + await waitFor(() => expect(screen.getByText("Beta Template")).toBeTruthy()); + fireEvent.click(screen.getByRole("button", { name: "Remove" })); + await waitFor(() => expect(uninstallTemplate).toHaveBeenCalledWith("ws-1")); + await waitFor(() => expect(screen.queryByText("Templates")).toBeNull()); + + fireEvent.click(screen.getByRole("button", { name: "Open templates" })); + await waitFor(() => expect(screen.getByText("Templates")).toBeTruthy()); + fireEvent.keyDown(window, { key: "Escape" }); + await waitFor(() => expect(screen.queryByText("Templates")).toBeNull()); + }); + + it("renders unknown installed template fallback text when lock is not in bundled list", async () => { + const templatesClientWithUnknownInstall: ForgeTemplatesClient = { + listBundledTemplates: async () => [ + { id: "tpl-known", title: "Known Template", version: "1.0.0" }, + ], + getInstalledTemplate: async () => ({ + schema: "forge-template-lock-v1", + installedTemplateId: "tpl-missing", + installedTemplateVersion: "9.9.9", + installedAtIso: "2026-02-12T00:00:00.000Z", + installedFiles: [], + }), + installTemplate: async () => { + throw new Error("unexpected install"); + }, + uninstallTemplate: async () => {}, + }; + + const plansClient: ForgePlansClient = { + listPlans: async () => [], + getPlanPrompt: async () => "", + prepareExecution: async () => {}, + resetExecutionProgress: async () => {}, + getNextPhasePrompt: async () => null, + getPhaseStatus: async () => ({ status: "pending", commitSha: null }), + runPhaseChecks: async () => ({ ok: true, results: [] }), + interruptTurn: async () => ({}), + connectWorkspace: async () => {}, + startThread: async () => ({ result: { thread: { id: "thread-1" } } }), + sendUserMessage: async () => ({}), + }; + + render( + , + ); + + fireEvent.click(await screen.findByRole("button", { name: "Open templates" })); + expect(await screen.findByText("tpl-missing")).toBeTruthy(); + expect(screen.getByText("Installed (9.9.9)")).toBeTruthy(); + }); + + it("shows a no-project blocker and disables Forge controls when no workspace is selected", async () => { + const listPlans = vi.fn().mockResolvedValue([]); + const plansClient: ForgePlansClient = { + listPlans, + getPlanPrompt: async () => "", + prepareExecution: async () => {}, + resetExecutionProgress: async () => {}, + getNextPhasePrompt: async () => null, + getPhaseStatus: async () => ({ status: "pending", commitSha: null }), + runPhaseChecks: async () => ({ ok: true, results: [] }), + interruptTurn: async () => ({}), + connectWorkspace: async () => {}, + startThread: async () => ({ result: { thread: { id: "thread-1" } } }), + sendUserMessage: async () => ({}), + }; + + render( + , + ); + + expect( + await screen.findByText(/Select a project first to use Forge/i), + ).toBeTruthy(); + + const templatesButton = screen.getByRole("button", { name: "Open templates" }); + expect(templatesButton.hasAttribute("disabled")).toBe(true); + const planSelectButton = screen.getByRole("button", { name: /Click to select/i }); + expect(planSelectButton.hasAttribute("disabled")).toBe(true); + const runButton = screen.getByRole("button", { name: "Resume plan" }); + expect(runButton.hasAttribute("disabled")).toBe(true); + const cleanProgressButton = screen.getByRole("button", { name: "Clean progress" }); + expect(cleanProgressButton.hasAttribute("disabled")).toBe(true); + + fireEvent.click(planSelectButton); + expect(screen.queryByText("New plan...")).toBeNull(); + expect(screen.queryByText("Other...")).toBeNull(); + expect(listPlans).not.toHaveBeenCalled(); + }); + + it("never calls connect/start/send plan backends in no-project mode", async () => { + const connectWorkspace = vi.fn(async () => {}); + const startThread = vi.fn(async () => ({ result: { thread: { id: "thread-1" } } })); + const sendUserMessage = vi.fn(async () => ({})); + + const plansClient: ForgePlansClient = { + listPlans: async () => [], + getPlanPrompt: async () => "", + prepareExecution: async () => {}, + resetExecutionProgress: async () => {}, + getNextPhasePrompt: async () => null, + getPhaseStatus: async () => ({ status: "pending", commitSha: null }), + runPhaseChecks: async () => ({ ok: true, results: [] }), + interruptTurn: async () => ({}), + connectWorkspace, + startThread, + sendUserMessage, + }; + + render( + , + ); + + const planSelectButton = screen.getByRole("button", { name: /Click to select/i }); + fireEvent.click(planSelectButton); + const newPlanAction = screen.queryByText("New plan..."); + if (newPlanAction) { + fireEvent.click(newPlanAction); + } + fireEvent.click(screen.getByRole("button", { name: "Resume plan" })); + fireEvent.click(screen.getByRole("button", { name: "Clean progress" })); + + await act(async () => { + await Promise.resolve(); + }); + + expect(connectWorkspace).not.toHaveBeenCalled(); + expect(startThread).not.toHaveBeenCalled(); + expect(sendUserMessage).not.toHaveBeenCalled(); + }); + + it("keeps no-project invariants under deterministic random click sequences", async () => { + const connectWorkspace = vi.fn(async () => {}); + const startThread = vi.fn(async () => ({ result: { thread: { id: "thread-1" } } })); + const sendUserMessage = vi.fn(async () => ({})); + + const plansClient: ForgePlansClient = { + listPlans: async () => [], + getPlanPrompt: async () => "", + prepareExecution: async () => {}, + resetExecutionProgress: async () => {}, + getNextPhasePrompt: async () => null, + getPhaseStatus: async () => ({ status: "pending", commitSha: null }), + runPhaseChecks: async () => ({ ok: true, results: [] }), + interruptTurn: async () => ({}), + connectWorkspace, + startThread, + sendUserMessage, + }; + + render( + , + ); + + const controls = () => ({ + templates: screen.getByRole("button", { name: "Open templates" }), + planSelect: screen.getByRole("button", { name: /Click to select/i }), + run: screen.getByRole("button", { name: "Resume plan" }), + clean: screen.getByRole("button", { name: "Clean progress" }), + }); + + let seed = 1337; + const next = () => { + seed = (seed * 1103515245 + 12345) % 2147483648; + return seed; + }; + + const actions = [ + () => fireEvent.click(controls().templates), + () => fireEvent.click(controls().planSelect), + () => fireEvent.click(controls().run), + () => fireEvent.click(controls().clean), + ]; + + for (let i = 0; i < 64; i += 1) { + await act(async () => { + actions[next() % actions.length](); + }); + expect(screen.getByText(/Select a project first to use Forge/i)).toBeTruthy(); + expect(screen.queryByText("New plan...")).toBeNull(); + expect(screen.queryByText("Templates")).toBeNull(); + } + + expect(connectWorkspace).not.toHaveBeenCalled(); + expect(startThread).not.toHaveBeenCalled(); + expect(sendUserMessage).not.toHaveBeenCalled(); + }); + it("lists workspace plans and enables the execute toggle after selecting one", async () => { const getNextPhasePrompt = vi .fn() @@ -221,7 +487,8 @@ describe("Forge plans", () => { const startThread = vi .fn() .mockResolvedValueOnce({ result: { thread: { id: "thread-1" } } }) - .mockResolvedValueOnce({ result: { thread: { id: "thread-2" } } }); + .mockResolvedValueOnce({ result: { thread: { id: "thread-2" } } }) + .mockResolvedValueOnce({ result: { thread: { id: "thread-3" } } }); const sendUserMessage = vi.fn().mockResolvedValue({}); const getNextPhasePrompt = vi .fn() @@ -414,15 +681,31 @@ describe("Forge plans", () => { expect(onSelectThread).toHaveBeenNthCalledWith(2, "ws-1", "thread-2"); }); - it("pauses by interrupting the active forge turn", async () => { - const interruptTurn = vi.fn(async () => ({})); + it("stops execution with an alert when next phase task id is not visible in the selected plan", async () => { + const startThread = vi + .fn() + .mockResolvedValueOnce({ result: { thread: { id: "thread-999" } } }); + const sendUserMessage = vi.fn().mockResolvedValue({}); + const getNextPhasePrompt = vi + .fn() + .mockResolvedValueOnce({ + planId: "alpha", + taskId: "task-999", + phaseId: "implementation", + isLastPhase: true, + promptText: "unexpected task prompt", + }) + .mockResolvedValueOnce(null); const plansClient: ForgePlansClient = { listPlans: async () => [ { id: "alpha", title: "Alpha", goal: "Alpha goal", - tasks: [{ id: "task-1", name: "Task 1", status: "pending" }], + tasks: [ + { id: "task-1", name: "Task 1", status: "pending" }, + { id: "task-2", name: "Task 2", status: "pending" }, + ], currentTaskId: null, planPath: "plans/alpha/plan.json", updatedAtMs: 0, @@ -431,19 +714,13 @@ describe("Forge plans", () => { getPlanPrompt: async () => "", prepareExecution: async () => {}, resetExecutionProgress: async () => {}, - getNextPhasePrompt: async () => ({ - planId: "alpha", - taskId: "task-1", - phaseId: "implementation", - isLastPhase: true, - promptText: "task 1 prompt", - }), - getPhaseStatus: async () => ({ status: "pending", commitSha: null }), + getNextPhasePrompt, + getPhaseStatus: async () => ({ status: "completed", commitSha: null }), runPhaseChecks: async () => ({ ok: true, results: [] }), - interruptTurn, + interruptTurn: async () => ({}), connectWorkspace: async () => {}, - startThread: async () => ({ result: { thread: { id: "thread-1" } } }), - sendUserMessage: async () => ({ result: { turn: { id: "turn-1" } } }), + startThread, + sendUserMessage, }; render( @@ -458,41 +735,422 @@ describe("Forge plans", () => { fireEvent.click(await screen.findByRole("button", { name: /Click to select/i })); fireEvent.click(screen.getByRole("menuitemradio", { name: "Alpha (alpha)" })); fireEvent.click(screen.getByRole("button", { name: "Resume plan" })); - fireEvent.click(await screen.findByRole("button", { name: "Pause plan" })); + await waitFor(() => expect(getNextPhasePrompt).toHaveBeenCalledTimes(1)); await waitFor(() => - expect(interruptTurn).toHaveBeenCalledWith("ws-1", "thread-1", "turn-1"), + expect(screen.getByRole("alert").textContent).toContain("unexpected task id"), ); + expect(startThread).not.toHaveBeenCalled(); + expect(sendUserMessage).not.toHaveBeenCalled(); + expect(screen.getByRole("button", { name: "Resume plan" })).toBeTruthy(); }); - it("cleans plan progress from the selected state with confirmation", async () => { - const listPlans = vi.fn().mockResolvedValue([ - { - id: "alpha", - title: "Alpha", - goal: "Alpha goal", - tasks: [{ id: "task-1", name: "Task 1", status: "pending" }], - currentTaskId: null, - planPath: "plans/alpha/plan.json", - updatedAtMs: 0, - }, - ]); - const resetExecutionProgress = vi.fn(async () => {}); - const confirmMock = vi.spyOn(window, "confirm").mockReturnValue(true); + it("stops without creating an extra thread when an unexpected task arrives after visible tasks", async () => { + const startThread = vi + .fn() + .mockResolvedValueOnce({ result: { thread: { id: "thread-1" } } }) + .mockResolvedValueOnce({ result: { thread: { id: "thread-2" } } }) + .mockResolvedValueOnce({ result: { thread: { id: "thread-3" } } }); + const sendUserMessage = vi.fn().mockResolvedValue({}); + const getNextPhasePrompt = vi + .fn() + .mockResolvedValueOnce({ + planId: "alpha", + taskId: "task-1", + phaseId: "implementation", + isLastPhase: true, + promptText: "task 1 prompt", + }) + .mockResolvedValueOnce({ + planId: "alpha", + taskId: "task-2", + phaseId: "implementation", + isLastPhase: true, + promptText: "task 2 prompt", + }) + .mockResolvedValueOnce({ + planId: "alpha", + taskId: "task-3", + phaseId: "implementation", + isLastPhase: true, + promptText: "unexpected task prompt", + }) + .mockResolvedValueOnce(null); const plansClient: ForgePlansClient = { - listPlans, + listPlans: async () => [ + { + id: "alpha", + title: "Alpha", + goal: "Alpha goal", + tasks: [ + { id: "task-1", name: "Task 1", status: "pending" }, + { id: "task-2", name: "Task 2", status: "pending" }, + ], + currentTaskId: null, + planPath: "plans/alpha/plan.json", + updatedAtMs: 0, + }, + ], getPlanPrompt: async () => "", prepareExecution: async () => {}, - resetExecutionProgress, - getNextPhasePrompt: async () => null, - getPhaseStatus: async () => ({ status: "pending", commitSha: null }), + resetExecutionProgress: async () => {}, + getNextPhasePrompt, + getPhaseStatus: async () => ({ status: "completed", commitSha: null }), runPhaseChecks: async () => ({ ok: true, results: [] }), interruptTurn: async () => ({}), connectWorkspace: async () => {}, - startThread: async () => ({ result: { thread: { id: "thread-1" } } }), - sendUserMessage: async () => ({}), - }; + startThread, + sendUserMessage, + }; + + render( + , + ); + + fireEvent.click(await screen.findByRole("button", { name: /Click to select/i })); + fireEvent.click(screen.getByRole("menuitemradio", { name: "Alpha (alpha)" })); + fireEvent.click(screen.getByRole("button", { name: "Resume plan" })); + + await waitFor(() => expect(startThread).toHaveBeenCalledTimes(2)); + await waitFor(() => + expect(screen.getByRole("alert").textContent).toContain("unexpected task id"), + ); + expect(sendUserMessage).toHaveBeenCalledTimes(2); + expect(getNextPhasePrompt).toHaveBeenCalledTimes(3); + expect(screen.getByRole("button", { name: "Resume plan" })).toBeTruthy(); + }); + + it("exits cleanly after final visible task when next phase returns null", async () => { + const startThread = vi + .fn() + .mockResolvedValueOnce({ result: { thread: { id: "thread-1" } } }); + const sendUserMessage = vi.fn().mockResolvedValue({}); + const getNextPhasePrompt = vi + .fn() + .mockResolvedValueOnce({ + planId: "alpha", + taskId: "task-1", + phaseId: "implementation", + isLastPhase: true, + promptText: "task 1 prompt", + }) + .mockResolvedValueOnce(null); + + const plansClient: ForgePlansClient = { + listPlans: async () => [ + { + id: "alpha", + title: "Alpha", + goal: "Alpha goal", + tasks: [{ id: "task-1", name: "Task 1", status: "pending" }], + currentTaskId: null, + planPath: "plans/alpha/plan.json", + updatedAtMs: 0, + }, + ], + getPlanPrompt: async () => "", + prepareExecution: async () => {}, + resetExecutionProgress: async () => {}, + getNextPhasePrompt, + getPhaseStatus: async () => ({ status: "completed", commitSha: null }), + runPhaseChecks: async () => ({ ok: true, results: [] }), + interruptTurn: async () => ({}), + connectWorkspace: async () => {}, + startThread, + sendUserMessage, + }; + + render( + , + ); + + fireEvent.click(await screen.findByRole("button", { name: /Click to select/i })); + fireEvent.click(screen.getByRole("menuitemradio", { name: "Alpha (alpha)" })); + fireEvent.click(screen.getByRole("button", { name: "Resume plan" })); + + await waitFor(() => expect(startThread).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(sendUserMessage).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(getNextPhasePrompt).toHaveBeenCalledTimes(2)); + expect(screen.queryByRole("alert")).toBeNull(); + expect(screen.getByRole("button", { name: "Resume plan" })).toBeTruthy(); + }); + + it("preserves execution guard invariants across deterministic prompt sequences", async () => { + const knownTasks = ["task-1", "task-2", "task-3"] as const; + const knownTaskSet = new Set(knownTasks); + let seed = 20260212; + const next = () => { + seed = (seed * 1664525 + 1013904223) % 4294967296; + return seed; + }; + + for (let caseIndex = 0; caseIndex < 8; caseIndex += 1) { + cleanup(); + + const sequenceLength = 2 + (next() % 4); // 2..5 prompts before terminal null + const unknownInsertionIndex = next() % (sequenceLength + 1); // if == length, no unknown + const taskSequence: string[] = []; + for (let i = 0; i < sequenceLength; i += 1) { + if (unknownInsertionIndex < sequenceLength && i === unknownInsertionIndex) { + taskSequence.push(`task-x-${caseIndex}`); + continue; + } + taskSequence.push(knownTasks[next() % knownTasks.length]); + } + + const prompts: Array< + | { + planId: string; + taskId: string; + phaseId: string; + isLastPhase: boolean; + promptText: string; + } + | null + > = taskSequence.map((taskId, i) => ({ + planId: "alpha", + taskId, + phaseId: `implementation-${i}`, + isLastPhase: true, + promptText: `${taskId} prompt ${i}`, + })); + prompts.push(null); + + const getNextPhasePrompt = vi + .fn() + .mockImplementation(async () => prompts.shift() ?? null); + let threadCounter = 0; + const startThread = vi + .fn() + .mockImplementation(async () => { + threadCounter += 1; + return { + result: { + thread: { id: `thread-${caseIndex}-${threadCounter}` }, + }, + }; + }); + const sendUserMessage = vi.fn().mockResolvedValue({}); + + const plansClient: ForgePlansClient = { + listPlans: async () => [ + { + id: "alpha", + title: "Alpha", + goal: "Alpha goal", + tasks: [ + { id: "task-1", name: "Task 1", status: "pending" }, + { id: "task-2", name: "Task 2", status: "pending" }, + { id: "task-3", name: "Task 3", status: "pending" }, + ], + currentTaskId: null, + planPath: "plans/alpha/plan.json", + updatedAtMs: 0, + }, + ], + getPlanPrompt: async () => "", + prepareExecution: async () => {}, + resetExecutionProgress: async () => {}, + getNextPhasePrompt, + getPhaseStatus: async () => ({ status: "completed", commitSha: null }), + runPhaseChecks: async () => ({ ok: true, results: [] }), + interruptTurn: async () => ({}), + connectWorkspace: async () => {}, + startThread, + sendUserMessage, + }; + + render( + , + ); + + fireEvent.click(await screen.findByRole("button", { name: /Click to select/i })); + fireEvent.click(screen.getByRole("menuitemradio", { name: "Alpha (alpha)" })); + fireEvent.click(screen.getByRole("button", { name: "Resume plan" })); + + const firstUnknown = taskSequence.findIndex((taskId) => !knownTaskSet.has(taskId)); + const executedTaskSequence = + firstUnknown >= 0 ? taskSequence.slice(0, firstUnknown) : taskSequence; + const expectedThreadCount = new Set(executedTaskSequence).size; + + await waitFor(() => expect(startThread).toHaveBeenCalledTimes(expectedThreadCount)); + await waitFor(() => + expect(sendUserMessage).toHaveBeenCalledTimes(executedTaskSequence.length), + ); + + if (firstUnknown >= 0) { + await waitFor(() => + expect(screen.getByRole("alert").textContent).toContain("unexpected task id"), + ); + } else { + await waitFor(() => + expect(screen.getByRole("button", { name: "Resume plan" })).toBeTruthy(), + ); + expect(screen.queryByRole("alert")).toBeNull(); + } + } + }); + + it("waits and re-polls after failing phase checks without starting a new session", async () => { + const startThread = vi + .fn() + .mockResolvedValueOnce({ result: { thread: { id: "thread-1" } } }); + const sendUserMessage = vi.fn().mockResolvedValue({}); + const getNextPhasePrompt = vi + .fn() + .mockResolvedValueOnce({ + planId: "alpha", + taskId: "task-1", + phaseId: "implementation", + isLastPhase: true, + promptText: "task 1 prompt", + }) + .mockResolvedValueOnce(null); + const runPhaseChecks = vi + .fn() + .mockResolvedValueOnce({ ok: false, results: [] }); + + const plansClient: ForgePlansClient = { + listPlans: async () => [ + { + id: "alpha", + title: "Alpha", + goal: "Alpha goal", + tasks: [{ id: "task-1", name: "Task 1", status: "pending" }], + currentTaskId: null, + planPath: "plans/alpha/plan.json", + updatedAtMs: 0, + }, + ], + getPlanPrompt: async () => "", + prepareExecution: async () => {}, + resetExecutionProgress: async () => {}, + getNextPhasePrompt, + getPhaseStatus: async () => ({ status: "completed", commitSha: null }), + runPhaseChecks, + interruptTurn: async () => ({}), + connectWorkspace: async () => {}, + startThread, + sendUserMessage, + }; + + render( + , + ); + + fireEvent.click(await screen.findByRole("button", { name: /Click to select/i })); + fireEvent.click(screen.getByRole("menuitemradio", { name: "Alpha (alpha)" })); + fireEvent.click(screen.getByRole("button", { name: "Resume plan" })); + + await waitFor(() => expect(runPhaseChecks).toHaveBeenCalledTimes(1)); + await new Promise((resolve) => window.setTimeout(resolve, 1300)); + await waitFor(() => expect(getNextPhasePrompt).toHaveBeenCalledTimes(2)); + expect(startThread).toHaveBeenCalledTimes(1); + expect(sendUserMessage).toHaveBeenCalledTimes(1); + expect(screen.queryByRole("alert")).toBeNull(); + expect(screen.getByRole("button", { name: "Resume plan" })).toBeTruthy(); + }); + + it("pauses by interrupting the active forge turn", async () => { + const interruptTurn = vi.fn(async () => ({})); + const plansClient: ForgePlansClient = { + listPlans: async () => [ + { + id: "alpha", + title: "Alpha", + goal: "Alpha goal", + tasks: [{ id: "task-1", name: "Task 1", status: "pending" }], + currentTaskId: null, + planPath: "plans/alpha/plan.json", + updatedAtMs: 0, + }, + ], + getPlanPrompt: async () => "", + prepareExecution: async () => {}, + resetExecutionProgress: async () => {}, + getNextPhasePrompt: async () => ({ + planId: "alpha", + taskId: "task-1", + phaseId: "implementation", + isLastPhase: true, + promptText: "task 1 prompt", + }), + getPhaseStatus: async () => ({ status: "pending", commitSha: null }), + runPhaseChecks: async () => ({ ok: true, results: [] }), + interruptTurn, + connectWorkspace: async () => {}, + startThread: async () => ({ result: { thread: { id: "thread-1" } } }), + sendUserMessage: async () => ({ result: { turn: { id: "turn-1" } } }), + }; + + render( + , + ); + + fireEvent.click(await screen.findByRole("button", { name: /Click to select/i })); + fireEvent.click(screen.getByRole("menuitemradio", { name: "Alpha (alpha)" })); + fireEvent.click(screen.getByRole("button", { name: "Resume plan" })); + fireEvent.click(await screen.findByRole("button", { name: "Pause plan" })); + + await waitFor(() => + expect(interruptTurn).toHaveBeenCalledWith("ws-1", "thread-1", "turn-1"), + ); + }); + + it("cleans plan progress from the selected state with confirmation", async () => { + const listPlans = vi.fn().mockResolvedValue([ + { + id: "alpha", + title: "Alpha", + goal: "Alpha goal", + tasks: [{ id: "task-1", name: "Task 1", status: "pending" }], + currentTaskId: null, + planPath: "plans/alpha/plan.json", + updatedAtMs: 0, + }, + ]); + const resetExecutionProgress = vi.fn(async () => {}); + const confirmMock = vi.spyOn(window, "confirm").mockReturnValue(true); + + const plansClient: ForgePlansClient = { + listPlans, + getPlanPrompt: async () => "", + prepareExecution: async () => {}, + resetExecutionProgress, + getNextPhasePrompt: async () => null, + getPhaseStatus: async () => ({ status: "pending", commitSha: null }), + runPhaseChecks: async () => ({ ok: true, results: [] }), + interruptTurn: async () => ({}), + connectWorkspace: async () => {}, + startThread: async () => ({ result: { thread: { id: "thread-1" } } }), + sendUserMessage: async () => ({}), + }; render( { fireEvent.click(screen.getByRole("button", { name: "Pause plan" })); }); + it("shows only the active task as running and demotes stale in-progress rows to pending", async () => { + const loadPhaseView = vi + .fn>() + .mockResolvedValue({ + phases: [ + { + id: "test-case-mapping", + title: "Test Case Mapping", + iconId: "taskfile", + order: 1, + }, + { + id: "implementation", + title: "Implementation", + iconId: "console", + order: 2, + }, + ], + taskPhaseStatusByTaskId: { + "task-1": { + "test-case-mapping": "completed", + implementation: "pending", + }, + "task-2": { + "test-case-mapping": "completed", + implementation: "in_progress", + }, + }, + }); + + const plansClient: ForgePlansClient = { + listPlans: async () => [ + { + id: "alpha", + title: "Alpha", + goal: "Alpha goal", + tasks: [ + { id: "task-1", name: "Task 1", status: "in_progress" }, + { id: "task-2", name: "Task 2", status: "pending" }, + ], + currentTaskId: null, + planPath: "plans/alpha/plan.json", + updatedAtMs: 0, + }, + ], + loadPhaseView, + getPlanPrompt: async () => "", + prepareExecution: async () => {}, + resetExecutionProgress: async () => {}, + getNextPhasePrompt: async () => ({ + planId: "alpha", + taskId: "task-2", + phaseId: "implementation", + isLastPhase: true, + promptText: "task 2 prompt", + }), + getPhaseStatus: async () => ({ status: "pending", commitSha: null }), + runPhaseChecks: async () => ({ ok: true, results: [] }), + interruptTurn: async () => ({}), + connectWorkspace: async () => {}, + startThread: async () => ({ result: { thread: { id: "thread-2" } } }), + sendUserMessage: async () => ({}), + }; + + render( + , + ); + + fireEvent.click(await screen.findByRole("button", { name: /Click to select/i })); + fireEvent.click(screen.getByRole("menuitemradio", { name: "Alpha (alpha)" })); + + await waitFor(() => + expect(loadPhaseView).toHaveBeenCalledWith("ws-1", "alpha", null), + ); + + fireEvent.click(screen.getByRole("button", { name: "Resume plan" })); + expect(await screen.findByText("Phase implementation in progress")).toBeTruthy(); + + const staleRow = screen.getByText("Task 1").closest("li") as HTMLLIElement | null; + const activeRow = screen.getByText("Task 2").closest("li") as HTMLLIElement | null; + expect(staleRow).toBeTruthy(); + expect(activeRow).toBeTruthy(); + expect(staleRow?.className.includes("pending")).toBe(true); + expect(staleRow?.className.includes("inProgress")).toBe(false); + expect(activeRow?.className.includes("inProgress")).toBe(true); + + expect(staleRow?.querySelector(".forge-running-icon.spinning")).toBeNull(); + expect(activeRow?.querySelector(".forge-running-icon.spinning")).toBeTruthy(); + expect(document.querySelectorAll(".forge-running-icon.spinning")).toHaveLength(1); + + fireEvent.click(screen.getByRole("button", { name: "Pause plan" })); + }); + + it("demotes stale in-progress rows to completed when all phases are completed", async () => { + const loadPhaseView = vi + .fn>() + .mockResolvedValue({ + phases: [ + { + id: "test-case-mapping", + title: "Test Case Mapping", + iconId: "taskfile", + order: 1, + }, + { + id: "implementation", + title: "Implementation", + iconId: "console", + order: 2, + }, + ], + taskPhaseStatusByTaskId: { + "task-1": { + "test-case-mapping": "completed", + implementation: "completed", + }, + "task-2": { + "test-case-mapping": "pending", + implementation: "in_progress", + }, + }, + }); + + const plansClient: ForgePlansClient = { + listPlans: async () => [ + { + id: "alpha", + title: "Alpha", + goal: "Alpha goal", + tasks: [ + { id: "task-1", name: "Task 1", status: "in_progress" }, + { id: "task-2", name: "Task 2", status: "pending" }, + ], + currentTaskId: null, + planPath: "plans/alpha/plan.json", + updatedAtMs: 0, + }, + ], + loadPhaseView, + getPlanPrompt: async () => "", + prepareExecution: async () => {}, + resetExecutionProgress: async () => {}, + getNextPhasePrompt: async () => ({ + planId: "alpha", + taskId: "task-2", + phaseId: "implementation", + isLastPhase: true, + promptText: "task 2 prompt", + }), + getPhaseStatus: async () => ({ status: "pending", commitSha: null }), + runPhaseChecks: async () => ({ ok: true, results: [] }), + interruptTurn: async () => ({}), + connectWorkspace: async () => {}, + startThread: async () => ({ result: { thread: { id: "thread-2" } } }), + sendUserMessage: async () => ({}), + }; + + render( + , + ); + + fireEvent.click(await screen.findByRole("button", { name: /Click to select/i })); + fireEvent.click(screen.getByRole("menuitemradio", { name: "Alpha (alpha)" })); + + await waitFor(() => + expect(loadPhaseView).toHaveBeenCalledWith("ws-1", "alpha", null), + ); + + fireEvent.click(screen.getByRole("button", { name: "Resume plan" })); + expect(await screen.findByText("Phase implementation in progress")).toBeTruthy(); + + const staleRow = screen.getByText("Task 1").closest("li") as HTMLLIElement | null; + const activeRow = screen.getByText("Task 2").closest("li") as HTMLLIElement | null; + expect(staleRow).toBeTruthy(); + expect(activeRow).toBeTruthy(); + expect(staleRow?.className.includes("completed")).toBe(true); + expect(staleRow?.className.includes("inProgress")).toBe(false); + expect(activeRow?.className.includes("inProgress")).toBe(true); + + expect(staleRow?.querySelector(".forge-running-icon.spinning")).toBeNull(); + expect(activeRow?.querySelector(".forge-running-icon.spinning")).toBeTruthy(); + expect(document.querySelectorAll(".forge-running-icon.spinning")).toHaveLength(1); + + fireEvent.click(screen.getByRole("button", { name: "Pause plan" })); + }); + + it("preserves single-row running invariants across deterministic phase-status matrices", async () => { + const phaseIds = ["test-case-mapping", "implementation", "ai-review"] as const; + const statuses = ["pending", "in_progress", "completed", "failed", "blocked"] as const; + let seed = 4242; + const next = () => { + seed = (seed * 1664525 + 1013904223) % 4294967296; + return seed; + }; + + for (let index = 0; index < 12; index += 1) { + const phaseStatuses = phaseIds.reduce>( + (acc, phaseId) => { + // Randomly omit some phase entries to cover missing-status fallback behavior. + if (next() % 4 === 0) { + return acc; + } + acc[phaseId] = statuses[next() % statuses.length]; + return acc; + }, + {}, + ); + const allCompleted = phaseIds.every( + (phaseId) => phaseStatuses[phaseId] === "completed", + ); + const expectedStaleStatus = allCompleted ? "completed" : "pending"; + + const loadPhaseView = vi + .fn>() + .mockResolvedValue({ + phases: [ + { + id: "test-case-mapping", + title: "Test Case Mapping", + iconId: "taskfile", + order: 1, + }, + { + id: "implementation", + title: "Implementation", + iconId: "console", + order: 2, + }, + { + id: "ai-review", + title: "AI Review", + iconId: "folder-review", + order: 3, + }, + ], + taskPhaseStatusByTaskId: { + "task-1": phaseStatuses, + "task-2": { + "test-case-mapping": "completed", + implementation: "in_progress", + "ai-review": "pending", + }, + }, + }); + + const plansClient: ForgePlansClient = { + listPlans: async () => [ + { + id: `alpha-${index}`, + title: "Alpha", + goal: "Alpha goal", + tasks: [ + { id: "task-1", name: "Task 1", status: "in_progress" }, + { id: "task-2", name: "Task 2", status: "pending" }, + ], + currentTaskId: null, + planPath: "plans/alpha/plan.json", + updatedAtMs: 0, + }, + ], + loadPhaseView, + getPlanPrompt: async () => "", + prepareExecution: async () => {}, + resetExecutionProgress: async () => {}, + getNextPhasePrompt: async () => ({ + planId: `alpha-${index}`, + taskId: "task-2", + phaseId: "implementation", + isLastPhase: true, + promptText: "task 2 prompt", + }), + getPhaseStatus: async () => ({ status: "pending", commitSha: null }), + runPhaseChecks: async () => ({ ok: true, results: [] }), + interruptTurn: async () => ({}), + connectWorkspace: async () => {}, + startThread: async () => ({ result: { thread: { id: "thread-2" } } }), + sendUserMessage: async () => ({}), + }; + + const rendered = render( + , + ); + + fireEvent.click(await screen.findByRole("button", { name: /Click to select/i })); + fireEvent.click(screen.getByRole("menuitemradio", { name: "Alpha (alpha-" + index + ")" })); + await waitFor(() => + expect(loadPhaseView).toHaveBeenCalledWith("ws-1", `alpha-${index}`, null), + ); + + fireEvent.click(screen.getByRole("button", { name: "Resume plan" })); + expect(await screen.findByText("Phase implementation in progress")).toBeTruthy(); + + const staleRow = screen.getByText("Task 1").closest("li") as HTMLLIElement | null; + const activeRow = screen.getByText("Task 2").closest("li") as HTMLLIElement | null; + expect(staleRow).toBeTruthy(); + expect(activeRow).toBeTruthy(); + expect(staleRow?.className.includes(expectedStaleStatus)).toBe(true); + expect(staleRow?.className.includes("inProgress")).toBe(false); + expect(activeRow?.className.includes("inProgress")).toBe(true); + expect(staleRow?.querySelector(".forge-running-icon.spinning")).toBeNull(); + expect(activeRow?.querySelector(".forge-running-icon.spinning")).toBeTruthy(); + expect(document.querySelectorAll(".forge-running-icon.spinning")).toHaveLength(1); + + fireEvent.click(screen.getByRole("button", { name: "Pause plan" })); + rendered.unmount(); + } + }); + + it("does not render a running spinner when runningInfo points to a non-visible task", async () => { + const loadPhaseView = vi + .fn>() + .mockResolvedValue({ + phases: [ + { + id: "test-case-mapping", + title: "Test Case Mapping", + iconId: "taskfile", + order: 1, + }, + { + id: "implementation", + title: "Implementation", + iconId: "console", + order: 2, + }, + ], + taskPhaseStatusByTaskId: { + "task-1": { + "test-case-mapping": "completed", + implementation: "pending", + }, + "task-2": { + "test-case-mapping": "pending", + implementation: "pending", + }, + }, + }); + + const plansClient: ForgePlansClient = { + listPlans: async () => [ + { + id: "alpha", + title: "Alpha", + goal: "Alpha goal", + tasks: [ + { id: "task-1", name: "Task 1", status: "in_progress" }, + { id: "task-2", name: "Task 2", status: "pending" }, + ], + currentTaskId: null, + planPath: "plans/alpha/plan.json", + updatedAtMs: 0, + }, + ], + loadPhaseView, + getPlanPrompt: async () => "", + prepareExecution: async () => {}, + resetExecutionProgress: async () => {}, + getNextPhasePrompt: async () => ({ + planId: "alpha", + taskId: "task-3", + phaseId: "implementation", + isLastPhase: true, + promptText: "task 3 prompt", + }), + getPhaseStatus: async () => ({ status: "pending", commitSha: null }), + runPhaseChecks: async () => ({ ok: true, results: [] }), + interruptTurn: async () => ({}), + connectWorkspace: async () => {}, + startThread: async () => ({ result: { thread: { id: "thread-3" } } }), + sendUserMessage: async () => ({}), + }; + + render( + , + ); + + fireEvent.click(await screen.findByRole("button", { name: /Click to select/i })); + fireEvent.click(screen.getByRole("menuitemradio", { name: "Alpha (alpha)" })); + + await waitFor(() => + expect(loadPhaseView).toHaveBeenCalledWith("ws-1", "alpha", null), + ); + + fireEvent.click(screen.getByRole("button", { name: "Resume plan" })); + await waitFor(() => + expect(screen.getByRole("alert").textContent).toContain("unexpected task id"), + ); + expect(screen.getByRole("button", { name: "Resume plan" })).toBeTruthy(); + + const staleRow = screen.getByText("Task 1").closest("li") as HTMLLIElement | null; + const secondRow = screen.getByText("Task 2").closest("li") as HTMLLIElement | null; + expect(staleRow).toBeTruthy(); + expect(secondRow).toBeTruthy(); + expect(staleRow?.className.includes("pending")).toBe(true); + expect(staleRow?.className.includes("inProgress")).toBe(false); + expect(secondRow?.className.includes("inProgress")).toBe(false); + expect(document.querySelectorAll(".forge-running-icon.spinning")).toHaveLength(0); + }); + + it("renders ordered phase chips with status classes and keeps failed ai-review blocking", async () => { + const loadPhaseView = vi + .fn>() + .mockResolvedValue({ + phases: [ + { + id: "test-case-mapping", + title: "Test Case Mapping", + iconId: "taskfile", + order: 1, + }, + { + id: "behavioral-tests", + title: "Behavioral Tests", + iconId: "cucumber", + order: 2, + }, + { + id: "implementation", + title: "Implementation", + iconId: "console", + order: 3, + }, + { + id: "coverage-hardening", + title: "Coverage Hardening", + iconId: "codecov", + order: 4, + }, + { + id: "documentation", + title: "Documentation", + iconId: "readme", + order: 5, + }, + { + id: "ai-review", + title: "AI Review Gate", + iconId: "totally-invalid-icon", + order: 6, + }, + ], + taskPhaseStatusByTaskId: { + "task-1": { + "test-case-mapping": "completed", + "behavioral-tests": "in_progress", + implementation: "pending", + "coverage-hardening": "failed", + documentation: "pending", + "ai-review": "failed", + }, + }, + }); + + const plansClient: ForgePlansClient = { + listPlans: async () => [ + { + id: "alpha", + title: "Alpha", + goal: "Alpha goal", + tasks: [{ id: "task-1", name: "Task 1", status: "pending" }], + currentTaskId: null, + planPath: "plans/alpha/plan.json", + updatedAtMs: 0, + }, + ], + loadPhaseView, + getPlanPrompt: async () => "", + prepareExecution: async () => {}, + resetExecutionProgress: async () => {}, + getNextPhasePrompt: async () => null, + getPhaseStatus: async () => ({ status: "pending", commitSha: null }), + runPhaseChecks: async () => ({ ok: true, results: [] }), + interruptTurn: async () => ({}), + connectWorkspace: async () => {}, + startThread: async () => ({ result: { thread: { id: "thread-1" } } }), + sendUserMessage: async () => ({}), + }; + + render( + , + ); + + fireEvent.click(await screen.findByRole("button", { name: /Click to select/i })); + fireEvent.click(screen.getByRole("menuitemradio", { name: "Alpha (alpha)" })); + + await waitFor(() => + expect(loadPhaseView).toHaveBeenCalledWith("ws-1", "alpha", null), + ); + + const taskRow = screen.getByText("Task 1").closest("li"); + expect(taskRow).toBeTruthy(); + const rowElement = taskRow as HTMLLIElement; + const chips = Array.from(rowElement.querySelectorAll(".forge-phase")); + expect(chips.map((chip) => chip.getAttribute("data-phase-id"))).toEqual([ + "test-case-mapping", + "behavioral-tests", + "implementation", + "coverage-hardening", + "documentation", + "ai-review", + ]); + expect( + chips.map((chip) => + chip.querySelector(".forge-phase-name")?.textContent?.trim(), + ), + ).toEqual([ + "Test Case Mapping", + "Behavioral Tests", + "Implementation", + "Coverage Hardening", + "Documentation", + "AI Review Gate", + ]); + + const chipFor = (phaseId: string) => + rowElement.querySelector(`.forge-phase[data-phase-id="${phaseId}"]`) as HTMLElement | null; + + expect(chipFor("test-case-mapping")?.className.includes("is-complete")).toBe(true); + expect(chipFor("behavioral-tests")?.className.includes("is-current")).toBe(true); + expect(chipFor("implementation")?.className.includes("is-pending")).toBe(true); + expect(chipFor("coverage-hardening")?.className.includes("is-pending")).toBe(true); + expect(chipFor("documentation")?.className.includes("is-pending")).toBe(true); + expect(chipFor("ai-review")?.className.includes("is-pending")).toBe(true); + expect(chipFor("ai-review")?.className.includes("is-complete")).toBe(false); + + const behavioralIcon = chipFor("behavioral-tests") + ?.querySelector(".forge-phase-icon img") + ?.getAttribute("src"); + const aiReviewIcon = chipFor("ai-review") + ?.querySelector(".forge-phase-icon img") + ?.getAttribute("src"); + + expect(behavioralIcon).toBe("/assets/material-icons/cucumber.svg"); + expect(aiReviewIcon).toBe("/assets/material-icons/file.svg"); + }); + + it("keeps terminal ai-review failures visibly non-completed even after prior phases complete", async () => { + const loadPhaseView = vi + .fn>() + .mockResolvedValue({ + phases: [ + { id: "test-case-mapping", title: "Test Case Mapping", iconId: "taskfile", order: 1 }, + { id: "behavioral-tests", title: "Behavioral Tests", iconId: "cucumber", order: 2 }, + { id: "implementation", title: "Implementation", iconId: "console", order: 3 }, + { + id: "coverage-hardening", + title: "Coverage Hardening", + iconId: "codecov", + order: 4, + }, + { id: "documentation", title: "Documentation", iconId: "readme", order: 5 }, + { id: "ai-review", title: "AI Review Gate", iconId: "folder-review", order: 6 }, + ], + taskPhaseStatusByTaskId: { + "task-1": { + "test-case-mapping": "completed", + "behavioral-tests": "completed", + implementation: "completed", + "coverage-hardening": "completed", + documentation: "completed", + "ai-review": "failed", + }, + }, + }); + + const plansClient: ForgePlansClient = { + listPlans: async () => [ + { + id: "alpha", + title: "Alpha", + goal: "Alpha goal", + tasks: [{ id: "task-1", name: "Task 1", status: "completed" }], + currentTaskId: null, + planPath: "plans/alpha/plan.json", + updatedAtMs: 0, + }, + ], + loadPhaseView, + getPlanPrompt: async () => "", + prepareExecution: async () => {}, + resetExecutionProgress: async () => {}, + getNextPhasePrompt: async () => null, + getPhaseStatus: async () => ({ status: "pending", commitSha: null }), + runPhaseChecks: async () => ({ ok: true, results: [] }), + interruptTurn: async () => ({}), + connectWorkspace: async () => {}, + startThread: async () => ({ result: { thread: { id: "thread-1" } } }), + sendUserMessage: async () => ({}), + }; + + render( + , + ); + + fireEvent.click(await screen.findByRole("button", { name: /Click to select/i })); + fireEvent.click(screen.getByRole("menuitemradio", { name: "Alpha (alpha)" })); + + await waitFor(() => + expect(loadPhaseView).toHaveBeenCalledWith("ws-1", "alpha", null), + ); + + const taskRow = screen.getByText("Task 1").closest("li"); + expect(taskRow).toBeTruthy(); + const rowElement = taskRow as HTMLLIElement; + const aiReviewChip = rowElement.querySelector( + '.forge-phase[data-phase-id="ai-review"]', + ) as HTMLElement | null; + const docsChip = rowElement.querySelector( + '.forge-phase[data-phase-id="documentation"]', + ) as HTMLElement | null; + + expect(docsChip?.className.includes("is-complete")).toBe(true); + expect(aiReviewChip?.className.includes("is-pending")).toBe(true); + expect(aiReviewChip?.className.includes("is-complete")).toBe(false); + }); + it("keeps a pending phase on a single prompt without auto-resending", async () => { vi.useFakeTimers(); diff --git a/src/features/forge/components/Forge.templates.test.tsx b/src/features/forge/components/Forge.templates.test.tsx index a2f940d819..82a7b7aa87 100644 --- a/src/features/forge/components/Forge.templates.test.tsx +++ b/src/features/forge/components/Forge.templates.test.tsx @@ -8,49 +8,22 @@ afterEach(() => { }); describe("Forge templates", () => { - it("renders bundled templates and disables install when no workspace is active", async () => { - const calls = { - getInstalled: [] as string[], - install: [] as Array<{ workspaceId: string; templateId: string }>, - uninstall: [] as string[], - }; - + it("disables template controls when no workspace is active", () => { const templatesClient: ForgeTemplatesClient = { - listBundledTemplates: async () => [ - { id: "ralph-loop", title: "Ralph Loop", version: "0.2.1" }, - ], - getInstalledTemplate: async (workspaceId) => { - calls.getInstalled.push(workspaceId); - return null; - }, - installTemplate: async (workspaceId, templateId) => { - calls.install.push({ workspaceId, templateId }); - return { - schema: "forge-template-lock-v1", - installedTemplateId: templateId, - installedTemplateVersion: "0.2.1", - installedAtIso: "2026-02-10T00:00:00Z", - installedFiles: ["template.json"], - }; + listBundledTemplates: async () => [], + getInstalledTemplate: async () => null, + installTemplate: async () => { + throw new Error("unexpected install"); }, - uninstallTemplate: async (workspaceId) => { - calls.uninstall.push(workspaceId); + uninstallTemplate: async () => { + throw new Error("unexpected uninstall"); }, }; render(); - fireEvent.click(screen.getByRole("button", { name: "Open templates" })); - expect(screen.getByText("Templates")).toBeTruthy(); - - // Wait for the bundled templates to load. - await screen.findByText("Ralph Loop"); - - expect(screen.getByText("No template installed.")).toBeTruthy(); - - const installButton = screen.getByRole("button", { name: "Install" }); - expect(installButton.hasAttribute("disabled")).toBe(true); - expect(calls.getInstalled).toEqual([]); + const templateButton = screen.getByRole("button", { name: "Open templates" }); + expect(templateButton.hasAttribute("disabled")).toBe(true); }); it("shows an installed template even when it is missing from the bundled list", async () => { diff --git a/src/features/forge/components/Forge.tsx b/src/features/forge/components/Forge.tsx index 26a02e3113..6a07d95035 100644 --- a/src/features/forge/components/Forge.tsx +++ b/src/features/forge/components/Forge.tsx @@ -17,10 +17,13 @@ import { useDismissibleMenu } from "../../app/hooks/useDismissibleMenu"; import { forgeGetInstalledTemplate, forgeInstallTemplate, + forgeLoadPhaseView, forgeListBundledTemplates, forgeUninstallTemplate, type ForgeBundledTemplateInfo, type ForgeNextPhasePrompt, + type ForgePhaseViewStatus, + type ForgePhaseView, type ForgePhaseStatus, type ForgeRunPhaseChecksResponse, type ForgeTemplateLock, @@ -40,6 +43,7 @@ import { startThread, } from "../../../services/tauri"; import { useForgeExecution } from "../hooks/useForgeExecution"; +import { getForgePhaseIconUrl } from "../../../utils/forgePhaseIcons"; export type ForgeTemplatesClient = { listBundledTemplates: () => Promise; @@ -50,6 +54,11 @@ export type ForgeTemplatesClient = { export type ForgePlansClient = { listPlans: (workspaceId: string) => Promise; + loadPhaseView?: ( + workspaceId: string, + planId: string, + installedTemplateId: string | null, + ) => Promise; getPlanPrompt: (workspaceId: string) => Promise; prepareExecution: (workspaceId: string, planId: string) => Promise; resetExecutionProgress: (workspaceId: string, planId: string) => Promise; @@ -95,6 +104,7 @@ const defaultForgeTemplatesClient: ForgeTemplatesClient = { const defaultForgePlansClient: ForgePlansClient = { listPlans: forgeListPlans, + loadPhaseView: forgeLoadPhaseView, getPlanPrompt: forgeGetPlanPrompt, prepareExecution: forgePrepareExecution, resetExecutionProgress: forgeResetExecutionProgress, @@ -121,6 +131,14 @@ type ForgePlan = { items: ForgePlanItem[]; }; +const EMPTY_PHASE_VIEW: ForgePhaseView = { + phases: [], + taskPhaseStatusByTaskId: {}, +}; + +type ForgePhaseChipState = "is-complete" | "is-current" | "is-pending"; +type ForgeRunningInfo = { taskId: string; phaseId: string } | null; + function formatPlanLabel(plan: ForgeWorkspacePlan): string { const title = plan.title?.trim() ?? ""; const goal = plan.goal?.trim() ?? ""; @@ -150,6 +168,89 @@ function mapForgeItemStatus(status: string): ForgeItemStatus { return "pending"; } +function mapForgePhaseChipState( + taskId: string, + phaseId: string, + phaseStatus: ForgePhaseViewStatus, + runningInfo: ForgeRunningInfo, +): ForgePhaseChipState { + const normalizedStatus = phaseStatus.trim().toLowerCase(); + if (runningInfo?.taskId === taskId && runningInfo.phaseId === phaseId) { + return "is-current"; + } + if (normalizedStatus === "in_progress") { + return "is-current"; + } + if (normalizedStatus === "completed") { + return "is-complete"; + } + return "is-pending"; +} + +function allTaskPhasesCompleted( + taskPhaseStatuses: Record, + phases: ForgePhaseView["phases"], +): boolean { + if (phases.length > 0) { + return phases.every( + (phase) => + (taskPhaseStatuses[phase.id] ?? "pending").trim().toLowerCase() === + "completed", + ); + } + const phaseStatuses = Object.values(taskPhaseStatuses); + return ( + phaseStatuses.length > 0 && + phaseStatuses.every((status) => status.trim().toLowerCase() === "completed") + ); +} + +function deriveStatusFromTaskPhases( + taskPhaseStatuses: Record, + phases: ForgePhaseView["phases"], +): ForgeItemStatus | null { + const phaseStatuses = Object.values(taskPhaseStatuses); + if (phaseStatuses.length === 0) { + return null; + } + if (allTaskPhasesCompleted(taskPhaseStatuses, phases)) { + return "completed"; + } + if (phaseStatuses.some((status) => status.trim().toLowerCase() === "in_progress")) { + return "inProgress"; + } + return "pending"; +} + +function resolveForgeTaskRowStatus({ + taskId, + planTaskStatus, + taskPhaseStatuses, + phases, + runningInfo, +}: { + taskId: string; + planTaskStatus: ForgeItemStatus; + taskPhaseStatuses: Record; + phases: ForgePhaseView["phases"]; + runningInfo: ForgeRunningInfo; +}): ForgeItemStatus { + if (runningInfo?.taskId === taskId) { + return "inProgress"; + } + + const phaseDerivedStatus = deriveStatusFromTaskPhases(taskPhaseStatuses, phases); + const resolvedStatus = phaseDerivedStatus ?? planTaskStatus; + + if (runningInfo && resolvedStatus === "inProgress") { + return allTaskPhasesCompleted(taskPhaseStatuses, phases) + ? "completed" + : "pending"; + } + + return resolvedStatus; +} + function ForgeTemplatesModal({ templates, installedTemplate, @@ -304,6 +405,7 @@ export function Forge({ const installTemplate = client.installTemplate; const uninstallTemplate = client.uninstallTemplate; const listPlans = plansApi.listPlans; + const loadPhaseView = plansApi.loadPhaseView ?? forgeLoadPhaseView; const getPlanPrompt = plansApi.getPlanPrompt; const prepareExecution = plansApi.prepareExecution; const resetExecutionProgress = plansApi.resetExecutionProgress; @@ -314,6 +416,7 @@ export function Forge({ const connectWorkspaceToRun = plansApi.connectWorkspace; const startThreadForPlan = plansApi.startThread; const sendUserMessageToPlanThread = plansApi.sendUserMessage; + const hasActiveWorkspace = Boolean(activeWorkspaceId?.trim()); const findCollaborationMode = useCallback( (wanted: string) => { @@ -502,6 +605,7 @@ export function Forge({ const selectedPlan = selectedPlanId ? plans.find((p) => p.id === selectedPlanId) ?? null : null; + const [phaseView, setPhaseView] = useState(EMPTY_PHASE_VIEW); const [planMenuOpen, setPlanMenuOpen] = useState(false); const planMenuRef = useRef(null); @@ -514,6 +618,13 @@ export function Forge({ onClose: () => setPlanMenuOpen(false), }); + useEffect(() => { + if (hasActiveWorkspace) { + return; + } + setPlanMenuOpen(false); + }, [hasActiveWorkspace]); + useEffect(() => { if (!selectedPlanId) { return; @@ -523,6 +634,49 @@ export function Forge({ } }, [plans, selectedPlanId]); + useEffect(() => { + let cancelled = false; + if (!activeWorkspaceId || !selectedPlanId) { + setPhaseView(EMPTY_PHASE_VIEW); + return () => { + cancelled = true; + }; + } + + const installedTemplateId = installedTemplate?.installedTemplateId ?? null; + const refreshPhaseView = async () => { + try { + const next = await loadPhaseView( + activeWorkspaceId, + selectedPlanId, + installedTemplateId, + ); + if (cancelled) { + return; + } + setPhaseView(next); + } catch (error) { + if (cancelled) { + return; + } + console.warn("Failed to load Forge phase view.", { error }); + setPhaseView(EMPTY_PHASE_VIEW); + } + }; + + void refreshPhaseView(); + const interval = window.setInterval(refreshPhaseView, 2000); + return () => { + cancelled = true; + window.clearInterval(interval); + }; + }, [ + activeWorkspaceId, + installedTemplate?.installedTemplateId, + loadPhaseView, + selectedPlanId, + ]); + const executionMode = useMemo(() => { const mode = findCollaborationMode("default") ?? @@ -531,6 +685,10 @@ export function Forge({ null; return buildCollaborationModePayloadFor(mode); }, [buildCollaborationModePayloadFor, collaborationModes, findCollaborationMode]); + const selectedPlanTaskIds = useMemo( + () => selectedPlan?.items.map((item) => item.id) ?? null, + [selectedPlan], + ); const { isExecuting, @@ -540,6 +698,7 @@ export function Forge({ pauseExecution, } = useForgeExecution({ workspaceId: activeWorkspaceId, + knownTaskIds: selectedPlanTaskIds, connectWorkspace: connectWorkspaceToRun, prepareExecution, getNextPhasePrompt, @@ -674,6 +833,7 @@ export function Forge({ title="Templates" data-tauri-drag-region="false" onClick={() => setTemplatesOpen(true)} + disabled={!hasActiveWorkspace} > @@ -681,9 +841,15 @@ export function Forge({ type="button" className={`ghost forge-plan-toggle${planMenuOpen ? " is-open" : ""}`} data-tauri-drag-region="false" - onClick={() => setPlanMenuOpen((open) => !open)} + onClick={() => { + if (!hasActiveWorkspace) { + return; + } + setPlanMenuOpen((open) => !open); + }} aria-haspopup="menu" aria-expanded={planMenuOpen} + disabled={!hasActiveWorkspace} > {selectedPlan ? selectedPlan.name : "Click to select"} @@ -746,18 +912,20 @@ export function Forge({ type="button" className="ghost forge-execute-toggle" data-tauri-drag-region="false" - disabled={!selectedPlan} + disabled={!hasActiveWorkspace || !selectedPlan} aria-label={isExecuting ? "Pause plan" : "Resume plan"} aria-pressed={isExecuting} title={ - !selectedPlan + !hasActiveWorkspace + ? "Select a project to use Forge" + : !selectedPlan ? "Select a plan to run" : isExecuting ? "Pause plan" : "Resume plan" } onClick={() => { - if (!selectedPlan) { + if (!hasActiveWorkspace || !selectedPlan) { return; } if (isExecuting) { @@ -773,10 +941,12 @@ export function Forge({ type="button" className="ghost forge-reset-progress" data-tauri-drag-region="false" - disabled={!selectedPlan || isResettingProgress} + disabled={!hasActiveWorkspace || !selectedPlan || isResettingProgress} aria-label="Clean progress" title={ - !selectedPlan + !hasActiveWorkspace + ? "Select a project to use Forge" + : !selectedPlan ? "Select a plan to clean progress" : "Reset state and derived execution files" } @@ -794,13 +964,22 @@ export function Forge({ {lastError} ) : null} - {selectedPlan ? ( + {!hasActiveWorkspace ? ( +
+ Select a project first to use Forge. +
+ ) : selectedPlan ? (
    {selectedPlan.items.map((item, index) => { const isActiveTask = runningInfo?.taskId === item.id; - const effectiveStatus: ForgeItemStatus = isActiveTask - ? "inProgress" - : item.status; + const taskPhaseStatuses = phaseView.taskPhaseStatusByTaskId[item.id] ?? {}; + const effectiveStatus = resolveForgeTaskRowStatus({ + taskId: item.id, + planTaskStatus: item.status, + taskPhaseStatuses, + phases: phaseView.phases, + runningInfo, + }); const isCompleted = effectiveStatus === "completed"; const isRunning = effectiveStatus === "inProgress"; const isActivePhase = isActiveTask && runningInfo?.phaseId; @@ -824,6 +1003,31 @@ export function Forge({ ) : null} + {phaseView.phases.length > 0 ? ( +
    + {phaseView.phases.map((phase) => { + const chipState = mapForgePhaseChipState( + item.id, + phase.id, + taskPhaseStatuses[phase.id] ?? "pending", + runningInfo, + ); + const iconUrl = getForgePhaseIconUrl(phase.iconId); + return ( + + + + + {phase.title} + + ); + })} +
    + ) : null} ); })} diff --git a/src/features/forge/hooks/useForgeExecution.ts b/src/features/forge/hooks/useForgeExecution.ts index 0f6e0c9930..b3a0d5f425 100644 --- a/src/features/forge/hooks/useForgeExecution.ts +++ b/src/features/forge/hooks/useForgeExecution.ts @@ -15,6 +15,7 @@ type ForgeExecutionStatusLike = type ForgeExecutionArgs = { workspaceId: string | null; + knownTaskIds?: string[] | null; connectWorkspace: (workspaceId: string) => Promise; prepareExecution: (workspaceId: string, planId: string) => Promise; getNextPhasePrompt: ( @@ -151,6 +152,7 @@ function wait(ms: number): Promise { export function useForgeExecution({ workspaceId, + knownTaskIds = null, connectWorkspace, prepareExecution, getNextPhasePrompt, @@ -172,6 +174,19 @@ export function useForgeExecution({ threadId: string; turnId: string; } | null>(null); + const normalizedKnownTaskIds = useMemo(() => { + if (!knownTaskIds) { + return null; + } + const next = new Set(); + for (const taskId of knownTaskIds) { + const trimmed = taskId.trim(); + if (trimmed) { + next.add(trimmed); + } + } + return next; + }, [knownTaskIds]); const clearExecutionState = useCallback(() => { setIsExecuting(false); @@ -261,6 +276,9 @@ export function useForgeExecution({ let activeThreadId: string | null = null; let activeThreadTaskId: string | null = null; const threadByTaskId = new Map(); + const pendingKnownTaskIds = normalizedKnownTaskIds + ? new Set(normalizedKnownTaskIds) + : null; while (isActive()) { const phase = await getNextPhasePrompt(workspace, normalizedPlanId); @@ -279,7 +297,25 @@ export function useForgeExecution({ throw new Error("Forge next phase prompt returned an empty phase id."); } + if (normalizedKnownTaskIds && !normalizedKnownTaskIds.has(taskId)) { + throw new Error( + `Forge execution received unexpected task id "${taskId}" not present in the selected plan.`, + ); + } + const mappedThreadId = threadByTaskId.get(taskId) ?? null; + if ( + !mappedThreadId && + normalizedKnownTaskIds && + normalizedKnownTaskIds.size > 0 && + pendingKnownTaskIds && + pendingKnownTaskIds.size === 0 + ) { + throw new Error( + `Forge execution received unexpected task id "${taskId}" after exhausting visible plan tasks.`, + ); + } + pendingKnownTaskIds?.delete(taskId); if (!mappedThreadId) { const previousTaskId = activeThreadTaskId; const previousThreadId = activeThreadId; @@ -402,6 +438,7 @@ export function useForgeExecution({ runPhaseChecks, sendUserMessage, startThread, + normalizedKnownTaskIds, workspaceId, ], ); diff --git a/src/features/forge/scripts/testFirstLoopScripts.test.ts b/src/features/forge/scripts/testFirstLoopScripts.test.ts new file mode 100644 index 0000000000..370c88019b --- /dev/null +++ b/src/features/forge/scripts/testFirstLoopScripts.test.ts @@ -0,0 +1,174 @@ +import { execFile } from "node:child_process"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { promisify } from "node:util"; +import { describe, expect, it } from "vitest"; + +const execFileAsync = promisify(execFile); + +const TEMPLATE_ROOT = path.resolve( + "src-tauri/resources/forge/templates/test-first-loop", +); +const PHASE_IDS = [ + "test-case-mapping", + "behavioral-tests", + "implementation", + "coverage-hardening", + "documentation", + "ai-review", +]; + +type ScriptContext = { + workspaceRoot: string; + templateRoot: string; + planId: string; + planDir: string; + planPath: string; + statePath: string; + progressPath: string; + generatedPlanMdPath: string; + generatedExecutePromptPath: string; + todayIso: string; +}; + +async function createScriptContext(planId: string): Promise<{ + contextPath: string; + context: ScriptContext; +}> { + const tempRoot = await fs.mkdtemp( + path.join(os.tmpdir(), "codex-monitor-test-first-loop-"), + ); + const planDir = path.join(tempRoot, "plans", planId); + const planPath = path.join(planDir, "plan.json"); + const statePath = path.join(planDir, "state.json"); + const progressPath = path.join(planDir, "progress.md"); + const generatedPlanMdPath = path.join(planDir, "plan.md"); + const generatedExecutePromptPath = path.join(planDir, "execute-prompt.md"); + const contextPath = path.join(tempRoot, "context.json"); + + await fs.mkdir(planDir, { recursive: true }); + await fs.writeFile( + planPath, + `${JSON.stringify( + { + $schema: "plan-v1", + id: planId, + title: "Test-First Loop Script Test", + goal: "Validate test-first-loop script behavior for multi-phase execution.", + context: { + tech_stack: ["Node.js", "Vitest"], + constraints: ["Keep script tests deterministic."], + }, + tasks: [ + { + id: "task-1", + name: "Template hook regression", + description: + "Ensure phase initialization and prompt routing follow test-first-loop metadata.", + depends_on: [], + files: ["src/features/forge/scripts/testFirstLoopScripts.test.ts"], + verification: [ + "State initializes all template phases.", + "Prompt selects first non-completed phase metadata.", + ], + }, + ], + }, + null, + 2, + )}\n`, + "utf8", + ); + + const context: ScriptContext = { + workspaceRoot: tempRoot, + templateRoot: TEMPLATE_ROOT, + planId, + planDir, + planPath, + statePath, + progressPath, + generatedPlanMdPath, + generatedExecutePromptPath, + todayIso: "2026-02-12", + }; + + await fs.writeFile(contextPath, `${JSON.stringify(context, null, 2)}\n`, "utf8"); + return { contextPath, context }; +} + +async function runScript(scriptName: "post-plan.mjs" | "post-step.mjs", contextPath: string) { + const scriptPath = path.join(TEMPLATE_ROOT, "scripts", scriptName); + await execFileAsync(process.execPath, [scriptPath, "--context", contextPath]); +} + +describe("test-first-loop template scripts", () => { + it("post-plan initializes all six task phases as pending in template order", async () => { + const { contextPath, context } = await createScriptContext("phase-init"); + await runScript("post-plan.mjs", contextPath); + + const state = JSON.parse(await fs.readFile(context.statePath, "utf8")); + expect(state.tasks).toHaveLength(1); + expect(state.tasks[0].phases).toHaveLength(6); + expect(state.tasks[0].phases.map((phase: { id: string }) => phase.id)).toEqual( + PHASE_IDS, + ); + expect( + state.tasks[0].phases.map( + (phase: { status: string; attempts: number; notes: string }) => ({ + status: phase.status, + attempts: phase.attempts, + notes: phase.notes, + }), + ), + ).toEqual( + PHASE_IDS.map(() => ({ + status: "pending", + attempts: 0, + notes: "", + })), + ); + }); + + it("post-step advances prompt to the first non-completed phase, including ai-review", async () => { + const { contextPath, context } = await createScriptContext("phase-advance"); + await runScript("post-plan.mjs", contextPath); + + const firstPrompt = await fs.readFile(context.generatedExecutePromptPath, "utf8"); + expect(firstPrompt).toContain("Current phase: test-case-mapping - Test Case Mapping"); + + const state = JSON.parse(await fs.readFile(context.statePath, "utf8")); + state.tasks[0].status = "in_progress"; + state.tasks[0].phases[0].status = "completed"; + state.tasks[0].phases[0].attempts = 1; + state.tasks[0].phases[0].notes = "Mapped coverage criteria."; + await fs.writeFile(context.statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8"); + + await runScript("post-step.mjs", contextPath); + const behavioralPrompt = await fs.readFile( + context.generatedExecutePromptPath, + "utf8", + ); + expect(behavioralPrompt).toContain( + "Current phase: behavioral-tests - Behavioral Tests", + ); + + for (let i = 1; i < 5; i += 1) { + state.tasks[0].phases[i].status = "completed"; + state.tasks[0].phases[i].attempts = 1; + state.tasks[0].phases[i].notes = `Completed ${state.tasks[0].phases[i].id}.`; + } + await fs.writeFile(context.statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8"); + + await runScript("post-step.mjs", contextPath); + const aiReviewPrompt = await fs.readFile( + context.generatedExecutePromptPath, + "utf8", + ); + expect(aiReviewPrompt).toContain("Current phase: ai-review - AI Review Gate"); + expect(aiReviewPrompt).toContain( + "Run an AI quality review pass and enforce a strict zero-findings completion gate.", + ); + }); +}); diff --git a/src/services/tauri.test.ts b/src/services/tauri.test.ts index b980e24a00..222b309e7b 100644 --- a/src/services/tauri.test.ts +++ b/src/services/tauri.test.ts @@ -47,6 +47,7 @@ import { forgeInstallTemplate, forgeListBundledTemplates, forgeListPlans, + forgeLoadPhaseView, forgeUninstallTemplate, } from "./tauri"; @@ -215,6 +216,298 @@ describe("tauri invoke wrappers", () => { expect(invokeMock).toHaveBeenCalledWith("forge_get_plan_prompt", { workspaceId: "ws-1" }); }); + it("loads forge phase view from state and template phases files", async () => { + const invokeMock = vi.mocked(invoke); + invokeMock.mockImplementation(async (command: string, args?: unknown) => { + if (command === "is_macos_debug_build") { + return false; + } + if (command !== "read_workspace_file") { + return undefined; + } + + const path = + typeof args === "object" && args !== null && "path" in args + ? (args as { path?: unknown }).path + : undefined; + if (path === "plans/alpha/state.json") { + return { + content: JSON.stringify({ + tasks: [ + { + id: "task-1", + phases: [ + { id: "implementation", status: " COMPLETED " }, + { id: "verification", status: "unknown-value" }, + ], + }, + ], + }), + truncated: false, + }; + } + if (path === ".agent/templates/test-first-loop/phases.json") { + return { + content: JSON.stringify({ + schema: "forge-phases-v1", + phases: [ + { id: "verification", title: "Verification", iconId: "fact_check", order: 2 }, + { id: "implementation", title: "Implementation", iconId: "code_blocks", order: 1 }, + ], + }), + truncated: false, + }; + } + + throw new Error(`unexpected path: ${String(path)}`); + }); + + await expect(forgeLoadPhaseView("ws-1", "alpha", "test-first-loop")).resolves.toEqual({ + phases: [ + { id: "implementation", title: "Implementation", iconId: "code_blocks", order: 1 }, + { id: "verification", title: "Verification", iconId: "fact_check", order: 2 }, + ], + taskPhaseStatusByTaskId: { + "task-1": { + implementation: "completed", + verification: "pending", + }, + }, + }); + + expect(invokeMock).toHaveBeenCalledWith("read_workspace_file", { + workspaceId: "ws-1", + path: "plans/alpha/state.json", + }); + expect(invokeMock).toHaveBeenCalledWith("read_workspace_file", { + workspaceId: "ws-1", + path: ".agent/templates/test-first-loop/phases.json", + }); + }); + + it("preserves blocked and failed phase statuses for forge phase view", async () => { + const invokeMock = vi.mocked(invoke); + invokeMock.mockImplementation(async (command: string, args?: unknown) => { + if (command === "is_macos_debug_build") { + return false; + } + if (command !== "read_workspace_file") { + return undefined; + } + + const path = + typeof args === "object" && args !== null && "path" in args + ? (args as { path?: unknown }).path + : undefined; + if (path === "plans/alpha/state.json") { + return { + content: JSON.stringify({ + tasks: [ + { + id: "task-1", + phases: [ + { id: "implementation", status: "blocked" }, + { id: "ai-review", status: "failed" }, + ], + }, + ], + }), + truncated: false, + }; + } + if (path === ".agent/templates/test-first-loop/phases.json") { + return { + content: JSON.stringify({ + schema: "forge-phases-v1", + phases: [ + { id: "implementation", title: "Implementation", iconId: "console", order: 1 }, + { id: "ai-review", title: "AI Review", iconId: "folder-review", order: 2 }, + ], + }), + truncated: false, + }; + } + + throw new Error(`unexpected path: ${String(path)}`); + }); + + await expect(forgeLoadPhaseView("ws-1", "alpha", "test-first-loop")).resolves.toEqual({ + phases: [ + { id: "implementation", title: "Implementation", iconId: "console", order: 1 }, + { id: "ai-review", title: "AI Review", iconId: "folder-review", order: 2 }, + ], + taskPhaseStatusByTaskId: { + "task-1": { + implementation: "blocked", + "ai-review": "failed", + }, + }, + }); + }); + + it("normalizes forge phase view statuses with case and whitespace", async () => { + const invokeMock = vi.mocked(invoke); + invokeMock.mockImplementation(async (command: string, args?: unknown) => { + if (command === "is_macos_debug_build") { + return false; + } + if (command !== "read_workspace_file") { + return undefined; + } + + const path = + typeof args === "object" && args !== null && "path" in args + ? (args as { path?: unknown }).path + : undefined; + if (path === "plans/alpha/state.json") { + return { + content: JSON.stringify({ + tasks: [ + { + id: "task-1", + phases: [ + { id: "implementation", status: " BLOCKED " }, + { id: "ai-review", status: " Failed " }, + ], + }, + ], + }), + truncated: false, + }; + } + if (path === ".agent/templates/test-first-loop/phases.json") { + return { + content: JSON.stringify({ + schema: "forge-phases-v1", + phases: [ + { id: "implementation", title: "Implementation", iconId: "console", order: 1 }, + { id: "ai-review", title: "AI Review", iconId: "folder-review", order: 2 }, + ], + }), + truncated: false, + }; + } + + throw new Error(`unexpected path: ${String(path)}`); + }); + + await expect(forgeLoadPhaseView("ws-1", "alpha", "test-first-loop")).resolves.toEqual({ + phases: [ + { id: "implementation", title: "Implementation", iconId: "console", order: 1 }, + { id: "ai-review", title: "AI Review", iconId: "folder-review", order: 2 }, + ], + taskPhaseStatusByTaskId: { + "task-1": { + implementation: "blocked", + "ai-review": "failed", + }, + }, + }); + }); + + it("returns a safe fallback when forge phase-view files are malformed or truncated", async () => { + const invokeMock = vi.mocked(invoke); + invokeMock.mockImplementation(async (command: string, args?: unknown) => { + if (command === "is_macos_debug_build") { + return false; + } + if (command !== "read_workspace_file") { + return undefined; + } + + const path = + typeof args === "object" && args !== null && "path" in args + ? (args as { path?: unknown }).path + : undefined; + if (path === "plans/alpha/state.json") { + return { content: "{not-json", truncated: false }; + } + if (path === ".agent/templates/test-first-loop/phases.json") { + return { + content: JSON.stringify({ + schema: "forge-phases-v1", + phases: [{ id: "implementation", title: "Implementation", iconId: "code_blocks", order: 1 }], + }), + truncated: true, + }; + } + + throw new Error(`unexpected path: ${String(path)}`); + }); + + await expect(forgeLoadPhaseView("ws-1", "alpha", "test-first-loop")).resolves.toEqual({ + phases: [], + taskPhaseStatusByTaskId: {}, + }); + }); + + it("keeps failed phase statuses from state for downstream blocking UI", async () => { + const invokeMock = vi.mocked(invoke); + invokeMock.mockImplementation(async (command: string, args?: unknown) => { + if (command === "is_macos_debug_build") { + return false; + } + if (command !== "read_workspace_file") { + return undefined; + } + + const path = + typeof args === "object" && args !== null && "path" in args + ? (args as { path?: unknown }).path + : undefined; + if (path === "plans/alpha/state.json") { + return { + content: JSON.stringify({ + tasks: [ + { + id: "task-1", + phases: [ + { id: "ai-review", status: "failed" }, + { id: "documentation", status: "blocked" }, + ], + }, + ], + }), + truncated: false, + }; + } + throw new Error(`unexpected path: ${String(path)}`); + }); + + await expect(forgeLoadPhaseView("ws-1", "alpha", null)).resolves.toEqual({ + phases: [], + taskPhaseStatusByTaskId: { + "task-1": { + "ai-review": "failed", + documentation: "blocked", + }, + }, + }); + }); + + it("returns a safe fallback when forge phase-view files are missing", async () => { + const invokeMock = vi.mocked(invoke); + invokeMock.mockImplementation(async (command: string) => { + if (command === "is_macos_debug_build") { + return false; + } + if (command === "read_workspace_file") { + throw new Error("missing"); + } + return undefined; + }); + + await expect(forgeLoadPhaseView("ws-1", "alpha", null)).resolves.toEqual({ + phases: [], + taskPhaseStatusByTaskId: {}, + }); + + expect(invokeMock).toHaveBeenCalledWith("read_workspace_file", { + workspaceId: "ws-1", + path: "plans/alpha/state.json", + }); + }); + it("returns fallbacks for forge wrappers when Tauri invoke bridge is missing", async () => { const invokeMock = vi.mocked(invoke); invokeMock.mockRejectedValueOnce( diff --git a/src/services/tauri.ts b/src/services/tauri.ts index d00fd497e8..4987cfacfe 100644 --- a/src/services/tauri.ts +++ b/src/services/tauri.ts @@ -1011,6 +1011,190 @@ export type ForgeRunPhaseChecksResponse = { results: ForgePhaseCheckResult[]; }; +export type ForgePhaseViewStatus = + | "pending" + | "in_progress" + | "completed" + | "blocked" + | "failed"; + +export type ForgePhaseViewMetadata = { + id: string; + title: string; + iconId: string; + order: number; +}; + +export type ForgePhaseView = { + phases: ForgePhaseViewMetadata[]; + taskPhaseStatusByTaskId: Record>; +}; + +const EMPTY_FORGE_PHASE_VIEW: ForgePhaseView = { + phases: [], + taskPhaseStatusByTaskId: {}, +}; + +const DEFAULT_FORGE_PHASE_ICON_ID = "check_circle"; + +function normalizeForgePhaseViewStatus(value: unknown): ForgePhaseViewStatus { + if (typeof value !== "string") { + return "pending"; + } + const normalized = value.trim().toLowerCase(); + if ( + normalized === "pending" || + normalized === "in_progress" || + normalized === "completed" || + normalized === "blocked" || + normalized === "failed" + ) { + return normalized; + } + return "pending"; +} + +function parseForgeStatePhaseStatuses( + value: unknown, +): Record> { + if (!value || typeof value !== "object") { + return {}; + } + const tasks = (value as { tasks?: unknown }).tasks; + if (!Array.isArray(tasks)) { + return {}; + } + const taskPhaseStatusByTaskId: Record> = {}; + + for (const taskValue of tasks) { + if (!taskValue || typeof taskValue !== "object") { + continue; + } + const taskIdRaw = (taskValue as { id?: unknown }).id; + if (typeof taskIdRaw !== "string") { + continue; + } + const taskId = taskIdRaw.trim(); + if (!taskId) { + continue; + } + + const phases = (taskValue as { phases?: unknown }).phases; + if (!Array.isArray(phases)) { + continue; + } + + const phaseStatusById: Record = {}; + for (const phaseValue of phases) { + if (!phaseValue || typeof phaseValue !== "object") { + continue; + } + const phaseIdRaw = (phaseValue as { id?: unknown }).id; + if (typeof phaseIdRaw !== "string") { + continue; + } + const phaseId = phaseIdRaw.trim(); + if (!phaseId) { + continue; + } + phaseStatusById[phaseId] = normalizeForgePhaseViewStatus( + (phaseValue as { status?: unknown }).status, + ); + } + + if (Object.keys(phaseStatusById).length > 0) { + taskPhaseStatusByTaskId[taskId] = phaseStatusById; + } + } + + return taskPhaseStatusByTaskId; +} + +function parseForgePhaseMetadata(value: unknown): ForgePhaseViewMetadata[] { + if (!value || typeof value !== "object") { + return []; + } + const phases = (value as { phases?: unknown }).phases; + if (!Array.isArray(phases)) { + return []; + } + + const parsed = phases + .map((phaseValue, index): ForgePhaseViewMetadata | null => { + if (!phaseValue || typeof phaseValue !== "object") { + return null; + } + const phaseIdRaw = (phaseValue as { id?: unknown }).id; + if (typeof phaseIdRaw !== "string") { + return null; + } + const id = phaseIdRaw.trim(); + if (!id) { + return null; + } + + const titleRaw = (phaseValue as { title?: unknown }).title; + const iconIdRaw = (phaseValue as { iconId?: unknown }).iconId; + const orderRaw = (phaseValue as { order?: unknown }).order; + + const title = typeof titleRaw === "string" && titleRaw.trim() ? titleRaw.trim() : id; + const iconId = + typeof iconIdRaw === "string" && iconIdRaw.trim() + ? iconIdRaw.trim() + : DEFAULT_FORGE_PHASE_ICON_ID; + const order = + typeof orderRaw === "number" && Number.isFinite(orderRaw) ? orderRaw : index + 1; + + return { id, title, iconId, order }; + }) + .filter((phase): phase is ForgePhaseViewMetadata => Boolean(phase)); + + parsed.sort((a, b) => a.order - b.order); + return parsed; +} + +async function readWorkspaceJsonValue( + workspaceId: string, + path: string, +): Promise { + try { + const response = await readWorkspaceFile(workspaceId, path); + if (response.truncated) { + return null; + } + return JSON.parse(response.content); + } catch { + return null; + } +} + +export async function forgeLoadPhaseView( + workspaceId: string, + planId: string, + installedTemplateId: string | null, +): Promise { + const normalizedPlanId = planId.trim(); + if (!normalizedPlanId) { + return EMPTY_FORGE_PHASE_VIEW; + } + + const normalizedTemplateId = installedTemplateId?.trim() ?? ""; + const statePath = `plans/${normalizedPlanId}/state.json`; + const phasesPath = normalizedTemplateId + ? `.agent/templates/${normalizedTemplateId}/phases.json` + : null; + + const [stateJson, phasesJson] = await Promise.all([ + readWorkspaceJsonValue(workspaceId, statePath), + phasesPath ? readWorkspaceJsonValue(workspaceId, phasesPath) : Promise.resolve(null), + ]); + + return { + phases: parseForgePhaseMetadata(phasesJson), + taskPhaseStatusByTaskId: parseForgeStatePhaseStatuses(stateJson), + }; +} + export async function forgeListBundledTemplates(): Promise { try { return await invoke("forge_list_bundled_templates"); diff --git a/src/styles/forge.css b/src/styles/forge.css index 0b9c207c0a..359c345e85 100644 --- a/src/styles/forge.css +++ b/src/styles/forge.css @@ -306,6 +306,12 @@ flex-shrink: 0; } +.forge-phase-icon img { + width: 14px; + height: 14px; + display: block; +} + .forge-phase-name { font-size: 12px; } diff --git a/src/utils/forgePhaseIcons.test.ts b/src/utils/forgePhaseIcons.test.ts new file mode 100644 index 0000000000..2b725a41a1 --- /dev/null +++ b/src/utils/forgePhaseIcons.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from "vitest"; +import { getForgePhaseIconUrl } from "./forgePhaseIcons"; + +describe("forgePhaseIcons", () => { + it("resolves valid material icon ids to the expected asset url", () => { + expect(getForgePhaseIconUrl("cucumber")).toBe("/assets/material-icons/cucumber.svg"); + }); + + it("falls back to the safe default icon when the id is invalid", () => { + expect(getForgePhaseIconUrl("definitely-not-a-real-material-icon")).toBe( + "/assets/material-icons/file.svg", + ); + }); + + it("accepts valid icon ids with surrounding whitespace", () => { + expect(getForgePhaseIconUrl(" taskfile ")).toBe("/assets/material-icons/taskfile.svg"); + }); +}); diff --git a/src/utils/forgePhaseIcons.ts b/src/utils/forgePhaseIcons.ts new file mode 100644 index 0000000000..b3f90b6828 --- /dev/null +++ b/src/utils/forgePhaseIcons.ts @@ -0,0 +1,29 @@ +import { + getIconUrlByName, + isMaterialIconName, + type MaterialIcon, +} from "vscode-material-icons"; + +const MATERIAL_ICONS_BASE_URL = "/assets/material-icons"; +const DEFAULT_FORGE_PHASE_ICON_ID: MaterialIcon = "file"; +const forgePhaseIconUrlCache = new Map(); + +function resolveForgePhaseIconId(iconId: string | null | undefined): MaterialIcon { + const normalized = typeof iconId === "string" ? iconId.trim() : ""; + if (normalized && isMaterialIconName(normalized)) { + return normalized; + } + return DEFAULT_FORGE_PHASE_ICON_ID; +} + +export function getForgePhaseIconUrl(iconId: string | null | undefined): string { + const resolvedIconId = resolveForgePhaseIconId(iconId); + const cached = forgePhaseIconUrlCache.get(resolvedIconId); + if (cached) { + return cached; + } + + const iconUrl = getIconUrlByName(resolvedIconId, MATERIAL_ICONS_BASE_URL); + forgePhaseIconUrlCache.set(resolvedIconId, iconUrl); + return iconUrl; +}