@manhquy/flow-skill 0.1.0-rc.3 → 0.2.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.
@@ -136,9 +136,17 @@ stage_name_at() { # $1 = index -> "NN-name"
136
136
  current_stage_idx() { # highest CONTIGUOUS-from-00 stage index, or -1 if 00 missing
137
137
  # Contiguous (not just highest-existing) so a manually-dropped future stage file
138
138
  # cannot make 'next' report a false PLANNING COMPLETE while earlier stages are missing.
139
+ # A DEBT-SKIPPED stage satisfies contiguity without a file: cmd_skip deliberately
140
+ # scaffolds only the successor, so treating its absence as a gap would pin the index
141
+ # at the pre-skip stage forever (05 never scaffolded, planning_complete never true,
142
+ # '/flow card' deadlocked). Skips stay governed by cmd_skip alone - this only READS
143
+ # flow/.skipped, exactly like planning_complete does.
139
144
  local i=0 s idx=-1
140
145
  for s in $STAGES; do
141
- if [ -f "$FLOW_DIR/$s.md" ]; then idx=$i; else break; fi
146
+ if [ -f "$FLOW_DIR/$s.md" ]; then idx=$i
147
+ elif stage_skipped "$s"; then :
148
+ else break
149
+ fi
142
150
  i=$((i + 1))
143
151
  done
144
152
  echo "$idx"
@@ -180,15 +188,104 @@ _python() {
180
188
  return 1
181
189
  }
182
190
 
183
- harness_call() {
184
- # best-effort durable-layer write; NEVER breaks the engine if python/harness absent.
191
+ # Durable write honesty (plan trust-align D5/D8):
192
+ # unset FLOW_HARNESS_STRICT → soft: engine stays 0; one-line stderr warn on fail
193
+ # FLOW_HARNESS_STRICT=1 → soft exit; louder multi-line stderr
194
+ # FLOW_HARNESS_STRICT=fail → propagate nonzero from harness (card/check paths)
195
+ # harness_call_checked always returns the real harness exit (for callers that care).
196
+ _harness_run() {
197
+ # runs harness; prints stderr (masked lightly); echoes nothing; returns real rc
198
+ # usage: _harness_run [--emit-stdout] <args...> sets _HARNESS_LAST_RC
199
+ # --emit-stdout: pass harness stdout THROUGH (value-carrying graph verbs) and
200
+ # suppress the warn line for the semantic rc 3 (no-next/paused, not a failure).
201
+ local _emit=0
202
+ [ "${1:-}" = "--emit-stdout" ] && { _emit=1; shift; }
203
+ _HARNESS_LAST_RC=0
185
204
  [ -n "${FLOW_HARNESS_DISABLE:-}" ] && return 0
186
205
  [ -f "$HARNESS_PY" ] || return 0
187
206
  local py; py="$(_python)"; [ -n "$py" ] || return 0
188
- FLOW_PROJECT_ROOT="$ROOT" "$py" "$HARNESS_PY" "$@" >/dev/null 2>&1
207
+ local errf out rc=0
208
+ errf="$(mktemp 2>/dev/null || echo "$ROOT/.flow/.harness-err.$$")"
209
+ mkdir -p "$ROOT/.flow" 2>/dev/null || true
210
+ # capture rc without flipping the caller's set -e state
211
+ out="$(FLOW_PROJECT_ROOT="$ROOT" "$py" "$HARNESS_PY" "$@" 2>"$errf")" || rc=$?
212
+ _HARNESS_LAST_RC=$rc
213
+ [ "$_emit" = 1 ] && [ -n "$out" ] && printf '%s\n' "$out"
214
+ if [ "$_emit" = 1 ] && [ "$rc" -eq 3 ]; then rm -f "$errf" 2>/dev/null || true; return "$rc"; fi
215
+ if [ "$rc" -ne 0 ]; then
216
+ local errline summary
217
+ errline="$(tr -d '\r' <"$errf" 2>/dev/null | head -n 3 | tr '\n' ' ')"
218
+ # light redact of secret-shaped stderr / never dump free-text evidence args
219
+ case "$errline" in
220
+ *token*|*secret*|*passwd*|*password*|*credential*|*api_key*|*bearer*) errline="***redacted***" ;;
221
+ esac
222
+ summary="$1"
223
+ [ -n "${2:-}" ] && summary="$summary $2"
224
+ [ -n "${3:-}" ] && summary="$summary $3"
225
+ local mode="${FLOW_HARNESS_STRICT:-}"
226
+ if [ "$mode" = "1" ] || [ "$mode" = "fail" ]; then
227
+ printf 'flow-harness: durable command failed (rc=%s) cmd=%s\nflow-harness: %s\n' \
228
+ "$rc" "$summary" "$errline" >&2
229
+ else
230
+ printf 'flow-harness: warn durable write failed (rc=%s)\n' "$rc" >&2
231
+ fi
232
+ fi
233
+ rm -f "$errf" 2>/dev/null || true
234
+ return "$rc"
235
+ }
236
+
237
+ harness_call() {
238
+ # soft durable write: NEVER breaks engine unless STRICT=fail
239
+ _harness_run "$@" || true
240
+ if [ "${FLOW_HARNESS_STRICT:-}" = "fail" ] && [ "${_HARNESS_LAST_RC:-0}" -ne 0 ]; then
241
+ return "${_HARNESS_LAST_RC}"
242
+ fi
189
243
  return 0
190
244
  }
191
245
 
246
+ harness_call_checked() {
247
+ # returns real harness exit code (and still prints STRICT-aware warnings)
248
+ _harness_run "$@"
249
+ }
250
+
251
+ # Wrap a command in a wall-clock bound when `timeout` exists (GNU coreutils / Git Bash);
252
+ # a missing timeout degrades to running unbounded, exactly as before.
253
+ _with_timeout() { # $1 = seconds, rest = command
254
+ local secs="$1"; shift
255
+ if command -v timeout >/dev/null 2>&1; then timeout "$secs" "$@"; else "$@"; fi
256
+ }
257
+
258
+ harness_capture_checked() {
259
+ # VALUE-carrying variant for graph verbs: emits harness stdout and returns the
260
+ # real exit code, via _harness_run --emit-stdout so the availability triad,
261
+ # stderr redaction, and rc plumbing live in ONE place. Fail-closed contract:
262
+ # 0=value, 3=no-next/paused, 4=harness unavailable (DISABLE / file / python
263
+ # missing - the soft rc-0 ambiguity _harness_run keeps for durable writes is
264
+ # exactly what value calls must NOT inherit), 1/2=failure.
265
+ # No consumers yet; cmd_next delegates through this when FLOW_GRAPH_EXECUTOR lands.
266
+ [ -n "${FLOW_HARNESS_DISABLE:-}" ] && return 4
267
+ [ -f "$HARNESS_PY" ] || return 4
268
+ local py; py="$(_python)"; [ -n "$py" ] || return 4
269
+ # 20s: generous for a contended BEGIN IMMEDIATE (busy_timeout is 5s) yet bounded.
270
+ # rc 124 = timed out -> treated as "harness unavailable" by callers, never a hang.
271
+ local rc=0
272
+ _with_timeout "${FLOW_HARNESS_TIMEOUT:-20}" env FLOW_PROJECT_ROOT="$ROOT" \
273
+ "$(_python)" "$HARNESS_PY" "$@" || rc=$?
274
+ [ "$rc" = "124" ] && { printf 'flow-harness: call timed out (%ss)\n' "${FLOW_HARNESS_TIMEOUT:-20}" >&2; return 4; }
275
+ return "$rc"
276
+ }
277
+
278
+ # When STRICT=fail, surface durable failure from card/check call sites (D5).
279
+ _harness_or_fail() {
280
+ harness_call "$@" || {
281
+ if [ "${FLOW_HARNESS_STRICT:-}" = "fail" ]; then
282
+ echo "FAIL: durable harness write failed (FLOW_HARNESS_STRICT=fail)."
283
+ return 1
284
+ fi
285
+ return 0
286
+ }
287
+ }
288
+
192
289
  harness_available() { # 0 = durable layer usable (python + harness present, not disabled)
193
290
  [ -n "${FLOW_HARNESS_DISABLE:-}" ] && return 1
194
291
  [ -f "$HARNESS_PY" ] || return 1
@@ -202,6 +299,65 @@ harness_emit() { # run a harness subcommand and ECHO its stdout (best-effort; no
202
299
  FLOW_PROJECT_ROOT="$ROOT" "$(_python)" "$HARNESS_PY" "$@" 2>/dev/null || true
203
300
  }
204
301
 
302
+ # ---------- graph-executor recording contract (dark until FLOW_GRAPH_EXECUTOR=1) ----------
303
+ # The auto-run loop is agent-driven prose (auto-run.md), so boundaries are recorded by
304
+ # instrumenting verbs the agent ALREADY calls: workspace add -> card-dispatch,
305
+ # check -> card-build/review evidence, card done -> card-verify-live, workspace remove ->
306
+ # merge proof (executor-computed) + worktree telemetry ingest. Everything here is
307
+ # best-effort: a recording failure must never break the mechanical engine.
308
+ _graph_on() { [ "${FLOW_GRAPH_EXECUTOR:-}" = "1" ] && harness_available; }
309
+
310
+ # Execution IDENTITY must live beside the DB, and the DB path is the harness's own
311
+ # decision (worktree translation, monorepo sub-projects, --separate-git-dir). Re-deriving
312
+ # it here with a shell heuristic drifted on exactly those shapes and re-split the journal,
313
+ # so ASK the resolver instead of guessing: `graph root` prints dirname(default_db_path()).
314
+ _graph_exec_id() { # [--no-create] -> echo the project's running auto_run execution id
315
+ # The whole selection is ONE atomic harness call: the DB row IS the pin, and SQLite's
316
+ # write transaction is the arbitration point. A bash pin file needed a claim dir,
317
+ # corpse recovery, TOCTOU-free reclaim AND a liveness check to approximate this - and
318
+ # each of those was a defect (stolen claims forked the journal; an existence-only
319
+ # check adopted abandoned executions and blackholed every later record).
320
+ _graph_on || return 1
321
+ local id
322
+ if [ "${1:-}" = "--no-create" ]; then
323
+ id="$(harness_capture_checked graph session --no-create 2>/dev/null | tr -d '\r\n')" || return 1
324
+ else
325
+ id="$(harness_capture_checked graph session 2>/dev/null | tr -d '\r\n')" || return 1
326
+ fi
327
+ [ -n "$id" ] || return 1
328
+ printf '%s' "$id"
329
+ }
330
+
331
+ _graph_planning_id() { # echo the planning execution id (kind=planning, ns='')
332
+ _graph_on || return 1
333
+ local id
334
+ id="$(harness_capture_checked graph session --kind planning 2>/dev/null | tr -d '\r\n')" || return 1
335
+ [ -n "$id" ] || return 1
336
+ printf '%s' "$id"
337
+ }
338
+
339
+ _graph_stage_record() { # $1 = stage just passed -> record the planning boundary (ns='')
340
+ _graph_on || return 0
341
+ local stage="$1" eid
342
+ eid="$(_graph_planning_id)" || return 0
343
+ local node="stage-${stage%%-*}" # 03-prd -> stage-03 (topology node id)
344
+ harness_capture_checked graph record --execution "$eid" --ns "" --node "$node" \
345
+ --gate-exit 0 --gate-cmd "gate $stage" >/dev/null 2>&1 || true
346
+ return 0
347
+ }
348
+
349
+ _graph_record() { # $1=card-id (REQUIRED for card-scoped nodes) $2=node ; rest passed through
350
+ _graph_on || return 0
351
+ local card="$1" node="$2"; shift 2
352
+ # A card-scoped node with no card would land in the ROOT (stage) namespace and derail
353
+ # the planning walk - workspace add/remove are legitimately card-less for spikes.
354
+ [ -n "$card" ] || return 0
355
+ local eid; eid="$(_graph_exec_id)" || return 0
356
+ harness_capture_checked graph record --execution "$eid" --ns "card:$card" --node "$node" "$@" \
357
+ >/dev/null 2>&1 || true
358
+ return 0
359
+ }
360
+
205
361
  cmd_harness() {
206
362
  # passthrough to the full durable-layer CLI (visible output + real exit code)
207
363
  if [ ! -f "$HARNESS_PY" ]; then echo "harness not installed at $HARNESS_PY"; return 1; fi
@@ -820,6 +976,10 @@ cmd_next() {
820
976
  if [ "$idx" -ge "$LAST_STAGE_IDX" ]; then
821
977
  FLOW_LOG_STAGE_FROM="$cur"
822
978
  if planning_complete; then
979
+ # Record ONLY when the plan actually advanced. stage-05 is the terminal planning
980
+ # node, so recording it while an earlier stage is still blocked would make
981
+ # `graph next` answer "complete" for a project flow.sh itself calls BLOCKED.
982
+ _graph_stage_record "$cur"
823
983
  echo "PASS: stage $cur gate clean. Planning is COMPLETE."
824
984
  echo "All planning stages passed (or were debt-skipped). Run '/flow card' to create build cards."
825
985
  if prd_declares_fr; then
@@ -833,14 +993,39 @@ cmd_next() {
833
993
  return 0
834
994
  fi
835
995
  local nxt; nxt="$(stage_name_at "$((idx + 1))")"
996
+ # Walk PAST every debt-skipped successor instead of stopping on one: cmd_skip already
997
+ # recorded the accepted exposure, so re-creating its file would resurrect a stage the
998
+ # DEBT line says is knowingly missing - but simply refusing to scaffold would wedge the
999
+ # project forever when the skipped stage's own successor is also absent (a failed
1000
+ # scaffold in cmd_skip, or an operator deleting the file). Advance to the first stage
1001
+ # that is neither present nor skipped; if that runs off the end, planning is complete.
1002
+ local nidx=$((idx + 1)) skipped_over=""
1003
+ while [ "$nidx" -le "$LAST_STAGE_IDX" ] && stage_skipped "$nxt"; do
1004
+ skipped_over="$skipped_over $nxt"
1005
+ nidx=$((nidx + 1))
1006
+ [ "$nidx" -gt "$LAST_STAGE_IDX" ] && break
1007
+ nxt="$(stage_name_at "$nidx")"
1008
+ done
1009
+ if [ "$nidx" -gt "$LAST_STAGE_IDX" ]; then
1010
+ echo "PASS: stage $cur gate clean. Remaining stages are debt-skipped ($(printf '%s' "${skipped_over# }"))."
1011
+ if planning_complete; then
1012
+ _graph_stage_record "$cur"
1013
+ echo "Planning is COMPLETE. Run '/flow card' to create build cards."
1014
+ else
1015
+ echo "An earlier stage is still BLOCKED (and not debt-skipped) - run '/flow' to see which."
1016
+ fi
1017
+ return 0
1018
+ fi
836
1019
  if [ -f "$FLOW_DIR/$nxt.md" ]; then
837
- echo "PASS: stage $cur gate clean. Stage $((idx + 1)) ($nxt.md) already exists - not overwritten."
1020
+ echo "PASS: stage $cur gate clean. Stage $nidx ($nxt.md) already exists - not overwritten."
1021
+ _graph_stage_record "$cur"
838
1022
  return 0
839
1023
  fi
840
1024
  cp "$TEMPLATE_DIR/$nxt.md" "$FLOW_DIR/$nxt.md"
841
1025
  FLOW_LOG_STAGE_FROM="$cur"; FLOW_LOG_STAGE_TO="$nxt"
842
- echo "PASS: stage $cur gate clean -> unlocked stage $((idx + 1)) (flow/$nxt.md)"
1026
+ echo "PASS: stage $cur gate clean -> unlocked stage $nidx (flow/$nxt.md)"
843
1027
  gate_durable_hook "$cur"
1028
+ _graph_stage_record "$cur"
844
1029
  echo " tip: '/flow recall' surfaces prior debt/retro/friction before you fill this stage."
845
1030
  return 0
846
1031
  }
@@ -973,7 +1158,7 @@ cmd_card_start() { # mark a card actively in progress (operator-visible in_progr
973
1158
  local id; id="$(basename "$file" .md)"; FLOW_LOG_CARD="$id"
974
1159
  if [ "$(card_status "$file")" = "done" ]; then echo "FAIL: $id is already done — nothing to start."; return 1; fi
975
1160
  _inflight_set "$id"
976
- harness_call story update --id "$id" --status in_progress # durable mirror (best-effort)
1161
+ _harness_or_fail story update --id "$id" --status in_progress # durable mirror (STRICT=fail propagates)
977
1162
  echo "PASS: $id marked in flight. Build it, then '/flow card done $id' (or hand-edit status: done + '/flow check $id')."
978
1163
  return 0
979
1164
  }
@@ -989,6 +1174,10 @@ cmd_card_done() { # CLI-owned flip to done, gated by the SAME done-rules as '/fl
989
1174
  if ! _set_card_status "$file" done; then echo "FAIL: could not write status for $id"; return 1; fi
990
1175
  if cmd_check "$id"; then # cmd_check enforces evidence/verify AND records the durable done-trace
991
1176
  _inflight_clear "$id"
1177
+ # card-verify-live: the done boundary is the EVIDENCE gate passing, not the merge
1178
+ # (merge != shipped) - dependents unblock on card status, matching cmd_ready.
1179
+ # The nested cmd_check already recorded card-review; this advances past it.
1180
+ _graph_record "$id" card-verify-live --gate-exit 0 --card-status done
992
1181
  return 0
993
1182
  fi
994
1183
  _set_card_status "$file" "${orig:-todo}" # NOT done — never leave a hollow 'done'
@@ -1033,7 +1222,7 @@ cmd_card() {
1033
1222
  [ -n "$pc" ] && printf '%s\n' "$pc" | sed 's/^/ /'
1034
1223
  [ -n "$dbt" ] && { echo " open debt:"; printf '%s\n' "$dbt" | sed 's/^/ /'; }
1035
1224
  fi
1036
- harness_call story add --id "$id" --title "$id" --lane normal # durable tracking handle
1225
+ _harness_or_fail story add --id "$id" --title "$id" --lane normal # durable tracking handle
1037
1226
  return 0
1038
1227
  }
1039
1228
 
@@ -1105,16 +1294,34 @@ cmd_check() {
1105
1294
 
1106
1295
  if [ "$found" -eq 0 ]; then
1107
1296
  echo "PASS: $id is valid (status: $st)."
1297
+ # card-review boundary: the gate verdict IS the evidence the executor records
1298
+ # (green here is what unblocks card-merge in the topology).
1299
+ # Typed flags, never a bash-built JSON literal: $st is free text from the card file.
1300
+ _graph_record "$id" card-review --gate-exit 0 --gate-cmd "check $id" --card-status "$st"
1108
1301
  case "$st" in
1109
- todo) harness_call story update --id "$id" --status in_progress ;;
1110
- done) harness_call story update --id "$id" --status implemented
1111
- local tr; tr="$(harness_emit trace --summary "card $id reached done-evidence" --story "$id" --outcome completed)"
1302
+ todo) _harness_or_fail story update --id "$id" --status in_progress || return 1 ;;
1303
+ done)
1304
+ # trust-align: complete with honest card_markdown_gate provenance (D3/D7/D8)
1305
+ if ! harness_call_checked story complete --id "$id" \
1306
+ --proof-source card_markdown_gate \
1307
+ --evidence "card $id markdown gate PASS"; then
1308
+ if [ "${FLOW_HARNESS_STRICT:-}" = "fail" ]; then
1309
+ echo "FAIL: durable story complete failed (FLOW_HARNESS_STRICT=fail)."
1310
+ return 1
1311
+ fi
1312
+ fi
1313
+ # auto-trace: enrich tier-2 fields from known card context (do not fake --lane tiny)
1314
+ local tr
1315
+ tr="$(harness_emit trace --summary "card $id reached done-evidence" --story "$id" \
1316
+ --outcome completed --agent flow-check \
1317
+ --actions "check $id gate PASS" --files-changed "cards/$id.md" --friction none)"
1112
1318
  [ -n "$tr" ] && printf '%s\n' "$tr" | grep -iE 'tier|to reach' | sed 's/^/ harness: /' ;;
1113
1319
  esac
1114
1320
  return 0
1115
1321
  fi
1116
1322
  FLOW_LAST_GATE_FAIL="fill:$(grep -c '\[FILL' "$file" 2>/dev/null || echo 0),unchecked:$(grep -cE '^[[:space:]]*- \[ \]' "$file" 2>/dev/null || echo 0)"
1117
1323
  echo "FAIL: $id has gate violations (above)."
1324
+ _graph_record "$id" card-review --gate-exit 1 --gate-cmd "check $id" --card-status "$st"
1118
1325
  return 1
1119
1326
  }
1120
1327
 
@@ -1193,6 +1400,34 @@ cmd_skip() {
1193
1400
  return 0
1194
1401
  }
1195
1402
 
1403
+ cmd_gate() {
1404
+ # Read-only mechanical gate scan (no unlock, no template copy, no durable writes):
1405
+ # the ONLY flow.sh verb the graph executor may run from topology cmd position
1406
+ # (`check` mutates story/trace on pass and stays Must-ask). Exit 0 clean / 1 findings.
1407
+ local target="${1:-}"
1408
+ if [ "$target" = "--card" ]; then
1409
+ local card="${2:-}" f
1410
+ if [ -z "$card" ]; then echo "usage: /flow gate --card C-NNN"; return 2; fi
1411
+ f="$(resolve_card_file "$card" 2>/dev/null)"
1412
+ if [ -z "$f" ]; then echo "usage: /flow gate --card C-NNN"; return 2; fi
1413
+ # A nonexistent card is NOT a gate verdict: reporting it as exit 1 would journal a
1414
+ # typo'd id as a genuine red review and burn a two-strikes repair budget.
1415
+ if [ ! -f "$f" ]; then echo "FAIL: card not found for '$card' (looked for $f)"; return 2; fi
1416
+ echo "GATE card ${card}:"
1417
+ if scan_gate "$f"; then echo " clean"; return 0; fi
1418
+ return 1
1419
+ fi
1420
+ local known=0 s
1421
+ for s in $STAGES; do [ "$s" = "$target" ] && known=1; done
1422
+ if [ "$known" -eq 0 ]; then
1423
+ echo "usage: /flow gate <stage: $STAGES> | /flow gate --card C-NNN"
1424
+ return 2
1425
+ fi
1426
+ echo "GATE stage ${target}:"
1427
+ if scan_gate "$FLOW_DIR/$target.md"; then echo " clean"; return 0; fi
1428
+ return 1
1429
+ }
1430
+
1196
1431
  cmd_retro() {
1197
1432
  [ -f "$RETRO_FILE" ] || { [ -f "$LAW_DIR/RETRO.md" ] && cp "$LAW_DIR/RETRO.md" "$RETRO_FILE"; }
1198
1433
  echo "flow retro - answer these 3, then append ONE honest line to RETRO.md:"
@@ -1941,6 +2176,10 @@ _ws_add() {
1941
2176
  done
1942
2177
  fi
1943
2178
  echo "PASS: created worktree for '$branch' (vendor ${vendor:-?}, port-offset $port) -> $wt"
2179
+ # card-dispatch boundary. NOTE: no FLOW_PROJECT_ROOT export in the enter block — the DB
2180
+ # resolver already maps a linked worktree to the main DB (_db.default_db_path), and
2181
+ # exporting the project root would hijack CARDS_DIR/locks/DEBT for every call in there.
2182
+ _graph_record "$card" card-dispatch --branch "$branch" --worktree "$wt" --vendor "$vendor"
1944
2183
  _ws_print_enter "$branch" "$wt" "$vendor" "$port"
1945
2184
  echo " (tracked in .flow/workspaces.jsonl; see 'workspace list' / 'workspace doctor')"
1946
2185
  return 0
@@ -2023,6 +2262,20 @@ _ws_remove() {
2023
2262
  '/^worktree /{p=substr($0,10)} /^branch /{b=$2; sub(/^refs\/heads\//,"",b); if(b==want) print p}')"
2024
2263
  [ -n "$wt" ] || wt="$(_ws_get "$rec" worktree_path)"
2025
2264
  if [ -z "$wt" ]; then echo "workspace: no worktree found for branch '$branch'."; return 1; fi
2265
+ # Telemetry ingest must happen BEFORE the tree is gone (the sink lives inside it); the
2266
+ # merge boundary is recorded only AFTER a successful removal, so a refused (dirty-tree)
2267
+ # remove never journals a merge for a card still under active work.
2268
+ local _gcard _gcreated
2269
+ _gcard="$(_ws_get "$rec" card_id)"; _gcreated="$(_ws_get_num "$rec" created_at)"
2270
+ if _graph_on && [ -f "$wt/.flow/events.jsonl" ]; then
2271
+ # Lifecycle-unique key: worktree paths are recycled, so a bare-path cursor would
2272
+ # swallow the next lifecycle's lines. Prefix = destination sink (usage/prune group it).
2273
+ # Path and tag stay in separate arguments so the path is still a path when it crosses
2274
+ # into python — pre-joining them with '#' defeats Git-Bash path translation on Windows.
2275
+ harness_capture_checked rollup --src "$wt/.flow/events.jsonl" \
2276
+ --src-key-path "$LOG_DIR/events.jsonl" \
2277
+ --src-key-tag "$branch#${_gcreated:-0}" >/dev/null 2>&1 || true
2278
+ fi
2026
2279
  if [ "$force" -eq 1 ]; then
2027
2280
  rmout="$(git -C "$ROOT" worktree remove --force "$wt" 2>&1)"; rc=$?
2028
2281
  else
@@ -2033,6 +2286,9 @@ _ws_remove() {
2033
2286
  echo "FAIL: git worktree remove refused (commit/clean the tree, or re-run with --force)."
2034
2287
  return 1
2035
2288
  fi
2289
+ # The executor computes the ancestry itself - this verb has no merge knowledge (it only
2290
+ # removes a tree) - and records card-abandon when the branch was never merged.
2291
+ _graph_record "$_gcard" card-merge --merge --branch "$branch"
2036
2292
  # tombstone ONLY on clean success (git is truth; a failed remove changed nothing)
2037
2293
  local vendor card port
2038
2294
  vendor="$(_ws_get "$rec" vendor)"; card="$(_ws_get "$rec" card_id)"; port="$(_ws_get_num "$rec" port_offset)"
@@ -2106,6 +2362,24 @@ _ws_doctor() {
2106
2362
  if [ -n "$pdup" ]; then echo " [!] duplicate port_offset across active workspaces (advisory): $(printf '%s ' $pdup)"; warn=1; fi
2107
2363
  local maxw="${FLOW_WORKSPACE_MAX:-4}"
2108
2364
  if [ "$nact" -gt "$maxw" ]; then echo " [!] $nact active workspaces exceeds FLOW_WORKSPACE_MAX=$maxw (realistic solo ceiling is 3-4; advisory)."; warn=1; fi
2365
+ # Execution-aware (graph executor): a PAUSED execution is waiting on an operator, so
2366
+ # its worktree must not be reconciled away. git + card files stay the authority; the
2367
+ # journal is a derived record, so this only ever WARNS.
2368
+ if _graph_on; then
2369
+ local eid gstat opens
2370
+ # --no-create: doctor is advisory and read-only; it must never mint an execution.
2371
+ eid="$(_graph_exec_id --no-create 2>/dev/null || true)"
2372
+ if [ -n "$eid" ]; then
2373
+ gstat="$(harness_capture_checked graph status --execution "$eid" 2>/dev/null)"
2374
+ opens="$(printf '%s' "$gstat" | tr ',' '\n' | grep -c '"interrupt_id"' || true)"
2375
+ opens="$(printf '%s' "${opens:-0}" | tr -dc '0-9' | head -c 6)"; opens="${opens:-0}"
2376
+ if [ "${opens:-0}" -gt 0 ]; then
2377
+ echo " [!] graph execution $eid has $opens OPEN interrupt(s) - do not remove the backing"
2378
+ echo " worktree(s) until resolved: 'flow harness graph status --execution $eid'"
2379
+ warn=1
2380
+ fi
2381
+ fi
2382
+ fi
2109
2383
  rm -rf "$td"
2110
2384
 
2111
2385
  if [ "$drift" -eq 0 ]; then
@@ -3442,6 +3716,7 @@ usage: bash flow.sh <command> [args]
3442
3716
  assess Brownfield: scaffold + gate a current-state assessment (flow/00-inspect.md) before planning
3443
3717
  card Create the next build card (after planning complete)
3444
3718
  check C-NNN Validate a card (FILL/status/sections/done-evidence)
3719
+ gate <stage>|--card C-NNN Read-only mechanical scan (no unlock, no durable writes)
3445
3720
  mode [teach|work] Show or set who writes the artifacts
3446
3721
  project-type [t] Show or set project type (web|cli|library|skill); adapts done-evidence
3447
3722
  skip <stage> --reason Advance past a gate that has a matching open DEBT (non-security only)
@@ -3507,7 +3782,7 @@ _flow_version() {
3507
3782
 
3508
3783
  _log_is_readonly() { # $1 = command -> true|false (does it mutate the flow plan?)
3509
3784
  case "$1" in
3510
- status|resume|recall|ready|usage|tokens|coherence|consistency|contract|constitution|doctor|design|help|-h|--help|"") echo true ;;
3785
+ status|resume|recall|ready|usage|tokens|coherence|consistency|contract|constitution|doctor|design|gate|help|-h|--help|"") echo true ;;
3511
3786
  *) echo false ;;
3512
3787
  esac
3513
3788
  }
@@ -3615,6 +3890,10 @@ cmd_usage() {
3615
3890
  }
3616
3891
 
3617
3892
  # ---------- dispatch ----------
3893
+ # Library mode: tests source this file to exercise real helpers instead of regex-slicing
3894
+ # them out (a slice silently breaks the moment a helper gains a dependency, and then the
3895
+ # test reports 127 instead of the behavior it claims to check).
3896
+ if [ -n "${FLOW_LIB_ONLY:-}" ]; then return 0 2>/dev/null || exit 0; fi
3618
3897
  cmd="${1:-status}"
3619
3898
  shift 2>/dev/null || true
3620
3899
  FLOW_LOG_CMD="$cmd"
@@ -3628,6 +3907,7 @@ case "$cmd" in
3628
3907
  assess) cmd_assess ;;
3629
3908
  card) cmd_card "$@" ;;
3630
3909
  check) cmd_check "${1:-}" ;;
3910
+ gate) cmd_gate "$@" ;;
3631
3911
  mode) cmd_mode "${1:-}" ;;
