crewforth 3.1.1 → 3.2.0-rc.2
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/CHANGELOG.md +171 -0
- package/README.md +4 -3
- package/VERSION +1 -1
- package/adopt.sh +56 -5
- package/kit/AGENT_TEMPLATE.md +2 -1
- package/kit/CLAUDE.md +14 -13
- package/kit/agents/crew-commit-agent.md +7 -2
- package/kit/agents/crew-docs-agent.md +71 -0
- package/kit/agents/crew-planner.md +2 -0
- package/kit/agents/crew-refuter.md +77 -0
- package/kit/agents/crew-security-expert.md +3 -1
- package/kit/agents/crew-session-manager.md +11 -9
- package/kit/eval/doctor.sh +117 -6
- package/kit/eval/golden-routing.txt +9 -0
- package/kit/eval/lib/crew-map/crew-map.sh +26 -0
- package/kit/eval/lib/crew-map/detect.mjs +238 -0
- package/kit/eval/lib/crew-map/narrative.mjs +98 -0
- package/kit/eval/lib/crew-map/parse.mjs +305 -0
- package/kit/eval/lib/crew-map/project.mjs +163 -0
- package/kit/eval/lib/crew-map/rules/names.json +38 -0
- package/kit/eval/lib/crew-map/rules/packages.json +142 -0
- package/kit/eval/lib/crew-map/rules/python-stdlib.json +197 -0
- package/kit/eval/lib/crew-map/scan.mjs +214 -0
- package/kit/eval/lib/crew-map/stacks.mjs +441 -0
- package/kit/eval/lib/crew-map/surface.mjs +457 -0
- package/kit/eval/lib/findings/findings.mjs +410 -0
- package/kit/eval/lib/findings/findings.schema.json +122 -0
- package/kit/eval/lib/findings/findings.sh +29 -0
- package/kit/eval/lib/settings-default.sh +36 -0
- package/kit/eval/lib/settings-json.awk +15 -4
- package/kit/eval/lib/star.sh +23 -7
- package/kit/eval/lib/statusline.settings.json +6 -0
- package/kit/eval/lib/statusline.sh +100 -0
- package/kit/eval/lib/version.sh +41 -0
- package/kit/eval/smoke-test.sh +576 -41
- package/kit/hooks/agent-outcome.sh +26 -12
- package/kit/hooks/guard-agent-model.sh +109 -9
- package/kit/hooks/guard-bash.sh +240 -182
- package/kit/hooks/guard-question.sh +73 -0
- package/kit/hooks/guard-write.sh +21 -0
- package/kit/hooks/prompt-approval.sh +320 -103
- package/kit/hooks/refuter-ledger.sh +80 -0
- package/kit/hooks/session-update-check.sh +9 -8
- package/kit/owned-blobs.tsv +4 -4
- package/kit/settings.json +33 -0
- package/kit/skills/crew-approve/SKILL.md +7 -0
- package/kit/skills/crew-doctor/SKILL.md +5 -1
- package/kit/skills/crew-handoff/SKILL.md +3 -2
- package/kit/skills/crew-map/SKILL.md +44 -0
- package/kit/skills/crew-review/SKILL.md +2 -0
- package/kit/skills/crew-update/SKILL.md +1 -1
- package/kit/skills/incident-runbook/SKILL.md +2 -2
- package/kit/skills/security-scan/references/verify.md +6 -3
- package/kit/studio/README.md +12 -0
- package/kit/studio/server/index.js +125 -11
- package/kit/studio/server/lib/code-stamp.js +35 -0
- package/kit/studio/server/lib/crew-map.js +113 -0
- package/kit/studio/server/lib/model-outcomes.js +130 -15
- package/kit/studio/server/lib/owned-stream.js +56 -0
- package/kit/studio/server/lib/pricing.js +3 -0
- package/kit/studio/server/lib/projects.js +11 -4
- package/kit/studio/server/lib/remote.js +99 -0
- package/kit/studio/server/lib/usage.js +74 -14
- package/kit/studio/web/app.js +223 -15
- package/kit/studio/web/canvas.js +44 -120
- package/kit/studio/web/chat.js +82 -17
- package/kit/studio/web/graph-plan.js +10 -4
- package/kit/studio/web/index.html +10 -5
- package/kit/studio/web/liveness.js +14 -0
- package/kit/studio/web/map-plan.js +341 -0
- package/kit/studio/web/map.js +503 -0
- package/kit/studio/web/models-plan.js +61 -7
- package/kit/studio/web/models.js +19 -3
- package/kit/studio/web/style.css +131 -0
- package/kit/studio/web/usage.js +12 -0
- package/kit/studio/web/viewport.js +160 -0
- package/package.json +6 -5
- package/start.sh +10 -0
package/kit/studio/web/style.css
CHANGED
|
@@ -402,6 +402,24 @@ mark { background: color-mix(in srgb, var(--studio-accent) 30%, transparent); co
|
|
|
402
402
|
}
|
|
403
403
|
.nav-foot-label:hover { color: var(--ink); }
|
|
404
404
|
|
|
405
|
+
/* Remote viewers, under the machines: who holds a read-only link, and the way to take it back. */
|
|
406
|
+
.rv { margin-top: var(--space-3); }
|
|
407
|
+
.rv-head { display: flex; align-items: center; gap: var(--space-2); padding: 0 var(--space-1); }
|
|
408
|
+
.rv-title {
|
|
409
|
+
font-size: 11px; line-height: var(--type-label-line); font-weight: var(--type-label-weight);
|
|
410
|
+
letter-spacing: var(--type-label-tracking); text-transform: uppercase; color: var(--ink-dim);
|
|
411
|
+
}
|
|
412
|
+
.rv-say { white-space: normal; padding: 0 var(--space-1); }
|
|
413
|
+
.rv-give {
|
|
414
|
+
display: flex; flex-direction: column; gap: var(--space-2);
|
|
415
|
+
padding: var(--space-2); border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--surface-raised);
|
|
416
|
+
}
|
|
417
|
+
.rv-give .rv-say { padding: 0; }
|
|
418
|
+
.rv-url { font-size: 12px; line-height: 16px; overflow-wrap: anywhere; user-select: all; color: var(--ink); }
|
|
419
|
+
.rv-acts { display: flex; gap: var(--space-2); }
|
|
420
|
+
/* A viewer's page offers no way to start a session: the server refuses it, and a button that only fails is noise. */
|
|
421
|
+
body[data-role="viewer"] #new-session { display: none; }
|
|
422
|
+
|
|
405
423
|
/* ---- menu and toast ---- */
|
|
406
424
|
.menu {
|
|
407
425
|
position: fixed; z-index: 60; min-width: 190px; max-width: 272px;
|
|
@@ -525,6 +543,12 @@ mark { background: color-mix(in srgb, var(--studio-accent) 30%, transparent); co
|
|
|
525
543
|
border-bottom-color: color-mix(in srgb, var(--status-fail) 45%, var(--line));
|
|
526
544
|
}
|
|
527
545
|
.stage-note-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
546
|
+
/* A server running older code than the page: the whole sentence is the instruction, so it wraps, not ellipsizes. */
|
|
547
|
+
.stage-note[data-kind="restart"] {
|
|
548
|
+
color: var(--ink); background: color-mix(in srgb, var(--status-waiting) 12%, var(--surface));
|
|
549
|
+
border-bottom-color: color-mix(in srgb, var(--status-waiting) 45%, var(--line));
|
|
550
|
+
}
|
|
551
|
+
.stage-note[data-kind="restart"] .stage-note-text { white-space: normal; }
|
|
528
552
|
/* The last picture stays while the server is away, and is drawn as what it is: old. */
|
|
529
553
|
.stage[data-offline="true"] .canvas,
|
|
530
554
|
.stage[data-offline="true"] .tl,
|
|
@@ -553,6 +577,107 @@ mark { background: color-mix(in srgb, var(--studio-accent) 30%, transparent); co
|
|
|
553
577
|
.mv { flex: 1; min-height: 0; min-width: 0; overflow: auto; background: var(--ground); padding: var(--space-4); }
|
|
554
578
|
.mv[hidden] { display: none; }
|
|
555
579
|
.mv-empty { padding: var(--space-6); color: var(--ink-dim); font-size: 13px; }
|
|
580
|
+
|
|
581
|
+
/* The Map view: the project as its crew map shows it. Bands, cards, lines, the findings, the inspector. */
|
|
582
|
+
.mp { flex: 1; min-height: 0; min-width: 0; overflow: auto; background: var(--ground); padding: var(--space-4); }
|
|
583
|
+
.mp[hidden] { display: none; }
|
|
584
|
+
.mp .sub { white-space: normal; flex: initial; min-width: 0; }
|
|
585
|
+
.mp .lbl { font-size: 11px; line-height: 16px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-dim); }
|
|
586
|
+
.mp-empty { padding: var(--space-6); color: var(--ink-dim); font-size: 13px; display: flex; flex-direction: column; gap: 6px; }
|
|
587
|
+
.mp-empty p { margin: 0; }
|
|
588
|
+
.mp-pane { border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--surface); overflow: hidden; display: flex; flex-direction: column; min-height: 100%; }
|
|
589
|
+
.mp-head, .mp-toolbar, .mp-source { display: flex; align-items: center; gap: var(--space-2); padding: 10px 18px; border-bottom: 1px solid var(--line); flex-wrap: wrap; }
|
|
590
|
+
.mp-title { margin: 0; font-size: 15px; font-weight: 700; }
|
|
591
|
+
.mp-pill { display: inline-flex; align-items: center; height: 22px; padding: 0 8px; border-radius: var(--radius-sm); background: var(--surface-raised); font-size: 12px; font-weight: 600; color: var(--ink); white-space: nowrap; }
|
|
592
|
+
.mp-pill[data-tone="behind"], .mp-pill[data-tone="missing"] { color: var(--status-bad); background: color-mix(in srgb, var(--status-bad) 14%, transparent); }
|
|
593
|
+
.mp-tog { height: 28px; padding: 0 10px; border-radius: var(--radius-pill); border: 1px solid var(--line); background: var(--surface); color: var(--ink-dim); font: inherit; font-size: 12px; font-weight: 600; cursor: pointer; }
|
|
594
|
+
.mp-tog[aria-pressed="true"] { background: var(--surface-raised); color: var(--ink); }
|
|
595
|
+
.mp-compare { height: 30px; border-radius: var(--radius-md); border: 1px solid var(--line); background: var(--surface); color: var(--ink); font: inherit; font-size: 12px; padding: 0 8px; max-width: 260px; }
|
|
596
|
+
.mp-legend { position: relative; }
|
|
597
|
+
.mp-legend-box { position: absolute; right: 0; top: 34px; z-index: 5; padding: 10px 12px; display: flex; flex-direction: column; gap: 6px; border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--surface-raised); min-width: 220px; }
|
|
598
|
+
.mp-legend-row { display: flex; align-items: center; gap: 8px; }
|
|
599
|
+
.mp-legend-line { width: 28px; height: 0; border-top: 2px solid var(--ink-faint); flex: none; }
|
|
600
|
+
.mp-legend-line[data-kind="inferred"] { border-top-style: dotted; }
|
|
601
|
+
.mp-legend-line[data-kind="model"] { border-top-style: dashed; border-top-color: var(--studio-accent); opacity: 0.7; }
|
|
602
|
+
.mp-legend-line[data-kind="lit"] { border-top-color: var(--studio-accent); }
|
|
603
|
+
.mp-legend-card { width: 22px; height: 14px; border: 1px solid var(--ink-faint); border-radius: 4px; flex: none; }
|
|
604
|
+
.mp-legend-card[data-kind="dashed"] { border-style: dashed; }
|
|
605
|
+
.mp-file { font-family: var(--font-mono); font-size: 12px; color: var(--ink); }
|
|
606
|
+
.mp-body { display: flex; min-height: 0; flex: 1; }
|
|
607
|
+
.mp-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
|
608
|
+
/* The pane pans and zooms (viewport.js): the layer is moved by one transform, so nothing inside restyles. */
|
|
609
|
+
.mp-canvas-pane { position: relative; flex: 1; min-height: 380px; overflow: hidden; cursor: grab; background-color: var(--ground); background-image: radial-gradient(var(--line) 1px, transparent 1.2px); background-size: 20px 20px; touch-action: none; }
|
|
610
|
+
.mp-canvas-pane.cv-panning { cursor: grabbing; }
|
|
611
|
+
.mp-canvas { position: absolute; left: 0; top: 0; transform-origin: 0 0; }
|
|
612
|
+
.mp-band { position: absolute; left: 0; border-bottom: 1px solid var(--line); padding: 12px 16px; box-sizing: border-box; }
|
|
613
|
+
.mp-group { position: absolute; border: 1px dashed var(--line); border-radius: var(--radius-md); box-sizing: border-box; }
|
|
614
|
+
.mp-group-fold { position: absolute; left: 8px; top: 3px; border: 0; background: none; padding: 0; font: inherit; font-family: var(--font-mono); font-size: 11px; color: var(--ink-dim); cursor: pointer; }
|
|
615
|
+
.mp-group-fold:hover { color: var(--ink); }
|
|
616
|
+
.mp-lines { position: absolute; left: 0; top: 0; pointer-events: none; }
|
|
617
|
+
.mp-line { fill: none; stroke: var(--ink-faint); stroke-width: 1.5; }
|
|
618
|
+
.mp-line[data-line="inferred"] { stroke-dasharray: 2 4; }
|
|
619
|
+
.mp-line[data-line="model"] { stroke-dasharray: 7 4; stroke: var(--studio-accent); opacity: 0.7; }
|
|
620
|
+
.mp-line[data-lit="false"] { opacity: 0.35; }
|
|
621
|
+
.mp-line[data-lit="true"] { stroke: var(--studio-accent); stroke-width: 2.2; }
|
|
622
|
+
.mp-elab { position: absolute; transform: translate(-50%, -50%); display: flex; gap: 4px; z-index: 2; }
|
|
623
|
+
.mp-mod, .mp-ext { position: absolute; box-sizing: border-box; display: flex; flex-direction: column; gap: 4px; text-align: left; font: inherit; color: var(--ink); cursor: pointer; border: 1px solid var(--line); border-radius: var(--radius-md); z-index: 1; overflow: hidden; }
|
|
624
|
+
.mp-mod { background: var(--surface); padding: 10px 14px; }
|
|
625
|
+
.mp-mod[data-kind="client"] { border-style: dashed; }
|
|
626
|
+
.mp-mod[data-kind="group"] { background: var(--surface-raised); }
|
|
627
|
+
.mp-ext[data-inferred="true"] { border-style: dashed; }
|
|
628
|
+
.mp-card.cv-dragging { cursor: grabbing; z-index: 3; }
|
|
629
|
+
.mp-ext { background: var(--ground); padding: 6px 10px; justify-content: center; }
|
|
630
|
+
.mp-card:focus-visible { outline: 2px solid var(--studio-accent); outline-offset: 2px; }
|
|
631
|
+
.mp-mod[data-selected="true"], .mp-ext[data-selected="true"] { border-color: var(--studio-accent); box-shadow: 0 0 0 1px var(--studio-accent); }
|
|
632
|
+
.mp-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
633
|
+
.mp-name { font-family: var(--font-mono); font-size: 14px; line-height: 20px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
|
634
|
+
.mp-ename { font-family: var(--font-mono); font-size: 13px; line-height: 18px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
635
|
+
.mp-kind { font-size: 10px; line-height: 12px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-dim); flex: none; }
|
|
636
|
+
.mp-fb { min-width: 20px; height: 20px; padding: 0 6px; box-sizing: border-box; border-radius: var(--radius-pill); background: color-mix(in srgb, var(--status-bad) 18%, transparent); color: var(--status-bad); font-size: 11px; font-weight: 700; display: inline-flex; align-items: center; justify-content: center; flex: none; }
|
|
637
|
+
.mp-chips { display: flex; flex-wrap: wrap; gap: 4px; }
|
|
638
|
+
.mp-chip { font-family: var(--font-mono); font-size: 11px; height: 22px; padding: 0 8px; border-radius: var(--radius-sm); background: var(--surface-raised); display: inline-flex; align-items: center; white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
|
|
639
|
+
.mp-findings { border-top: 1px solid var(--line); padding: 10px 14px; display: flex; flex-direction: column; gap: 4px; }
|
|
640
|
+
.mp-flist { max-height: 132px; overflow: auto; display: flex; flex-direction: column; }
|
|
641
|
+
.mp-frow { display: flex; align-items: center; gap: 10px; min-height: 28px; padding: 0 8px; border: 0; border-radius: var(--radius-md); background: transparent; color: var(--ink); font: inherit; text-align: left; cursor: pointer; }
|
|
642
|
+
.mp-frow:hover { background: var(--surface-raised); }
|
|
643
|
+
.mp-dot { width: 8px; height: 8px; border-radius: 999px; flex: none; box-sizing: border-box; border: 1.5px solid var(--status-bad); }
|
|
644
|
+
.mp-dot[data-proven="true"] { background: var(--status-bad); }
|
|
645
|
+
.mp-ftitle { font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; flex: 0 1 auto; }
|
|
646
|
+
.mp-fcard .mp-ftitle { white-space: normal; }
|
|
647
|
+
.mp-fwhere { font-family: var(--font-mono); font-size: 12px; color: var(--ink-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; flex: 0 2 auto; }
|
|
648
|
+
.mp-badge { font-family: var(--font-mono); font-size: 11px; height: 20px; padding: 0 6px; border-radius: var(--radius-pill); border: 1px solid var(--line); color: var(--ink-dim); display: inline-flex; align-items: center; flex: none; }
|
|
649
|
+
.mp-inspect { width: 340px; flex: none; border-left: 1px solid var(--line); padding: 14px 16px; display: flex; flex-direction: column; gap: 10px; overflow: auto; }
|
|
650
|
+
.mp-inspect p { margin: 0; font-size: 13px; line-height: 19px; }
|
|
651
|
+
.mp-tabs { display: flex; gap: 16px; border-bottom: 1px solid var(--line); }
|
|
652
|
+
.mp-tab { font: inherit; font-size: 13px; font-weight: 600; color: var(--ink-dim); background: none; border: 0; border-bottom: 2px solid transparent; padding: 8px 0; cursor: pointer; }
|
|
653
|
+
.mp-tab[aria-selected="true"] { color: var(--ink); border-bottom-color: var(--accent); }
|
|
654
|
+
.mp-isec { display: flex; flex-direction: column; gap: 8px; }
|
|
655
|
+
.mp-overview { white-space: pre-wrap; margin: 4px 0 10px; line-height: 1.5; }
|
|
656
|
+
.mp-cites { font-family: var(--font-mono); font-size: 11px; overflow-wrap: anywhere; }
|
|
657
|
+
.mp-conn { display: flex; flex-direction: column; gap: 2px; padding: 6px 0; border-bottom: 1px solid var(--line); }
|
|
658
|
+
.mp-ev { font-family: var(--font-mono); font-size: 11px; color: var(--ink-dim); }
|
|
659
|
+
.mp-fcard { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; padding: 10px; border: 1px solid var(--line); border-radius: var(--radius-md); }
|
|
660
|
+
.mp-fcard[data-proven="false"] { border-style: dashed; }
|
|
661
|
+
.mp-notmeasured { margin-top: auto; padding-top: 10px; border-top: 1px solid var(--line); display: flex; flex-direction: column; gap: 4px; }
|
|
662
|
+
/* The navigator's Map entry under a project, and its count of findings (spec §11). */
|
|
663
|
+
.map-row .map-word { font-size: 13px; font-weight: 600; }
|
|
664
|
+
.map-count { min-width: 20px; height: 20px; padding: 0 6px; box-sizing: border-box; border-radius: var(--radius-pill); background: color-mix(in srgb, var(--status-bad) 18%, transparent); color: var(--status-bad); font-size: 11px; font-weight: 700; display: inline-flex; align-items: center; justify-content: center; flex: none; }
|
|
665
|
+
.mp-cite { border: 0; background: none; padding: 0; text-align: left; cursor: pointer; }
|
|
666
|
+
.mp-cite:hover { color: var(--ink); text-decoration: underline; }
|
|
667
|
+
.mp-pill[data-tone="model"] { color: var(--studio-accent); background: color-mix(in srgb, var(--studio-accent) 14%, transparent); }
|
|
668
|
+
/* A phone shows the findings and a list of the modules in place of the pane (spec §11). */
|
|
669
|
+
.mp-modlist { display: none; flex-direction: column; gap: 2px; padding: 10px 14px; border-top: 1px solid var(--line); }
|
|
670
|
+
.mp-mrow { display: flex; align-items: center; gap: 8px; min-height: 36px; padding: 0 8px; border: 0; border-radius: var(--radius-md); background: transparent; color: var(--ink); font: inherit; text-align: left; cursor: pointer; }
|
|
671
|
+
.mp-mrow:hover { background: var(--surface-raised); }
|
|
672
|
+
@media (max-width: 1023px) {
|
|
673
|
+
.mp-body { flex-direction: column; }
|
|
674
|
+
.mp-inspect { width: auto; border-left: 0; border-top: 1px solid var(--line); }
|
|
675
|
+
}
|
|
676
|
+
@media (max-width: 639px) {
|
|
677
|
+
.mp-canvas-pane, .mp-hint { display: none; }
|
|
678
|
+
.mp-modlist { display: flex; }
|
|
679
|
+
.mp-flist { max-height: none; }
|
|
680
|
+
}
|
|
556
681
|
.mv-pane { border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--surface); overflow: hidden; }
|
|
557
682
|
.mv-head { display: flex; align-items: center; gap: var(--space-2); padding: 14px 18px; border-bottom: 1px solid var(--line); flex-wrap: wrap; }
|
|
558
683
|
.mv-title { margin: 0; font-size: 15px; font-weight: 700; }
|
|
@@ -571,6 +696,7 @@ mark { background: color-mix(in srgb, var(--studio-accent) 30%, transparent); co
|
|
|
571
696
|
.mv-raised .mv-tag[data-kind="lowered"] { margin-left: 0; }
|
|
572
697
|
.mv-tag[data-tone="pass"] { color: var(--status-good); background: color-mix(in srgb, var(--status-good) 14%, transparent); }
|
|
573
698
|
.mv-tag[data-tone="fail"] { color: var(--status-fail); background: color-mix(in srgb, var(--status-fail) 14%, transparent); }
|
|
699
|
+
.mv-tag[data-kind="advisor"] { margin-left: 6px; color: var(--studio-accent); background: color-mix(in srgb, var(--studio-accent) 16%, transparent); }
|
|
574
700
|
.mv-tag[data-kind="escalated"] { color: var(--studio-accent); background: color-mix(in srgb, var(--studio-accent) 16%, transparent); }
|
|
575
701
|
.mv-table td[data-partial="true"] { color: var(--ink-dim); }
|
|
576
702
|
.mv-meter { display: inline-block; width: 72px; height: 6px; margin-left: 8px; border-radius: 999px; background: var(--surface-raised); overflow: hidden; vertical-align: middle; }
|
|
@@ -581,6 +707,11 @@ mark { background: color-mix(in srgb, var(--studio-accent) 30%, transparent); co
|
|
|
581
707
|
.mv-suggest-h { margin: 0; font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--studio-accent); }
|
|
582
708
|
.mv-suggest-text { margin: 0; font-size: 14px; line-height: 21px; }
|
|
583
709
|
.mv-suggest-row { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
|
|
710
|
+
.mv-above { margin: 16px 18px; padding: 14px 16px; display: flex; flex-direction: column; gap: 6px; border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--surface-raised); }
|
|
711
|
+
.mv-above .mv-suggest-h { color: var(--ink-dim); }
|
|
712
|
+
.mv-above p { margin: 0; }
|
|
713
|
+
.mv-above-verdict { font-size: 13px; line-height: 19px; }
|
|
714
|
+
.mv-above-verdict[data-measured="false"] { color: var(--ink-dim); }
|
|
584
715
|
.mv-cmd { padding: 2px 6px; border-radius: var(--radius-sm); background: var(--surface-raised); font-size: 12px; user-select: all; }
|
|
585
716
|
.mv-raised { display: flex; align-items: baseline; gap: var(--space-2); padding: 0 18px 16px; }
|
|
586
717
|
.mv-raised .mv-tag { margin-left: 0; flex: none; }
|
package/kit/studio/web/usage.js
CHANGED
|
@@ -111,6 +111,18 @@ export function summaryRows(u, { live = false, now = null } = {}) {
|
|
|
111
111
|
};
|
|
112
112
|
part('Session', u.parts?.session, 'The session\'s own transcript');
|
|
113
113
|
part('Agents', u.parts?.agents, 'Everything its agents did');
|
|
114
|
+
// The advisor: a second model the main one consulted mid-response. Its tokens are in the rows above already; this
|
|
115
|
+
// says how much of them it was. No call recorded, no row: a session that never consulted it shows no zero.
|
|
116
|
+
if (u.advisor?.calls) {
|
|
117
|
+
const a = u.advisor;
|
|
118
|
+
const who = Object.keys(u.advisorModels ?? {}).map(modelName).join(', ');
|
|
119
|
+
rows.push({
|
|
120
|
+
label: 'Advisor',
|
|
121
|
+
value: `${a.calls} ${a.calls === 1 ? 'call' : 'calls'} \u00b7 ${fmtCount(a.tokens.fresh)} \u00b7 ${fmtCost(a.cost)}`,
|
|
122
|
+
note: `Advice the main model asked a second model for${who ? ` (${who})` : ''}, priced at that model's rates. Included in the totals above; the API's own token count leaves it out\n${costNote(a)}`,
|
|
123
|
+
detail: kinds(a.tokens),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
114
126
|
// Behind the cost: each model's new tokens and what they come to. A model with no price says so with a dash.
|
|
115
127
|
const perModel = Object.entries(u.byModel ?? {}).map(([model, t]) => [modelName(model), `${fmtCount(t.tokens.fresh)} \u00b7 ${fmtCost(t.cost)}`]);
|
|
116
128
|
rows.push({ label: 'Cost', value: k.total ? fmtCost(u.cost) : NO_COST, note: costNote(u), detail: perModel.length ? perModel : undefined });
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// A pane that pans and zooms one layer, and cards that can be dragged on it.
|
|
2
|
+
//
|
|
3
|
+
// The graph (canvas.js) and the project map (map.js) both draw cards on a layer the viewer moves around. This is
|
|
4
|
+
// that part, once: a drag on the empty pane pans, the wheel zooms about the pointer (or pans, when it is mostly
|
|
5
|
+
// sideways), Fit puts the whole picture in the pane, and a card dragged more than a few pixels is placed by hand.
|
|
6
|
+
// Where things go and what a card says stay with the view that owns them.
|
|
7
|
+
import { ZOOM, fitZoom } from './graph-plan.js';
|
|
8
|
+
|
|
9
|
+
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
10
|
+
|
|
11
|
+
export class Viewport {
|
|
12
|
+
/**
|
|
13
|
+
* @param pane the element the pointer works on; its size is the pane's
|
|
14
|
+
* @param layer the element moved and scaled: one transform, so a zoom restyles nothing
|
|
15
|
+
* @param skip (target) => true when a press on it is not a pan (a card, a control on the pane)
|
|
16
|
+
* @param onApply called after every change of the view
|
|
17
|
+
*/
|
|
18
|
+
constructor(pane, layer, { skip = () => false, onApply = () => {} } = {}) {
|
|
19
|
+
this.pane = pane;
|
|
20
|
+
this.layer = layer;
|
|
21
|
+
this.skip = skip;
|
|
22
|
+
this.onApply = onApply;
|
|
23
|
+
this.view = { x: 0, y: 0, k: 1 };
|
|
24
|
+
this.touched = false; // the viewer moved the view themselves
|
|
25
|
+
this.#wire();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#wire() {
|
|
29
|
+
let panning = null;
|
|
30
|
+
this.pane.addEventListener('pointerdown', (e) => {
|
|
31
|
+
if (e.button !== 0) return; // right/middle must not pan
|
|
32
|
+
if (this.skip(e.target)) return;
|
|
33
|
+
panning = { px: e.clientX, py: e.clientY, x: this.view.x, y: this.view.y };
|
|
34
|
+
this.pane.setPointerCapture(e.pointerId);
|
|
35
|
+
this.pane.classList.add('cv-panning');
|
|
36
|
+
});
|
|
37
|
+
this.pane.addEventListener('pointermove', (e) => {
|
|
38
|
+
if (!panning) return;
|
|
39
|
+
this.view.x = panning.x + (e.clientX - panning.px);
|
|
40
|
+
this.view.y = panning.y + (e.clientY - panning.py);
|
|
41
|
+
this.touched = true;
|
|
42
|
+
this.apply();
|
|
43
|
+
});
|
|
44
|
+
const endPan = () => { panning = null; this.pane.classList.remove('cv-panning'); };
|
|
45
|
+
this.pane.addEventListener('pointerup', endPan);
|
|
46
|
+
this.pane.addEventListener('pointercancel', endPan);
|
|
47
|
+
|
|
48
|
+
this.pane.addEventListener('wheel', (e) => {
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
const r = this.pane.getBoundingClientRect();
|
|
51
|
+
const mx = e.clientX - r.left;
|
|
52
|
+
const my = e.clientY - r.top;
|
|
53
|
+
// deltaMode 1 is lines and 2 is pages; Firefox reports ~3 where Chrome reports ~100, which made zoom
|
|
54
|
+
// imperceptible and pan crawl.
|
|
55
|
+
const unit = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? r.height : 1;
|
|
56
|
+
const dY = e.deltaY * unit;
|
|
57
|
+
const dX = e.deltaX * unit;
|
|
58
|
+
if (e.ctrlKey || e.metaKey || Math.abs(dY) > Math.abs(dX)) {
|
|
59
|
+
this.zoomAbout(this.view.k * Math.exp(-dY * 0.0016), mx, my);
|
|
60
|
+
} else {
|
|
61
|
+
this.view.x -= dX;
|
|
62
|
+
this.view.y -= dY;
|
|
63
|
+
}
|
|
64
|
+
this.touched = true;
|
|
65
|
+
this.apply();
|
|
66
|
+
}, { passive: false });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Zoom to `k`, keeping the world point under (mx, my) where it is. */
|
|
70
|
+
zoomAbout(k, mx, my) {
|
|
71
|
+
const next = clamp(k, ZOOM.min, ZOOM.max);
|
|
72
|
+
this.view.x = mx - (mx - this.view.x) * (next / this.view.k);
|
|
73
|
+
this.view.y = my - (my - this.view.y) * (next / this.view.k);
|
|
74
|
+
this.view.k = next;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Step the zoom from a toolbar: about the middle of the pane. */
|
|
78
|
+
zoomBy(factor) { this.zoomTo(this.view.k * factor); }
|
|
79
|
+
|
|
80
|
+
zoomTo(k) {
|
|
81
|
+
const r = this.pane.getBoundingClientRect();
|
|
82
|
+
this.zoomAbout(k, r.width / 2, r.height / 2);
|
|
83
|
+
this.touched = true;
|
|
84
|
+
this.apply();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Turn `view` into pixels: the single place that does. */
|
|
88
|
+
apply() {
|
|
89
|
+
const { x, y, k } = this.view;
|
|
90
|
+
this.layer.style.transform = `translate(${x}px, ${y}px) scale(${k})`;
|
|
91
|
+
this.onApply();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The whole picture (width × height, in the layer's own pixels) in the pane, never above 100%. */
|
|
95
|
+
/** Fit the picture to the pane; `floor` is a zoom it does not go under (the picture then starts at its top). */
|
|
96
|
+
fit(width, height, floor = 0) {
|
|
97
|
+
const r = this.pane.getBoundingClientRect();
|
|
98
|
+
const k = Math.max(fitZoom(width, height, r.width, r.height), Math.min(floor, ZOOM.fitMax));
|
|
99
|
+
this.view.k = k;
|
|
100
|
+
this.view.x = Math.max(ZOOM.pad, (r.width - width * k) / 2);
|
|
101
|
+
this.view.y = Math.max(ZOOM.pad, (r.height - height * k) / 2);
|
|
102
|
+
this.touched = false;
|
|
103
|
+
this.apply();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Bring a point of the layer to the middle of the pane, at least at `minK`. */
|
|
107
|
+
centerOn(cx, cy, minK = 1) {
|
|
108
|
+
const r = this.pane.getBoundingClientRect();
|
|
109
|
+
this.view.k = clamp(Math.max(this.view.k, minK), ZOOM.min, ZOOM.max);
|
|
110
|
+
this.view.x = r.width / 2 - cx * this.view.k;
|
|
111
|
+
this.view.y = r.height / 2 - cy * this.view.k;
|
|
112
|
+
this.touched = true;
|
|
113
|
+
this.apply();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* A card that can be placed by hand. A press that moves more than 3 px (at the pane's zoom) is a drag, and the
|
|
119
|
+
* click that ends it is swallowed: without that, every reposition also toggled the selection.
|
|
120
|
+
* @param scale () => the pane's zoom
|
|
121
|
+
* @param at () => the card's position { x, y } when the drag starts
|
|
122
|
+
* @param move (x, y) => the card is now here
|
|
123
|
+
* @param end () => the drag is over (persist the place)
|
|
124
|
+
* @param ignore (target) => true for a part of the card that answers the press itself
|
|
125
|
+
*/
|
|
126
|
+
export function draggable(el, { scale, at, move, end = () => {}, ignore = () => false }) {
|
|
127
|
+
let drag = null;
|
|
128
|
+
el.addEventListener('pointerdown', (e) => {
|
|
129
|
+
if (e.button !== 0) return;
|
|
130
|
+
e.stopPropagation(); // do not pan the pane too
|
|
131
|
+
if (ignore(e.target)) return;
|
|
132
|
+
const p = at() ?? { x: 0, y: 0 };
|
|
133
|
+
drag = { px: e.clientX, py: e.clientY, x: p.x, y: p.y, moved: false };
|
|
134
|
+
el.setPointerCapture(e.pointerId);
|
|
135
|
+
});
|
|
136
|
+
el.addEventListener('pointermove', (e) => {
|
|
137
|
+
if (!drag) return;
|
|
138
|
+
const dx = (e.clientX - drag.px) / scale();
|
|
139
|
+
const dy = (e.clientY - drag.py) / scale();
|
|
140
|
+
if (!drag.moved && Math.abs(dx) <= 3 && Math.abs(dy) <= 3) return;
|
|
141
|
+
drag.moved = true;
|
|
142
|
+
el.classList.add('cv-dragging');
|
|
143
|
+
move(Math.round(drag.x + dx), Math.round(drag.y + dy));
|
|
144
|
+
});
|
|
145
|
+
const stop = () => {
|
|
146
|
+
if (!drag) return;
|
|
147
|
+
if (drag.moved) { end(); el.dataset.suppressClick = '1'; }
|
|
148
|
+
drag = null;
|
|
149
|
+
el.classList.remove('cv-dragging');
|
|
150
|
+
};
|
|
151
|
+
el.addEventListener('pointerup', stop);
|
|
152
|
+
el.addEventListener('pointercancel', stop);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Did a drag just end on this element? Answers once, so the click that ends a drag does nothing. */
|
|
156
|
+
export function swallowClick(el) {
|
|
157
|
+
if (el.dataset.suppressClick !== '1') return false;
|
|
158
|
+
el.dataset.suppressClick = '0';
|
|
159
|
+
return true;
|
|
160
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crewforth",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "Crewforth
|
|
3
|
+
"version": "3.2.0-rc.2",
|
|
4
|
+
"description": "Crewforth — your engineering crew for Claude Code: agents, skills and tool-level gates, in any stack.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"crewforth": "bin/cli.js"
|
|
7
7
|
},
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"kit/",
|
|
16
16
|
"VERSION",
|
|
17
17
|
"LICENSE",
|
|
18
|
-
"README.md",
|
|
18
|
+
"README.md",
|
|
19
|
+
"CHANGELOG.md"
|
|
19
20
|
],
|
|
20
21
|
"engines": {
|
|
21
22
|
"node": ">=20"
|
|
@@ -42,10 +43,10 @@
|
|
|
42
43
|
"code-review",
|
|
43
44
|
"git-hooks"
|
|
44
45
|
],
|
|
45
|
-
"author": "
|
|
46
|
+
"author": "Barış Yerlikaya",
|
|
46
47
|
"homepage": "https://crewforth.com/",
|
|
47
48
|
"repository": {
|
|
48
49
|
"type": "git",
|
|
49
|
-
"url": "git+https://github.com/
|
|
50
|
+
"url": "git+https://github.com/crewforth/crewforth.git"
|
|
50
51
|
}
|
|
51
52
|
}
|
package/start.sh
CHANGED
|
@@ -96,6 +96,8 @@ _mt() { # $1 = English text (the key); further args fill %s; result in _M
|
|
|
96
96
|
if [ "$CREW_LANG" = tr ]; then
|
|
97
97
|
case "$s" in
|
|
98
98
|
"Crewforth · setup wizard") s='Crewforth · kurulum' ;;
|
|
99
|
+
"Status line: Crewforth's (the gate's last word, a standing approval, crew agents, fill, cost).") s="Durum satırı: Crewforth'unki (kapının son sözü, geçerli onay, crew ajanları, doluluk, maliyet)." ;;
|
|
100
|
+
"Status line: yours is kept; Crewforth's is one command away (see /crew-doctor).") s="Durum satırı: seninki korundu; Crewforth'unki tek komutla açılır (/crew-doctor'a bak)." ;;
|
|
99
101
|
"stack-agnostic") s='her yığınla çalışır' ;;
|
|
100
102
|
"[1/2] Who is this install for?") s='[1/2] Kurulumu kim kullanacak?' ;;
|
|
101
103
|
"Decides whether your teammates get Crewforth's configuration — and what goes into .gitignore.") s="Crewforth ayarlarının ekiple paylaşılıp paylaşılmayacağını ve .gitignore'a nelerin ekleneceğini belirler." ;;
|
|
@@ -638,6 +640,14 @@ count_showcase .claude/skills
|
|
|
638
640
|
# studio's gate hook is invoked as `bash <path>` today, so the bit is not load-bearing
|
|
639
641
|
# yet; set it anyway, because a future direct exec would fail silently.
|
|
640
642
|
chmod +x .claude/hooks/*.sh .claude/hooks/pre-commit .claude/hooks/commit-msg .claude/eval/*.sh .claude/studio/server/hooks/*.sh 2>/dev/null || true
|
|
643
|
+
# The status line, only where none is set: a user's own would lose to the project's setting, so it is never
|
|
644
|
+
# replaced. eval/lib/statusline.sh --install decides and writes; this only says what it did.
|
|
645
|
+
if [ -f .claude/eval/lib/statusline.sh ]; then
|
|
646
|
+
case "$(bash .claude/eval/lib/statusline.sh --install 2>/dev/null)" in
|
|
647
|
+
installed) _mt "Status line: Crewforth's (the gate's last word, a standing approval, crew agents, fill, cost)."; echo " ${_M}" ;;
|
|
648
|
+
kept) _mt "Status line: yours is kept; Crewforth's is one command away (see /crew-doctor)."; echo " ${_M}" ;;
|
|
649
|
+
esac
|
|
650
|
+
fi
|
|
641
651
|
|
|
642
652
|
# §4.2: a vendor line that was armed before this run stays armed (the copy above resets it to a comment); nothing
|
|
643
653
|
# else arms it. Rationale, and the measured failure of "arm it whenever the pattern skill exists", in adopt.sh.
|