@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.
- package/CHANGELOG.md +12 -0
- package/README.md +11 -8
- package/README_VN.md +8 -3
- package/package.json +1 -1
- package/skills/flow/SKILL.md +2 -2
- package/skills/flow/harness/GAP-MATRIX-0.1.17.md +42 -0
- package/skills/flow/harness/README.md +66 -5
- package/skills/flow/harness/_db.py +175 -6
- package/skills/flow/harness/flow_harness.py +235 -10
- package/skills/flow/harness/graph_executor.py +1205 -0
- package/skills/flow/harness/graph_ids.py +35 -0
- package/skills/flow/harness/graph_predicates.py +48 -0
- package/skills/flow/harness/pins/flow-topology.sha256 +1 -0
- package/skills/flow/harness/pins/harness-cli-v0.1.17.sha256sums +9 -0
- package/skills/flow/harness/schema/014-graph-executor.sql +67 -0
- package/skills/flow/references/agent-stage-mapping.md +1 -1
- package/skills/flow/references/auto-run.md +30 -3
- package/skills/flow/references/command-dispatch.md +1 -0
- package/skills/flow/references/concierge.md +1 -0
- package/skills/flow/references/flow-topology.json +146 -0
- package/skills/flow/runner/flow.sh +292 -12
- package/skills-manifest.json +10 -2
|
@@ -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
|
|
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
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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 $
|
|
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 $
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
1110
|
-
done)
|
|
1111
|
-
|
|
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 "$@" ;;
|
package/skills-manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"source": "../skills/flow",
|
|
3
3
|
"destRelative": "skills/flow",
|
|
4
|
-
"fileCount":
|
|
5
|
-
"syncedAt": "2026-07-
|
|
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",
|