Explorar o código

feat(skills): fleetflow schema-retry (fence strip + --repair)

ff-collect now strips markdown code fences (```json ... ```) from the final text before --schema JSON validation (both codex last-message and claude result paths). New --repair flag: on schema failure, writes the bad output to <run>/<id>.invalid.txt and respawns a <id>-repair lane via ff-spawn (same brain, --max-turns 3, no worktree, corrective prompt), then gates the repair artifact and prints ITS text on success. One attempt only; exit 10 if the repair also fails. FLEETFLOW_REPAIR_DRYRUN forces the respawn to --dry-run (offline test seam).

Co-Authored-By: Claude <noreply@anthropic.com>
0xDarkMatter hai 2 semanas
pai
achega
e2d9f7b01f
Modificáronse 1 ficheiros con 55 adicións e 3 borrados
  1. 55 3
      skills/fleetflow/scripts/ff-collect.sh

+ 55 - 3
skills/fleetflow/scripts/ff-collect.sh

@@ -40,13 +40,14 @@ EOF
 
 
 err() { echo "ff-collect: $*" >&2; }
 err() { echo "ff-collect: $*" >&2; }
 
 
-RUN="" ID="" REPO="" SCHEMA=0 CHECK_CLEAN=0
+RUN="" ID="" REPO="" SCHEMA=0 CHECK_CLEAN=0 REPAIR=0 JQERR=""
 while [ $# -gt 0 ]; do
 while [ $# -gt 0 ]; do
   case "$1" in
   case "$1" in
     --run) RUN="${2:-}"; shift 2 ;;
     --run) RUN="${2:-}"; shift 2 ;;
     --id) ID="${2:-}"; shift 2 ;;
     --id) ID="${2:-}"; shift 2 ;;
     --repo) REPO="${2:-}"; shift 2 ;;
     --repo) REPO="${2:-}"; shift 2 ;;
     --schema) SCHEMA=1; shift ;;
     --schema) SCHEMA=1; shift ;;
+    --repair) REPAIR=1; shift ;;
     --check-main-clean) CHECK_CLEAN=1; shift ;;
     --check-main-clean) CHECK_CLEAN=1; shift ;;
     -h|--help) usage; exit 0 ;;
     -h|--help) usage; exit 0 ;;
     *) err "unknown argument: $1"; usage >&2; exit 2 ;;
     *) err "unknown argument: $1"; usage >&2; exit 2 ;;
@@ -57,6 +58,45 @@ command -v jq >/dev/null || { err "jq required"; exit 2; }
 [ -n "$REPO" ] || REPO="$(git rev-parse --show-toplevel 2>/dev/null)" || true
 [ -n "$REPO" ] || REPO="$(git rev-parse --show-toplevel 2>/dev/null)" || true
 [ -n "$REPO" ] && [ -d "$REPO" ] || { err "not in a git repo (or --repo invalid)"; exit 2; }
 [ -n "$REPO" ] && [ -d "$REPO" ] || { err "not in a git repo (or --repo invalid)"; exit 2; }
 
 
+HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+SPAWN="$HERE/ff-spawn.sh"
+COLLECT="$HERE/ff-collect.sh"
+
+# strip markdown code fences (```json ... ```) so schema gating tolerates fenced replies
+strip_fences() { sed -E '/^[[:space:]]*```[[:alnum:]]*[[:space:]]*$/d'; }
+
+# stdin = text; returns 0 if valid JSON else 1; sets global JQERR to the parse error
+json_ok() {
+  local t; t="$(cat)"
+  JQERR="$(printf '%s' "$t" | jq empty 2>&1 1>/dev/null | head -1)"
+  printf '%s' "$t" | jq empty >/dev/null 2>&1
+}
+
+# do_repair <bad_text> <jq_error>: save bad output, respawn a <id>-repair lane
+# (same brain, --max-turns 3, no worktree), gate it, print its text on success.
+# One attempt only. Exits 0 (repaired) or 10 (repair also failed). FLEETFLOW_REPAIR_DRYRUN
+# forces the respawn to --dry-run (test/offline seam).
+do_repair() {
+  local bad="$1" jqerr="$2" rid="${ID}-repair" pf spawn_rc=0 rout rc2
+  pf="$RUNDIR/$rid.prompt-src.txt"
+  printf '%s' "$bad" > "$RUNDIR/$ID.invalid.txt"
+  err "schema validation failed (--repair): saved $RUNDIR/$ID.invalid.txt; respawning lane $rid"
+  printf 'Your previous FINAL REPLY failed JSON validation. Error: %s. Previous output: %s. Reply with ONLY the corrected JSON object, nothing else.' \
+    "$jqerr" "$bad" > "$pf"
+  if [ -n "${FLEETFLOW_REPAIR_DRYRUN:-}" ]; then
+    bash "$SPAWN" --run "$RUN" --id "$rid" --brain "$BRAIN" --max-turns 3 \
+      --repo "$REPO" --prompt-file "$pf" --dry-run >/dev/null 2>"$RUNDIR/$rid.spawn.err" || spawn_rc=$?
+  else
+    bash "$SPAWN" --run "$RUN" --id "$rid" --brain "$BRAIN" --max-turns 3 \
+      --repo "$REPO" --prompt-file "$pf" >/dev/null 2>"$RUNDIR/$rid.spawn.err" || spawn_rc=$?
+  fi
+  case "$spawn_rc" in 0|3) ;; *) err "repair spawn failed (rc=$spawn_rc; see $RUNDIR/$rid.spawn.err)"; exit 10 ;; esac
+  # gate the repair lane WITHOUT --repair (one attempt); its stdout is the repaired text
+  rout="$(bash "$COLLECT" --run "$RUN" --id "$rid" --repo "$REPO" --schema 2>"$RUNDIR/$rid.collect.err")"; rc2=$?
+  if [ "$rc2" = 0 ]; then printf '%s\n' "$rout"; exit 0; fi
+  err "repair lane also failed validation (see $RUNDIR/$rid.collect.err)"; exit 10
+}
+
 # --- escape guard -------------------------------------------------------------
 # --- escape guard -------------------------------------------------------------
 if [ "$CHECK_CLEAN" = 1 ]; then
 if [ "$CHECK_CLEAN" = 1 ]; then
   BASELINE=""
   BASELINE=""
@@ -92,7 +132,13 @@ if [ "$BRAIN" = "codex" ] || { [ -z "$BRAIN" ] && [ -f "$RUNDIR/$ID.last.txt" ];
   ART="$RUNDIR/$ID.last.txt"
   ART="$RUNDIR/$ID.last.txt"
   [ -s "$ART" ] || { err "codex last-message missing/empty: $ART"; exit 3; }
   [ -s "$ART" ] || { err "codex last-message missing/empty: $ART"; exit 3; }
   if [ "$SCHEMA" = 1 ]; then
   if [ "$SCHEMA" = 1 ]; then
-    jq empty < "$ART" 2>/dev/null || { err "final message is not valid JSON (schema lane)"; exit 10; }
+    CLEAN="$(strip_fences < "$ART")"
+    if ! printf '%s' "$CLEAN" | json_ok; then
+      [ "$REPAIR" = 1 ] && do_repair "$CLEAN" "$JQERR"
+      err "final message is not valid JSON (schema lane): $JQERR"; exit 10
+    fi
+    printf '%s\n' "$CLEAN"
+    exit 0
   fi
   fi
   cat "$ART"
   cat "$ART"
   exit 0
   exit 0
@@ -110,7 +156,13 @@ if [ "$IS_ERR" != "false" ]; then
 fi
 fi
 TEXT="$(jq -r '.result // empty' "$ART")"
 TEXT="$(jq -r '.result // empty' "$ART")"
 if [ "$SCHEMA" = 1 ]; then
 if [ "$SCHEMA" = 1 ]; then
-  printf '%s' "$TEXT" | jq empty 2>/dev/null || { err "final text is not valid JSON (schema lane)"; exit 10; }
+  CLEAN="$(printf '%s' "$TEXT" | strip_fences)"
+  if ! printf '%s' "$CLEAN" | json_ok; then
+    [ "$REPAIR" = 1 ] && do_repair "$CLEAN" "$JQERR"
+    err "final text is not valid JSON (schema lane): $JQERR"; exit 10
+  fi
+  printf '%s\n' "$CLEAN"
+  exit 0
 fi
 fi
 printf '%s\n' "$TEXT"
 printf '%s\n' "$TEXT"
 exit 0
 exit 0