3632
3912
  project-type) cmd_project_type "${1:-}" ;;
3633
3913
  skip) cmd_skip "$@" ;;
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "source": "../skills/flow",
3
3
  "destRelative": "skills/flow",
4
- "fileCount": 74,
5
- "syncedAt": "2026-07-18T07:42:58.153Z",
4
+ "fileCount": 82,
5
+ "syncedAt": "2026-07-27T03:21:41.774Z",
6
6
  "fileList": [
7
7
  "SKILL.md",
8
8
  "_templates/00-idea.md",
@@ -31,11 +31,17 @@
31
31
  "eval/fixtures/routing/states/state-review-pending.txt",
32
32
  "eval/fixtures/routing/states/state-scope-stage.txt",
33
33
  "eval/manifest.tsv",
34
+ "harness/GAP-MATRIX-0.1.17.md",
34
35
  "harness/README.md",
35
36
  "harness/_db.py",
36
37
  "harness/_domain.py",
37
38
  "harness/_presence.py",
38
39
  "harness/flow_harness.py",
40
+ "harness/graph_executor.py",
41
+ "harness/graph_ids.py",
42
+ "harness/graph_predicates.py",
43
+ "harness/pins/flow-topology.sha256",
44
+ "harness/pins/harness-cli-v0.1.17.sha256sums",
39
45
  "harness/repo_map.py",
40
46
  "harness/schema/001-init.sql",
41
47
  "harness/schema/002-story-verify.sql",
@@ -46,6 +52,7 @@
46
52
  "harness/schema/010-usage-event.sql",
47
53
  "harness/schema/011-usage-gate-reason.sql",
48
54
  "harness/schema/012-usage-ephemeral.sql",
55
+ "harness/schema/014-graph-executor.sql",
49
56
  "law/CLAUDE.md",
50
57
  "law/DESIGN.md",
51
58
  "law/RETRO.md",
@@ -66,6 +73,7 @@
66
73
  "references/debt-and-halts.md",
67
74
  "references/design-review-checklist.md",
68
75
  "references/flow-catalog.tsv",
76
+ "references/flow-topology.json",
69
77
  "references/forge-idea.md",
70
78
  "references/gate-eval.md",
71
79
  "references/gate-rules.md",