@manhquy/flow-skill 0.5.0 → 0.6.0

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.
Files changed (29) hide show
  1. package/README.md +4 -4
  2. package/package.json +1 -1
  3. package/skills/flow/SKILL.md +12 -5
  4. package/skills/flow/_templates/02-scope.md +9 -0
  5. package/skills/flow/_templates/03-prd.md +11 -0
  6. package/skills/flow/_templates/05-contract.md +8 -0
  7. package/skills/flow/_templates/card.md +4 -0
  8. package/skills/flow/eval/fixtures/converge/conv-01/cards/C-001.md +21 -0
  9. package/skills/flow/eval/fixtures/converge/conv-01/flow/03-prd.md +7 -0
  10. package/skills/flow/eval/fixtures/converge/conv-01/src/app.py +3 -0
  11. package/skills/flow/eval/fixtures/converge/gap-01/cards/C-001.md +21 -0
  12. package/skills/flow/eval/fixtures/converge/gap-01/cards/C-002.md +21 -0
  13. package/skills/flow/eval/fixtures/converge/gap-01/flow/03-prd.md +8 -0
  14. package/skills/flow/eval/fixtures/converge/gap-01/flow/05-contract.md +7 -0
  15. package/skills/flow/eval/fixtures/converge/gap-01/src/app.py +11 -0
  16. package/skills/flow/eval/fixtures/converge/manifest.tsv +3 -0
  17. package/skills/flow/law/CLAUDE.md +7 -0
  18. package/skills/flow/references/artifact-lifecycle.md +58 -0
  19. package/skills/flow/references/clarify.md +191 -0
  20. package/skills/flow/references/command-dispatch.md +3 -1
  21. package/skills/flow/references/concierge.md +2 -0
  22. package/skills/flow/references/converge.md +109 -0
  23. package/skills/flow/references/flow-catalog.tsv +2 -0
  24. package/skills/flow/references/gate-rules.md +32 -0
  25. package/skills/flow/references/mode-work.md +11 -1
  26. package/skills/flow/references/stage-state-machine.md +9 -1
  27. package/skills/flow/runner/attestations.sh +249 -138
  28. package/skills/flow/runner/flow.sh +425 -5
  29. package/skills-manifest.json +14 -2
@@ -217,17 +217,15 @@ _att_subject_lock_name() { # kind subject_id
217
217
  }
218
218
 
219
219
  # ---------- cleanliness ----------
220
- # Returns 0 if clean enough for attestation (ignore .flow/ only via pathspec magic).
220
+ # Allowed dirt: .flow/**, flow/.lock{,.d/**}, cards/C-*.md with projection==HEAD.
221
221
  _att_git_clean_for_attest() {
222
222
  local porcelain
223
223
  porcelain="$(git -C "$ROOT" status --porcelain=v1 --untracked-files=all 2>/dev/null || echo FAIL)"
224
224
  [ "$porcelain" = "FAIL" ] && return 1
225
225
  [ -z "$porcelain" ] && return 0
226
- # Allow only changes under .flow/
227
226
  local line path
228
227
  while IFS= read -r line; do
229
228
  [ -z "$line" ] && continue
230
- # format: XY PATH or XY ORIG -> PATH
231
229
  path="${line#?? }"
232
230
  case "$path" in
233
231
  *" -> "*) path="${path##* -> }" ;;
