Просмотр исходного кода

feat(skills): fleetflow monitor v3 — brain icon chips, name labels, phase grouping

Per-brain letter chips with distinct hues (G/C/S/O/H/F), brain name next to
the lane id, and native-style phase sections (ff-spawn --phase, journaled as
display metadata outside the cache key; ff-status propagates; monitor groups
lanes under phase headers with per-phase square pips and done counts).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
0xDarkMatter 2 недель назад
Родитель
Сommit
4db2c25959

+ 29 - 6
skills/fleetflow/assets/ff-monitor.html

@@ -25,15 +25,23 @@
   .sq.running { background:var(--run); animation:pulse 1.1s ease-in-out infinite; }
   .sq.done { background:var(--ok); } .sq.failed { background:var(--bad); }
   @keyframes pulse { 50% { opacity:.3 } }
+  .phase { margin-bottom:10px; }
+  .phase-h { display:flex; align-items:center; gap:8px; padding:2px 2px 6px;
+    font-size:10px; color:var(--muted); text-transform:uppercase; letter-spacing:.08em; }
+  .phase-h .sq-row { margin:0; }
   .grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(300px,1fr)); gap:8px; }
   .agent { background:var(--card); border:1px solid var(--border); border-radius:8px; }
   .agent summary { list-style:none; cursor:pointer; padding:8px 10px; }
   .agent summary::-webkit-details-marker { display:none; }
   .row1 { display:flex; align-items:center; gap:7px; }
   .row1 .sq { flex:none; }
+  .chip { width:15px; height:15px; border-radius:3.5px; flex:none; display:flex;
+    align-items:center; justify-content:center; color:#fff; font-size:9px;
+    font-weight:700; font-family:ui-monospace, Consolas, monospace; }
   .id { font-weight:600; font-size:12px; }
-  .brain { margin-left:auto; font-size:10px; color:var(--muted); text-transform:uppercase;
+  .brain { font-size:10px; color:var(--muted); text-transform:uppercase;
     letter-spacing:.06em; }
+  .row1 .sq.state { margin-left:auto; }
   .cols { display:grid; grid-template-columns:1fr 1fr 1fr 1fr; gap:4px; margin-top:7px;
     font-size:11px; color:var(--muted); }
   .cols b { display:block; color:var(--text); font-weight:500; font-size:11px; }
@@ -51,7 +59,7 @@
     <div class="head-row"><h1 id="run">fleetflow</h1><span class="meta mono" id="meta"></span></div>
     <div class="sq-row" id="sqs"></div>
   </div>
-  <div class="grid" id="grid"></div>
+  <div id="phases"></div>
   <div class="foot mono"><span id="foot">waiting for status.json…</span><span id="ts"></span></div>
 </div>
 <script>
@@ -78,11 +86,16 @@ async function tick() {
     (oldest ? " · " + fmt(d.generated_at - oldest) : "");
   document.getElementById("sqs").innerHTML =
     lanes.map(l => `<div class="sq ${l.state}" title="${esc(l.id)}"></div>`).join("");
-  document.getElementById("grid").innerHTML = lanes.map(l => `
+  const BRAIN = { glm:["G","#7f77dd"], codex:["C","#0f6e56"], sonnet:["S","#d85a30"],
+    opus:["O","#ba7517"], haiku:["H","#d4537e"], fable:["F","#378add"] };
+  const card = l => {
+    const [ltr, col] = BRAIN[l.brain] || ["?", "#888780"];
+    return `
     <details class="agent" data-id="${esc(l.id)}" ${open.has(l.id) ? "open" : ""}>
       <summary>
-        <div class="row1"><span class="sq ${l.state}"></span>
-          <span class="id mono">${esc(l.id)}</span><span class="brain mono">${esc(l.brain)}</span></div>
+        <div class="row1"><span class="chip" style="background:${col}">${ltr}</span>
+          <span class="id mono">${esc(l.id)}</span><span class="brain mono">${esc(l.brain)}</span>
+          <span class="sq state ${l.state}"></span></div>
         <div class="cols mono">
           <span><b>${fmt(l.elapsed_s)}</b>time</span>
           <span><b>${l.tools || "—"}</b>tools</span>
@@ -96,7 +109,17 @@ async function tick() {
         ${l.err_tail ? `<div class="err">${esc(l.err_tail)}</div>` : ""}
         <div><span class="k">out</span> ${esc((l.artifact||"pending").split(/[\\/]/).pop())}</div>
       </div>
-    </details>`).join("");
+    </details>`; };
+  const phases = [];
+  lanes.forEach(l => { const p = l.phase || "build"; if (!phases.includes(p)) phases.push(p); });
+  document.getElementById("phases").innerHTML = phases.map(p => {
+    const pl = lanes.filter(l => (l.phase || "build") === p);
+    return `<div class="phase">
+      <div class="phase-h mono"><span>${esc(p)}</span>
+        <div class="sq-row">${pl.map(l => `<div class="sq ${l.state}"></div>`).join("")}</div>
+        <span>${pl.filter(l => l.state !== "running").length}/${pl.length}</span></div>
+      <div class="grid">${pl.map(card).join("")}</div>
+    </div>`; }).join("");
   document.querySelectorAll("details.agent").forEach(el =>
     el.addEventListener("toggle", () => el.open ? open.add(el.dataset.id) : open.delete(el.dataset.id)));
   document.getElementById("foot").textContent = d.repo;

+ 6 - 3
skills/fleetflow/scripts/ff-spawn.sh

@@ -20,6 +20,7 @@ Usage: ff-spawn.sh --run NAME --id ID --brain BRAIN --prompt-file FILE
   --id ID          lane id within the run ([a-z0-9-]+)
   --brain BRAIN    glm | codex | sonnet | opus | haiku | fable
   --prompt-file F  packet file (guard preamble is prepended unless --no-guard)
+  --phase NAME     progress-group label (default: build) - display only
   --worktree       give the worker its own worktree lane (branch fleetflow/RUN/ID)
   --base BRANCH    worktree base (default: main, falls back to HEAD)
   --repo PATH      repo root (default: git toplevel of cwd)
@@ -42,10 +43,11 @@ EOF
 err() { echo "ff-spawn: $*" >&2; }
 
 RUN="" ID="" BRAIN="" PROMPT_FILE="" WORKTREE=0 BASE="main" REPO=""
-MAX_TURNS=100 SCHEMA="" GUARD=1 FORCE=0 DRYRUN=0
+MAX_TURNS=100 SCHEMA="" GUARD=1 FORCE=0 DRYRUN=0 PHASE="build"
 while [ $# -gt 0 ]; do
   case "$1" in
     --run) RUN="${2:-}"; shift 2 ;;
+    --phase) PHASE="${2:-}"; shift 2 ;;
     --id) ID="${2:-}"; shift 2 ;;
     --brain) BRAIN="${2:-}"; shift 2 ;;
     --prompt-file) PROMPT_FILE="${2:-}"; shift 2 ;;
