@manhquy/flow-skill 0.5.0 → 0.5.1

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/README.md CHANGED
@@ -5,7 +5,7 @@ Install the **flow** skill into coding agents with one command.
5
5
  | | |
6
6
  |---|---|
7
7
  | **Package** | [`@manhquy/flow-skill`](https://www.npmjs.com/package/@manhquy/flow-skill) (installer CLI) |
8
- | **Current GA** | **0.5.0** — ships skill product **v0.28.0** |
8
+ | **Current GA** | **0.5.1** — ships skill product **v0.28.1** |
9
9
  | **Platforms** | macOS · Linux · Windows (same Node path) |
10
10
  | **Requirement** | Node.js **≥ 22.14** |
11
11
  | **Source** | [github.com/manhquydev/flow-skill](https://github.com/manhquydev/flow-skill) |
@@ -89,7 +89,7 @@ npx @manhquy/flow-skill@latest --yes --all --dry-run --json
89
89
  |----|--------|
90
90
  | `npx @manhquy/flow-skill@latest` | Bare `npx @manhquy/flow-skill` (stale npx cache) |
91
91
  | **Run** the CLI to copy the skill | `npm i` alone (package only; no skill files in agent homes) |
92
- | Pin installer `@0.5.0` if needed | Pin npm `@0.28.0` (skill version ≠ package version) |
92
+ | Pin installer `@0.5.1` if needed | Pin npm `@0.28.1` (skill version ≠ package version) |
93
93
  | Prefer `@latest` | `@rc` (retired / behind) |
94
94
 
95
95
  **Two version axes:** `package.json` version = installer; `SKILL.md` metadata = skill product. Both appear in `--help` and in the `plan` JSONL event (`version` + `skillVersion`).
@@ -137,7 +137,7 @@ rm -rf <project>/.claude/skills/flow
137
137
  |---------|-----|
138
138
  | Agent has no `/flow` after `npm i` | Run `npx @manhquy/flow-skill@latest` (install must **execute**) |
139
139
  | Old skill after reinstall | Always use `@latest`; avoid bare package name |
140
- | `No matching version found` for `@0.28.0` | That is the **skill** version. Use `@latest` or `@0.5.x` |
140
+ | `No matching version found` for `@0.28.1` | That is the **skill** version. Use `@latest` or `@0.5.x` |
141
141
  | Node too old | Install Node ≥ 22.14 (`nvm install 22`, etc.) |
142
142
  | Windows `EBUSY` / `EPERM` | Close the agent holding files under the skill dir; re-run |
143
143
  | Stale install lock | Delete `<parent>/.flow-skill.installing.lock` if reclaim fails |
@@ -147,7 +147,7 @@ rm -rf <project>/.claude/skills/flow
147
147
  ## JSONL (`--json`)
148
148
 
149
149
  ```jsonl
150
- {"event":"plan","version":"0.5.0","skillVersion":"0.28.0","dryRun":false,"scope":"global","targets":["claude"]}
150
+ {"event":"plan","version":"0.5.1","skillVersion":"0.28.1","dryRun":false,"scope":"global","targets":["claude"]}
151
151
  {"event":"install:start","target":"claude","dests":["~/.claude/skills/flow"]}
152
152
  {"event":"install:done","target":"claude","dests":["~/.claude/skills/flow"],"result":"success","error":null,"warnings":[]}
153
153
  {"event":"summary","success":true,"total":1,"attempted":1,"installed":1,"failed":0,"skipped":0,"aborted":false}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manhquy/flow-skill",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Install the flow skill (buildflow gated build process) into your coding agent(s) via one npx command",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,7 +8,7 @@ keywords: [flow, buildflow, gate, build, ship, scope, prd, contract, card, deplo
8
8
  license: MIT
9
9
  metadata:
10
10
  author: flow-skill
11
- version: "0.28.0"
11
+ version: "0.28.1"
12
12
  attribution: "Methodology from ai20k-build-phase/buildflow (Tony, arealisticdreamer.com); durable-layer ancestry and improve-ritual spirit from repository-harness (protocol v1 EOL — flow-owned fork); agent patterns from claudekit-engineer, BMAD-METHOD. v0.22 concierge from BMAD bmad-help; forge-idea from bmad-forge-idea (MIT, BMad Code LLC)."
13
13
  ---
14
14
 
@@ -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"
@@ -611,116 +680,95 @@ revision_oracle_fingerprint: ${_ATT_W_revision_oracle_fingerprint}
611
680
  }
612
681
 
613
682
  # ---------- process supervisor ----------
614
- # Probe: can we create a process group and kill it?
615
683
  _att_supervisor_capable() {
616
684
  if [ "${FLOW_ATTEST_SUPERVISOR:-}" = "force-unsupported" ]; then return 1; fi
617
685
  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
686
+ command -v setsid >/dev/null 2>&1 || return 1
687
+ if [ "${_ATT_SUP_CAP_CACHED:-}" = "1" ]; then
688
+ [ "${_ATT_SUP_CAP_OK:-0}" = "1" ]; return $?
689
+ fi
690
+ _ATT_SUP_CAP_CACHED=1; _ATT_SUP_CAP_OK=0
691
+ local outf pid pgid
692
+ outf="$(mktemp 2>/dev/null || echo "${TMPDIR:-/tmp}/.att-cap.$$")"
693
+ : > "$outf"
694
+ ( cd "${ROOT:-/tmp}" || exit 127; exec setsid sleep 30 ) >"$outf" 2>&1 &
695
+ pid=$!
696
+ sleep 0.15 2>/dev/null || true
697
+ pgid="$(ps -o pgid= -p "$pid" 2>/dev/null | tr -d ' ')"
698
+ [ -n "$pgid" ] || pgid="$pid"
699
+ kill -TERM -"$pgid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true
700
+ sleep 0.25 2>/dev/null || true
701
+ if kill -0 "$pid" 2>/dev/null; then
702
+ kill -KILL -"$pgid" 2>/dev/null || kill -KILL "$pid" 2>/dev/null || true
703
+ sleep 0.1 2>/dev/null || true
704
+ fi
705
+ wait "$pid" 2>/dev/null || true
706
+ rm -f "$outf" 2>/dev/null || true
707
+ kill -0 "$pid" 2>/dev/null && return 1
708
+ _ATT_SUP_CAP_OK=1
621
709
  return 0
622
710
  }
623
-
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
711
  _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
712
+ local timeout_s="${FLOW_ATTEST_TIMEOUT_S:-30}" cap="${FLOW_ATTEST_OUT_CAP:-65536}"
713
+ local comb="${FLOW_ATTEST_COMBINED_CAP:-98304}" grace_s="${FLOW_ATTEST_GRACE_S:-2}"
714
+ local outf errf start end rc=0 pid="" pgid="" i
634
715
  _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
716
+ if ! _att_supervisor_capable; then _ATT_RUN_RC="spawn-error"; return 1; fi
638
717
  case "$timeout_s" in ''|*[!0-9]*|0) timeout_s=30 ;; esac
639
718
  case "$cap" in ''|*[!0-9]*|0) cap=65536 ;; esac
640
719
  case "$comb" in ''|*[!0-9]*|0) comb=98304 ;; esac
720
+ case "$grace_s" in ''|*[!0-9]*) grace_s=2 ;; esac
641
721
  outf="$(mktemp 2>/dev/null || echo "${TMPDIR:-/tmp}/.att-out.$$")"
642
722
  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"
723
+ : > "$outf"; : > "$errf"
645
724
  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=$!
653
- else
654
- (
655
- cd "$ROOT" || exit 127
656
- set -m
657
- "$@" >"$outf" 2>"$errf"
658
- ) &
659
- pid=$!
660
- 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
725
+ ( cd "$ROOT" || exit 127; exec setsid "$@" ) >"$outf" 2>"$errf" &
726
+ pid=$!
727
+ i=0
728
+ while [ "$i" -lt 20 ]; do
729
+ if kill -0 "$pid" 2>/dev/null; then
730
+ pgid="$(ps -o pgid= -p "$pid" 2>/dev/null | tr -d ' ')"
731
+ [ -n "$pgid" ] && break
669
732
  fi
670
- }
671
- # Watchdog
672
- (
673
- sleep "$timeout_s" 2>/dev/null
733
+ i=$((i+1)); sleep 0.05 2>/dev/null || true
734
+ done
735
+ [ -n "$pgid" ] || pgid="$pid"
736
+ ( sleep "$timeout_s" 2>/dev/null
674
737
  if kill -0 "$pid" 2>/dev/null; then
675
- _att_kill_tree TERM
738
+ kill -TERM -"$pgid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true
676
739
  sleep "$grace_s" 2>/dev/null
677
- _att_kill_tree KILL
740
+ kill -KILL -"$pgid" 2>/dev/null || kill -KILL "$pid" 2>/dev/null || true
678
741
  echo timeout > "${outf}.to"
679
- fi
680
- ) &
742
+ fi ) &
681
743
  local wd=$!
682
- # Stream cap monitor
683
- (
684
- while kill -0 "$pid" 2>/dev/null; do
744
+ ( while kill -0 "$pid" 2>/dev/null; do
685
745
  local os es
686
746
  os="$(wc -c < "$outf" 2>/dev/null | tr -d ' ')"
687
747
  es="$(wc -c < "$errf" 2>/dev/null | tr -d ' ')"
688
748
  if [ "${os:-0}" -gt "$cap" ] || [ "${es:-0}" -gt "$cap" ] || [ $(( ${os:-0} + ${es:-0} )) -gt "$comb" ]; then
689
- _att_kill_tree TERM
749
+ kill -TERM -"$pgid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true
690
750
  sleep 1
691
- _att_kill_tree KILL
692
- echo cap > "${outf}.cap"
693
- break
751
+ kill -KILL -"$pgid" 2>/dev/null || kill -KILL "$pid" 2>/dev/null || true
752
+ echo cap > "${outf}.cap"; break
694
753
  fi
695
754
  sleep 0.1 2>/dev/null || sleep 1
696
- done
697
- ) &
755
+ done ) &
698
756
  local capw=$!
699
757
  wait "$pid" 2>/dev/null; rc=$?
700
758
  kill "$wd" 2>/dev/null; wait "$wd" 2>/dev/null
701
759
  kill "$capw" 2>/dev/null; wait "$capw" 2>/dev/null
760
+ kill -KILL -"$pgid" 2>/dev/null || true
702
761
  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"
762
+ _ATT_RUN_MS=$(( (end - start) * 1000 )); [ "$_ATT_RUN_MS" -lt 0 ] && _ATT_RUN_MS=0
763
+ if [ -f "${outf}.cap" ]; then _ATT_RUN_RC="output-cap"; _ATT_RUN_CODE=1; rm -f "${outf}.cap"
764
+ elif [ -f "${outf}.to" ]; then _ATT_RUN_RC="timeout"; _ATT_RUN_CODE=124; rm -f "${outf}.to"
711
765
  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
719
- else
720
- _ATT_RUN_RC="exit"; _ATT_RUN_CODE="$rc"
721
- fi
766
+ if [ $((end - start)) -ge "$timeout_s" ]; then _ATT_RUN_RC="timeout"; _ATT_RUN_CODE=124
767
+ else _ATT_RUN_RC="signal"; _ATT_RUN_CODE="$rc"; fi
768
+ elif [ "$rc" -eq 127 ]; then _ATT_RUN_RC="spawn-error"; _ATT_RUN_CODE=127
769
+ else _ATT_RUN_RC="exit"; _ATT_RUN_CODE="$rc"; fi
722
770
  _ATT_RUN_OUT="$(head -c "$cap" "$outf" 2>/dev/null | tr -d '\000')"
723
- rm -f "$outf" "$errf" "$pgid_file" 2>/dev/null || true
771
+ rm -f "$outf" "$errf" 2>/dev/null || true
724
772
  return 0
725
773
  }
726
774
 
@@ -769,6 +817,7 @@ _att_receipt_status() { # $1 path -> current|missing|stale|invalid|red|blocked-a
769
817
  if [ -n "$headfp" ] && [ "$headfp" != "$_ATT_R_subject_fingerprint" ]; then
770
818
  echo "stale"; return 1
771
819
  fi
820
+ if ! _att_require_clean_for_consume; then echo "stale"; return 1; fi
772
821
  ;;
773
822
  semantic_gate:card)
774
823
  local file; file="$(resolve_card_file "$_ATT_R_subject_id")"
@@ -799,21 +848,24 @@ _att_receipt_status() { # $1 path -> current|missing|stale|invalid|red|blocked-a
799
848
  echo "stale"; return 1
800
849
  fi
801
850
  fi
851
+ if ! _att_require_clean_for_consume; then echo "stale"; return 1; fi
802
852
  ;;
803
853
  live_verify:card)
804
854
  [ "$_ATT_R_verdict" = "pass" ] || { echo "red"; return 1; }
805
- local head file proj cur_fp
855
+ local head file proj cur_fp ofp_now owner_rel
806
856
  head="$(_att_full_commit HEAD)"
807
857
  file="$(resolve_card_file "$_ATT_R_subject_id")"
808
858
  [ -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.
859
+ [ -n "$_ATT_R_subject_revision" ] || { echo "stale"; return 1; }
860
+ [ "$_ATT_R_subject_revision" = "$head" ] || { echo "stale"; return 1; }
861
+ case "${_ATT_R_owner_ref:-}" in repo:*) owner_rel="${_ATT_R_owner_ref#repo:}" ;; *) echo "stale"; return 1 ;; esac
862
+ if ! _att_load_owner_at_rev "$head" "$owner_rel"; then echo "stale"; return 1; fi
863
+ ofp_now="$(_att_owner_fingerprint "$owner_rel" "$head")" || { echo "stale"; return 1; }
864
+ [ "$ofp_now" = "${_ATT_R_owner_fingerprint}" ] || { echo "stale"; return 1; }
814
865
  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}")"
866
+ cur_fp="$(_att_hash_text "live|${head}|${_ATT_R_target_id}|${ofp_now}|${proj}")"
816
867
  [ "$cur_fp" = "$_ATT_R_subject_fingerprint" ] || { echo "stale"; return 1; }
868
+ if ! _att_require_clean_for_consume; then echo "stale"; return 1; fi
817
869
  ;;
818
870
  esac
819
871
  case "$_ATT_R_verdict" in
@@ -991,28 +1043,26 @@ _att_mint_semantic_stage() {
991
1043
  _att_is_stage "$stage" || { echo "FAIL: invalid stage '$stage'"; return 2; }
992
1044
  _att_git_ok || { echo "FAIL: attest requires a Git repository"; return 2; }
993
1045
  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; }
1046
+ _att_git_clean_for_attest || { echo "FAIL: dirty worktree (only .flow/, locks, status/Evidence allowed)"; return 1; }
1047
+ if ! _att_load_owner_at_rev "$full" "$owner_rel"; then
1048
+ echo "FAIL: owner/command must be committed regular files matching disk at revision (blob equality)"
1049
+ return 1
1050
+ fi
1000
1051
  [ "$_ATT_KV_kind" = "semantic_gate" ] || { echo "FAIL: owner kind must be semantic_gate"; return 2; }
1001
1052
  [ "$_ATT_KV_subject_id" = "$stage" ] || { echo "FAIL: owner subject_id mismatch"; return 2; }
1002
1053
  [ "$_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
1054
  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
1055
  if ! _att_stage_fingerprint "$stage" "$full" >/dev/null; then echo "FAIL: stage fingerprint"; return 1; fi
1008
1056
  fp="$_ATT_LAST_FP"
1009
1057
  ofp="$(_att_owner_fingerprint "$owner_rel" "$full")" || { echo "FAIL: owner fingerprint"; return 1; }
1010
1058
 
1011
- local lname; lname="$(_att_subject_lock_name semantic_gate "$stage")"
1059
+ local lname execf; lname="$(_att_subject_lock_name semantic_gate "$stage")"
1012
1060
  _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"
1061
+ execf="$(_att_materialize_blob_exec "$full" "$cmd_rel")" || {
1062
+ _att_lock_release "$lname"; echo "FAIL: could not materialize committed producer"; return 1
1063
+ }
1064
+ _att_run_supervised "$execf" --subject-id "$stage" --subject-revision "$full" --subject-fingerprint "$fp"
1065
+ rm -f "$execf" 2>/dev/null || true
1016
1066
  if [ "$_ATT_RUN_RC" != "exit" ] || [ "$_ATT_RUN_CODE" -ne 0 ]; then
1017
1067
  _ATT_W_kind=semantic_gate; _ATT_W_subject_type=stage; _ATT_W_subject_id="$stage"
1018
1068
  _ATT_W_subject_fingerprint="$fp"; _ATT_W_verdict=fail
@@ -1067,20 +1117,27 @@ _att_mint_semantic_card() {
1067
1117
  _att_git_clean_for_attest || { echo "FAIL: dirty worktree"; return 1; }
1068
1118
  full_base="$(_att_full_commit "$base")" || { echo "FAIL: bad base"; return 2; }
1069
1119
  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; }
1120
+ if [ "$full_base" = "$full_head" ]; then
1121
+ echo "FAIL: card semantic requires non-empty base..revision (base must differ from --revision)"
1122
+ return 2
1123
+ fi
1124
+ if ! _att_load_owner_at_rev "$full_head" "$owner_rel"; then
1125
+ echo "FAIL: owner/command must be committed regular files matching disk at revision"
1126
+ return 1
1127
+ fi
1074
1128
  [ "$_ATT_KV_kind" = "semantic_gate" ] || { echo "FAIL: owner kind"; return 2; }
1075
1129
  [ "$_ATT_KV_subject_id" = "$id" ] || { echo "FAIL: subject_id mismatch"; return 2; }
1076
1130
  local cmd_rel="${_ATT_KV_command#repo:}"
1077
1131
  if ! _att_card_semantic_fingerprint "$file" "$full_base" "$full_head" >/dev/null; then echo "FAIL: fingerprint"; return 1; fi
1078
1132
  fp="$_ATT_LAST_FP"
1079
1133
  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")"
1134
+ local lname execf; lname="$(_att_subject_lock_name semantic_gate "$id")"
1081
1135
  _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"
1136
+ execf="$(_att_materialize_blob_exec "$full_head" "$cmd_rel")" || {
1137
+ _att_lock_release "$lname"; echo "FAIL: could not materialize committed producer"; return 1
1138
+ }
1139
+ _att_run_supervised "$execf" --subject-id "$id" --subject-revision "$full_head" --subject-fingerprint "$fp"
1140
+ rm -f "$execf" 2>/dev/null || true
1084
1141
  local verdict=fail eref=none
1085
1142
  if [ "$_ATT_RUN_RC" = "exit" ] && [ "$_ATT_RUN_CODE" -eq 0 ]; then
1086
1143
  local sch sfp crit high
@@ -1109,16 +1166,17 @@ _att_mint_semantic_card() {
1109
1166
 
1110
1167
  _att_mint_live() {
1111
1168
  local id="$1" rev="$2" owner_rel="$3"
1112
- local file full owner_abs cmd_rel oracle_rel target ofp cfp lname ap
1169
+ local file full cmd_rel oracle_rel target ofp cfp lname ap
1113
1170
  id="$(_att_norm_card_id "$id")" || { echo "FAIL: bad card"; return 2; }
1114
1171
  file="$(resolve_card_file "$id")"; [ -f "$file" ] || { echo "FAIL: card missing"; return 2; }
1115
1172
  _att_git_ok || { echo "FAIL: git required"; return 2; }
1116
1173
  if ! _att_supervisor_capable; then echo "FAIL: process supervisor unsupported on this platform"; return 1; fi
1174
+ _att_git_clean_for_attest || { echo "FAIL: dirty worktree (live mint requires clean subject)"; return 1; }
1117
1175
  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; }
1176
+ if ! _att_load_owner_at_rev "$full" "$owner_rel"; then
1177
+ echo "FAIL: owner/command/oracle must be committed regular files matching disk at revision"
1178
+ return 1
1179
+ fi
1122
1180
  [ "$_ATT_KV_kind" = "live_verify" ] || { echo "FAIL: owner kind must be live_verify"; return 2; }
1123
1181
  [ "$_ATT_KV_subject_id" = "$id" ] || { echo "FAIL: subject mismatch"; return 2; }
1124
1182
  target="$_ATT_KV_target_id"
@@ -1126,9 +1184,13 @@ _att_mint_live() {
1126
1184
  case "$_ATT_KV_revision_oracle" in repo:*) : ;; *) echo "FAIL: live needs revision_oracle"; return 2 ;; esac
1127
1185
  cmd_rel="${_ATT_KV_command#repo:}"; oracle_rel="${_ATT_KV_revision_oracle#repo:}"
1128
1186
  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
1187
+ local o_blob o_mode o_fp cmd_blob cmd_mode
1188
+ o_blob="$(_att_blob_at_rev "$full" "$oracle_rel")"
1189
+ o_mode="$(_att_path_mode_at_rev "$full" "$oracle_rel")"
1190
+ o_fp="$(_att_hash_text "oracle:${oracle_rel}:${o_mode}:${o_blob}")"
1191
+ cmd_blob="$(_att_blob_at_rev "$full" "$cmd_rel")"
1192
+ cmd_mode="$(_att_path_mode_at_rev "$full" "$cmd_rel")"
1193
+ cfp="$(_att_hash_text "argv:${cmd_rel}:${cmd_mode}:${cmd_blob}|${id}|${full}|${target}")"
1132
1194
  local proj fp
1133
1195
  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
1196
  fp="$(_att_hash_text "live|${full}|${target}|${ofp}|${proj}")"
@@ -1137,20 +1199,26 @@ _att_mint_live() {
1137
1199
  ap="$(_att_attempt_path "$id")"
1138
1200
  mkdir -p "$(dirname "$ap")" 2>/dev/null || true
1139
1201
  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
1202
+ local oracle_exec cmd_exec oracle_oid verdict=fail rcode
1203
+ oracle_exec="$(_att_materialize_blob_exec "$full" "$oracle_rel")" || {
1204
+ rm -f "$ap"; _att_lock_release "$lname"; echo "FAIL: materialize oracle"; return 1
1205
+ }
1206
+ _att_run_supervised "$oracle_exec" --subject-id "$id" --subject-revision "$full" --target-id "$target"
1207
+ rm -f "$oracle_exec" 2>/dev/null || true
1145
1208
  rcode="$(_att_result_code_str)"
1146
1209
  if [ "$_ATT_RUN_RC" = "exit" ] && [ "$_ATT_RUN_CODE" -eq 0 ]; then
1147
1210
  oracle_oid="$(printf '%s' "$_ATT_RUN_OUT" | tr -d '\r\n[:space:]')"
1148
1211
  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
1212
+ cmd_exec="$(_att_materialize_blob_exec "$full" "$cmd_rel")" || true
1213
+ if [ -n "${cmd_exec:-}" ]; then
1214
+ _att_run_supervised "$cmd_exec" --subject-id "$id" --subject-revision "$full" --subject-fingerprint "$fp"
1215
+ rm -f "$cmd_exec" 2>/dev/null || true
1216
+ rcode="$(_att_result_code_str)"
1217
+ if [ "$_ATT_RUN_RC" = "exit" ] && [ "$_ATT_RUN_CODE" -eq 0 ]; then
1218
+ verdict=pass
1219
+ fi
1220
+ else
1221
+ rcode="spawn-error"
1154
1222
  fi
1155
1223
  else
1156
1224
  rcode="exit:1"
@@ -1814,6 +1814,11 @@ cmd_auto() {
1814
1814
  echo " mint: flow attest semantic --stage 05-contract --revision HEAD --owner <committed-manifest>"
1815
1815
  _att_auto_lock_release; return 1
1816
1816
  fi
1817
+ # Cleanliness last among preflight diagnostics so risk/receipt messages still print.
1818
+ if ! _att_require_clean_for_consume; then
1819
+ echo "FAIL: dirty worktree — auto activation requires clean tree (except .flow/, locks, status/Evidence)."
1820
+ _att_auto_lock_release; return 1
1821
+ fi
1817
1822
  local cfp; cfp="$_ATT_R_subject_fingerprint"
1818
1823
  local rfp; rfp="$(_att_risk_fingerprint)"
1819
1824
  local by; by="$(_att_sanitize_label "${FLOW_SESSION_ID:-session}")"
@@ -2,7 +2,7 @@
2
2
  "source": "../skills/flow",
3
3
  "destRelative": "skills/flow",
4
4
  "fileCount": 88,
5
- "syncedAt": "2026-08-11T12:32:58.107Z",
5
+ "syncedAt": "2026-08-11T13:52:41.584Z",
6
6
  "fileList": [
7
7
  "SKILL.md",
8
8
  "_templates/00-idea.md",