@@ -235,6 +233,11 @@ _att_git_clean_for_attest() {
235
233
  path="${path#\"}"; path="${path%\"}"
236
234
  case "$path" in
237
235
  .flow|.flow/*) continue ;;
236
+ flow/.lock|flow/.lock.d|flow/.lock.d/*) continue ;;
237
+ cards/C-*.md)
238
+ _att_card_projection_matches_head "$path" || return 1
239
+ continue
240
+ ;;
238
241
  *) return 1 ;;
239
242
  esac
240
243
  done <<EOF
@@ -242,6 +245,23 @@ $porcelain
242
245
  EOF
243
246
  return 0
244
247
  }
248
+ _att_card_projection_matches_head() {
249
+ local rel="$1" tmp now old
250
+ [ -f "$ROOT/$rel" ] || return 1
251
+ git -C "$ROOT" cat-file -e "HEAD:${rel}" 2>/dev/null || return 1
252
+ tmp="$(mktemp 2>/dev/null || echo "${TMPDIR:-/tmp}/.att-proj.$$")"
253
+ if ! git -C "$ROOT" show "HEAD:${rel}" >"$tmp" 2>/dev/null; then
254
+ rm -f "$tmp"; return 1
255
+ fi
256
+ now="$(_att_card_contract_projection "$ROOT/$rel")"
257
+ old="$(_att_card_contract_projection "$tmp")"
258
+ rm -f "$tmp"
259
+ [ "$now" = "$old" ]
260
+ }
261
+ _att_require_clean_for_consume() {
262
+ _att_git_ok || return 0
263
+ _att_git_clean_for_attest
264
+ }
245
265
 
246
266
  # ---------- card field parsing ----------
247
267
  _att_field_count() { # $1 file $2 field
@@ -414,11 +434,13 @@ _att_parse_kv_file() { # $1 path — sets _ATT_KV_<key> via eval-safe names only
414
434
  }
415
435
 
416
436
  _att_owner_fingerprint() { # $1 owner path relative, $2 revision
417
- local rel="$1" rev="$2" cmd_path oracle_path mode blob o_mode o_blob body fmt oid
437
+ local rel="$1" rev="$2" cmd_path oracle_path mode blob o_mode o_blob body fmt oid owner_blob owner_mode
418
438
  cmd_path="${_ATT_KV_command#repo:}"
439
+ owner_blob="$(git -C "$ROOT" rev-parse "${rev}:${rel}" 2>/dev/null | tr -d '\r\n')" || return 1
440
+ owner_mode="$(git -C "$ROOT" ls-tree "$rev" -- "$rel" 2>/dev/null | awk '{print $1}' | head -1)"
419
441
  blob="$(git -C "$ROOT" rev-parse "${rev}:${cmd_path}" 2>/dev/null | tr -d '\r\n')" || return 1
420
442
  mode="$(git -C "$ROOT" ls-tree "$rev" -- "$cmd_path" 2>/dev/null | awk '{print $1}' | head -1)"
421
- body="owner=${rel}"$'\n'"cmd=${cmd_path}:${mode}:${blob}"$'\n'
443
+ body="owner=${rel}:${owner_mode}:${owner_blob}"$'\n'"cmd=${cmd_path}:${mode}:${blob}"$'\n'
422
444
  if [ "${_ATT_KV_revision_oracle}" != "none" ] && [ -n "${_ATT_KV_revision_oracle}" ]; then
423
445
  oracle_path="${_ATT_KV_revision_oracle#repo:}"
424
446
  o_blob="$(git -C "$ROOT" rev-parse "${rev}:${oracle_path}" 2>/dev/null | tr -d '\r\n')" || return 1
@@ -431,6 +453,53 @@ _att_owner_fingerprint() { # $1 owner path relative, $2 revision
431
453
  printf 'git-%s:%s' "$fmt" "$oid"
432
454
  }
433
455
 
456
+ # ---------- committed-blob execution ----------
457
+ _att_path_mode_at_rev() { git -C "$ROOT" ls-tree "$1" -- "$2" 2>/dev/null | awk '{print $1}' | head -1; }
458
+ _att_blob_at_rev() { git -C "$ROOT" rev-parse "${1}:${2}" 2>/dev/null | tr -d '\r\n'; }
459
+ _att_blob_on_disk() {
460
+ local f="$ROOT/$1"
461
+ [ -f "$f" ] || return 1
462
+ [ ! -L "$f" ] || return 1
463
+ git -C "$ROOT" hash-object -- "$f" 2>/dev/null | tr -d '\r\n'
464
+ }
465
+ _att_assert_committed_regular() {
466
+ local rev="$1" rel="$2" mode blob disk
467
+ mode="$(_att_path_mode_at_rev "$rev" "$rel")"
468
+ case "$mode" in 100644|100755) : ;; *) return 1 ;; esac
469
+ blob="$(_att_blob_at_rev "$rev" "$rel")" || return 1
470
+ [ -n "$blob" ] || return 1
471
+ disk="$(_att_blob_on_disk "$rel")" || return 1
472
+ [ "$disk" = "$blob" ] || return 1
473
+ _ATT_BLOB_OID="$blob"
474
+ return 0
475
+ }
476
+ _att_materialize_blob_exec() {
477
+ local rev="$1" rel="$2" mode blob tmp
478
+ mode="$(_att_path_mode_at_rev "$rev" "$rel")"
479
+ case "$mode" in 100644|100755) : ;; *) return 1 ;; esac
480
+ blob="$(_att_blob_at_rev "$rev" "$rel")" || return 1
481
+ tmp="$(mktemp 2>/dev/null || echo "${TMPDIR:-/tmp}/.att-exec.$$.$RANDOM")"
482
+ if ! git -C "$ROOT" cat-file blob "$blob" >"$tmp" 2>/dev/null; then
483
+ rm -f "$tmp"; return 1
484
+ fi
485
+ chmod 700 "$tmp" 2>/dev/null || chmod +x "$tmp" 2>/dev/null || true
486
+ printf '%s' "$tmp"
487
+ return 0
488
+ }
489
+ _att_load_owner_at_rev() {
490
+ local rev="$1" rel="$2" cmd_rel
491
+ _att_assert_committed_regular "$rev" "$rel" || return 1
492
+ _att_parse_kv_file "$ROOT/$rel" || return 1
493
+ case "${_ATT_KV_command:-}" in repo:*) : ;; *) return 1 ;; esac
494
+ cmd_rel="${_ATT_KV_command#repo:}"
495
+ _att_assert_committed_regular "$rev" "$cmd_rel" || return 1
496
+ if [ "${_ATT_KV_revision_oracle:-none}" != "none" ] && [ -n "${_ATT_KV_revision_oracle:-}" ]; then
497
+ case "$_ATT_KV_revision_oracle" in repo:*) : ;; *) return 1 ;; esac
498
+ _att_assert_committed_regular "$rev" "${_ATT_KV_revision_oracle#repo:}" || return 1
499
+ fi
500
+ return 0
501
+ }
502
+
434
503
  # ---------- projections / fingerprints ----------
435
504
  _att_card_contract_projection() { # $1 card file
436
505
  local f="$1"
@@ -465,17 +534,20 @@ _att_card_semantic_fingerprint() { # $1 card file $2 base $3 head -> prints fp;
465
534
  head="$(_att_full_commit "$head")" || return 1
466
535
  tree="$(git -C "$ROOT" rev-parse "${head}^{tree}" 2>/dev/null | tr -d '\r\n')" || return 1
467
536
  proj="$(_att_card_contract_projection "$file")"
468
- # changed paths base..head excluding card path
469
- path_manifest="$(git -C "$ROOT" diff-tree -r --no-commit-id --name-status "$base" "$head" 2>/dev/null \
470
- | awk -v card="cards/${id}.md" '$2 != card && $3 != card {print}' \
471
- | while read -r st p rest; do
472
- case "$st" in
473
- R*|C*) p="$rest" ;;
474
- esac
475
- [ -z "$p" ] && continue
476
- mode_oid="$(git -C "$ROOT" ls-tree "$head" -- "$p" 2>/dev/null | awk '{print $1" "$3}')"
477
- printf '%s %s\n' "$p" "$mode_oid"
478
- done | sort)"
537
+ # changed paths base..head excluding card path.
538
+ # Bash 3.2 + set -u: avoid `read st p rest` (unbound rest) and case R*|C* inside
539
+ # command substitution (macOS /bin/bash syntax error). Use name-only only.
540
+ path_manifest="$(
541
+ git -C "$ROOT" diff-tree -r --name-only "$base" "$head" 2>/dev/null \
542
+ | while IFS= read -r p || [ -n "$p" ]; do
543
+ p="$(printf '%s' "$p" | tr -d '\r')"
544
+ [ -z "$p" ] && continue
545
+ [ "$p" = "cards/${id}.md" ] && continue
546
+ mode_oid="$(git -C "$ROOT" ls-tree "$head" -- "$p" 2>/dev/null | awk '{print $1" "$3}')"
547
+ [ -n "$mode_oid" ] || continue
548
+ printf '%s %s\n' "$p" "$mode_oid"
549
+ done | sort
550
+ )"
479
551
  body="base=${base}"$'\n'"head=${head}"$'\n'"tree=${tree}"$'\n'"proj<<"$'\n'"${proj}"$'\n'">>"$'\n'"paths<<"$'\n'"${path_manifest}"$'\n'">>"$'\n'
480
552
  fmt="$(_att_object_format)"
481
553
  oid="$(printf '%s' "$body" | git -C "$ROOT" hash-object --stdin 2>/dev/null | tr -d '\r\n')"
@@ -611,116 +683,135 @@ revision_oracle_fingerprint: ${_ATT_W_revision_oracle_fingerprint}
611
683
  }
612
684
 
613
685
  # ---------- process supervisor ----------
614
- # Probe: can we create a process group and kill it?
686
+ # Prefer setsid (Linux). macOS often lacks setsid: fall back to best-effort kill of
687
+ # the launched PID (not a full session). Live-auto residual is documented for that case.
688
+ _att_has_setsid() { command -v setsid >/dev/null 2>&1; }
689
+
615
690
  _att_supervisor_capable() {
616
691
  if [ "${FLOW_ATTEST_SUPERVISOR:-}" = "force-unsupported" ]; then return 1; fi
617
692
  if [ "${FLOW_ATTEST_SUPERVISOR:-}" = "force-ok" ]; then return 0; fi
618
- # Prefer setsid or bash job control with set -m
619
- if command -v setsid >/dev/null 2>&1; then return 0; fi
620
- # bash with job control usually available
693
+ if [ "${_ATT_SUP_CAP_CACHED:-}" = "1" ]; then
694
+ [ "${_ATT_SUP_CAP_OK:-0}" = "1" ]; return $?
695
+ fi
696
+ _ATT_SUP_CAP_CACHED=1
697
+ _ATT_SUP_CAP_OK=0
698
+ local outf pid pgid
699
+ outf="$(mktemp 2>/dev/null || echo "${TMPDIR:-/tmp}/.att-cap.$$")"
700
+ : > "$outf"
701
+ if _att_has_setsid; then
702
+ ( cd "${ROOT:-/tmp}" || exit 127; exec setsid sleep 30 ) >"$outf" 2>&1 &
703
+ else
704
+ ( cd "${ROOT:-/tmp}" || exit 127; exec sleep 30 ) >"$outf" 2>&1 &
705
+ fi
706
+ pid=$!
707
+ sleep 0.15 2>/dev/null || true
708
+ pgid="$(ps -o pgid= -p "$pid" 2>/dev/null | tr -d ' ')"
709
+ [ -n "$pgid" ] || pgid="$pid"
710
+ if _att_has_setsid; then
711
+ kill -TERM -"$pgid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true
712
+ else
713
+ kill -TERM "$pid" 2>/dev/null || true
714
+ fi
715
+ sleep 0.25 2>/dev/null || true
716
+ if kill -0 "$pid" 2>/dev/null; then
717
+ if _att_has_setsid; then
718
+ kill -KILL -"$pgid" 2>/dev/null || kill -KILL "$pid" 2>/dev/null || true
719
+ else
720
+ kill -KILL "$pid" 2>/dev/null || true
721
+ fi
722
+ sleep 0.1 2>/dev/null || true
723
+ fi
724
+ wait "$pid" 2>/dev/null || true
725
+ rm -f "$outf" 2>/dev/null || true
726
+ if kill -0 "$pid" 2>/dev/null; then
727
+ return 1
728
+ fi
729
+ _ATT_SUP_CAP_OK=1
621
730
  return 0
622
731
  }
623
732
 
624
- # Run argv array: _att_run_supervised cmd args...
625
- # Sets: _ATT_RUN_RC (exit|timeout|output-cap|signal|spawn-error), _ATT_RUN_CODE (numeric),
626
- # _ATT_RUN_OUT (bounded stdout), _ATT_RUN_MS
627
733
  _att_run_supervised() {
628
- # Re-read bounds each call (tests/operators export FLOW_ATTEST_* after source).
629
- local timeout_s="${FLOW_ATTEST_TIMEOUT_S:-30}"
630
- local cap="${FLOW_ATTEST_OUT_CAP:-65536}"
631
- local comb="${FLOW_ATTEST_COMBINED_CAP:-98304}"
632
- local grace_s="${FLOW_ATTEST_GRACE_S:-2}"
633
- local outf errf start end rc=0 pid="" pgid_file
734
+ local timeout_s="${FLOW_ATTEST_TIMEOUT_S:-30}" cap="${FLOW_ATTEST_OUT_CAP:-65536}"
735
+ local comb="${FLOW_ATTEST_COMBINED_CAP:-98304}" grace_s="${FLOW_ATTEST_GRACE_S:-2}"
736
+ local outf errf start end rc=0 pid="" pgid="" i use_setsid=0
634
737
  _ATT_RUN_RC="spawn-error"; _ATT_RUN_CODE=127; _ATT_RUN_OUT=""; _ATT_RUN_MS=0
635
- if ! _att_supervisor_capable; then
636
- _ATT_RUN_RC="spawn-error"; return 1
637
- fi
738
+ if ! _att_supervisor_capable; then _ATT_RUN_RC="spawn-error"; return 1; fi
638
739
  case "$timeout_s" in ''|*[!0-9]*|0) timeout_s=30 ;; esac
639
740
  case "$cap" in ''|*[!0-9]*|0) cap=65536 ;; esac
640
741
  case "$comb" in ''|*[!0-9]*|0) comb=98304 ;; esac
742
+ case "$grace_s" in ''|*[!0-9]*) grace_s=2 ;; esac
743
+ _att_has_setsid && use_setsid=1
641
744
  outf="$(mktemp 2>/dev/null || echo "${TMPDIR:-/tmp}/.att-out.$$")"
642
745
  errf="$(mktemp 2>/dev/null || echo "${TMPDIR:-/tmp}/.att-err.$$")"
643
- pgid_file="$(mktemp 2>/dev/null || echo "${TMPDIR:-/tmp}/.att-pgid.$$")"
644
- : > "$outf"; : > "$errf"; : > "$pgid_file"
746
+ : > "$outf"; : > "$errf"
645
747
  start="$(_att_now_s)"
646
- # Prefer setsid so the child owns a distinct session we can kill by PGID.
647
- if command -v setsid >/dev/null 2>&1; then
648
- (
649
- cd "$ROOT" || exit 127
650
- setsid "$@" >"$outf" 2>"$errf"
651
- ) &
652
- pid=$!
748
+ if [ "$use_setsid" -eq 1 ]; then
749
+ ( cd "$ROOT" || exit 127; exec setsid "$@" ) >"$outf" 2>"$errf" &
653
750
  else
654
- (
655
- cd "$ROOT" || exit 127
656
- set -m
657
- "$@" >"$outf" 2>"$errf"
658
- ) &
659
- pid=$!
751
+ ( cd "$ROOT" || exit 127; exec "$@" ) >"$outf" 2>"$errf" &
660
752
  fi
661
- # Best-effort record of process group for the wrapper
662
- printf '%s\n' "$pid" > "$pgid_file"
663
- _att_kill_tree() {
664
- local sig="$1"
665
- kill -"$sig" -"$pid" 2>/dev/null || kill -"$sig" "$pid" 2>/dev/null || true
666
- # also try children via pkill when available
667
- if command -v pkill >/dev/null 2>&1; then
668
- pkill -"$sig" -P "$pid" 2>/dev/null || true
753
+ pid=$!
754
+ i=0
755
+ while [ "$i" -lt 20 ]; do
756
+ if kill -0 "$pid" 2>/dev/null; then
757
+ pgid="$(ps -o pgid= -p "$pid" 2>/dev/null | tr -d ' ')"
758
+ [ -n "$pgid" ] && break
669
759
  fi
670
- }
671
- # Watchdog
672
- (
673
- sleep "$timeout_s" 2>/dev/null
760
+ i=$((i+1)); sleep 0.05 2>/dev/null || true
761
+ done
762
+ [ -n "$pgid" ] || pgid="$pid"
763
+ ( sleep "$timeout_s" 2>/dev/null
674
764
  if kill -0 "$pid" 2>/dev/null; then
675
- _att_kill_tree TERM
676
- sleep "$grace_s" 2>/dev/null
677
- _att_kill_tree KILL
765
+ if [ "$use_setsid" -eq 1 ]; then
766
+ kill -TERM -"$pgid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true
767
+ sleep "$grace_s" 2>/dev/null
768
+ kill -KILL -"$pgid" 2>/dev/null || kill -KILL "$pid" 2>/dev/null || true
769
+ else
770
+ kill -TERM "$pid" 2>/dev/null || true
771
+ sleep "$grace_s" 2>/dev/null
772
+ kill -KILL "$pid" 2>/dev/null || true
773
+ fi
678
774
  echo timeout > "${outf}.to"
679
- fi
680
- ) &
775
+ fi ) &
681
776
  local wd=$!
682
- # Stream cap monitor
683
- (
684
- while kill -0 "$pid" 2>/dev/null; do
777
+ ( while kill -0 "$pid" 2>/dev/null; do
685
778
  local os es
686
779
  os="$(wc -c < "$outf" 2>/dev/null | tr -d ' ')"
687
780
  es="$(wc -c < "$errf" 2>/dev/null | tr -d ' ')"
688
781
  if [ "${os:-0}" -gt "$cap" ] || [ "${es:-0}" -gt "$cap" ] || [ $(( ${os:-0} + ${es:-0} )) -gt "$comb" ]; then
689
- _att_kill_tree TERM
690
- sleep 1
691
- _att_kill_tree KILL
692
- echo cap > "${outf}.cap"
693
- break
782
+ if [ "$use_setsid" -eq 1 ]; then
783
+ kill -TERM -"$pgid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true
784
+ sleep 1
785
+ kill -KILL -"$pgid" 2>/dev/null || kill -KILL "$pid" 2>/dev/null || true
786
+ else
787
+ kill -TERM "$pid" 2>/dev/null || true
788
+ sleep 1
789
+ kill -KILL "$pid" 2>/dev/null || true
790
+ fi
791
+ echo cap > "${outf}.cap"; break
694
792
  fi
695
793
  sleep 0.1 2>/dev/null || sleep 1
696
- done
697
- ) &
794
+ done ) &
698
795
  local capw=$!
699
796
  wait "$pid" 2>/dev/null; rc=$?
700
797
  kill "$wd" 2>/dev/null; wait "$wd" 2>/dev/null
701
798
  kill "$capw" 2>/dev/null; wait "$capw" 2>/dev/null
702
- end="$(_att_now_s)"
703
- _ATT_RUN_MS=$(( (end - start) * 1000 ))
704
- [ "$_ATT_RUN_MS" -lt 0 ] && _ATT_RUN_MS=0
705
- if [ -f "${outf}.cap" ]; then
706
- _ATT_RUN_RC="output-cap"; _ATT_RUN_CODE=1
707
- rm -f "${outf}.cap"
708
- elif [ -f "${outf}.to" ]; then
709
- _ATT_RUN_RC="timeout"; _ATT_RUN_CODE=124
710
- rm -f "${outf}.to"
711
- elif [ "$rc" -eq 137 ] || [ "$rc" -eq 143 ]; then
712
- if [ $((end - start)) -ge "$timeout_s" ]; then
713
- _ATT_RUN_RC="timeout"; _ATT_RUN_CODE=124
714
- else
715
- _ATT_RUN_RC="signal"; _ATT_RUN_CODE="$rc"
716
- fi
717
- elif [ "$rc" -eq 127 ]; then
718
- _ATT_RUN_RC="spawn-error"; _ATT_RUN_CODE=127
799
+ if [ "$use_setsid" -eq 1 ]; then
800
+ kill -KILL -"$pgid" 2>/dev/null || true
719
801
  else
720
- _ATT_RUN_RC="exit"; _ATT_RUN_CODE="$rc"
802
+ kill -KILL "$pid" 2>/dev/null || true
721
803
  fi
804
+ end="$(_att_now_s)"
805
+ _ATT_RUN_MS=$(( (end - start) * 1000 )); [ "$_ATT_RUN_MS" -lt 0 ] && _ATT_RUN_MS=0
806
+ if [ -f "${outf}.cap" ]; then _ATT_RUN_RC="output-cap"; _ATT_RUN_CODE=1; rm -f "${outf}.cap"
807
+ elif [ -f "${outf}.to" ]; then _ATT_RUN_RC="timeout"; _ATT_RUN_CODE=124; rm -f "${outf}.to"
808
+ elif [ "$rc" -eq 137 ] || [ "$rc" -eq 143 ]; then
809
+ if [ $((end - start)) -ge "$timeout_s" ]; then _ATT_RUN_RC="timeout"; _ATT_RUN_CODE=124
810
+ else _ATT_RUN_RC="signal"; _ATT_RUN_CODE="$rc"; fi
811
+ elif [ "$rc" -eq 127 ]; then _ATT_RUN_RC="spawn-error"; _ATT_RUN_CODE=127
812
+ else _ATT_RUN_RC="exit"; _ATT_RUN_CODE="$rc"; fi
722
813
  _ATT_RUN_OUT="$(head -c "$cap" "$outf" 2>/dev/null | tr -d '\000')"
723
- rm -f "$outf" "$errf" "$pgid_file" 2>/dev/null || true
814
+ rm -f "$outf" "$errf" 2>/dev/null || true
724
815
  return 0
725
816
  }
726
817
 
@@ -769,6 +860,7 @@ _att_receipt_status() { # $1 path -> current|missing|stale|invalid|red|blocked-a
769
860
  if [ -n "$headfp" ] && [ "$headfp" != "$_ATT_R_subject_fingerprint" ]; then
770
861
  echo "stale"; return 1
771
862
  fi
863
+ if ! _att_require_clean_for_consume; then echo "stale"; return 1; fi
772
864
  ;;
773
865
  semantic_gate:card)
774
866
  local file; file="$(resolve_card_file "$_ATT_R_subject_id")"
@@ -799,21 +891,24 @@ _att_receipt_status() { # $1 path -> current|missing|stale|invalid|red|blocked-a
799
891
  echo "stale"; return 1
800
892
  fi
801
893
  fi
894
+ if ! _att_require_clean_for_consume; then echo "stale"; return 1; fi
802
895
  ;;
803
896
  live_verify:card)
804
897
  [ "$_ATT_R_verdict" = "pass" ] || { echo "red"; return 1; }
805
- local head file proj cur_fp
898
+ local head file proj cur_fp ofp_now owner_rel
806
899
  head="$(_att_full_commit HEAD)"
807
900
  file="$(resolve_card_file "$_ATT_R_subject_id")"
808
901
  [ -f "$file" ] || { echo "stale"; return 1; }
809
- if [ -n "$_ATT_R_subject_revision" ] && [ "$_ATT_R_subject_revision" != "$head" ] \
810
- && ! _att_is_ancestor "$_ATT_R_subject_revision" "$head"; then
811
- echo "stale"; return 1
812
- fi
813
- # Recompute live subject fingerprint from current Verify/Done-evidence + recorded owner/target/rev.
902
+ [ -n "$_ATT_R_subject_revision" ] || { echo "stale"; return 1; }
903
+ [ "$_ATT_R_subject_revision" = "$head" ] || { echo "stale"; return 1; }
904
+ case "${_ATT_R_owner_ref:-}" in repo:*) owner_rel="${_ATT_R_owner_ref#repo:}" ;; *) echo "stale"; return 1 ;; esac
905
+ if ! _att_load_owner_at_rev "$head" "$owner_rel"; then echo "stale"; return 1; fi
906
+ ofp_now="$(_att_owner_fingerprint "$owner_rel" "$head")" || { echo "stale"; return 1; }
907
+ [ "$ofp_now" = "${_ATT_R_owner_fingerprint}" ] || { echo "stale"; return 1; }
814
908
  proj="$(awk '/^## Verify/{p=1} /^## Done-evidence/{p=1} /^## Evidence/{p=0} /^## /{if($0!~/^## (Verify|Done-evidence)/)p=0} p{print}' "$file" | tr -d '\r')"
815
- cur_fp="$(_att_hash_text "live|${_ATT_R_subject_revision}|${_ATT_R_target_id}|${_ATT_R_owner_fingerprint}|${proj}")"
909
+ cur_fp="$(_att_hash_text "live|${head}|${_ATT_R_target_id}|${ofp_now}|${proj}")"
816
910
  [ "$cur_fp" = "$_ATT_R_subject_fingerprint" ] || { echo "stale"; return 1; }
911
+ if ! _att_require_clean_for_consume; then echo "stale"; return 1; fi
817
912
  ;;
818
913
  esac
819
914
  case "$_ATT_R_verdict" in
@@ -991,28 +1086,26 @@ _att_mint_semantic_stage() {
991
1086
  _att_is_stage "$stage" || { echo "FAIL: invalid stage '$stage'"; return 2; }
992
1087
  _att_git_ok || { echo "FAIL: attest requires a Git repository"; return 2; }
993
1088
  full="$(_att_full_commit "$rev")" || { echo "FAIL: bad revision"; return 2; }
994
- _att_git_clean_for_attest || { echo "FAIL: dirty worktree (only .flow/ excluded)"; return 1; }
995
- owner_abs="$ROOT/$owner_rel"
996
- [ -f "$owner_abs" ] || { echo "FAIL: owner manifest not found: $owner_rel"; return 2; }
997
- # owner must be tracked at revision
998
- git -C "$ROOT" cat-file -e "${full}:${owner_rel}" 2>/dev/null || { echo "FAIL: owner not committed at revision"; return 1; }
999
- _att_parse_kv_file "$owner_abs" || { echo "FAIL: invalid owner manifest"; return 2; }
1089
+ _att_git_clean_for_attest || { echo "FAIL: dirty worktree (only .flow/, locks, status/Evidence allowed)"; return 1; }
1090
+ if ! _att_load_owner_at_rev "$full" "$owner_rel"; then
1091
+ echo "FAIL: owner/command must be committed regular files matching disk at revision (blob equality)"
1092
+ return 1
1093
+ fi
1000
1094
  [ "$_ATT_KV_kind" = "semantic_gate" ] || { echo "FAIL: owner kind must be semantic_gate"; return 2; }
1001
1095
  [ "$_ATT_KV_subject_id" = "$stage" ] || { echo "FAIL: owner subject_id mismatch"; return 2; }
1002
1096
  [ "$_ATT_KV_target_id" = "none" ] && [ "$_ATT_KV_revision_oracle" = "none" ] || { echo "FAIL: semantic owner must have target/oracle none"; return 2; }
1003
- case "$_ATT_KV_command" in repo:*) : ;; *) echo "FAIL: command must be repo:..."; return 2 ;; esac
1004
1097
  local cmd_rel="${_ATT_KV_command#repo:}"
1005
- [ -f "$ROOT/$cmd_rel" ] || { echo "FAIL: command not found"; return 2; }
1006
- git -C "$ROOT" cat-file -e "${full}:${cmd_rel}" 2>/dev/null || { echo "FAIL: command not committed"; return 1; }
1007
1098
  if ! _att_stage_fingerprint "$stage" "$full" >/dev/null; then echo "FAIL: stage fingerprint"; return 1; fi
1008
1099
  fp="$_ATT_LAST_FP"
1009
1100
  ofp="$(_att_owner_fingerprint "$owner_rel" "$full")" || { echo "FAIL: owner fingerprint"; return 1; }
1010
1101
 
1011
- local lname; lname="$(_att_subject_lock_name semantic_gate "$stage")"
1102
+ local lname execf; lname="$(_att_subject_lock_name semantic_gate "$stage")"
1012
1103
  _att_lock_acquire "$lname" || return 1
1013
- # run producer
1014
- chmod +x "$ROOT/$cmd_rel" 2>/dev/null || true
1015
- _att_run_supervised "$ROOT/$cmd_rel" --subject-id "$stage" --subject-revision "$full" --subject-fingerprint "$fp"
1104
+ execf="$(_att_materialize_blob_exec "$full" "$cmd_rel")" || {
1105
+ _att_lock_release "$lname"; echo "FAIL: could not materialize committed producer"; return 1
1106
+ }
1107
+ _att_run_supervised "$execf" --subject-id "$stage" --subject-revision "$full" --subject-fingerprint "$fp"
1108
+ rm -f "$execf" 2>/dev/null || true
1016
1109
  if [ "$_ATT_RUN_RC" != "exit" ] || [ "$_ATT_RUN_CODE" -ne 0 ]; then
1017
1110
  _ATT_W_kind=semantic_gate; _ATT_W_subject_type=stage; _ATT_W_subject_id="$stage"
1018
1111
  _ATT_W_subject_fingerprint="$fp"; _ATT_W_verdict=fail
@@ -1067,20 +1160,27 @@ _att_mint_semantic_card() {
1067
1160
  _att_git_clean_for_attest || { echo "FAIL: dirty worktree"; return 1; }
1068
1161
  full_base="$(_att_full_commit "$base")" || { echo "FAIL: bad base"; return 2; }
1069
1162
  full_head="$(_att_full_commit "$rev")" || { echo "FAIL: bad revision"; return 2; }
1070
- local owner_abs="$ROOT/$owner_rel"
1071
- [ -f "$owner_abs" ] || { echo "FAIL: owner missing"; return 2; }
1072
- git -C "$ROOT" cat-file -e "${full_head}:${owner_rel}" 2>/dev/null || { echo "FAIL: owner not at revision"; return 1; }
1073
- _att_parse_kv_file "$owner_abs" || { echo "FAIL: invalid owner"; return 2; }
1163
+ if [ "$full_base" = "$full_head" ]; then
1164
+ echo "FAIL: card semantic requires non-empty base..revision (base must differ from --revision)"
1165
+ return 2
1166
+ fi
1167
+ if ! _att_load_owner_at_rev "$full_head" "$owner_rel"; then
1168
+ echo "FAIL: owner/command must be committed regular files matching disk at revision"
1169
+ return 1
1170
+ fi
1074
1171
  [ "$_ATT_KV_kind" = "semantic_gate" ] || { echo "FAIL: owner kind"; return 2; }
1075
1172
  [ "$_ATT_KV_subject_id" = "$id" ] || { echo "FAIL: subject_id mismatch"; return 2; }
1076
1173
  local cmd_rel="${_ATT_KV_command#repo:}"
1077
1174
  if ! _att_card_semantic_fingerprint "$file" "$full_base" "$full_head" >/dev/null; then echo "FAIL: fingerprint"; return 1; fi
1078
1175
  fp="$_ATT_LAST_FP"
1079
1176
  ofp="$(_att_owner_fingerprint "$owner_rel" "$full_head")" || { echo "FAIL: owner fp"; return 1; }
1080
- local lname; lname="$(_att_subject_lock_name semantic_gate "$id")"
1177
+ local lname execf; lname="$(_att_subject_lock_name semantic_gate "$id")"
1081
1178
  _att_lock_acquire "$lname" || return 1
1082
- chmod +x "$ROOT/$cmd_rel" 2>/dev/null || true
1083
- _att_run_supervised "$ROOT/$cmd_rel" --subject-id "$id" --subject-revision "$full_head" --subject-fingerprint "$fp"
1179
+ execf="$(_att_materialize_blob_exec "$full_head" "$cmd_rel")" || {
1180
+ _att_lock_release "$lname"; echo "FAIL: could not materialize committed producer"; return 1
1181
+ }
1182
+ _att_run_supervised "$execf" --subject-id "$id" --subject-revision "$full_head" --subject-fingerprint "$fp"
1183
+ rm -f "$execf" 2>/dev/null || true
1084
1184
  local verdict=fail eref=none
1085
1185
  if [ "$_ATT_RUN_RC" = "exit" ] && [ "$_ATT_RUN_CODE" -eq 0 ]; then
1086
1186
  local sch sfp crit high
@@ -1109,16 +1209,17 @@ _att_mint_semantic_card() {
1109
1209
 
1110
1210
  _att_mint_live() {
1111
1211
  local id="$1" rev="$2" owner_rel="$3"
1112
- local file full owner_abs cmd_rel oracle_rel target ofp cfp lname ap
1212
+ local file full cmd_rel oracle_rel target ofp cfp lname ap
1113
1213
  id="$(_att_norm_card_id "$id")" || { echo "FAIL: bad card"; return 2; }
1114
1214
  file="$(resolve_card_file "$id")"; [ -f "$file" ] || { echo "FAIL: card missing"; return 2; }
1115
1215
  _att_git_ok || { echo "FAIL: git required"; return 2; }
1116
1216
  if ! _att_supervisor_capable; then echo "FAIL: process supervisor unsupported on this platform"; return 1; fi
1217
+ _att_git_clean_for_attest || { echo "FAIL: dirty worktree (live mint requires clean subject)"; return 1; }
1117
1218
  full="$(_att_full_commit "$rev")" || { echo "FAIL: bad revision"; return 2; }
1118
- owner_abs="$ROOT/$owner_rel"
1119
- [ -f "$owner_abs" ] || { echo "FAIL: owner missing"; return 2; }
1120
- git -C "$ROOT" cat-file -e "${full}:${owner_rel}" 2>/dev/null || { echo "FAIL: owner not committed"; return 1; }
1121
- _att_parse_kv_file "$owner_abs" || { echo "FAIL: invalid owner"; return 2; }
1219
+ if ! _att_load_owner_at_rev "$full" "$owner_rel"; then
1220
+ echo "FAIL: owner/command/oracle must be committed regular files matching disk at revision"
1221
+ return 1
1222
+ fi
1122
1223
  [ "$_ATT_KV_kind" = "live_verify" ] || { echo "FAIL: owner kind must be live_verify"; return 2; }
1123
1224
  [ "$_ATT_KV_subject_id" = "$id" ] || { echo "FAIL: subject mismatch"; return 2; }
1124
1225
  target="$_ATT_KV_target_id"
@@ -1126,9 +1227,13 @@ _att_mint_live() {
1126
1227
  case "$_ATT_KV_revision_oracle" in repo:*) : ;; *) echo "FAIL: live needs revision_oracle"; return 2 ;; esac
1127
1228
  cmd_rel="${_ATT_KV_command#repo:}"; oracle_rel="${_ATT_KV_revision_oracle#repo:}"
1128
1229
  ofp="$(_att_owner_fingerprint "$owner_rel" "$full")" || { echo "FAIL: owner fp"; return 1; }
1129
- local o_fp; o_fp="$(_att_hash_text "oracle:${oracle_rel}")"
1130
- cfp="$(_att_hash_text "argv:${cmd_rel}|${id}|${full}|${target}")"
1131
- # live subject fingerprint from verify+done-evidence projection
1230
+ local o_blob o_mode o_fp cmd_blob cmd_mode
1231
+ o_blob="$(_att_blob_at_rev "$full" "$oracle_rel")"
1232
+ o_mode="$(_att_path_mode_at_rev "$full" "$oracle_rel")"
1233
+ o_fp="$(_att_hash_text "oracle:${oracle_rel}:${o_mode}:${o_blob}")"
1234
+ cmd_blob="$(_att_blob_at_rev "$full" "$cmd_rel")"
1235
+ cmd_mode="$(_att_path_mode_at_rev "$full" "$cmd_rel")"
1236
+ cfp="$(_att_hash_text "argv:${cmd_rel}:${cmd_mode}:${cmd_blob}|${id}|${full}|${target}")"
1132
1237
  local proj fp
1133
1238
  proj="$(awk '/^## Verify/{p=1} /^## Done-evidence/{p=1} /^## Evidence/{p=0} /^## /{if($0!~/^## (Verify|Done-evidence)/)p=0} p{print}' "$file" | tr -d '\r')"
1134
1239
  fp="$(_att_hash_text "live|${full}|${target}|${ofp}|${proj}")"
@@ -1137,20 +1242,26 @@ _att_mint_live() {
1137
1242
  ap="$(_att_attempt_path "$id")"
1138
1243
  mkdir -p "$(dirname "$ap")" 2>/dev/null || true
1139
1244
  printf 'pid=%s\nstart=%s\nhost=%s\n' "$$" "$(_att_now_s)" "$(uname -n 2>/dev/null || echo host)" > "$ap"
1140
-
1141
- # oracle first
1142
- chmod +x "$ROOT/$oracle_rel" 2>/dev/null || true
1143
- _att_run_supervised "$ROOT/$oracle_rel" --subject-id "$id" --subject-revision "$full" --target-id "$target"
1144
- local oracle_oid verdict=fail rcode
1245
+ local oracle_exec cmd_exec oracle_oid verdict=fail rcode
1246
+ oracle_exec="$(_att_materialize_blob_exec "$full" "$oracle_rel")" || {
1247
+ rm -f "$ap"; _att_lock_release "$lname"; echo "FAIL: materialize oracle"; return 1
1248
+ }
1249
+ _att_run_supervised "$oracle_exec" --subject-id "$id" --subject-revision "$full" --target-id "$target"
1250
+ rm -f "$oracle_exec" 2>/dev/null || true
1145
1251
  rcode="$(_att_result_code_str)"
1146
1252
  if [ "$_ATT_RUN_RC" = "exit" ] && [ "$_ATT_RUN_CODE" -eq 0 ]; then
1147
1253
  oracle_oid="$(printf '%s' "$_ATT_RUN_OUT" | tr -d '\r\n[:space:]')"
1148
1254
  if [ "$oracle_oid" = "$full" ]; then
1149
- chmod +x "$ROOT/$cmd_rel" 2>/dev/null || true
1150
- _att_run_supervised "$ROOT/$cmd_rel" --subject-id "$id" --subject-revision "$full" --subject-fingerprint "$fp"
1151
- rcode="$(_att_result_code_str)"
1152
- if [ "$_ATT_RUN_RC" = "exit" ] && [ "$_ATT_RUN_CODE" -eq 0 ]; then
1153
- verdict=pass
1255
+ cmd_exec="$(_att_materialize_blob_exec "$full" "$cmd_rel")" || true
1256
+ if [ -n "${cmd_exec:-}" ]; then
1257
+ _att_run_supervised "$cmd_exec" --subject-id "$id" --subject-revision "$full" --subject-fingerprint "$fp"
1258
+ rm -f "$cmd_exec" 2>/dev/null || true
1259
+ rcode="$(_att_result_code_str)"
1260
+ if [ "$_ATT_RUN_RC" = "exit" ] && [ "$_ATT_RUN_CODE" -eq 0 ]; then
1261
+ verdict=pass
1262
+ fi
1263
+ else
1264
+ rcode="spawn-error"
1154
1265
  fi
1155
1266
  else
1156
1267
  rcode="exit:1"