@@ -121,8 +123,9 @@ if [ "$WORKTREE" = 1 ]; then
   fi
 fi
 
-jq -nc --arg k "$KEY" --arg id "$ID" --arg b "$BRAIN" \
-  '{type:"started",key:$k,id:$id,brain:$b}' >> "$JOURNAL"
+# phase is display metadata only - deliberately NOT part of the cache key
+jq -nc --arg k "$KEY" --arg id "$ID" --arg b "$BRAIN" --arg p "$PHASE" \
+  '{type:"started",key:$k,id:$id,brain:$b,phase:$p}' >> "$JOURNAL"
 
 # --- launch -------------------------------------------------------------------
 ART="$RUNDIR/$ID.result.json"

+ 3 - 2
skills/fleetflow/scripts/ff-status.sh

@@ -54,6 +54,7 @@ emit() {
   lanes="[]"
   for id in $(jq -r 'select(.type=="started") | .id' "$RUNDIR/journal.jsonl" | awk '!seen[$0]++'); do
     brain="$(jq -r --arg id "$id" 'select(.id==$id) | .brain' "$RUNDIR/journal.jsonl" | head -1)"
+    phase="$(jq -r --arg id "$id" 'select(.type=="started" and .id==$id) | .phase // "build"' "$RUNDIR/journal.jsonl" | head -1)"
     rc="$(jq -r --arg id "$id" 'select(.type=="result" and .id==$id) | .rc' "$RUNDIR/journal.jsonl" | tail -1)"
     art="$(jq -r --arg id "$id" 'select(.type=="result" and .id==$id) | .artifact' "$RUNDIR/journal.jsonl" | tail -1)"
     if [ -z "$rc" ]; then state="running"; finished=0
@@ -95,10 +96,10 @@ emit() {
 
     lanes="$(jq -nc --argjson L "$lanes" \
       --arg id "$id" --arg brain "$brain" --arg state "$state" --arg activity "$activity" \
-      --arg last_c "$last_c" --arg etail "$etail" --arg art "${art:-}" \
+      --arg last_c "$last_c" --arg etail "$etail" --arg art "${art:-}" --arg phase "${phase:-build}" \
       --argjson started "$started" --argjson elapsed "$elapsed" \
       --argjson commits "${commits:-0}" --argjson tools "${tools:-0}" --argjson tokens "${tokens:-0}" \
-      '$L + [{id:$id,brain:$brain,state:$state,started:$started,elapsed_s:$elapsed,
+      '$L + [{id:$id,brain:$brain,phase:$phase,state:$state,started:$started,elapsed_s:$elapsed,
               commits:$commits,tools:$tools,tokens:$tokens,activity:$activity,
               last_commit:$last_c,artifact:$art,err_tail:$etail}]')"
   done

+ 7 - 0
skills/fleetflow/tests/run.sh

@@ -78,6 +78,13 @@ check "collect: codex schema-valid JSON" 0 bash "$S/ff-collect.sh" --run r1 --id
 printf 'not json' > "$REPO/.fleetflow/r1/cx.last.txt"
 check "collect: codex schema-invalid fails" 10 bash "$S/ff-collect.sh" --run r1 --id cx --repo "$REPO" --schema
 
+# --- phases --------------------------------------------------------------------------
+check "spawn: --phase accepted" 0 bash "$S/ff-spawn.sh" --run r1 --id ver --brain opus --phase verify --prompt-file "$PKT" --repo "$REPO" --dry-run
+bash "$S/ff-status.sh" --run r1 --repo "$REPO" 2>/dev/null | jq -e '.lanes[] | select(.id=="ver") | .phase=="verify"' >/dev/null \
+  && ok "status: phase propagates" || bad "status: phase missing"
+bash "$S/ff-status.sh" --run r1 --repo "$REPO" 2>/dev/null | jq -e '.lanes[] | select(.id=="a") | .phase=="build"' >/dev/null \
+  && ok "status: default phase is build" || bad "status: default phase wrong"
+
 # --- status feed --------------------------------------------------------------------
 check "status: no args" 2 bash "$S/ff-status.sh"
 check "status: watch without out" 2 bash "$S/ff-status.sh" --run r1 --repo "$REPO" --watch 3