@galda/cli 0.10.15 → 0.10.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/index.html +153 -30
- package/engine/lib.mjs +166 -10
- package/engine/server.mjs +124 -44
- package/package.json +1 -1
package/app/index.html
CHANGED
|
@@ -610,7 +610,10 @@
|
|
|
610
610
|
.confetti-piece{position:fixed;pointer-events:none;z-index:9999;will-change:transform,opacity}
|
|
611
611
|
|
|
612
612
|
/* connect modal */
|
|
613
|
-
.overlay{position:fixed;inset:0;background:rgba(0,0,0,.55);backdrop-filter:blur(6px);z-index:
|
|
613
|
+
.overlay{position:fixed;inset:0;background:rgba(0,0,0,.55);backdrop-filter:blur(6px);z-index:90;display:none;place-items:center}
|
|
614
|
+
/* z-index:90 (was 60): the customize-panel discard confirm (#deleteConfirmOverlay, shares this
|
|
615
|
+
class) can now fire while .gearpop (the customize panel, z-index:80) is open — it was
|
|
616
|
+
rendering BEHIND the panel it's confirming a discard for (Masa 2026-07-16, real screenshot). */
|
|
614
617
|
.overlay.show{display:grid}
|
|
615
618
|
.modal{width:640px;max-width:92vw;background:var(--surface);border:1px solid var(--hair2);border-radius:16px;padding:20px 22px;box-shadow:0 24px 70px rgba(0,0,0,.6)}
|
|
616
619
|
.modal h3{margin:0 0 6px;font-size:15px;letter-spacing:-.01em}
|
|
@@ -619,6 +622,12 @@
|
|
|
619
622
|
border-radius:9px;padding:11px 12px;white-space:pre-wrap;word-break:break-all;line-height:1.55}
|
|
620
623
|
.modal .mrow{display:flex;gap:8px;margin-top:10px}
|
|
621
624
|
.mbtn{height:30px;padding:0 14px;border-radius:9px;border:0;background:var(--iri);color:var(--on-accent);font-size:12.5px;font-weight:600;cursor:pointer}
|
|
625
|
+
/* the bare .mbtn fill is --iri, the AI-signature rainbow (logo/send-ring only per DESIGN-RULES
|
|
626
|
+
§5.5 "虹はAI要素だけ") — it must never be a generic button fill. .primary is the actual
|
|
627
|
+
neutral affirmative-action look, lifted from the customize panel's own existing Save button
|
|
628
|
+
(.gearpop .cfoot .cbtn.save below: same --ink/--invink tokens) rather than inventing a new
|
|
629
|
+
one (CDO decision, Masa 2026-07-16: "todo削除や他UIを参考にしてほしい"). */
|
|
630
|
+
.mbtn.primary{background:var(--ink);color:var(--invink)}
|
|
622
631
|
.mbtn.ghost{background:transparent;border:1px solid var(--hair2);color:var(--ink2)}
|
|
623
632
|
/* Soft destructive (DESIGN-RULES §7 決定ログ 2026-07-07深夜3: See allアクション色=Soft確定
|
|
624
633
|
— 淡赤面+濃赤文字。redは面を塗らない小要素専用の原則に沿い、フル赤塗りは使わない). */
|
|
@@ -1074,8 +1083,10 @@
|
|
|
1074
1083
|
the layout starts at the three cards. Frame = 14px all around (flagship .app{inset:14px}).
|
|
1075
1084
|
Dev escape hatch: ?dev=1 (or localStorage devbar=1) adds body.devbar to show the bar. */
|
|
1076
1085
|
body[data-layout="flagship"]:not(.devbar) .top{display:none}
|
|
1077
|
-
body[data-layout="flagship"] #fsRoot{flex:1;min-height:0;display:flex;gap:12px;padding:14px;
|
|
1078
|
-
--fscenterw:760px} /*
|
|
1086
|
+
body[data-layout="flagship"] #fsRoot{flex:1;min-height:0;display:flex;gap:12px;padding:14px;position:relative;
|
|
1087
|
+
--fscenterw:760px} /* position:relative so .railgrip (a direct child, kept outside .fsrail's
|
|
1088
|
+
overflow:hidden — see below) can be placed via left:calc(var(--railw)...) and never get clipped.
|
|
1089
|
+
single source of truth for the reading column — .fsstream (feed) and
|
|
1079
1090
|
.compwrap (composer) both read this var so they can never drift apart (fix: composer was
|
|
1080
1091
|
reported "narrow vs the text above" — both already used the same 760px, but as two separate
|
|
1081
1092
|
literals; centralising removes the risk of future edits widening one without the other) */
|
|
@@ -1092,17 +1103,23 @@
|
|
|
1092
1103
|
#fsRoot .fsrail{flex:0 0 var(--railw,168px);display:flex;flex-direction:column;padding:14px 10px;position:relative;transition:flex-basis .28s cubic-bezier(.4,0,.2,1);
|
|
1093
1104
|
min-width:0;overflow:hidden} /* CDO reference: the rail is a narrow lane; long project names ellipsize inside it instead of widening the whole left side */
|
|
1094
1105
|
#fsRoot .fsrail.noanim{transition:none}
|
|
1095
|
-
#fsRoot .
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1106
|
+
/* .railgrip is a DIRECT CHILD OF #fsRoot (moved out of .fsrail in the markup below), not a
|
|
1107
|
+
descendant of the rail — .fsrail has overflow:hidden for name-ellipsizing, and any part of a
|
|
1108
|
+
child that pokes past an overflow:hidden ancestor's box is both unpainted AND un-hit-testable,
|
|
1109
|
+
so a grip nested inside the rail can never straddle the rail's own edge (only sit fully inside
|
|
1110
|
+
it, biased away from the visible seam — Masa 2026-07-16). Positioned via left:calc(14px + var(--railw))
|
|
1111
|
+
so it tracks the live rail width automatically (no JS repositioning needed) and is centered
|
|
1112
|
+
exactly on the visible edge, mirroring .lanegrip's left:-3px/width:7px pattern on the other side.
|
|
1113
|
+
The leading 14px is #fsRoot's own left padding (line ~1085): left/top on an absolutely positioned
|
|
1114
|
+
child resolve against the PADDING edge of its containing block, not the content edge where the
|
|
1115
|
+
flex children (.fsrail etc.) actually start — omitting it left the whole grip sitting 14px too
|
|
1116
|
+
far left of the seam (caught from a real screenshot, Masa 2026-07-16 2nd pass). */
|
|
1117
|
+
#fsRoot .railgrip{position:absolute;top:0;bottom:0;left:calc(14px + var(--railw, 168px) - 4px);width:7px;cursor:col-resize;z-index:5}
|
|
1118
|
+
/* Gradient theme: the rail is transparent (no box-shadow edge of its own), so the seam the user
|
|
1119
|
+
actually SEES is .fscenter's own left edge, one flex gap (12px) further right (Masa 2026-07-08). */
|
|
1120
|
+
body[data-theme="gradient"] #fsRoot .railgrip{left:calc(14px + var(--railw, 168px) + 8px)}
|
|
1104
1121
|
#fsRoot .railgrip::after{content:"";position:absolute;right:3px;top:50%;transform:translateY(-50%);width:2px;height:34px;border-radius:2px;background:var(--hair2);opacity:0;transition:opacity .15s}
|
|
1105
|
-
#fsRoot .fsrail:hover .railgrip::after{opacity:1}
|
|
1122
|
+
#fsRoot .fsrail:hover ~ .railgrip::after,#fsRoot .railgrip:hover::after{opacity:1}
|
|
1106
1123
|
#fsRoot.railfold .railgrip{display:none}
|
|
1107
1124
|
#fsRoot .fslogo{display:flex;align-items:center;gap:9px;margin:2px 0 16px 6px;height:22px;flex:0 0 auto}
|
|
1108
1125
|
#fsRoot .logoring{width:22px;height:22px;flex:0 0 22px;border-radius:50%;
|
|
@@ -1296,6 +1313,11 @@
|
|
|
1296
1313
|
discard path. */
|
|
1297
1314
|
#fsRoot .cc-ask .acts{position:absolute;top:8px;right:8px;display:flex;opacity:0;transition:opacity .15s}
|
|
1298
1315
|
#fsRoot .cc-ask:hover .acts{opacity:1}
|
|
1316
|
+
/* Touch devices have no hover → the hover-only trash was permanently invisible
|
|
1317
|
+
and the「Needs your answer」card had no reachable discard path (Masa
|
|
1318
|
+
2026-07-16: 右上のdeleteが押せない). Reveal it (dimmed, so it still reads as
|
|
1319
|
+
secondary per §1.5-10) whenever the pointer can't hover. */
|
|
1320
|
+
@media (hover: none){ #fsRoot .cc-ask .acts{opacity:.55} }
|
|
1299
1321
|
#fsRoot .qchips{display:flex;gap:6px;flex-wrap:wrap}
|
|
1300
1322
|
#fsRoot .achip{height:28px;padding:0 12px;border-radius:8px;border:1px solid var(--hair2);background:var(--panel);
|
|
1301
1323
|
color:var(--ink2);font:inherit;font-size:12.5px;cursor:pointer;transition:border-color .15s,color .15s,background .15s}
|
|
@@ -1920,7 +1942,7 @@
|
|
|
1920
1942
|
#fsClawdHero span{display:block;margin-top:5px;font:500 10px/1.4 var(--mono);letter-spacing:.08em;color:var(--ink3)}
|
|
1921
1943
|
/* Welcome P1 (H17 item 4, flagship f9c75ad): grounded hop + clean-cut eyes on settle + our CSS confetti.
|
|
1922
1944
|
Two pixel frames (neutral black-eyes / laughing), no arm-raise, no official confetti. reduce-motion=static. */
|
|
1923
|
-
#fsClawdHero .cstage{position:relative;width:170px;height:132px;display:flex;align-items:flex-end;justify-content:center;margin:0 auto 2px}#fsClawdHero .cw{position:relative;width:150px;transform-origin:50% 100%;will-change:transform;animation:chHop 2.4s linear infinite}#fsClawdHero .cw img{position:absolute;left:0;bottom:0;width:100%;display:block;image-rendering:pixelated}#fsClawdHero .cw .chNeu{animation:chNeu 2.4s linear infinite}#fsClawdHero .cw .chLau{opacity:0;animation:chLau 2.4s linear infinite}#fsClawdHero .chConf{position:absolute;left:50%;top:34px;width:0;height:0;pointer-events:none;z-index:3}#fsClawdHero .chConf i{position:absolute;width:4px;height:7px;border-radius:1.5px;opacity:0;transform:translate(-50%,-50%);animation:chPop 2.4s cubic-bezier(.2,.7,.3,1) infinite}@keyframes chHop{0%,52%{transform:translateY(0) scale(1,1)}55%{transform:translateY(0) scale(1.08,.85)}58%{transform:translateY(-16px) scale(.95,1.14)}61%{transform:translateY(-27px) scale(.98,1.05)}64%{transform:translateY(-31px) scale(1,1)}67%{transform:translateY(-26px) scale(.98,1.05)}70%{transform:translateY(-14px) scale(.95,1.12)}73%{transform:translateY(0) scale(1.1,.82)}77%{transform:translateY(-4px) scale(.99,1.02)}80%,100%{transform:translateY(0) scale(1,1)}}@keyframes chLau{0%,53.8%{opacity:0}54%,85.8%{opacity:1}86%,100%{opacity:0}}@keyframes chNeu{0%,53.8%{opacity:1}54%,85.8%{opacity:0}86%,100%{opacity:1}}@keyframes chPop{0%,54%{opacity:0;transform:translate(0,6px) rotate(0) scale(.5)}60%{opacity:1;transform:translate(calc(var(--dx)*.5),calc(var(--dy)*.5)) rotate(calc(var(--rot)*.5)) scale(1)}72%{opacity:1;transform:translate(var(--dx),var(--dy)) rotate(var(--rot))}100%{opacity:0;transform:translate(calc(var(--dx)*1.1),calc(var(--dy) + 44px)) rotate(calc(var(--rot)*1.4)) scale(.9)}}@media (prefers-reduced-motion:reduce){#fsClawdHero .cw,#fsClawdHero .cw .chNeu,#fsClawdHero .cw .chLau{animation:none}#fsClawdHero .cw .chLau{opacity:0}#fsClawdHero .chConf{display:none}}
|
|
1945
|
+
#fsClawdHero .cstage{position:relative;left:-13px;width:170px;height:132px;display:flex;align-items:flex-end;justify-content:center;margin:0 auto 2px}#fsClawdHero .cw{position:relative;width:150px;transform-origin:50% 100%;will-change:transform;animation:chHop 2.4s linear infinite}#fsClawdHero .cw img{position:absolute;left:0;bottom:0;width:100%;display:block;image-rendering:pixelated}#fsClawdHero .cw .chNeu{animation:chNeu 2.4s linear infinite}#fsClawdHero .cw .chLau{opacity:0;animation:chLau 2.4s linear infinite}#fsClawdHero .chConf{position:absolute;left:50%;top:34px;width:0;height:0;pointer-events:none;z-index:3}#fsClawdHero .chConf i{position:absolute;width:4px;height:7px;border-radius:1.5px;opacity:0;transform:translate(-50%,-50%);animation:chPop 2.4s cubic-bezier(.2,.7,.3,1) infinite}@keyframes chHop{0%,52%{transform:translateY(0) scale(1,1)}55%{transform:translateY(0) scale(1.08,.85)}58%{transform:translateY(-16px) scale(.95,1.14)}61%{transform:translateY(-27px) scale(.98,1.05)}64%{transform:translateY(-31px) scale(1,1)}67%{transform:translateY(-26px) scale(.98,1.05)}70%{transform:translateY(-14px) scale(.95,1.12)}73%{transform:translateY(0) scale(1.1,.82)}77%{transform:translateY(-4px) scale(.99,1.02)}80%,100%{transform:translateY(0) scale(1,1)}}@keyframes chLau{0%,53.8%{opacity:0}54%,85.8%{opacity:1}86%,100%{opacity:0}}@keyframes chNeu{0%,53.8%{opacity:1}54%,85.8%{opacity:0}86%,100%{opacity:1}}@keyframes chPop{0%,54%{opacity:0;transform:translate(0,6px) rotate(0) scale(.5)}60%{opacity:1;transform:translate(calc(var(--dx)*.5),calc(var(--dy)*.5)) rotate(calc(var(--rot)*.5)) scale(1)}72%{opacity:1;transform:translate(var(--dx),var(--dy)) rotate(var(--rot))}100%{opacity:0;transform:translate(calc(var(--dx)*1.1),calc(var(--dy) + 44px)) rotate(calc(var(--rot)*1.4)) scale(.9)}}@media (prefers-reduced-motion:reduce){#fsClawdHero .cw,#fsClawdHero .cw .chNeu,#fsClawdHero .cw .chLau{animation:none}#fsClawdHero .cw .chLau{opacity:0}#fsClawdHero .chConf{display:none}}
|
|
1924
1946
|
#fsRoot.clawdhero #fsClawdHero{display:block}
|
|
1925
1947
|
/* Non-Claude agent (e.g. Codex) hero: no Clawd mascot (that's a Claude Code brand mark,
|
|
1926
1948
|
DESIGN-RULES v3 §6.5) — a plain glyph badge stands in so the hero still names the
|
|
@@ -2090,6 +2112,12 @@
|
|
|
2090
2112
|
<div class="errbar" id="errbar"></div>
|
|
2091
2113
|
<div class="composer-wrap" id="composerWrap">
|
|
2092
2114
|
<div class="queue" id="queue"></div>
|
|
2115
|
+
<!-- Conversation-first composer (SLICE 2, docs/HANDOFF-ONBOARDING-conversational.md):
|
|
2116
|
+
a chat/help/settings message gets a quiet reply HERE instead of
|
|
2117
|
+
becoming a To-Do. Reuses the .peraanswer surface; hidden until a
|
|
2118
|
+
reply exists (progressive disclosure — DESIGN-RULES §1.5-3, "存在
|
|
2119
|
+
しないものの器を先に見せない"). -->
|
|
2120
|
+
<div class="peraanswer" id="chatReply" hidden></div>
|
|
2093
2121
|
<div class="composer">
|
|
2094
2122
|
<div class="attachbar" id="attachbar" style="display:none"></div>
|
|
2095
2123
|
<!-- Slash-command palette (Masa決定 2026-07-07: chips→slash). Floats
|
|
@@ -2175,7 +2203,6 @@
|
|
|
2175
2203
|
<div id="fsRoot" data-rv="3">
|
|
2176
2204
|
<div class="fscard fsrail">
|
|
2177
2205
|
<button class="railtg" id="fsRailTg" title="Fold the rail"></button><!-- panel icon filled by tgUpdate (v45 §4) -->
|
|
2178
|
-
<div class="railgrip" id="fsRgrip" title="Drag to resize"></div>
|
|
2179
2206
|
<button class="fsprojbtn" id="fsProjBtn" title="Switch project"><span class="pname" id="fsProjBtnName"></span><svg viewBox="0 0 24 24"><path d="M6 9l6 6 6-6"/></svg></button>
|
|
2180
2207
|
<div class="fslogo"><span class="logoring"></span><span class="logoname">Galda</span></div>
|
|
2181
2208
|
<!-- gear = RIGHT END of the Projects row (HANDOFF-v45 §2, flagship .lbrow port):
|
|
@@ -2193,6 +2220,10 @@
|
|
|
2193
2220
|
<button class="fshamb" id="fsHamburger" title="Open To Do and Review"><svg viewBox="0 0 24 24"><path d="M5 7h14M5 12h14M5 17h14"/></svg></button>
|
|
2194
2221
|
<div id="fsProjects"></div>
|
|
2195
2222
|
</div>
|
|
2223
|
+
<!-- .railgrip lives here (sibling of .fsrail, not a child — see the CSS comment above)
|
|
2224
|
+
so it can straddle the rail's visible edge without being clipped by .fsrail's
|
|
2225
|
+
overflow:hidden (Masa 2026-07-16). -->
|
|
2226
|
+
<div class="railgrip" id="fsRgrip" title="Drag to resize"></div>
|
|
2196
2227
|
<div class="fscard fscenter">
|
|
2197
2228
|
<div id="fsBand"></div>
|
|
2198
2229
|
<button id="fsBandBtn" title="Change the band image"><svg viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 15l5-4 4 3 4-5 5 6"/></svg></button>
|
|
@@ -2274,7 +2305,7 @@
|
|
|
2274
2305
|
<div class="mrow">
|
|
2275
2306
|
<span style="flex:1"></span>
|
|
2276
2307
|
<button class="mbtn ghost" id="wfcancel">Cancel</button>
|
|
2277
|
-
<button class="mbtn" id="wfsave">Save</button>
|
|
2308
|
+
<button class="mbtn primary" id="wfsave">Save</button>
|
|
2278
2309
|
</div>
|
|
2279
2310
|
<div class="wferr" id="wferr"></div>
|
|
2280
2311
|
</div>
|
|
@@ -5347,10 +5378,15 @@ function applyFsBand(){
|
|
|
5347
5378
|
const CLAWD_HERO_IMGS = '<img class="chNeu" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARgAAAETCAYAAADtSRlPAAAMdElEQVR4nO3dPYwk6VkH8Keqq2f2E4uzzydzCToCH8JCIiAxgY1EDMIBSByS5cQkthCCHCFBgGTJSBA5cGQnRDYJws6xxElA6C9ZBEjGt+e1rZvdnZmurkJvdfds9+581HTvTL9d/fsF+0xrd7tm3u5+3v+89RUBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMBDFtr8BLvaPn3+nfWvypPu6Lo1UzqpmVn80vh9f/MrXfa7mqsUX5Oe1hw/izTpiFNOYxiiqqoy6btQMx2E6OYnxqIr32/vbfttkRYPJWD1to4g2Ttv0aBr1ZNpFTjW/cRhHxGRaR0iaKwxHxqpREdHWcZBKRHQP1SzHISkLH6cXGZHME0x60zZt082U03a2aKbmNw5Jep1YpcFknmDSmzY1mW3P0KoEsw4NJmMSzO4lmLKcP6CjwWRMgtmd5FRHOft1trGHepkGkzlrMLtRq2gkmHNoMJmzBpNHQpFg1qPBZMwazO4lGFZpMBmzBrP9ZOI4mM04kndnEkzZayYdF2WU0UQ1X2vsU7uZt+j3/LtYmzbi8IWft5qP63J98e/r6L+d9L9n43jLb5LMaTC5J5jJ/DiYdjajTi+pqbk8Oj6Jnx09jdNmGlVZRd3Ul9eTp1Ed3otfPjyI1x/eufT5d7U+OT6N/338uPs5rxyPpXF5642PxsGo7LWdFHUcyfsyDWYgCWYcTYzLMr79kw/iq9/4VjwqUr6/+piMURQxjTb+5pNvx+996nfjwZOfdh+WpmdiyrWm8Ug/xy8evhHf/M634svvfi+ix5gUZRlt08TrEfH5P/nDeOeNu905RleNxyLBlCPHwSyzBpOxdNBW371I3XEYo6WXM32Q0gcquaSm5nL2ODW1+VtiCHt1LnTJeKTmsnhcFqNoy9nX9iKtR4PJWDpoq+9xMN1ejOlsL8az9MfybN2zpmvOpOfp/vsA9uq8pM84LI1b006jSAs4PcbDcTDn02AGmGDupj/atov7nR5JZi8STJ9xWBq3lGAWJJj1aDADSzDVvfurcT/pm2S6RTkJ5mzc0tDMf0Xqm2BYpcEMLMHUT2eX2JRgNk8wy64a/+71cj2Yl2gwA1yD6UgwryTBnA3nFePfvV4SzEs0mIEcyXveXiRrMLeXYBZnU7PKiGROgtmNBGMv0vk0mMxJMNvbi7TMXqT1aDADOZvaGkweCYZVGswOrMEkN3Uk73JNNw9zHMxqglkcaNc3wbQ+Uis0mIwtz4jXPpI3ueaRvMneHweThqNpujFMR/IumrYEsx4nO2bqL3774+3Dg4Oo67L70C9myovO5q3bMqqyiDfvjOOPfu0j8d6DD/fazsHxpKtvvvZaV9NM3Gd7udduPIoy7p4+id9448PxmY+/1XvsT++M46NHP41PlHWcTNK5SVW0bXPl9tL9q8ajtnvtvvzu954fHr3HDMI1ffev/rSdtm2M0omCN1CXI/bJZPbhb+cvVJ+a3uzjahxF1yTKK+vCk3Ico5OnvbezKzWpilHcGRdRt0VURdvV9PMnadyXHy+ksUm3ha3b6dkHpc/20rbS3TirxQV3lvR5Pa7zuqWf5e0vfS3rz7AEc03pzTitm3jWTm9oBt78eaOezB9Pe9aIUTvJInncRG1jGkens8fH13wdkuslp9n/ezY57++nr6x2S0PjdMPavFmDuabZDOhOi7d9drQaK+MwP0UqexrMNc1itjstbvts6X2vzfN1+axpMNckwUgUOSSqUoIZJgkmjxl832sjwQyXNZjtz+D7XksJZni+8Ae/37b1xBpMBjP4vtdGghmmohpLMBnM4PteSwlmeP7pm//Svaz2Im1/Bt/32kgww2QvUh4z+L7XUoIZJnuR8pjB9702EswwSTB5zOD7XksJZpgkmDxm8H2vjQQzTBJMHjP4vtdSghkmCSaPGXzfayPBDJMEk8cMvu+1lGCGSYLJYwbf99pIMMMkweQxg+97LSWYYZJg8pjB9702EsxwOZt6+zP4vtdSghkeZ1Nvf+ZWQ4IZ8smOzqbOYwbf91pKMMPlbGpJYttJqrEGM0z2IuUxg+97LSWYYUo3R0t38EsOb7hac9jdpHTT748m3S64yD/GuKvANU3qiKN6dmvVm6on3c3C8pgp1euNQ8zri6/r6bR55e+T40n+DWZHgtZ++vpf/ln79uQX3ZrP5Ip7I6vbHYeUKFKy/c/il+Kz//AVn6s5CSZjk2nR3Yg+2lqSyDxRpdepaZsoy/xTxW3SYDI2Hj1/s257TUG9es2lLMpoGuFlmQaTsXrxS33kM1OrF6+9pATDKg0mY9ViapRgsk9QdZRdgmGVEcnY8owoOeSdoKzBnE+D2RHbnqHVfgnGGswqDWZHbHuGVvslmOVfa9Fgsrb8O70EsSMJxkLvimr1ITmxBrM7yWncJZhtv2Py41ekjEkwO5hgHAezQoPJmONgtp9M7EXajAaTseXDzrc9Q6sSzDo0mIwtx+1tz9Cq42DWocFkzJG8u5OcrMGcT4PJmL1Iu5OcHMl7Pg0mY/YibT+ZSDCb0WAyJsFsP5lIMJvRYDImwWw/mUgwm9FgMuY4mO0nEwlmMxpMxhwHs/1kIsFsRoPJmONgtp9MJJjNaDAZcxzM9pOJBLMZDSZj9iJtP5lIMJvRYDJmL9L2k4kEsxkNJmP2Im0/mUgwm3HBqdzXYObX/V7MpOlNf1lNFz26Mx5HW0+iqGa1rEbR1NNza123MapGcTKZRFWMom6nvbaz+PCNilEclG1MmyZGZdnVy7a3Tl08b6pPp0233b7j0R3GX4yiiNnPedV4LGobZdR1041Hn+3Vbdnd2dH1YFZpMANZg0lv8uTHx5P4nx/8KJrxg5hEE+NLQuri71P99dcexK986EO9Z+wklf/68ftx9OxZtMXNvpWKto4Hd+/Gb33sI2cf+j7f5/TwXrz36L34wfvz5ymfRtvcu7Au3I/H8atvfixev3MYddv0vqJduXSzPDSYnVuDuSy53B2V8Z2jJv7uX/9j3m76+/tP/2Z773c+FQ8/eNR9WCZR9koGn/vnf7v2tjbx33/+Tlv0HI/0cxwd3I93v//D+Ot//+61v8+//ewft5+5F1E2Eafp+S65R7gEcz5rMANJMN09rDdQlaNZnT9PnwSTksQ2tLcwHsu6e053TeTiJONs6vNpMAPZi5SuR7KJukm/dtRnz3PV9pKUYG7b9IU1kV7jscH3OZnW8+2m18P1YK5LgxlYginX/DDtSoJJi8q3mWAWTb7P9rqks3SZUzSYwSWYZs0PfUowXR1ignkFTd4V7dYjwQzkOBgJRoLJkQYzkLOpN00wCxLMc9VIgtmUBrMjbjrBHB6Ou2oN5rl6ag1mUxpMxm5zDebkZNJVCeY5CWZzGkzGbnMv0mKRV4LZLMEsX2IDDSZrt5lgFiSYDRPM0qSABpO120wwCxLMhmswS5MCGkzWJJjtHgcjwWxOu82YBLP9BFONZ3vXJJj1aDAZk2AySDCpy1iDWZsGkzEJZvsJJl18KpFg1qPBZEyCeZkEs1s0mIxJMC+TYHaLBpMxCWb3EszyCapoMFmTYHYwwbgezAoJJmO3mWAWF5xyJO/SmIwi6snsHC3Xg1mPBpOx20wwzqY+nyvabUaDydhtJpgnz467KsGsckW7zWgwGbMGk8EajGvybkSDyVi3VyK9REXVK8G0ZXHjZ1Mn6f5Axbxuay/S6fwOj4vv58W6GLeNtxejs+dZXIP4ojoeVe7s+AINJnPpdqTR1mcfnsWH/LCIKOc3ATv7dxveVaBqZs+TGtuLz794vPh+un/3Cu891NfheNylmHtV1d1GpJqPy+LnX9R0E7r0/Y2b2W1H1pVui5t+3rSddPvZi2pqLonrwaxy69iMPf7gKB7Haff1SX0ah1UZJ3XT1XQTsLYcRdGcdm/y9PjewUH87Gj276/r6OQ0nj75II6fHXfPt9hOquV4FM1kurLd43Yad7ZwbaX/O3ra1bRzJ52HeFFN32/6vn9SNfHz0/WazLPJaTx6UpyN/2IczqtFM+3G7fFZrgIAAAAAAAAAAAAAAAAgevp/2dsONs8JxOUAAAAASUVORK5CYII=" alt=""><img class="chLau" src="data:image/png;base64,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" alt=""></div></div>';
|
|
5348
5379
|
function clawdHeroHTML(agent){
|
|
5349
5380
|
const app = connectedAppMeta(agent);
|
|
5381
|
+
// Codex: wordmark + status only — no Clawd mascot (that's a Claude Code brand
|
|
5382
|
+
// mark, §6.5) and no glyph badge either (Masa 2026-07-16: 「codexの時は文字だけでよい」).
|
|
5350
5383
|
const stage = agent === 'codex'
|
|
5351
|
-
?
|
|
5384
|
+
? ''
|
|
5352
5385
|
: `<div class="cstage"><div class="chConf"></div><div class="cw">${CLAWD_HERO_IMGS}`;
|
|
5353
|
-
|
|
5386
|
+
// .hstat: the status line is the ONLY element renderClawdHero()/bootDisconnected()
|
|
5387
|
+
// may rewrite — never target it by bare `span` (the codex badge used to carry an
|
|
5388
|
+
// .appglyph <span>, so querySelector('span') clobbered the glyph with the status).
|
|
5389
|
+
return stage + `<b>${esc(app.label)}</b><span class="hstat">CONNECTED · WAITING FOR GOALS</span>`;
|
|
5354
5390
|
}
|
|
5355
5391
|
function spawnHeroConfetti(){
|
|
5356
5392
|
const conf = document.querySelector('#fsClawdHero .chConf'); if (!conf) return;
|
|
@@ -5371,9 +5407,9 @@ function spawnHeroConfetti(){
|
|
|
5371
5407
|
}
|
|
5372
5408
|
function renderClawdHero(){
|
|
5373
5409
|
const el = $('fsClawdHero'); if (!el) return;
|
|
5374
|
-
const prevStatus = el.querySelector('
|
|
5410
|
+
const prevStatus = el.querySelector('.hstat')?.textContent;
|
|
5375
5411
|
el.innerHTML = clawdHeroHTML(state.connectedApp);
|
|
5376
|
-
if (prevStatus) { const s = el.querySelector('
|
|
5412
|
+
if (prevStatus) { const s = el.querySelector('.hstat'); if (s) s.textContent = prevStatus; }
|
|
5377
5413
|
if (state.connectedApp !== 'codex') spawnHeroConfetti(); // Welcome P1 confetti (H17 item 4)
|
|
5378
5414
|
}
|
|
5379
5415
|
// one-time static wiring for the flagship shell (folds, lane resize, rail menu, band picker)
|
|
@@ -5437,10 +5473,15 @@ function renderClawdHero(){
|
|
|
5437
5473
|
const defaultLaneW = 372, defaultRailW = 108, minReadableCenterW = 640;
|
|
5438
5474
|
const canRestoreFlagshipWidths = (railW, laneW) =>
|
|
5439
5475
|
window.innerWidth - 28 - 24 - railW - laneW >= minReadableCenterW;
|
|
5476
|
+
// rail's own live max: however wide it can go without crushing the center column below
|
|
5477
|
+
// minReadableCenterW, given however wide the lane currently is — no more flat 280px ceiling
|
|
5478
|
+
// that stopped rightward drags well short of the window edge (Masa 2026-07-16: "もっと広げ
|
|
5479
|
+
// られるようにできる?"). Floors at 72 (the min) on a narrow/laned-out window.
|
|
5480
|
+
const railMax = () => Math.max(72, window.innerWidth - 28 - 24 - lane.getBoundingClientRect().width - minReadableCenterW);
|
|
5440
5481
|
const saved = Number(localStorage.getItem('fsLaneW'));
|
|
5441
5482
|
const savedRail = Number(localStorage.getItem('fsRailW'));
|
|
5442
5483
|
const restoreLane = saved >= 280 && saved <= 540;
|
|
5443
|
-
const restoreRail = savedRail >= 72 && savedRail <=
|
|
5484
|
+
const restoreRail = savedRail >= 72 && savedRail <= railMax();
|
|
5444
5485
|
if (restoreLane && canRestoreFlagshipWidths(restoreRail ? savedRail : defaultRailW, saved)) {
|
|
5445
5486
|
root.style.setProperty('--lanew', `${saved}px`);
|
|
5446
5487
|
}
|
|
@@ -5456,16 +5497,17 @@ function renderClawdHero(){
|
|
|
5456
5497
|
drag = false; lane.classList.remove('noanim');
|
|
5457
5498
|
localStorage.setItem('fsLaneW', String(parseInt(root.style.getPropertyValue('--lanew')) || 372));
|
|
5458
5499
|
});
|
|
5459
|
-
// rail width drag:
|
|
5500
|
+
// rail width drag: 72px – railMax() (freely widens, only bounded by keeping the center column
|
|
5501
|
+
// readable), persisted (flagship .railgrip port, v36 JS; grip hides when folded)
|
|
5460
5502
|
const rail = root.querySelector('.fsrail'), rgrip = $('fsRgrip');
|
|
5461
|
-
if (restoreRail &&
|
|
5503
|
+
if (restoreRail && canRestoreFlagshipWidths(savedRail, restoreLane ? saved : defaultLaneW)) {
|
|
5462
5504
|
root.style.setProperty('--railw', `${savedRail}px`);
|
|
5463
5505
|
}
|
|
5464
5506
|
let rsx = 0, rsw = 0, rdrag = false;
|
|
5465
5507
|
rgrip.addEventListener('mousedown', (e) => { rdrag = true; rsx = e.clientX; rsw = rail.getBoundingClientRect().width; rail.classList.add('noanim'); e.preventDefault(); });
|
|
5466
5508
|
document.addEventListener('mousemove', (e) => {
|
|
5467
5509
|
if (!rdrag) return;
|
|
5468
|
-
const w = Math.max(72, Math.min(
|
|
5510
|
+
const w = Math.max(72, Math.min(railMax(), rsw + (e.clientX - rsx)));
|
|
5469
5511
|
root.style.setProperty('--railw', `${w}px`);
|
|
5470
5512
|
});
|
|
5471
5513
|
document.addEventListener('mouseup', () => {
|
|
@@ -5756,7 +5798,7 @@ function bootDisconnected(info){
|
|
|
5756
5798
|
state.active = '__onboard';
|
|
5757
5799
|
state.billing = { licensed: false, email };
|
|
5758
5800
|
try { render(); } catch (e) { console.warn('disconnected render', e); }
|
|
5759
|
-
const heroSpan = document.querySelector('#fsClawdHero
|
|
5801
|
+
const heroSpan = document.querySelector('#fsClawdHero .hstat');
|
|
5760
5802
|
if (heroSpan) heroSpan.textContent = 'CONNECT YOUR MACHINE TO BEGIN';
|
|
5761
5803
|
|
|
5762
5804
|
const style = document.createElement('style');
|
|
@@ -5827,7 +5869,17 @@ function bootDisconnected(info){
|
|
|
5827
5869
|
const es = new EventSource('/presence');
|
|
5828
5870
|
es.addEventListener('presence', (e) => {
|
|
5829
5871
|
let d; try { d = JSON.parse(e.data); } catch { return; }
|
|
5830
|
-
if (d.state === 'connected') {
|
|
5872
|
+
if (d.state === 'connected') {
|
|
5873
|
+
// Confirm the engine is REALLY reachable before reloading. A stale/spurious
|
|
5874
|
+
// 'connected' presence otherwise reloads us into a white flash that lands
|
|
5875
|
+
// right back on this same connect screen — the welcome animation plays, then
|
|
5876
|
+
// 一瞬真っ白 (Masa 2026-07-16). Reuse the proven 6d71c32 contract: the relay
|
|
5877
|
+
// 503s every engine-backed path while offline and 200s once proxied, so
|
|
5878
|
+
// status !== 503 flips exactly at a genuine connect.
|
|
5879
|
+
fetch('/api/state', { cache: 'no-store', credentials: 'same-origin' })
|
|
5880
|
+
.then((r) => { if (r.status !== 503) { es.close(); location.reload(); } })
|
|
5881
|
+
.catch(() => {}); // engine unreachable → not connected yet; keep waiting
|
|
5882
|
+
}
|
|
5831
5883
|
else if (cgWait) cgWait.innerHTML = '<i></i>WAITING FOR THIS DEVICE';
|
|
5832
5884
|
});
|
|
5833
5885
|
window.addEventListener('beforeunload', () => { try { es.close(); } catch {} });
|
|
@@ -5891,6 +5943,19 @@ async function postOutboxItem(item){
|
|
|
5891
5943
|
}
|
|
5892
5944
|
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
5893
5945
|
const goal = await r.json();
|
|
5946
|
+
if (goal.kind === 'chat') {
|
|
5947
|
+
// Conversation-first composer (SLICE 2): this was a chat/help/settings
|
|
5948
|
+
// turn, not a To-Do. Retire the optimistic outbox item — a chat turn is
|
|
5949
|
+
// ephemeral by design, so dropping it does NOT weaken the never-lose
|
|
5950
|
+
// guarantee (which protects real tasks/goals). Show the reply quietly
|
|
5951
|
+
// above the input. We must NOT reach land()/upsert(): a kind:'chat'
|
|
5952
|
+
// response carries no goal id, so upserting it would wedge a ghost
|
|
5953
|
+
// "Sending…" row in the queue forever and pollute state.goals.
|
|
5954
|
+
outbox.remove(item.oid);
|
|
5955
|
+
showChatReply(goal.reply);
|
|
5956
|
+
render();
|
|
5957
|
+
return; // finally still runs posting.delete(); skip land()/upsert()
|
|
5958
|
+
}
|
|
5894
5959
|
goal.imageUrls = item.imageUrls;
|
|
5895
5960
|
outbox.land(item.oid, goal.id);
|
|
5896
5961
|
upsert(state.goals, goal);
|
|
@@ -5902,6 +5967,17 @@ async function postOutboxItem(item){
|
|
|
5902
5967
|
render();
|
|
5903
5968
|
}
|
|
5904
5969
|
|
|
5970
|
+
// SLICE 2: render / clear the conversation-first composer reply (reuses the
|
|
5971
|
+
// .peraanswer surface). Quiet, transient — cleared the moment the user sends
|
|
5972
|
+
// the next message so a stale reply never lingers above a new turn.
|
|
5973
|
+
function showChatReply(text){
|
|
5974
|
+
const el = $('chatReply'); if (!el) return;
|
|
5975
|
+
const t = (text || '').trim();
|
|
5976
|
+
if (!t) { el.hidden = true; el.textContent = ''; return; }
|
|
5977
|
+
el.className = 'peraanswer'; el.textContent = t; el.hidden = false;
|
|
5978
|
+
}
|
|
5979
|
+
function clearChatReply(){ const el = $('chatReply'); if (el) { el.hidden = true; el.textContent = ''; } }
|
|
5980
|
+
|
|
5905
5981
|
async function flushOutbox(){
|
|
5906
5982
|
// Only (re)send items that still need it — never re-POST 'landed' memos that
|
|
5907
5983
|
// are merely waiting for reconcile() (that was the double-send path).
|
|
@@ -5978,6 +6054,7 @@ async function send(){
|
|
|
5978
6054
|
renderMsgHint();
|
|
5979
6055
|
|
|
5980
6056
|
$('input').value = ''; $('input').style.height = 'auto';
|
|
6057
|
+
clearChatReply(); // a new turn starts — drop any prior conversational reply
|
|
5981
6058
|
closePalette();
|
|
5982
6059
|
localStorage.removeItem('draft');
|
|
5983
6060
|
renderAttach(); render();
|
|
@@ -6738,11 +6815,27 @@ for (const b of $('langSeg').querySelectorAll('button')) b.onclick = () => saveL
|
|
|
6738
6815
|
// true (Delete) or false (Cancel/backdrop-click/Escape) — every caller `await`s it, never
|
|
6739
6816
|
// deletes on native confirm() any more.
|
|
6740
6817
|
let _delConfirmResolve = null;
|
|
6741
|
-
|
|
6818
|
+
// dismissValue: what backdrop-click/Escape resolve to. Defaults to false (= the cancelLabel
|
|
6819
|
+
// button's own value) for every existing delete-confirmation caller, where that's the safe/no-op
|
|
6820
|
+
// outcome. custCancel() (Discard/Save on the customize panel) overrides this to true/"save",
|
|
6821
|
+
// because there Discard — not Save — is the LEFT/cancelLabel-slot button (Masa 2026-07-16: wants
|
|
6822
|
+
// Discard positioned left); dismissing the dialog must still land on the non-destructive choice
|
|
6823
|
+
// regardless of which label happens to sit in which slot.
|
|
6824
|
+
let _delDismissValue = false;
|
|
6825
|
+
function confirmDelete({ title, body, preview, confirmLabel, cancelLabel, dismissValue = false, confirmDanger = true, cancelDanger = false } = {}){
|
|
6742
6826
|
$('delModalTitle').textContent = title || 'Delete this goal?';
|
|
6743
6827
|
$('delModalBody').textContent = body || "This can't be undone.";
|
|
6828
|
+
$('delConfirmBtn').textContent = confirmLabel || 'Delete';
|
|
6829
|
+
$('delCancelBtn').textContent = cancelLabel || 'Cancel';
|
|
6830
|
+
// confirmLabel is danger-red by default (every existing caller is a real delete); cancelLabel
|
|
6831
|
+
// is plain ghost by default. custCancel() flips both — Discard (danger-red) sits in the
|
|
6832
|
+
// cancelLabel slot, Save (plain) in the confirmLabel slot — since Discard is the one Masa
|
|
6833
|
+
// wants positioned left, not the one that happens to be "the button confirmDelete() calls confirm".
|
|
6834
|
+
$('delConfirmBtn').className = 'mbtn' + (confirmDanger ? ' danger' : ' primary');
|
|
6835
|
+
$('delCancelBtn').className = 'mbtn' + (cancelDanger ? ' danger' : ' ghost');
|
|
6744
6836
|
const pv = $('delPreviewTitle');
|
|
6745
6837
|
if (preview) { pv.textContent = preview; pv.style.display = ''; } else { pv.textContent = ''; pv.style.display = 'none'; }
|
|
6838
|
+
_delDismissValue = dismissValue;
|
|
6746
6839
|
$('deleteConfirmOverlay').classList.add('show');
|
|
6747
6840
|
return new Promise((resolve) => { _delConfirmResolve = resolve; });
|
|
6748
6841
|
}
|
|
@@ -6754,8 +6847,8 @@ function closeDeleteConfirm(result){
|
|
|
6754
6847
|
}
|
|
6755
6848
|
$('delCancelBtn').onclick = () => closeDeleteConfirm(false);
|
|
6756
6849
|
$('delConfirmBtn').onclick = () => closeDeleteConfirm(true);
|
|
6757
|
-
$('deleteConfirmOverlay').addEventListener('click', (e) => { if (e.target === $('deleteConfirmOverlay')) closeDeleteConfirm(
|
|
6758
|
-
document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && $('deleteConfirmOverlay').classList.contains('show')) closeDeleteConfirm(
|
|
6850
|
+
$('deleteConfirmOverlay').addEventListener('click', (e) => { if (e.target === $('deleteConfirmOverlay')) closeDeleteConfirm(_delDismissValue); });
|
|
6851
|
+
document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && $('deleteConfirmOverlay').classList.contains('show')) closeDeleteConfirm(_delDismissValue); });
|
|
6759
6852
|
$('input').addEventListener('keydown', (e) => {
|
|
6760
6853
|
// Palette navigation takes priority while it is open (Claude-Code keys).
|
|
6761
6854
|
if (state.palette && !e.isComposing) {
|
|
@@ -7672,7 +7765,37 @@ function custSave(){
|
|
|
7672
7765
|
set('tbAnim', cust.tban); set('tbColor', cust.tbc); // logoPal no longer persisted — logo is code-fixed (v45 §3)
|
|
7673
7766
|
custSnap = null; $('gearPop').classList.remove('open');
|
|
7674
7767
|
}
|
|
7675
|
-
function
|
|
7768
|
+
function custHasUnsavedChanges(){
|
|
7769
|
+
if (!custSnap) return false;
|
|
7770
|
+
if (state.theme !== custSnap.theme || fsUI.band !== custSnap.band) return true;
|
|
7771
|
+
return JSON.stringify(cust) !== JSON.stringify(custSnap.cust);
|
|
7772
|
+
}
|
|
7773
|
+
// Discarding used to happen instantly on any outside tap / Esc / gear re-click — one careless
|
|
7774
|
+
// click outside the panel silently threw away everything just picked (Masa 2026-07-16: "外枠
|
|
7775
|
+
// タップしたら一瞬で元に戻るのがストレスだから"). Now it only reverts after confirmation, reusing
|
|
7776
|
+
// the same Slack-style modal as goal delete (confirmDelete), copy in the project's own review
|
|
7777
|
+
// language (reviewLangFor) since this is the one UI dialog whose audience is the user, not a
|
|
7778
|
+
// worker's report. _custCancelling guards re-entrancy: the confirm modal's own Escape/backdrop
|
|
7779
|
+
// handlers can re-trigger this same Escape/outside-tap listener while it's already awaiting.
|
|
7780
|
+
let _custCancelling = false;
|
|
7781
|
+
async function custCancel(){
|
|
7782
|
+
if (_custCancelling) return;
|
|
7783
|
+
if (custHasUnsavedChanges()) {
|
|
7784
|
+
_custCancelling = true;
|
|
7785
|
+
const ja = reviewLangFor(state.active) === 'ja';
|
|
7786
|
+
// Discard sits LEFT (Masa 2026-07-16, 2nd pass — mirrors the macOS "unsaved changes"
|
|
7787
|
+
// layout). Because Discard, not Save, occupies the cancelLabel slot this time, backdrop
|
|
7788
|
+
// click/Escape must be told explicitly to resolve as Save (dismissValue: true) — the shared
|
|
7789
|
+
// default (false = whatever's in the cancelLabel slot) would otherwise silently discard on
|
|
7790
|
+
// an accidental Escape/outside-click on the CONFIRM DIALOG ITSELF, the exact class of bug
|
|
7791
|
+
// this whole feature exists to prevent.
|
|
7792
|
+
const saveChosen = await confirmDelete(ja
|
|
7793
|
+
? { title: '変更を保存しますか?', body: '配色・テーマの変更が保存されていません。', confirmLabel: '保存', cancelLabel: '破棄', confirmDanger: false, cancelDanger: true, dismissValue: true }
|
|
7794
|
+
: { title: 'Save your changes?', body: "Your color and theme customizations haven't been saved.", confirmLabel: 'Save', cancelLabel: 'Discard', confirmDanger: false, cancelDanger: true, dismissValue: true });
|
|
7795
|
+
_custCancelling = false;
|
|
7796
|
+
if (saveChosen) { custSave(); return; }
|
|
7797
|
+
// else: Discard was explicitly clicked (backdrop/Escape can no longer produce this) — revert below.
|
|
7798
|
+
}
|
|
7676
7799
|
if (custSnap) { Object.assign(cust, custSnap.cust); state.theme = custSnap.theme; fsUI.band = custSnap.band; custSnap = null; custApplyAll(); }
|
|
7677
7800
|
$('gearPop').classList.remove('open');
|
|
7678
7801
|
}
|
package/engine/lib.mjs
CHANGED
|
@@ -708,31 +708,71 @@ export function trimTaskActivityForState(tasks) {
|
|
|
708
708
|
}
|
|
709
709
|
|
|
710
710
|
// PROOF.md for a goal PR: one section per verified task, GIF auto-plays.
|
|
711
|
-
|
|
711
|
+
// `lang` picks a single language (matches the goal's own request language,
|
|
712
|
+
// same detection as the PR body) instead of the old EN+JA bilingual dump —
|
|
713
|
+
// a proof doc that mixes both isn't "in the requester's language".
|
|
714
|
+
export function buildGoalProofMd({ goalText, items, lang = 'ja' }) {
|
|
715
|
+
const L = (en, ja) => (lang === 'en' ? en : ja);
|
|
712
716
|
return [
|
|
713
|
-
|
|
717
|
+
L('# Proof', '# 動作確認'),
|
|
714
718
|
``,
|
|
715
|
-
|
|
719
|
+
`${L('Goal', 'ゴール')}: ${String(goalText ?? '').replace(/\s+/g, ' ').slice(0, 300)}`,
|
|
716
720
|
``,
|
|
717
|
-
|
|
718
|
-
|
|
721
|
+
L('Each task below was verified independently by Manager for AI in a headless browser (deterministic check, no LLM at verification time).',
|
|
722
|
+
'以下の各タスクは Manager for AI が headless ブラウザで独立検証しました(検証はdeterministic・検証時にLLMは使いません)。'),
|
|
719
723
|
``,
|
|
720
724
|
...items.flatMap((it) => [
|
|
721
725
|
`## ${it.num} ${it.title}`,
|
|
722
726
|
``,
|
|
723
|
-
|
|
727
|
+
`${L('Pass condition', '成功条件')}: ${it.passCondition}`,
|
|
724
728
|
``,
|
|
725
|
-
it.beforeShotName ?
|
|
726
|
-
it.beforeShotName ?
|
|
729
|
+
it.beforeShotName ? `${L('Before', '変更前')}:\n\n\n` : '',
|
|
730
|
+
it.beforeShotName ? `${L('After', '変更後')}:\n` : '',
|
|
727
731
|
it.gifName ? `\n` : '',
|
|
728
732
|
it.shotName ? `\n` : '',
|
|
729
|
-
it.videoName ?
|
|
730
|
-
|
|
733
|
+
it.videoName ? `${L('Full-quality video', '高画質mp4')}: [${it.videoName}](./${it.videoName})\n` : '',
|
|
734
|
+
`${L('Result', '結果')}: ${it.detail}`,
|
|
731
735
|
``,
|
|
732
736
|
]),
|
|
733
737
|
].filter((l) => l !== '').join('\n');
|
|
734
738
|
}
|
|
735
739
|
|
|
740
|
+
// PR body for a finished goal (Masa 2026-07-16: "1時間後に見ても一目で分かる"):
|
|
741
|
+
// the plain-language "what to check / what changed" headline (already
|
|
742
|
+
// generated per-goal by summarizeForReview, same request-language `lang`)
|
|
743
|
+
// goes FIRST, above proof/goal/task detail, so a reviewer returning later
|
|
744
|
+
// doesn't have to re-read the raw worker log to know what to look at.
|
|
745
|
+
export function buildGoalPrBody({ lang, goal, goalTasks, files, owner, branch, proofRel, proofItemsCount, reviewDescription, reviewSummary }) {
|
|
746
|
+
const L = (en, ja) => (lang === 'en' ? en : ja);
|
|
747
|
+
return [
|
|
748
|
+
...((reviewSummary?.check || reviewSummary?.changed) ? [
|
|
749
|
+
L('## Check first', '## 確認ポイント'),
|
|
750
|
+
'',
|
|
751
|
+
...(reviewSummary.check ? [L(`Check: ${reviewSummary.check}`, `確認: ${reviewSummary.check}`)] : []),
|
|
752
|
+
...(reviewSummary.changed ? [L(`Changed: ${reviewSummary.changed}`, `変更: ${reviewSummary.changed}`)] : []),
|
|
753
|
+
'',
|
|
754
|
+
] : []),
|
|
755
|
+
...(proofItemsCount && owner ? [
|
|
756
|
+
L('## Proof — one tap, plays in place', '## 動作確認 — 1タップでその場で再生'),
|
|
757
|
+
'',
|
|
758
|
+
`https://github.com/${owner}/blob/${branch}/${proofRel}/PROOF.md`,
|
|
759
|
+
'',
|
|
760
|
+
] : []),
|
|
761
|
+
L('## Goal', '## ゴール'),
|
|
762
|
+
'',
|
|
763
|
+
goal.text.slice(0, 1500),
|
|
764
|
+
'',
|
|
765
|
+
...buildReviewRuleSection(reviewDescription),
|
|
766
|
+
L(`Implemented by Manager for AI: the goal was decomposed into ${goalTasks.length} task(s), each executed by a Claude Code worker (flat-rate, no extra API), tests written and run per task.${proofItemsCount ? ' Tasks with a pass condition were verified independently in a headless browser.' : ''}`,
|
|
767
|
+
`Manager for AI が実装: ゴールを${goalTasks.length}個のタスクに分解し、Claude Code worker が順に実行(既存サブスク・追加API無し・タスク毎にテストを作成し実行)。${proofItemsCount ? '成功条件つきタスクは headless ブラウザで独立検証済み。' : ''}`),
|
|
768
|
+
'',
|
|
769
|
+
L('## Tasks and worker reports', '## タスクとworker報告'),
|
|
770
|
+
...goalTasks.map((t) => `\n### ${t.num} ${t.title} — ${t.status} (${t.secs}s)\n${(t.result ?? '').slice(0, 800)}`),
|
|
771
|
+
'',
|
|
772
|
+
L(`Changed files: ${files.map((f) => `\`${f}\``).join(' ')}`, `変更ファイル: ${files.map((f) => `\`${f}\``).join(' ')}`),
|
|
773
|
+
].join('\n');
|
|
774
|
+
}
|
|
775
|
+
|
|
736
776
|
// A finished task's changed files look like a UI edit: app/ (the Manager's
|
|
737
777
|
// own UI dir, or an equivalent frontend root) plus common frontend source
|
|
738
778
|
// extensions. Pure/deterministic — no LLM — so it can gate proof capture
|
|
@@ -1130,6 +1170,72 @@ export function detectRequestLanguage(text) {
|
|
|
1130
1170
|
return /[-ヿ㐀-鿿]/.test(text || '') ? 'ja' : 'en';
|
|
1131
1171
|
}
|
|
1132
1172
|
|
|
1173
|
+
// SLICE 2 intent routing (docs/HANDOFF-ONBOARDING-conversational.md): the single
|
|
1174
|
+
// composer is conversation-first — a chat/help/settings message ("what can you
|
|
1175
|
+
// do?", "reply in English", a greeting) must get a conversational reply, NOT a
|
|
1176
|
+
// To-Do; only implementation requests become goals. These are the pure,
|
|
1177
|
+
// testable pieces; the server wires them into POST /api/tasks.
|
|
1178
|
+
//
|
|
1179
|
+
// A dependency-free heuristic that short-circuits only the OBVIOUS cases so most
|
|
1180
|
+
// sends skip the LLM classifier:
|
|
1181
|
+
// 'task' — a clear implementation request (imperative verb / code token) and
|
|
1182
|
+
// no chat signal → skip the LLM, go straight to goal creation.
|
|
1183
|
+
// 'chat' — a clear help/settings/greeting message and no task signal.
|
|
1184
|
+
// 'unsure' — ambiguous (both signals, or neither) → defer to the LLM.
|
|
1185
|
+
// It is deliberately conservative about declaring 'task' (the only branch that
|
|
1186
|
+
// skips the LLM): a message carrying BOTH a task verb and a help/settings signal
|
|
1187
|
+
// is 'unsure', not 'task', so a help question phrased with "add"/"fix" isn't
|
|
1188
|
+
// silently turned into a goal.
|
|
1189
|
+
const INTENT_TASK_RE = /\b(fix|add|implement|create|build|refactor|remove|delete|rename|migrate|debug|integrate|revert|deploy)\b|実装|直し|直す|なおして|修正|作って|作成|追加|変更|削除|対応して|リファクタ|置き換え|バグ|不具合/i;
|
|
1190
|
+
const INTENT_CODE_RE = /\.(?:js|mjs|cjs|ts|tsx|jsx|css|scss|html|py|go|rs|java|rb|php|json|md|yml|yaml|sh|sql)\b|`[^`]+`|\b(?:function|const|class|import)\b|#\d+/;
|
|
1191
|
+
const INTENT_CHAT_META_RE = /何ができ|なにができ|何が出来|使い方|どう使|どうやって使|使える\?|説明して|教えて(?:ください|くれ|ほしい)?\s*[??]?$|about this (?:tool|app)|what (?:can|do|are|is|does) (?:you|it|this|i)|what'?s this|what can this|how (?:do|does|can) (?:i|this|you|it)|how to use|who are you|^\s*help\s*[??]?$|そもそも|初めて/i;
|
|
1192
|
+
const INTENT_CHAT_SETTINGS_RE = /英語で|日本語で|(?:reply|respond|answer|talk|speak|write)\s+(?:back\s+)?in\s+\w|語で(?:返|答|話|書)|言語/i;
|
|
1193
|
+
const INTENT_GREETING_RE = /^\s*(?:hi|hey|hello|yo|sup|thanks|thank you|thx|good (?:morning|evening|afternoon))\b[\s.!?]*$|^\s*(?:こんにち(?:は|わ)|こんばん(?:は|わ)|おはよう|はじめまして|やあ|ありがと(?:う)?|よろしく)[\s。!?!?]*$/i;
|
|
1194
|
+
|
|
1195
|
+
export function classifyComposerIntentHeuristic(text) {
|
|
1196
|
+
const t = (text || '').trim();
|
|
1197
|
+
if (!t) return 'chat';
|
|
1198
|
+
const hasTask = INTENT_TASK_RE.test(t) || INTENT_CODE_RE.test(t);
|
|
1199
|
+
const hasChat = INTENT_CHAT_META_RE.test(t) || INTENT_CHAT_SETTINGS_RE.test(t) || INTENT_GREETING_RE.test(t);
|
|
1200
|
+
if (hasTask && !hasChat) return 'task'; // pure implementation request → skip LLM
|
|
1201
|
+
if (hasChat && !hasTask) return 'chat'; // pure help/settings/greeting
|
|
1202
|
+
return 'unsure'; // both or neither → let the LLM decide
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// The one prompt that BOTH classifies and (for chat) writes the reply in the
|
|
1206
|
+
// requester's language — one Haiku round-trip, not two. The model returns a
|
|
1207
|
+
// single line of JSON: {"intent":"chat|task","reply":"…"}.
|
|
1208
|
+
export function buildIntentPrompt(text, lang) {
|
|
1209
|
+
const language = lang === 'ja' ? '日本語' : 'English';
|
|
1210
|
+
return [
|
|
1211
|
+
'You are the intent router for Agent Manager: a tool where a user delegates a coding task in plain language, an AI implements it, and it comes back as a reviewed pull request with proof.',
|
|
1212
|
+
'Classify the user message below into exactly one intent:',
|
|
1213
|
+
'- "task": a request to implement, change, fix, build, or remove something in code (even if vague, e.g. "add dark mode").',
|
|
1214
|
+
'- "chat": anything that is NOT an implementation request — questions about what this tool can do or how to use it, greetings, thanks, small talk, or a settings/preference request (e.g. "reply in English").',
|
|
1215
|
+
'When in doubt between the two, prefer "task".',
|
|
1216
|
+
`If intent is "chat", also write "reply": a short, warm, helpful answer in ${language} (at most 4 sentences). If the user asks what this can do or how to start, briefly explain: describe the change you want in plain language, and it will ask which folder/repo to work in, implement it, and return a PR with proof. If intent is "task", set "reply" to "".`,
|
|
1217
|
+
'Output ONLY one line of raw JSON with keys "intent" and "reply". No markdown, no code fence, no extra text.',
|
|
1218
|
+
'',
|
|
1219
|
+
'User message:',
|
|
1220
|
+
String(text || '').slice(0, 4000),
|
|
1221
|
+
].join('\n');
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// Tolerant parser for the router's reply — never throws. Extracts the first JSON
|
|
1225
|
+
// object; on any failure it fails safe to 'task' (so a garbled classification
|
|
1226
|
+
// can never silently swallow a real implementation request). Reply is clamped.
|
|
1227
|
+
export function parseIntentResponse(raw) {
|
|
1228
|
+
const fallback = { intent: 'task', reply: '' };
|
|
1229
|
+
if (!raw || typeof raw !== 'string') return fallback;
|
|
1230
|
+
const m = raw.match(/\{[\s\S]*\}/);
|
|
1231
|
+
if (!m) return fallback;
|
|
1232
|
+
let obj;
|
|
1233
|
+
try { obj = JSON.parse(m[0]); } catch { return fallback; }
|
|
1234
|
+
const intent = obj && obj.intent === 'chat' ? 'chat' : 'task';
|
|
1235
|
+
const reply = intent === 'chat' && typeof obj.reply === 'string' ? obj.reply.trim().slice(0, 2000) : '';
|
|
1236
|
+
return { intent, reply };
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1133
1239
|
// Normalize a nested boolean-flag object (prSections/reviewCard) key by key,
|
|
1134
1240
|
// so a partial payload — or one missing the whole sub-object — can't
|
|
1135
1241
|
// silently disable flags it never mentioned; each key falls back to its own
|
|
@@ -2485,6 +2591,56 @@ export function resolveFolderAnswer(answer, folders = []) {
|
|
|
2485
2591
|
return null;
|
|
2486
2592
|
}
|
|
2487
2593
|
|
|
2594
|
+
// Onboarding funnel, part 2: the user picked "somewhere else…" and typed a path
|
|
2595
|
+
// that ISN'T already a git repo. Before, the server just re-asked the same
|
|
2596
|
+
// folder question forever (a repo-less / vibe-coder user could never get past
|
|
2597
|
+
// setup). This decides what to offer instead. All filesystem probes (does it
|
|
2598
|
+
// exist / is it a directory / is it already a repo) are done by the CALLER and
|
|
2599
|
+
// passed in, so this stays pure/testable. Returns:
|
|
2600
|
+
// { action: 'use' } already a repo → bind as-is
|
|
2601
|
+
// { action: 'init', create: false } existing non-repo dir → offer `git init` here
|
|
2602
|
+
// { action: 'init', create: true } path doesn't exist yet → offer to create it + `git init`
|
|
2603
|
+
// { action: 'reject', reason } HOME (never a work root) or a path that is a file → re-ask
|
|
2604
|
+
export function classifyFolderTarget({ dir, homeDir, exists, isDirectory, isRepo } = {}) {
|
|
2605
|
+
if (!dir) return { action: 'reject', reason: 'empty' };
|
|
2606
|
+
const norm = (p) => String(p).replace(/\/+$/, '');
|
|
2607
|
+
if (homeDir && norm(dir) === norm(homeDir)) return { action: 'reject', reason: 'home' };
|
|
2608
|
+
if (isRepo) return { action: 'use' };
|
|
2609
|
+
if (!exists) return { action: 'init', create: true };
|
|
2610
|
+
if (!isDirectory) return { action: 'reject', reason: 'not-a-dir' };
|
|
2611
|
+
return { action: 'init', create: false };
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
// Build the confirm card shown before we `git init` a non-repo folder. Reuses
|
|
2615
|
+
// the same needsInput card shape ({text, options:string[]}) but tags it
|
|
2616
|
+
// kind:'folder-init' so the /answer handler runs the init (instead of another
|
|
2617
|
+
// folder rebind). `create` = the folder doesn't exist yet and we'll make it.
|
|
2618
|
+
// The chosen dir + create flag ride along on the question so the handler is
|
|
2619
|
+
// authoritative about WHAT it's initializing (never re-derived from free text).
|
|
2620
|
+
export function buildFolderInitConfirm(dir, { lang = 'ja', create = false, homeDir = '' } = {}) {
|
|
2621
|
+
const label = folderLabel(dir, homeDir);
|
|
2622
|
+
const yes = lang === 'en'
|
|
2623
|
+
? (create ? 'Create it and start a new project here' : 'Start a new project here')
|
|
2624
|
+
: (create ? '作成して、ここで新規プロジェクトを始める' : 'ここで新規プロジェクトを始める');
|
|
2625
|
+
const no = lang === 'en' ? 'Pick another folder…' : '別の場所を選ぶ…';
|
|
2626
|
+
const text = lang === 'en'
|
|
2627
|
+
? `“${label}” isn’t a Git repository yet. ${create ? 'Create it and start' : 'Start'} a new project here (git init)? Your existing files won’t be touched.`
|
|
2628
|
+
: `「${label}」はまだGitリポジトリではありません。${create ? '作成して、' : ''}ここで新しいプロジェクトを始めますか?(git init・既存のファイルには手を触れません)`;
|
|
2629
|
+
return { text, options: [yes, no], kind: 'folder-init', lang, dir, create, yes, no };
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
// Interpret the confirm-card answer → true (do the init) / false (decline →
|
|
2633
|
+
// re-ask the folder question). Match the exact "yes" option we offered, or a
|
|
2634
|
+
// plain affirmative word; anything else (incl. the "pick another folder" option)
|
|
2635
|
+
// declines. Defaults to decline on empty/unknown so we never init by accident.
|
|
2636
|
+
export function isFolderInitConfirmed(answer, question = {}) {
|
|
2637
|
+
const a = String(answer ?? '').trim();
|
|
2638
|
+
if (!a) return false;
|
|
2639
|
+
if (question.yes && a === question.yes) return true;
|
|
2640
|
+
if (question.no && a === question.no) return false;
|
|
2641
|
+
return /^(yes|y|はい|ok|する|始める)$/i.test(a);
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2488
2644
|
// Analytics identity (A+): a signed-in install scopes usage events to the
|
|
2489
2645
|
// ACCOUNT (its email, which the billing Worker hashes into the same account_id
|
|
2490
2646
|
// the accounts table carries) so per-user app activity joins WITHOUT a cookie;
|
package/engine/server.mjs
CHANGED
|
@@ -20,8 +20,8 @@ import { resolve, dirname, join, basename } from 'node:path';
|
|
|
20
20
|
import { homedir } from 'node:os';
|
|
21
21
|
import { fileURLToPath } from 'node:url';
|
|
22
22
|
import { pathToFileURL } from 'node:url';
|
|
23
|
-
import { needsProjectFolder, folderLabel, buildFolderQuestion, resolveFolderAnswer } from './lib.mjs';
|
|
24
|
-
import { parseStreamEvents, parsePlan, refinePlanTasks, findOverlappingGoal, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, isForcedStop, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, buildGoalProofMd, buildReviewRuleSection, nextGoalStatus, isPrMerged, isPrApproved, reviewToDoneStatus, advanceReviewGoal, revertReviewGoal, approveGoal, dismissGoal, permissionModeFor, isPlanReview, nextAfterPlanApprove, GOAL_MODES, parseSkillFrontmatter, collectSkills, retestGoal, cancelRetestGoal, computeRetestOutcome, revertGoal, planRevertActions, archiveGoal, unarchiveGoal, undismissGoal, parseNumstat, truncateDiffText, sumUsage, resolveEntryUrl, parseGitLog, diffNewCommits, replayQueueLog, reconcileOrphanGoals, reorderQueue, sortQueueByPriority, TASK_PRIORITIES, validateWorkflowColumns, DEFAULT_WORKFLOW_COLUMNS, validateReviewDefinition, DEFAULT_REVIEW_DEFINITION, buildEphemeralSeedLog, buildEphemeralSeedProjects, trimTaskActivityForState, buildAskContext, WORKER_TOOLS, verifyCfAccessJwt, resolveIdentity, goalVisibleTo, clampParallelLimit, canStartMore, nextRunnableTasks, goalsConflict, detectConflicts, isUiChange, shouldCaptureProof, shouldCaptureBaseline, hasTestRelevantChanges, buildExecutionPlan, buildContextHandoffSummary, buildFailurePostmortem, appendFailureMemory, latestFailurePolicy, classifyChangeRisk, checkRunBudget, usageBudgetTokens, isRateLimited, nextResumeDelay, checkFreeTierLimit, resolveEntitlement, resolveCachedEntitlement, FREE_TIER_LIMITS, verifyLicenseToken, shouldEmitSetupCompleted, pickAnalyticsUid, shouldEmitFreeExhausted, detectRequestLanguage, isNothingVerifiable } from './lib.mjs';
|
|
23
|
+
import { needsProjectFolder, folderLabel, buildFolderQuestion, resolveFolderAnswer, classifyFolderTarget, buildFolderInitConfirm, isFolderInitConfirmed } from './lib.mjs';
|
|
24
|
+
import { parseStreamEvents, parsePlan, refinePlanTasks, findOverlappingGoal, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, isForcedStop, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, buildGoalProofMd, buildGoalPrBody, buildReviewRuleSection, nextGoalStatus, isPrMerged, isPrApproved, reviewToDoneStatus, advanceReviewGoal, revertReviewGoal, approveGoal, dismissGoal, permissionModeFor, isPlanReview, nextAfterPlanApprove, GOAL_MODES, parseSkillFrontmatter, collectSkills, retestGoal, cancelRetestGoal, computeRetestOutcome, revertGoal, planRevertActions, archiveGoal, unarchiveGoal, undismissGoal, parseNumstat, truncateDiffText, sumUsage, resolveEntryUrl, parseGitLog, diffNewCommits, replayQueueLog, reconcileOrphanGoals, reorderQueue, sortQueueByPriority, TASK_PRIORITIES, validateWorkflowColumns, DEFAULT_WORKFLOW_COLUMNS, validateReviewDefinition, DEFAULT_REVIEW_DEFINITION, buildEphemeralSeedLog, buildEphemeralSeedProjects, trimTaskActivityForState, buildAskContext, WORKER_TOOLS, verifyCfAccessJwt, resolveIdentity, goalVisibleTo, clampParallelLimit, canStartMore, nextRunnableTasks, goalsConflict, detectConflicts, isUiChange, shouldCaptureProof, shouldCaptureBaseline, hasTestRelevantChanges, buildExecutionPlan, buildContextHandoffSummary, buildFailurePostmortem, appendFailureMemory, latestFailurePolicy, classifyChangeRisk, checkRunBudget, usageBudgetTokens, isRateLimited, nextResumeDelay, checkFreeTierLimit, resolveEntitlement, resolveCachedEntitlement, FREE_TIER_LIMITS, verifyLicenseToken, shouldEmitSetupCompleted, pickAnalyticsUid, shouldEmitFreeExhausted, detectRequestLanguage, isNothingVerifiable, classifyComposerIntentHeuristic, buildIntentPrompt, parseIntentResponse } from './lib.mjs';
|
|
25
25
|
import { openPR } from './pr.mjs';
|
|
26
26
|
import { runVerification, exerciseUi } from './verify.mjs';
|
|
27
27
|
|
|
@@ -582,7 +582,19 @@ function sendGoalEvent(obj, goal) {
|
|
|
582
582
|
if (goalVisibleTo(goal, identity, MANAGER_OWNER_EMAIL)) res.write(data);
|
|
583
583
|
}
|
|
584
584
|
}
|
|
585
|
-
|
|
585
|
+
// Durable append to the restart-safe log. A failing disk (ENOSPC when the
|
|
586
|
+
// volume is full, EACCES/EROFS) must NEVER freeze the board: without this guard
|
|
587
|
+
// an appendFileSync throw inside a handler leaves the HTTP request hanging (no
|
|
588
|
+
// response) AND skips the SSE broadcast that follows, so a delete/answer looks
|
|
589
|
+
// like it did nothing — the question card stays stuck forever (Masa 2026-07-16).
|
|
590
|
+
// We swallow the write error (logged), and let the in-memory mutation + SSE
|
|
591
|
+
// proceed regardless. Durability is best-effort here; a live UI wins over a
|
|
592
|
+
// perfectly-replayable log on a disk that can't be written to anyway.
|
|
593
|
+
function logAppend(line) {
|
|
594
|
+
try { appendFileSync(logFile, line + '\n'); return true; }
|
|
595
|
+
catch (e) { console.error(`[manager] log write failed (${e?.code || e}) — state not persisted this write, UI still updated`); return false; }
|
|
596
|
+
}
|
|
597
|
+
function saveGoal(g) { logAppend(JSON.stringify({ ...g, kind: 'goal' })); sendGoalEvent({ ev: 'goal', goal: g }, g); }
|
|
586
598
|
// Persist the orphan-goal reclassification computed right after replayQueueLog
|
|
587
599
|
// above (deferred to here because saveGoal/send/sseClients don't exist yet at
|
|
588
600
|
// that point in the module) — writes it to the restart-safe log so it settles
|
|
@@ -594,7 +606,7 @@ for (const fix of orphanFixes) {
|
|
|
594
606
|
}
|
|
595
607
|
function saveTask(t) {
|
|
596
608
|
const { activity, todos, ...persist } = t; // activity + todos are ephemeral (live only)
|
|
597
|
-
|
|
609
|
+
logAppend(JSON.stringify({ ...persist, kind: 'task' }));
|
|
598
610
|
const goal = goals.find((g) => g.id === t.goalId);
|
|
599
611
|
if (goal) sendGoalEvent({ ev: 'task', task: persist }, goal); else send({ ev: 'task', task: persist });
|
|
600
612
|
emitTaskAnalytics(t);
|
|
@@ -2160,6 +2172,11 @@ async function finishGoalIfComplete(goal, project, activityTask = null) {
|
|
|
2160
2172
|
// point (all its tasks are done), so its conflictsWith is accurate even if
|
|
2161
2173
|
// PR creation itself fails (e.g. no `gh` auth) or is still no-op'd (no code).
|
|
2162
2174
|
updateGoalConflicts(goal);
|
|
2175
|
+
// Computed BEFORE createGoalPR (not inside verifyGate, which used to run
|
|
2176
|
+
// after) so the PR body's "確認ポイント" headline is available the first
|
|
2177
|
+
// time the PR is opened, not only on the review card (Masa 2026-07-16).
|
|
2178
|
+
goalAct('Writing the review summary from the worker report and diff.');
|
|
2179
|
+
goal.reviewSummary = await summarizeForReview(goal, siblings, wdir, reviewDefinition);
|
|
2163
2180
|
goalAct('Checking changed files and preparing the PR branch if code changed.');
|
|
2164
2181
|
await createGoalPR(goal, prProject, siblings, reviewDefinition, activityTask);
|
|
2165
2182
|
// goal 427 §4: classify the change's risk from the SAME changed-files/diff
|
|
@@ -2228,8 +2245,13 @@ async function verifyGate(goal, prProject, siblings, baseProject, activityTask =
|
|
|
2228
2245
|
goal.testResult = { ran: false, skipped: true, skippedReason: changedFiles.length ? 'documentation-only change' : 'no changed files' };
|
|
2229
2246
|
goalAct(`Skipped project tests: ${goal.testResult.skippedReason}.`);
|
|
2230
2247
|
}
|
|
2231
|
-
|
|
2232
|
-
|
|
2248
|
+
// Normally already set by finishGoalIfComplete before the PR was opened
|
|
2249
|
+
// (so the PR body can include it too); only fall back here for the
|
|
2250
|
+
// standalone /reverify path or any older goal that never got one.
|
|
2251
|
+
if (!goal.reviewSummary) {
|
|
2252
|
+
goalAct('Writing the review summary from the worker report and diff.');
|
|
2253
|
+
goal.reviewSummary = await summarizeForReview(goal, siblings, wdir, reviewDefinition);
|
|
2254
|
+
}
|
|
2233
2255
|
const uiTasks = siblings.filter((t) => isUiChange(t.changedFiles));
|
|
2234
2256
|
// Only require/capture proof for projects we can actually boot & screenshot
|
|
2235
2257
|
// (Manager-shaped). A generic web repo that edits a stylesheet can't be
|
|
@@ -2371,26 +2393,12 @@ async function createGoalPR(goal, project, goalTasks, reviewDefinition = DEFAULT
|
|
|
2371
2393
|
// (Masa: 「ユーザーの言語で」).
|
|
2372
2394
|
const lang = reviewDefinition.language === 'auto' ? detectRequestLanguage(goal.text) : (reviewDefinition.language || 'ja');
|
|
2373
2395
|
const L = (en, ja) => (lang === 'en' ? en : ja);
|
|
2374
|
-
const bodyFor = (branch) =>
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
] : []),
|
|
2381
|
-
L('## Goal', '## ゴール'),
|
|
2382
|
-
'',
|
|
2383
|
-
goal.text.slice(0, 1500),
|
|
2384
|
-
'',
|
|
2385
|
-
...buildReviewRuleSection(reviewDefinition.description),
|
|
2386
|
-
L(`Implemented by Manager for AI: the goal was decomposed into ${goalTasks.length} task(s), each executed by a Claude Code worker (flat-rate, no extra API), tests written and run per task.${proofItems.length ? ' Tasks with a pass condition were verified independently in a headless browser.' : ''}`,
|
|
2387
|
-
`Manager for AI が実装: ゴールを${goalTasks.length}個のタスクに分解し、Claude Code worker が順に実行(既存サブスク・追加API無し・タスク毎にテストを作成し実行)。${proofItems.length ? '成功条件つきタスクは headless ブラウザで独立検証済み。' : ''}`),
|
|
2388
|
-
'',
|
|
2389
|
-
L('## Tasks and worker reports', '## タスクとworker報告'),
|
|
2390
|
-
...goalTasks.map((t) => `\n### ${t.num} ${t.title} — ${t.status} (${t.secs}s)\n${(t.result ?? '').slice(0, 800)}`),
|
|
2391
|
-
'',
|
|
2392
|
-
L(`Changed files: ${files.map((f) => `\`${f}\``).join(' ')}`, `変更ファイル: ${files.map((f) => `\`${f}\``).join(' ')}`),
|
|
2393
|
-
].join('\n');
|
|
2396
|
+
const bodyFor = (branch) => buildGoalPrBody({
|
|
2397
|
+
lang, goal, goalTasks, files, owner, branch, proofRel,
|
|
2398
|
+
proofItemsCount: proofItems.length,
|
|
2399
|
+
reviewDescription: reviewDefinition.description,
|
|
2400
|
+
reviewSummary: goal.reviewSummary,
|
|
2401
|
+
});
|
|
2394
2402
|
const title = `Manager for AI: ${isRework ? L('rework — ', '再作業 — ') : ''}${goal.plan?.[0] ?? goal.text.slice(0, 60)}${goal.plan?.length > 1 ? L(` +${goal.plan.length - 1} more`, ` 他${goal.plan.length - 1}件`) : ''}`;
|
|
2395
2403
|
const { url, branch } = openPR({
|
|
2396
2404
|
repoDir: project.dir, slug: `goal-${goal.id}`,
|
|
@@ -2401,7 +2409,7 @@ async function createGoalPR(goal, project, goalTasks, reviewDefinition = DEFAULT
|
|
|
2401
2409
|
extraCopies,
|
|
2402
2410
|
extraWrites: proofItems.length ? [{
|
|
2403
2411
|
destRel: `${proofRel}/PROOF.md`,
|
|
2404
|
-
content: buildGoalProofMd({ goalText: goal.text, items: proofItems }),
|
|
2412
|
+
content: buildGoalProofMd({ goalText: goal.text, items: proofItems, lang }),
|
|
2405
2413
|
}] : [],
|
|
2406
2414
|
...(isRework ? { existingBranch: goal.prBranch, existingPrUrl: goal.pr } : {}),
|
|
2407
2415
|
});
|
|
@@ -2705,6 +2713,32 @@ const server = createServer(async (req, res) => {
|
|
|
2705
2713
|
const { projectId, text, pr, images, model, effort, source, pending, review, note, mode, skill, agent } = JSON.parse(body);
|
|
2706
2714
|
const project = projects.find((p) => p.id === projectId);
|
|
2707
2715
|
if (!project || !text?.trim()) return json(res, 400, { error: 'projectId and text required' });
|
|
2716
|
+
// Onboarding (docs/HANDOFF-ONBOARDING-conversational.md, SLICE 2): the
|
|
2717
|
+
// composer is conversation-first. A chat/help/settings message ("what can
|
|
2718
|
+
// you do?", "reply in English", a greeting) gets a conversational reply
|
|
2719
|
+
// and must NOT become a To-Do. Classified BEFORE dedupe/billing/goal
|
|
2720
|
+
// creation, so a chat turn never folds into a goal, never counts against
|
|
2721
|
+
// the free-tier cap, and never persists a task. /later (pending) and
|
|
2722
|
+
// /review are explicit user intent → skip classification. A cheap
|
|
2723
|
+
// heuristic short-circuits obvious implementation requests (no LLM);
|
|
2724
|
+
// anything else gets one Haiku round-trip that both classifies and writes
|
|
2725
|
+
// the reply. LLM failure falls back to 'task' — never drop a real
|
|
2726
|
+
// implementation ask (the folder guard below still prevents no-ops).
|
|
2727
|
+
if (!pending && !review && classifyComposerIntentHeuristic(text.trim()) === 'chat') {
|
|
2728
|
+
// Only a HIGH-confidence chat/help/settings message reaches the LLM —
|
|
2729
|
+
// to write the reply, and to let the model veto back to 'task' if the
|
|
2730
|
+
// heuristic over-fired. Everything else (clear tasks AND ambiguous
|
|
2731
|
+
// 'unsure') goes straight to goal creation: bias to task keeps the
|
|
2732
|
+
// common path LLM-free (no latency/tokens on real work) and the folder
|
|
2733
|
+
// guard below still prevents no-ops. LLM failure → treat as task and
|
|
2734
|
+
// fall through, so a real implementation ask is never dropped.
|
|
2735
|
+
const intentLang = detectRequestLanguage(text);
|
|
2736
|
+
try {
|
|
2737
|
+
const r = await runClaude({ prompt: buildIntentPrompt(text.trim(), intentLang), cwd: ROOT, tools: 'Read', model: 'haiku' });
|
|
2738
|
+
const parsed = parseIntentResponse(r.result);
|
|
2739
|
+
if (parsed.intent === 'chat') return json(res, 200, { kind: 'chat', reply: parsed.reply.slice(0, 2000), lang: intentLang });
|
|
2740
|
+
} catch { /* LLM unavailable → fall through to goal creation */ }
|
|
2741
|
+
}
|
|
2708
2742
|
// Dedupe (Masa: 被ったやつもまとめて): if this near-duplicates a goal
|
|
2709
2743
|
// already in flight (running/review with a live worker session), fold it
|
|
2710
2744
|
// in as a thread follow-up (context kept via session resume) instead of
|
|
@@ -2854,7 +2888,7 @@ const server = createServer(async (req, res) => {
|
|
|
2854
2888
|
const q = queues.get(goal.projectId);
|
|
2855
2889
|
if (q) q.waiting = q.waiting.filter((t) => t.goalId !== goal.id);
|
|
2856
2890
|
goals = goals.filter((g) => g.id !== goal.id);
|
|
2857
|
-
|
|
2891
|
+
logAppend(JSON.stringify({ kind: 'goal', id: goal.id, deleted: true }));
|
|
2858
2892
|
sendGoalEvent({ ev: 'goal-deleted', id: goal.id }, goal);
|
|
2859
2893
|
startNextStacked(goal.projectId);
|
|
2860
2894
|
pump(goal.projectId);
|
|
@@ -3087,34 +3121,80 @@ const server = createServer(async (req, res) => {
|
|
|
3087
3121
|
let answer = '';
|
|
3088
3122
|
try { answer = String(JSON.parse(body || '{}').answer ?? '').trim(); } catch {}
|
|
3089
3123
|
if (!answer) return json(res, 400, { error: 'answer required' });
|
|
3124
|
+
// Bind this goal to a project rooted at `dir` and start planning. Shared by
|
|
3125
|
+
// the "picked a repo" and "git-init'd a new folder" paths below.
|
|
3126
|
+
const bindAndPlan = (dir) => {
|
|
3127
|
+
const existing = projects.find((p) => p.dir === dir);
|
|
3128
|
+
let proj = existing;
|
|
3129
|
+
if (!proj) {
|
|
3130
|
+
proj = { id: uniqueProjectId(basename(dir)), name: basename(dir), dir };
|
|
3131
|
+
projects.push(proj); saveProjects();
|
|
3132
|
+
}
|
|
3133
|
+
goal.projectId = proj.id;
|
|
3134
|
+
goal.question = null;
|
|
3135
|
+
goal.clarified = true;
|
|
3136
|
+
goal.status = 'planning';
|
|
3137
|
+
saveGoal(goal);
|
|
3138
|
+
planGoal(goal).catch((e) => { goal.status = 'failed'; goal.prError = String(e).slice(0, 300); saveGoal(goal); });
|
|
3139
|
+
return json(res, 200, goal);
|
|
3140
|
+
};
|
|
3090
3141
|
// Folder-choice answer (onboarding funnel): rebind the goal to the chosen
|
|
3091
3142
|
// repo rather than appending to goal.text — a folder isn't a task
|
|
3092
|
-
// clarification.
|
|
3093
|
-
//
|
|
3143
|
+
// clarification. If the picked path is already a repo, bind it. If it's a
|
|
3144
|
+
// non-repo dir (or a path that doesn't exist yet), DON'T loop re-asking —
|
|
3145
|
+
// offer to start a new project there via `git init` (the repo-less /
|
|
3146
|
+
// vibe-coder onboarding path). HOME / a file → re-ask.
|
|
3094
3147
|
if (goal.question?.kind === 'folder') {
|
|
3095
3148
|
const resolved = resolveFolderAnswer(answer, goal.question.folders || []);
|
|
3096
3149
|
let dir = resolved?.dir || answer;
|
|
3097
3150
|
if (dir.startsWith('~')) dir = join(HOME, dir.slice(1).replace(/^\/+/, ''));
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3151
|
+
const lang = goal.question.lang || 'ja';
|
|
3152
|
+
const cls = classifyFolderTarget({
|
|
3153
|
+
dir, homeDir: HOME,
|
|
3154
|
+
exists: existsSync(dir),
|
|
3155
|
+
isDirectory: (() => { try { return statSync(dir).isDirectory(); } catch { return false; } })(),
|
|
3156
|
+
isRepo: isGitRepo(dir),
|
|
3157
|
+
});
|
|
3158
|
+
if (cls.action === 'use') return bindAndPlan(dir);
|
|
3159
|
+
if (cls.action === 'init') {
|
|
3160
|
+
// Non-repo folder → confirm before we create a repo (non-destructive,
|
|
3161
|
+
// but a repo-less user should see what we're about to do).
|
|
3162
|
+
goal.question = buildFolderInitConfirm(dir, { lang, create: cls.create, homeDir: HOME });
|
|
3109
3163
|
saveGoal(goal);
|
|
3110
|
-
planGoal(goal).catch((e) => { goal.status = 'failed'; goal.prError = String(e).slice(0, 300); saveGoal(goal); });
|
|
3111
3164
|
return json(res, 200, goal);
|
|
3112
3165
|
}
|
|
3113
|
-
//
|
|
3114
|
-
goal.question = buildFolderQuestion(detectRepos(HOME), { lang
|
|
3166
|
+
// reject (HOME or a file) → re-ask with a fresh detected list
|
|
3167
|
+
goal.question = buildFolderQuestion(detectRepos(HOME), { lang });
|
|
3115
3168
|
saveGoal(goal);
|
|
3116
3169
|
return json(res, 200, goal);
|
|
3117
3170
|
}
|
|
3171
|
+
// Confirm card for "start a new project here (git init)?". Yes → run the
|
|
3172
|
+
// init (guarded: create the dir if needed, NEVER touch existing files,
|
|
3173
|
+
// NEVER re-init an existing repo), then bind + plan like a picked repo.
|
|
3174
|
+
// Anything else → back to the folder list.
|
|
3175
|
+
if (goal.question?.kind === 'folder-init') {
|
|
3176
|
+
const q = goal.question;
|
|
3177
|
+
const lang = q.lang || 'ja';
|
|
3178
|
+
if (!isFolderInitConfirmed(answer, q)) {
|
|
3179
|
+
goal.question = buildFolderQuestion(detectRepos(HOME), { lang });
|
|
3180
|
+
saveGoal(goal);
|
|
3181
|
+
return json(res, 200, goal);
|
|
3182
|
+
}
|
|
3183
|
+
const dir = q.dir;
|
|
3184
|
+
try {
|
|
3185
|
+
if (dir === HOME) throw new Error('refusing to init HOME as a project');
|
|
3186
|
+
if (q.create && !existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
3187
|
+
if (!statSync(dir).isDirectory()) throw new Error('target is not a directory');
|
|
3188
|
+
if (!isGitRepo(dir)) gitSync(dir, ['init', '--quiet']); // adds .git only; leaves files as-is
|
|
3189
|
+
} catch (e) {
|
|
3190
|
+
// couldn't init → don't run a no-op; surface why and re-ask
|
|
3191
|
+
goal.question = buildFolderQuestion(detectRepos(HOME), { lang });
|
|
3192
|
+
goal.prError = `git init failed: ${String(e?.message || e).slice(0, 200)}`;
|
|
3193
|
+
saveGoal(goal);
|
|
3194
|
+
return json(res, 200, goal);
|
|
3195
|
+
}
|
|
3196
|
+
return bindAndPlan(dir);
|
|
3197
|
+
}
|
|
3118
3198
|
goal.text = `${goal.text}\n[確認: ${goal.question?.text ?? ''} → ${answer}]`;
|
|
3119
3199
|
goal.clarified = true; // answered once → planGoal must never re-ask (persisted via saveGoal)
|
|
3120
3200
|
goal.question = null;
|
package/package.json
CHANGED