@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
|
@@ -20,6 +20,7 @@ import sys
|
|
|
20
20
|
import _db
|
|
21
21
|
import _domain as D
|
|
22
22
|
import _presence as P
|
|
23
|
+
import graph_executor as G
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
def _flow_lineage_db(argv):
|
|
@@ -58,6 +59,23 @@ def _flow_lineage_db(argv):
|
|
|
58
59
|
def _maybe_forward_to_rust(argv):
|
|
59
60
|
if os.environ.get("FLOW_HARNESS_BACKEND", "python").lower() != "rust":
|
|
60
61
|
return None
|
|
62
|
+
# graph verbs are flow-only (schema band 014+); the external harness-cli has no such
|
|
63
|
+
# surface, and on a FRESH db the lineage sniff below would not refuse yet — so the
|
|
64
|
+
# never-forward set must be explicit, not inferred from DB state. The verb is the
|
|
65
|
+
# first NON-OPTION token: `--db <path> graph run` must not slip past an argv[0] check.
|
|
66
|
+
verb, i = None, 0
|
|
67
|
+
while i < len(argv):
|
|
68
|
+
tok = argv[i]
|
|
69
|
+
if tok == "--db":
|
|
70
|
+
i += 2
|
|
71
|
+
continue
|
|
72
|
+
if tok.startswith("-"):
|
|
73
|
+
i += 1
|
|
74
|
+
continue
|
|
75
|
+
verb = tok
|
|
76
|
+
break
|
|
77
|
+
if verb == "graph":
|
|
78
|
+
return None
|
|
61
79
|
conflict = _flow_lineage_db(argv)
|
|
62
80
|
if conflict:
|
|
63
81
|
sys.stderr.write(
|
|
@@ -160,6 +178,21 @@ def cmd_intake(con, a):
|
|
|
160
178
|
return 0
|
|
161
179
|
|
|
162
180
|
|
|
181
|
+
_PROOF_SOURCES = ("card_markdown_gate", "manual", "verify_command")
|
|
182
|
+
_SECRET_RE = re.compile(
|
|
183
|
+
r"(?i)(token|secret|passwd|password|credential|api[_-]?key|bearer|authorization|-----begin)"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _mask_evidence(text):
|
|
188
|
+
"""Redact secret-shaped free text before durable store (trust-align RT)."""
|
|
189
|
+
if text is None:
|
|
190
|
+
return None
|
|
191
|
+
if _SECRET_RE.search(text):
|
|
192
|
+
return "***redacted***"
|
|
193
|
+
return text
|
|
194
|
+
|
|
195
|
+
|
|
163
196
|
def cmd_story(con, a):
|
|
164
197
|
if a.story_cmd == "add":
|
|
165
198
|
if a.lane not in D.LANES:
|
|
@@ -169,11 +202,58 @@ def cmd_story(con, a):
|
|
|
169
202
|
print(f"PASS: story {a.id} added (lane={a.lane})")
|
|
170
203
|
return 0
|
|
171
204
|
if a.story_cmd == "update":
|
|
205
|
+
# Trust boundary (US-101 spirit, flow-native): bare implemented is forbidden.
|
|
206
|
+
if a.status == "implemented":
|
|
207
|
+
print(
|
|
208
|
+
"FAIL: story update --status implemented is rejected.\n"
|
|
209
|
+
" Use: story complete --id <id> --proof-source card_markdown_gate|manual|verify_command\n"
|
|
210
|
+
" (honest proof provenance; never forge last_verified_result without story verify)"
|
|
211
|
+
)
|
|
212
|
+
return 1
|
|
172
213
|
n = _db.update(con, "story", "id", a.id, status=a.status,
|
|
173
214
|
unit_proof=a.unit, integration_proof=a.integration,
|
|
174
|
-
e2e_proof=a.e2e, platform_proof=a.platform,
|
|
215
|
+
e2e_proof=a.e2e, platform_proof=a.platform,
|
|
216
|
+
evidence=_mask_evidence(a.evidence))
|
|
175
217
|
print(f"{'PASS' if n else 'FAIL'}: story {a.id} {'updated' if n else 'not found'}")
|
|
176
218
|
return 0 if n else 1
|
|
219
|
+
if a.story_cmd == "complete":
|
|
220
|
+
s = _db.one(con, "SELECT * FROM story WHERE id=?", (a.id,))
|
|
221
|
+
if not s:
|
|
222
|
+
print(f"FAIL: story {a.id} not found"); return 1
|
|
223
|
+
src = (a.proof_source or "").strip()
|
|
224
|
+
if src not in _PROOF_SOURCES:
|
|
225
|
+
print(
|
|
226
|
+
"FAIL: --proof-source required: card_markdown_gate|manual|verify_command\n"
|
|
227
|
+
" card_markdown_gate = /flow check markdown gate (does NOT set last_verified=pass)\n"
|
|
228
|
+
" verify_command = requires prior story verify pass"
|
|
229
|
+
)
|
|
230
|
+
return 1
|
|
231
|
+
if src == "verify_command":
|
|
232
|
+
if s.get("last_verified_result") != "pass":
|
|
233
|
+
print(
|
|
234
|
+
f"FAIL: proof-source verify_command requires last_verified_result=pass "
|
|
235
|
+
f"(got {s.get('last_verified_result')!r}). Run: story verify --id {a.id}"
|
|
236
|
+
)
|
|
237
|
+
return 1
|
|
238
|
+
# Honest provenance: card/manual never forge last_verified_result=pass
|
|
239
|
+
ev = _mask_evidence(a.evidence) or s.get("evidence") or ""
|
|
240
|
+
note = (s.get("notes") or "").strip()
|
|
241
|
+
prov = f"proof_source={src}"
|
|
242
|
+
if prov not in note:
|
|
243
|
+
note = (note + "\n" + prov).strip() if note else prov
|
|
244
|
+
if src == "card_markdown_gate" and "last_verified_result=pass" not in (note or ""):
|
|
245
|
+
# explicit honesty marker for auditors
|
|
246
|
+
note = note + "\nverify_stamp=not_shell"
|
|
247
|
+
fields = dict(status="implemented", notes=note)
|
|
248
|
+
if ev:
|
|
249
|
+
fields["evidence"] = ev
|
|
250
|
+
# only keep last_verified_* when real verify already passed
|
|
251
|
+
n = _db.update(con, "story", "id", a.id, **fields)
|
|
252
|
+
print(
|
|
253
|
+
f"PASS: story {a.id} complete (status=implemented, proof_source={src})"
|
|
254
|
+
+ ("" if src == "verify_command" else " [no shell-verify stamp]")
|
|
255
|
+
)
|
|
256
|
+
return 0 if n else 1
|
|
177
257
|
if a.story_cmd in ("verify", "verify-all"):
|
|
178
258
|
if a.story_cmd == "verify":
|
|
179
259
|
stories = [_db.one(con, "SELECT * FROM story WHERE id=?", (a.id,))]
|
|
@@ -594,6 +674,18 @@ def _events_path(a):
|
|
|
594
674
|
return os.path.join(os.path.dirname(a._db_path), "events.jsonl")
|
|
595
675
|
|
|
596
676
|
|
|
677
|
+
def _canon_sink(p):
|
|
678
|
+
"""Spelling-independent name for a sink path, so 'same file' means 'same key'.
|
|
679
|
+
|
|
680
|
+
The same directory reaches us under several spellings: a macOS temp root arrives as
|
|
681
|
+
both /var/... and /private/var/..., and Windows hands back either an 8.3 short name
|
|
682
|
+
(C:/Users/RUNNER~1/...) or the long one depending on who resolved it. Keying rows on
|
|
683
|
+
the raw spelling silently splits one sink into two. realpath collapses the symlink and
|
|
684
|
+
short-name cases; normcase folds Windows separators and case.
|
|
685
|
+
"""
|
|
686
|
+
return os.path.normcase(os.path.realpath(p))
|
|
687
|
+
|
|
688
|
+
|
|
597
689
|
def _global_log_path():
|
|
598
690
|
return os.path.join(os.path.expanduser("~"), ".claude", "flow", "usage.jsonl")
|
|
599
691
|
|
|
@@ -609,21 +701,33 @@ def _coerce_event(o):
|
|
|
609
701
|
|
|
610
702
|
|
|
611
703
|
def cmd_rollup(con, a):
|
|
612
|
-
|
|
704
|
+
# (path, key) pairs: `path` is the file read; `key` is the rollup_cursor /
|
|
705
|
+
# UNIQUE(src,line_no) namespace. They differ only for --src ingests, where a
|
|
706
|
+
# lifecycle-unique key (--src-key-path + --src-key-tag) prevents a RECYCLED worktree
|
|
707
|
+
# path from silently swallowing lines 1..N of the next lifecycle's sink.
|
|
708
|
+
if getattr(a, "src", None):
|
|
709
|
+
kp = getattr(a, "src_key_path", None) or a.src
|
|
710
|
+
tag = getattr(a, "src_key_tag", None)
|
|
711
|
+
key = _canon_sink(kp) + ("#" + tag if tag else "")
|
|
712
|
+
pairs = [(a.src, key)]
|
|
713
|
+
else:
|
|
714
|
+
p = _events_path(a)
|
|
715
|
+
pairs = [(p, p)]
|
|
613
716
|
if getattr(a, "global_", False):
|
|
614
|
-
|
|
717
|
+
g = _global_log_path()
|
|
718
|
+
pairs.append((g, g))
|
|
615
719
|
rolled = skipped = 0
|
|
616
720
|
cols = ["src", "line_no"] + list(USAGE_COLS)
|
|
617
721
|
ph = ",".join("?" for _ in cols)
|
|
618
|
-
for src in
|
|
619
|
-
if not os.path.isfile(
|
|
722
|
+
for src_path, src in pairs:
|
|
723
|
+
if not os.path.isfile(src_path):
|
|
620
724
|
continue
|
|
621
725
|
cur = con.execute("SELECT last_line FROM rollup_cursor WHERE src=?", (src,)).fetchone()
|
|
622
726
|
last = cur[0] if cur else 0
|
|
623
727
|
# errors="replace": a single invalid byte anywhere in a shared, multi-writer log must
|
|
624
728
|
# not abort the WHOLE rollup - it degrades to a per-line json.loads failure below
|
|
625
729
|
# instead (one bad row skipped, not every row on the file).
|
|
626
|
-
with open(
|
|
730
|
+
with open(src_path, "r", encoding="utf-8", errors="replace") as fh:
|
|
627
731
|
text = fh.read()
|
|
628
732
|
lines = text.split("\n")
|
|
629
733
|
lines = lines[:-1] # drop trailing empty (after final \n) or a partial unterminated last line
|
|
@@ -668,15 +772,33 @@ def cmd_rollup(con, a):
|
|
|
668
772
|
return 0
|
|
669
773
|
|
|
670
774
|
|
|
775
|
+
def cmd_graph(con, a):
|
|
776
|
+
return {
|
|
777
|
+
"run": G.cmd_graph_run, "record": G.cmd_graph_record, "next": G.cmd_graph_next,
|
|
778
|
+
"resume": G.cmd_graph_resume, "status": G.cmd_graph_status,
|
|
779
|
+
"abandon": G.cmd_graph_abandon, "gc": G.cmd_graph_gc, "lint": G.cmd_graph_lint,
|
|
780
|
+
"cards": G.cmd_graph_cards, "root": G.cmd_graph_root,
|
|
781
|
+
"session": G.cmd_graph_session, "finish": G.cmd_graph_finish,
|
|
782
|
+
}[a.graph_cmd](con, a)
|
|
783
|
+
|
|
784
|
+
|
|
671
785
|
def cmd_usage(con, a):
|
|
672
786
|
src = _global_log_path() if getattr(a, "global_", False) else _events_path(a)
|
|
673
|
-
|
|
787
|
+
# `src=?` matches direct-sink rows; the LIKE arm matches lifecycle-keyed ingests
|
|
788
|
+
# (`rollup --src-key-path <this-sink> --src-key-tag <branch>#<created_at>` — worktree
|
|
789
|
+
# telemetry merged at
|
|
790
|
+
# workspace-remove time). Without it those rows would be write-only: invisible to
|
|
791
|
+
# usage and unreachable by prune.
|
|
792
|
+
w = "WHERE (src=? OR src LIKE ? || '#%')"
|
|
674
793
|
# Default-exclude throwaway/test runs so the view reflects real builds. COALESCE handles old
|
|
675
794
|
# rows (ephemeral NULL -> 0); the project-name fallback excludes legacy `tmp.*` runs that
|
|
676
795
|
# predate the field (no log rewrite). `--include-ephemeral` shows everything.
|
|
677
796
|
if not getattr(a, "include_ephemeral", False):
|
|
678
797
|
w += " AND NOT (COALESCE(ephemeral,0)=1 OR project LIKE 'tmp.%')"
|
|
679
|
-
|
|
798
|
+
# Direct-sink rows are keyed by the path as this process spells it, so `src=?` keeps
|
|
799
|
+
# matching them verbatim; only the lifecycle arm compares canonically, because those
|
|
800
|
+
# keys are written by whoever called rollup and their spelling is not ours to choose.
|
|
801
|
+
pr = (src, _canon_sink(src))
|
|
680
802
|
total = con.execute(f"SELECT COUNT(*) FROM usage_event {w}", pr).fetchone()[0]
|
|
681
803
|
if not total:
|
|
682
804
|
if not getattr(a, "summary", False): # --summary stays silent on no data (recall appends nothing)
|
|
@@ -941,8 +1063,12 @@ def cmd_prune(con, a):
|
|
|
941
1063
|
os.replace(tmp, src) # atomic on the same filesystem
|
|
942
1064
|
# line numbers changed -> the mirror + cursor for this sink are stale; reset so the next
|
|
943
1065
|
# rollup re-ingests the kept lines as line_no 1..K (UNIQUE(src,line_no) stays consistent).
|
|
944
|
-
|
|
945
|
-
|
|
1066
|
+
# Include lifecycle-keyed ingests for this sink (`<sink>#<branch>#<created_at>`):
|
|
1067
|
+
# their line numbers are unrelated to the rewritten file, but the reset contract is
|
|
1068
|
+
# "mirror rows for this sink are rebuilt from the kept lines", and orphaned keyed
|
|
1069
|
+
# rows would be unreachable forever after.
|
|
1070
|
+
con.execute("DELETE FROM usage_event WHERE src=? OR src LIKE ? || '#%'", (src, _canon_sink(src)))
|
|
1071
|
+
con.execute("DELETE FROM rollup_cursor WHERE src=? OR src LIKE ? || '#%'", (src, _canon_sink(src)))
|
|
946
1072
|
con.commit()
|
|
947
1073
|
print(json.dumps({"sink": src, "kept": len(kept), "dropped": len(lines) - len(kept)}))
|
|
948
1074
|
if getattr(a, "global_", False):
|
|
@@ -983,6 +1109,13 @@ def build_parser():
|
|
|
983
1109
|
a2.add_argument("--evidence")
|
|
984
1110
|
a3 = pss.add_parser("verify"); a3.add_argument("--id", required=True)
|
|
985
1111
|
pss.add_parser("verify-all")
|
|
1112
|
+
# flow-native complete (trust-align D7): not protocol-v1 positional parity
|
|
1113
|
+
ac = pss.add_parser("complete", help="mark implemented with honest proof_source (not bare update)")
|
|
1114
|
+
ac.add_argument("--id", required=True)
|
|
1115
|
+
ac.add_argument("--proof-source", dest="proof_source",
|
|
1116
|
+
choices=("card_markdown_gate", "manual", "verify_command"),
|
|
1117
|
+
help="card_markdown_gate never forges last_verified=pass")
|
|
1118
|
+
ac.add_argument("--evidence")
|
|
986
1119
|
|
|
987
1120
|
pt = sub.add_parser("trace", help="record an agent task execution trace (auto-scored)")
|
|
988
1121
|
pt.add_argument("--summary", required=True); pt.add_argument("--intake", type=int); pt.add_argument("--story", "--card")
|
|
@@ -1044,6 +1177,20 @@ def build_parser():
|
|
|
1044
1177
|
|
|
1045
1178
|
pr = sub.add_parser("rollup", help="ingest JSONL usage sinks into usage_event (idempotent)")
|
|
1046
1179
|
pr.add_argument("--global", dest="global_", action="store_true", help="also roll up the device-global log")
|
|
1180
|
+
pr.add_argument("--src", help="ingest this JSONL sink instead of the project sink")
|
|
1181
|
+
# Path and tag are SEPARATE flags on purpose. A single pre-joined
|
|
1182
|
+
# "<path>#<branch>#<created_at>" string is not a path, so the Git-Bash/MSYS argument
|
|
1183
|
+
# translation that rewrites POSIX paths into native Windows ones leaves it alone — the
|
|
1184
|
+
# key would then be spelled differently from the sink this python process resolves for
|
|
1185
|
+
# itself, and the rows became write-only: invisible to usage, unreachable by prune.
|
|
1186
|
+
pr.add_argument("--src-key-path", dest="src_key_path",
|
|
1187
|
+
help="DESTINATION project's events.jsonl path; canonicalized, then used as "
|
|
1188
|
+
"the cursor/dedupe key prefix so usage/prune group these rows with "
|
|
1189
|
+
"that project (default: the --src path itself)")
|
|
1190
|
+
pr.add_argument("--src-key-tag", dest="src_key_tag",
|
|
1191
|
+
help="lifecycle discriminator appended as '#<tag>', e.g. "
|
|
1192
|
+
"'<branch>#<created_at>' — makes the key unique per worktree "
|
|
1193
|
+
"lifecycle so a RECYCLED path cannot swallow the next lifecycle's lines")
|
|
1047
1194
|
pu = sub.add_parser("usage", help="print usage analytics from usage_event")
|
|
1048
1195
|
pu.add_argument("--global", dest="global_", action="store_true", help="read the device-global log instead of this project")
|
|
1049
1196
|
pu.add_argument("--json", action="store_true")
|
|
@@ -1055,6 +1202,79 @@ def build_parser():
|
|
|
1055
1202
|
ppr = sub.add_parser("prune", help="cap each JSONL sink to its last N lines (crash-safe; resets the mirror for that sink)")
|
|
1056
1203
|
ppr.add_argument("--keep", type=int, help="lines to keep (default 5000)")
|
|
1057
1204
|
ppr.add_argument("--global", dest="global_", action="store_true", help="also prune the device-global log")
|
|
1205
|
+
|
|
1206
|
+
pg = sub.add_parser("graph", help="graph executor: record/advance journal (flow-owned band 014+)")
|
|
1207
|
+
pgs = pg.add_subparsers(dest="graph_cmd", required=True)
|
|
1208
|
+
g1 = pgs.add_parser("run", help="create an execution; prints its id")
|
|
1209
|
+
g1.add_argument("--project"); g1.add_argument("--kind", required=True, choices=["auto_run", "planning", "card"])
|
|
1210
|
+
g1.add_argument("--topology-version", dest="topology_version", type=int, default=1)
|
|
1211
|
+
g1.add_argument("--topology-hash", dest="topology_hash", default="")
|
|
1212
|
+
g1.add_argument("--story", help="story id tracking handle (TEXT, e.g. C-001)")
|
|
1213
|
+
g2 = pgs.add_parser("record", help="record a step boundary: writes + checkpoint in ONE transaction; prints checkpoint id")
|
|
1214
|
+
g2.add_argument("--execution", required=True); g2.add_argument("--ns", default="")
|
|
1215
|
+
g2.add_argument("--node", required=True)
|
|
1216
|
+
g2.add_argument("--manifest", help="JSON evidence object (parsed + duplicate-key rejected); "
|
|
1217
|
+
"prefer the typed flags below so no caller concatenates JSON")
|
|
1218
|
+
g2.add_argument("--gate-exit", dest="gate_exit", type=int, help="gate verdict exit code")
|
|
1219
|
+
g2.add_argument("--gate-cmd", dest="gate_cmd", help="gate command that produced the verdict")
|
|
1220
|
+
g2.add_argument("--card-status", dest="card_status", help="card status token at record time")
|
|
1221
|
+
g2.add_argument("--worktree", help="worktree path (dispatch evidence)")
|
|
1222
|
+
g2.add_argument("--vendor", help="agent vendor (dispatch evidence)")
|
|
1223
|
+
g2.add_argument("--writes", help="JSON array of {task,channel,value} step writes")
|
|
1224
|
+
g2.add_argument("--parent", help="parent checkpoint id (fork chain, e.g. two-strikes attempt 2)")
|
|
1225
|
+
g2.add_argument("--source", default="loop", choices=["input", "loop", "update", "fork"])
|
|
1226
|
+
g2.add_argument("--interrupt", action="store_true", help="open an operator interrupt and pause (exit 3)")
|
|
1227
|
+
g2.add_argument("--prompt", help="interrupt prompt shown to the operator")
|
|
1228
|
+
g2.add_argument("--security-class", dest="security_class", action="store_true")
|
|
1229
|
+
g2.add_argument("--merge", action="store_true",
|
|
1230
|
+
help="record a merge boundary; the executor COMPUTES the ancestry proof "
|
|
1231
|
+
"(caller claims are ignored) and records card-abandon when unmerged")
|
|
1232
|
+
g2.add_argument("--branch", help="branch to prove merged (with --merge)")
|
|
1233
|
+
g2.add_argument("--base", help="merge base (default: origin/HEAD, else current branch)")
|
|
1234
|
+
g3 = pgs.add_parser("next", help="next node per topology+journal (exit 3 = complete/paused)")
|
|
1235
|
+
g3.add_argument("--execution", required=True); g3.add_argument("--ns", default="")
|
|
1236
|
+
g3.add_argument("--topology", help="explicit topology JSON path (tests/fixtures; default: "
|
|
1237
|
+
"the shipped pin-verified topology + hash policy)")
|
|
1238
|
+
g3.add_argument("--force-retopology", dest="force_retopology", action="store_true",
|
|
1239
|
+
help="after a skill upgrade changed the topology: fork the chain onto "
|
|
1240
|
+
"the current topology instead of refusing")
|
|
1241
|
+
g4 = pgs.add_parser("resume", help="resolve an open interrupt (guarded) and resume")
|
|
1242
|
+
g4.add_argument("--execution", required=True)
|
|
1243
|
+
g4.add_argument("--ns", default="", help="select the interrupt by namespace")
|
|
1244
|
+
g4.add_argument("--interrupt-id", dest="interrupt_id",
|
|
1245
|
+
help="select the exact interrupt (required when several are open)")
|
|
1246
|
+
g4.add_argument("--answer", help='JSON object with a non-empty string "reason"')
|
|
1247
|
+
g4.add_argument("--actor", help="resolving actor id (stored as audit evidence)")
|
|
1248
|
+
g4.add_argument("--target", help="closed-set target the DEBT line must name (C-NNN or NN-stage)")
|
|
1249
|
+
g5 = pgs.add_parser("status", help="execution + latest checkpoint per ns + open interrupts (JSON)")
|
|
1250
|
+
g5.add_argument("--execution", required=True)
|
|
1251
|
+
g6 = pgs.add_parser("abandon", help="terminal-state an execution (kill-at-gate, stale cleanup)")
|
|
1252
|
+
g6.add_argument("--execution", required=True); g6.add_argument("--outcome", help="e.g. killed")
|
|
1253
|
+
gf = pgs.add_parser("finish", help="mark an execution done (lane closed) so the next "
|
|
1254
|
+
"feature gets a fresh session and gc can reclaim")
|
|
1255
|
+
gf.add_argument("--execution", required=True); gf.add_argument("--outcome")
|
|
1256
|
+
gf.add_argument("--force", action="store_true", help="close despite open interrupts")
|
|
1257
|
+
g7 = pgs.add_parser("gc", help="purge terminal executions for ONE project (cascade), then "
|
|
1258
|
+
"--stale-days marks aged checkpoint-less running as abandoned "
|
|
1259
|
+
"(deleted on the NEXT gc, so doctor can surface them first)")
|
|
1260
|
+
g7.add_argument("--stale-days", dest="stale_days", type=int)
|
|
1261
|
+
g7.add_argument("--project", help="project scope (default: basename of FLOW_PROJECT_ROOT)")
|
|
1262
|
+
g9 = pgs.add_parser("cards", help="compile the card DAG: deps-met set + overlap-serialized "
|
|
1263
|
+
"ready set + blocked reasons (JSON); rejects dep cycles")
|
|
1264
|
+
g9.add_argument("--active-files", dest="active_files",
|
|
1265
|
+
help="space/comma list of allowed-file tokens already owned by live worktrees")
|
|
1266
|
+
pgs.add_parser("root", help="print the directory owning this project's durable state "
|
|
1267
|
+
"(worktree-translated; the one main-tree scope)")
|
|
1268
|
+
gse = pgs.add_parser("session", help="print the project's running auto_run execution, "
|
|
1269
|
+
"minting one atomically if absent (no pin file)")
|
|
1270
|
+
gse.add_argument("--project"); gse.add_argument("--no-create", dest="no_create",
|
|
1271
|
+
action="store_true",
|
|
1272
|
+
help="exit 3 instead of minting")
|
|
1273
|
+
gse.add_argument("--kind", choices=["auto_run", "planning"], default="auto_run",
|
|
1274
|
+
help="session lane: shipping (auto_run) or planning")
|
|
1275
|
+
g8 = pgs.add_parser("lint", help="validate a topology (default: the shipped one incl. pin)")
|
|
1276
|
+
g8.add_argument("--topology", help="explicit topology path (fixtures; skips the pin unless --pin)")
|
|
1277
|
+
g8.add_argument("--pin", help="explicit pin file to verify against")
|
|
1058
1278
|
return p
|
|
1059
1279
|
|
|
1060
1280
|
|
|
@@ -1078,6 +1298,10 @@ def main(argv):
|
|
|
1078
1298
|
"see harness/README.md for the full contract.\n"
|
|
1079
1299
|
)
|
|
1080
1300
|
raise
|
|
1301
|
+
if a.cmd == "graph" and getattr(a, "graph_cmd", "") == "lint":
|
|
1302
|
+
# lint touches no table: skip connect() so CI/linting never mints a DB
|
|
1303
|
+
# (or runs migrations) in the working tree.
|
|
1304
|
+
return G.cmd_graph_lint(None, a) or 0
|
|
1081
1305
|
con = _db.connect(db_path=a.db)
|
|
1082
1306
|
a._db_path = a.db or _db.default_db_path()
|
|
1083
1307
|
dispatch = {
|
|
@@ -1086,6 +1310,7 @@ def main(argv):
|
|
|
1086
1310
|
"intervention": cmd_intervention, "query": cmd_query,
|
|
1087
1311
|
"audit": cmd_audit, "propose": cmd_propose,
|
|
1088
1312
|
"rollup": cmd_rollup, "usage": cmd_usage, "prune": cmd_prune,
|
|
1313
|
+
"graph": cmd_graph,
|
|
1089
1314
|
}
|
|
1090
1315
|
try:
|
|
1091
1316
|
return dispatch[a.cmd](con, a) or 0
|