@manhquy/flow-skill 0.2.0 → 0.3.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 +30 -0
- package/README.md +17 -19
- package/README_VN.md +12 -13
- package/package.json +1 -1
- package/skills/flow/SKILL.md +1 -1
- package/skills/flow/eval/fixtures/fcdc/.flow/.gitkeep +0 -0
- package/skills/flow/eval/fixtures/fcdc/cards/C-001.md +39 -0
- package/skills/flow/eval/manifest.tsv +1 -0
- package/skills/flow/harness/graph_executor.py +141 -7
- package/skills/flow/references/auto-run.md +4 -1
- package/skills/flow/references/gate-eval.md +25 -7
- package/skills/flow/references/ground-truth-gates.md +11 -0
- package/skills/flow/runner/flow.sh +170 -14
- package/skills-manifest.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@manhquy/flow-skill`. Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning: [SemVer](https://semver.org/).
|
|
4
4
|
|
|
5
|
+
## [0.3.0] — 2026-08-11 — ships skill v0.26.0 (hollow-done trust floor)
|
|
6
|
+
|
|
7
|
+
Skill-content release; **installer code unchanged** vs 0.2.1. Ships skill product **v0.26.0**
|
|
8
|
+
(mechanical multi-signal done Evidence floor, ready/graph re-validate, fcdc residual fixture;
|
|
9
|
+
see root `CHANGELOG.md` `## 0.26.0`). Publishes to dist-tag **`latest`**.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- Bundled skill tree re-synced from monorepo `skills/flow/` (48 test suites green at ship).
|
|
13
|
+
- Install command remains **`npx @manhquy/flow-skill@latest`**.
|
|
14
|
+
|
|
15
|
+
## [0.2.1] — 2026-08-01 — docs sync (`@latest` canonical, `rc` retired)
|
|
16
|
+
|
|
17
|
+
Docs-only installer release. **No CLI, detection, lock, or target change** vs 0.2.0 — same
|
|
18
|
+
tarball behavior; this exists so npmjs.com renders the corrected README.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- Canonical install command is now **`npx @manhquy/flow-skill@latest`** everywhere. Bare
|
|
22
|
+
`npx @manhquy/flow-skill` can re-run a stale npx cache; `@latest` forces the newest release.
|
|
23
|
+
- Dropped the `@rc` channel from user-facing docs. The `rc` dist-tag had drifted to
|
|
24
|
+
`0.1.0-rc.3` (behind `latest`), so following `@rc` silently downgraded users. `rc` is
|
|
25
|
+
retired on the registry (`npm dist-tag rm @manhquy/flow-skill rc`); use `@next` for any
|
|
26
|
+
future pre-release.
|
|
27
|
+
- Clarified that `npm i` never installs the skill — the CLI must be **run**.
|
|
28
|
+
|
|
29
|
+
## [0.2.0] — 2026-07-27 — ships skill v0.25.0
|
|
30
|
+
|
|
31
|
+
Skill-content release; **installer code unchanged** vs 0.1.0. Ships skill product **v0.25.0**
|
|
32
|
+
(graph executor, opt-in and dark until `FLOW_GRAPH_EXECUTOR=1`; see root `CHANGELOG.md`
|
|
33
|
+
`## 0.25.0`). Publishes to dist-tag `latest`.
|
|
34
|
+
|
|
5
35
|
## [0.1.0] — 2026-07-19 — GA (first stable, `latest`)
|
|
6
36
|
|
|
7
37
|
First stable release of the installer. Graduates the rc.1→rc.3 line after two live OIDC
|
package/README.md
CHANGED
|
@@ -10,44 +10,42 @@ One-command installer that copies the [flow](https://github.com/manhquydev/flow-
|
|
|
10
10
|
## Install
|
|
11
11
|
|
|
12
12
|
```
|
|
13
|
-
#
|
|
14
|
-
npx @manhquy/flow-skill
|
|
15
|
-
|
|
16
|
-
# Pre-release channel (opt-in)
|
|
17
|
-
npx @manhquy/flow-skill@rc
|
|
13
|
+
# Newest GA — current: v0.3.0, ships skill v0.26.0
|
|
14
|
+
npx @manhquy/flow-skill@latest
|
|
18
15
|
```
|
|
19
16
|
|
|
20
17
|
An interactive prompt asks which agents to install to. Pick one or more, confirm, done.
|
|
21
18
|
|
|
22
|
-
> **
|
|
23
|
-
>
|
|
19
|
+
> **Always use `@latest`.** npx caches by bare name, so a plain `npx @manhquy/flow-skill` can
|
|
20
|
+
> re-run a stale cached copy; `@latest` forces npx to fetch the newest GA release. See
|
|
21
|
+
> [SECURITY.md](./SECURITY.md).
|
|
24
22
|
>
|
|
25
23
|
> **`npm i` alone is not enough.** `npm install @manhquy/flow-skill` only adds the installer
|
|
26
|
-
> package to `node_modules`. You must **run** it (`npx @manhquy/flow-skill` or
|
|
24
|
+
> package to `node_modules`. You must **run** it (`npx @manhquy/flow-skill@latest` or
|
|
27
25
|
> `npx flow-skill` after install) to copy the skill into agent homes.
|
|
28
26
|
>
|
|
29
|
-
> **Two version axes:** package `version` in this folder is the **installer** (e.g. `0.
|
|
30
|
-
> Skill product version lives in `skills/flow/SKILL.md` (`metadata.version`, e.g. `0.
|
|
27
|
+
> **Two version axes:** package `version` in this folder is the **installer** (e.g. `0.3.0`).
|
|
28
|
+
> Skill product version lives in `skills/flow/SKILL.md` (`metadata.version`, e.g. `0.26.0`).
|
|
31
29
|
> `--help` and the JSONL `plan` event expose both (`version` + `skillVersion`).
|
|
32
30
|
|
|
33
31
|
## Non-interactive
|
|
34
32
|
|
|
35
33
|
```
|
|
36
34
|
# Default selection (Claude + anything detected)
|
|
37
|
-
npx @manhquy/flow-skill@
|
|
35
|
+
npx @manhquy/flow-skill@latest --yes
|
|
38
36
|
|
|
39
37
|
# Explicit targets
|
|
40
|
-
npx @manhquy/flow-skill@
|
|
41
|
-
npx @manhquy/flow-skill@
|
|
38
|
+
npx @manhquy/flow-skill@latest --yes -t claude -t codex
|
|
39
|
+
npx @manhquy/flow-skill@latest --yes -t claude,codex # comma form OK
|
|
42
40
|
|
|
43
41
|
# Force all five targets even if not detected
|
|
44
|
-
npx @manhquy/flow-skill@
|
|
42
|
+
npx @manhquy/flow-skill@latest --yes --all
|
|
45
43
|
|
|
46
44
|
# Project scope (Claude only — see below)
|
|
47
|
-
npx @manhquy/flow-skill@
|
|
45
|
+
npx @manhquy/flow-skill@latest --yes --project --dir .
|
|
48
46
|
|
|
49
47
|
# CI-friendly JSONL output
|
|
50
|
-
npx @manhquy/flow-skill@
|
|
48
|
+
npx @manhquy/flow-skill@latest --yes --all --dry-run --json
|
|
51
49
|
```
|
|
52
50
|
|
|
53
51
|
## Targets
|
|
@@ -97,8 +95,8 @@ rm -rf <project>/.claude/skills/flow
|
|
|
97
95
|
|
|
98
96
|
- **Windows `EBUSY` / `EPERM` mid-install**: an agent (Claude Code, Codex, Antigravity IDE) is holding a file inside the destination. Close the agent and re-run. The installer already retries with 100/300/900 ms backoff before surfacing the error.
|
|
99
97
|
- **Stale advisory lock**: a prior run crashed. The next run detects the dead PID and reclaims the lock automatically. If it does not (very rare — the recorded PID was recycled by another live process), delete `<parent-of-dest>/.flow-skill.installing.lock`.
|
|
100
|
-
- **`No matching version found` on `@0.
|
|
101
|
-
- **Installed package but agent has no `/flow`**: `npm i` does not run the installer. Run `npx @manhquy/flow-skill@
|
|
98
|
+
- **`No matching version found` on `@0.26.0`**: that is the **skill product** version, not the npm package version. Install the installer by its own version (`@latest` / `@0.2.x`) — skill content version is printed by `--help` as `ships skill v…`.
|
|
99
|
+
- **Installed package but agent has no `/flow`**: `npm i` does not run the installer. Run `npx @manhquy/flow-skill@latest` (or `npx flow-skill` if already installed) and restart the agent.
|
|
102
100
|
- **Node too old** (`requires Node.js >=22.14.0`): upgrade with your preferred version manager (`nvm install 22`, `fnm install 22`, or Node's official installer). Node 20 reached end-of-life April 2026; npm OIDC Trusted Publishing needs npm >=11.5.1 which bundles with Node 22.14+.
|
|
103
101
|
|
|
104
102
|
## JSONL contract
|
|
@@ -106,7 +104,7 @@ rm -rf <project>/.claude/skills/flow
|
|
|
106
104
|
`--json` streams one JSON object per line:
|
|
107
105
|
|
|
108
106
|
```jsonl
|
|
109
|
-
{"event":"plan","version":"0.
|
|
107
|
+
{"event":"plan","version":"0.3.0","skillVersion":"0.26.0","dryRun":false,"scope":"global","targets":["claude","codex"]}
|
|
110
108
|
{"event":"install:start","target":"claude","dests":["~/.claude/skills/flow"]}
|
|
111
109
|
{"event":"install:done","target":"claude","dests":["~/.claude/skills/flow"],"result":"success","error":null,"warnings":[]}
|
|
112
110
|
{"event":"summary","success":true,"total":2,"attempted":2,"installed":2,"failed":0,"skipped":0,"aborted":false}
|
package/README_VN.md
CHANGED
|
@@ -10,37 +10,36 @@ Trình cài đặt một-lệnh sao chép skill [flow](https://github.com/manhqu
|
|
|
10
10
|
## Cài đặt
|
|
11
11
|
|
|
12
12
|
```
|
|
13
|
-
#
|
|
14
|
-
npx @manhquy/flow-skill
|
|
15
|
-
|
|
16
|
-
# Kênh pre-release (tùy chọn)
|
|
17
|
-
npx @manhquy/flow-skill@rc
|
|
13
|
+
# Newest GA — hiện tại: v0.3.0, ships skill v0.26.0
|
|
14
|
+
npx @manhquy/flow-skill@latest
|
|
18
15
|
```
|
|
19
16
|
|
|
20
17
|
Prompt tương tác hỏi cài vào agent nào. Chọn một hoặc nhiều, xác nhận, xong.
|
|
21
18
|
|
|
22
|
-
> **
|
|
19
|
+
> **Luôn dùng `@latest`.** npx cache theo bare name nên `npx @manhquy/flow-skill` có thể chạy
|
|
20
|
+
> bản cache cũ; `@latest` ép lấy GA mới nhất. Kênh `rc` đã retire — không dùng `@rc`.
|
|
23
21
|
>
|
|
24
|
-
> **`npm i` alone chưa đủ** — phải **chạy** CLI
|
|
22
|
+
> **`npm i` alone chưa đủ** — phải **chạy** CLI. Hai trục version: package npm vs skill product
|
|
23
|
+
> (`ships skill v…` / `skillVersion` trong JSONL). Xem [SECURITY.md](./SECURITY.md).
|
|
25
24
|
|
|
26
25
|
## Non-interactive
|
|
27
26
|
|
|
28
27
|
```
|
|
29
28
|
# Chọn mặc định (Claude + những agent detect được)
|
|
30
|
-
npx @manhquy/flow-skill@
|
|
29
|
+
npx @manhquy/flow-skill@latest --yes
|
|
31
30
|
|
|
32
31
|
# Target rõ ràng
|
|
33
|
-
npx @manhquy/flow-skill@
|
|
34
|
-
npx @manhquy/flow-skill@
|
|
32
|
+
npx @manhquy/flow-skill@latest --yes -t claude -t codex
|
|
33
|
+
npx @manhquy/flow-skill@latest --yes -t claude,codex # Dạng comma OK
|
|
35
34
|
|
|
36
35
|
# Ép cài cả 5 target dù không detect
|
|
37
|
-
npx @manhquy/flow-skill@
|
|
36
|
+
npx @manhquy/flow-skill@latest --yes --all
|
|
38
37
|
|
|
39
38
|
# Project scope (chỉ Claude — xem bên dưới)
|
|
40
|
-
npx @manhquy/flow-skill@
|
|
39
|
+
npx @manhquy/flow-skill@latest --yes --project --dir .
|
|
41
40
|
|
|
42
41
|
# JSONL cho CI
|
|
43
|
-
npx @manhquy/flow-skill@
|
|
42
|
+
npx @manhquy/flow-skill@latest --yes --all --dry-run --json
|
|
44
43
|
```
|
|
45
44
|
|
|
46
45
|
## Targets
|
package/package.json
CHANGED
package/skills/flow/SKILL.md
CHANGED
|
@@ -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.
|
|
11
|
+
version: "0.26.0"
|
|
12
12
|
attribution: "Methodology from ai20k-build-phase/buildflow (Tony, arealisticdreamer.com); harness/agent layers from repository-harness, claudekit-engineer, BMAD-METHOD. v0.22 concierge routing pattern adapted from BMAD-METHOD's bmad-help; forge-idea ritual adapted from BMAD-METHOD's bmad-forge-idea (both MIT, BMad Code LLC)."
|
|
13
13
|
---
|
|
14
14
|
|
|
File without changes
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# C-001 — Add debt-simplification nudge to the household expense app
|
|
2
|
+
|
|
3
|
+
status: done
|
|
4
|
+
deps: none
|
|
5
|
+
implements: FR3
|
|
6
|
+
|
|
7
|
+
## Scope
|
|
8
|
+
|
|
9
|
+
Compute the minimum set of repayment transactions across a household group (N members,
|
|
10
|
+
arbitrary unequal splits) and surface it as a single "who pays whom, how much" nudge on
|
|
11
|
+
the group's balance screen.
|
|
12
|
+
|
|
13
|
+
## Allowed files
|
|
14
|
+
|
|
15
|
+
src/balance/simplify.ts, src/balance/simplify.test.ts, src/screens/GroupBalance.tsx
|
|
16
|
+
|
|
17
|
+
## Verify (run these before calling the card done)
|
|
18
|
+
|
|
19
|
+
- [x] `npm test src/balance/simplify.test.ts` — all green in CI logs pasted below
|
|
20
|
+
- [x] Staging URL opened once by the author
|
|
21
|
+
- [x] PR approved by two teammates
|
|
22
|
+
|
|
23
|
+
## Done-evidence (world-state proof, named BEFORE building)
|
|
24
|
+
|
|
25
|
+
A live staging URL where a 4-person group's balance screen shows the simplified nudge
|
|
26
|
+
instead of a raw pairwise list.
|
|
27
|
+
|
|
28
|
+
## Evidence (paste the actual proof here when done)
|
|
29
|
+
|
|
30
|
+
- Staging URL: https://staging.householdsplit.app/g/demo-4person/balance
|
|
31
|
+
- CI / test paste:
|
|
32
|
+
```
|
|
33
|
+
PASS src/balance/simplify.test.ts
|
|
34
|
+
Tests: 14 passed, 14 total
|
|
35
|
+
```
|
|
36
|
+
- The pull request went through normal review, picked up two required approvals, and the
|
|
37
|
+
release notes list the nudge under "new features." The team is confident this covers
|
|
38
|
+
the card; no separate live user verification of the nudge math is attached beyond the
|
|
39
|
+
URL and CI paste above (process narrative coexists with multi-signal paste).
|
|
@@ -795,6 +795,132 @@ def _card_id(tok):
|
|
|
795
795
|
return "C-%03d" % int(tok.split("-")[1]) # C-1 == C-001
|
|
796
796
|
|
|
797
797
|
|
|
798
|
+
def _evidence_body_from_text(text):
|
|
799
|
+
"""Extract ## Evidence section body (mirrors flow.sh _evidence_body)."""
|
|
800
|
+
lines, in_ev = [], False
|
|
801
|
+
for line in text.splitlines():
|
|
802
|
+
s = line.replace("\r", "")
|
|
803
|
+
if s.startswith("## Evidence"):
|
|
804
|
+
in_ev = True
|
|
805
|
+
continue
|
|
806
|
+
if in_ev and s.startswith("## "):
|
|
807
|
+
break
|
|
808
|
+
if in_ev and s.strip():
|
|
809
|
+
lines.append(s)
|
|
810
|
+
return "\n".join(lines)
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def _evidence_is_empty(ev):
|
|
814
|
+
if not ev or not ev.strip():
|
|
815
|
+
return True
|
|
816
|
+
low = ev.lower().strip()
|
|
817
|
+
if "(empty until done)" in low or low in ("---", "--"):
|
|
818
|
+
return True
|
|
819
|
+
return False
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
_URL_HOST_RE = re.compile(r"https?://([^/\s]+)", re.IGNORECASE)
|
|
823
|
+
_DENY_HOSTS = {
|
|
824
|
+
"example.com", "www.example.com", "example.org", "www.example.org",
|
|
825
|
+
"localhost", "127.0.0.1", "0.0.0.0", "[::1]", "invalid", "test", "example",
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
def _normalize_url_host(host):
|
|
830
|
+
"""Lower, strip userinfo/port/trailing dots (mirrors flow.sh _evidence_url_host)."""
|
|
831
|
+
if not host:
|
|
832
|
+
return ""
|
|
833
|
+
h = host.lower()
|
|
834
|
+
if "@" in h:
|
|
835
|
+
h = h.rsplit("@", 1)[-1]
|
|
836
|
+
if h.startswith("["):
|
|
837
|
+
end = h.find("]")
|
|
838
|
+
if end != -1:
|
|
839
|
+
h = h[: end + 1]
|
|
840
|
+
else:
|
|
841
|
+
if h.count(":") == 1:
|
|
842
|
+
h = h.rsplit(":", 1)[0]
|
|
843
|
+
while h.endswith("."):
|
|
844
|
+
h = h[:-1]
|
|
845
|
+
return h
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
def _url_host_ok(host):
|
|
849
|
+
if not host:
|
|
850
|
+
return False
|
|
851
|
+
h = _normalize_url_host(host)
|
|
852
|
+
if h in _DENY_HOSTS:
|
|
853
|
+
return False
|
|
854
|
+
for suf in (".invalid", ".test", ".example", ".localhost",
|
|
855
|
+
".example.com", ".example.org"):
|
|
856
|
+
if h.endswith(suf) or h == suf.lstrip("."):
|
|
857
|
+
return False
|
|
858
|
+
return True
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def _evidence_signal_score(ev, root):
|
|
862
|
+
"""Multi-signal score ≥2 required for done trust (mirrors flow.sh table v2)."""
|
|
863
|
+
if not ev:
|
|
864
|
+
return 0
|
|
865
|
+
score = 0
|
|
866
|
+
low = ev.lower()
|
|
867
|
+
# Strip URLs before C-token matching (https://ok.dev must not award C).
|
|
868
|
+
ev_c = re.sub(r"https?://\S+", "", low)
|
|
869
|
+
m = _URL_HOST_RE.search(ev)
|
|
870
|
+
if m and _url_host_ok(m.group(1)):
|
|
871
|
+
score += 1
|
|
872
|
+
if re.search(r"curl|(^|\s)\$\s|exit 0|exit code|->\s*200|http/[0-9]", low):
|
|
873
|
+
score += 1
|
|
874
|
+
if re.search(r"(^|[^a-z])(pass|passed|ok)([^a-z]|$)", ev_c):
|
|
875
|
+
score += 1
|
|
876
|
+
else:
|
|
877
|
+
# fence awards C only when body has positive tokens
|
|
878
|
+
fence = False
|
|
879
|
+
for line in ev.splitlines():
|
|
880
|
+
if line.strip().startswith("```"):
|
|
881
|
+
fence = not fence
|
|
882
|
+
continue
|
|
883
|
+
if fence and re.search(r"(^|[^a-z])(pass|passed|ok)([^a-z]|$)", line.lower()):
|
|
884
|
+
score += 1
|
|
885
|
+
break
|
|
886
|
+
for p in re.findall(r"[A-Za-z0-9_./-]+\.(?:png|jpg|jpeg|webp|gif|log|txt)", ev):
|
|
887
|
+
if p.startswith("/") or p.startswith("~"):
|
|
888
|
+
continue
|
|
889
|
+
if os.path.isfile(os.path.join(root, p)) or os.path.isfile(p):
|
|
890
|
+
score += 1
|
|
891
|
+
break
|
|
892
|
+
if re.search(r"rows?=[0-9]+|select\s+[a-z_*]|sqlite|inserted\s+[a-z0-9]", low):
|
|
893
|
+
score += 1
|
|
894
|
+
if re.search(r"~/\.claude/skills|~/\.codex/skills|~/\.agents/skills|skills/flow", ev):
|
|
895
|
+
score += 1
|
|
896
|
+
return score
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
def _card_done_evidence_ok(path, root):
|
|
900
|
+
"""status=done + multi-signal Evidence + no unchecked Verify + no [FILL]
|
|
901
|
+
(parity with flow.sh _card_done_evidence_ok / cmd_check done branch)."""
|
|
902
|
+
try:
|
|
903
|
+
with open(path, encoding="utf-8", errors="replace") as fh:
|
|
904
|
+
text = fh.read()
|
|
905
|
+
except OSError:
|
|
906
|
+
return False
|
|
907
|
+
status = ""
|
|
908
|
+
for line in text.splitlines():
|
|
909
|
+
if line.startswith("status:"):
|
|
910
|
+
status = line.split(":", 1)[1].strip().split()[0] if line.split(":", 1)[1].strip() else ""
|
|
911
|
+
break
|
|
912
|
+
if status != "done":
|
|
913
|
+
return False
|
|
914
|
+
if "[FILL" in text:
|
|
915
|
+
return False
|
|
916
|
+
if re.search(r"^\s*- \[ \]", text, re.M):
|
|
917
|
+
return False
|
|
918
|
+
ev = _evidence_body_from_text(text)
|
|
919
|
+
if _evidence_is_empty(ev):
|
|
920
|
+
return False
|
|
921
|
+
return _evidence_signal_score(ev, root) >= 2
|
|
922
|
+
|
|
923
|
+
|
|
798
924
|
def _read_cards(cards_dir):
|
|
799
925
|
"""Parse the REAL card format (no YAML frontmatter): a `deps:` body line whose
|
|
800
926
|
free text is scraped for C-NNN ids (mirrors flow.sh:1310-1319) and a
|
|
@@ -808,8 +934,9 @@ def _read_cards(cards_dir):
|
|
|
808
934
|
if not m:
|
|
809
935
|
continue
|
|
810
936
|
cid = _card_id(m.group(1))
|
|
937
|
+
path = os.path.join(cards_dir, name)
|
|
811
938
|
status, deps, files, in_allowed, fill = "", [], set(), False, False
|
|
812
|
-
with open(
|
|
939
|
+
with open(path, encoding="utf-8", errors="replace") as fh:
|
|
813
940
|
for line in fh:
|
|
814
941
|
s = line.rstrip("\n")
|
|
815
942
|
if s.startswith("## Allowed files"):
|
|
@@ -837,7 +964,7 @@ def _read_cards(cards_dir):
|
|
|
837
964
|
# A self-dep is kept (not silently dropped): cmd_ready blocks such a card, so the
|
|
838
965
|
# compiler must too - dropping it would advertise an undispatchable card as ready.
|
|
839
966
|
cards[cid] = {"id": cid, "status": status, "deps": sorted(set(deps)),
|
|
840
|
-
"files": sorted(files), "fill": fill}
|
|
967
|
+
"files": sorted(files), "fill": fill, "path": path}
|
|
841
968
|
return cards
|
|
842
969
|
|
|
843
970
|
|
|
@@ -865,10 +992,11 @@ def _dep_cycles(cards):
|
|
|
865
992
|
|
|
866
993
|
|
|
867
994
|
def compile_cards(root, active_files=()):
|
|
868
|
-
"""Card DAG + ready-set. deps-met mirrors cmd_ready
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
995
|
+
"""Card DAG + ready-set. deps-met mirrors cmd_ready: each dep is status=done AND
|
|
996
|
+
Evidence passes the multi-signal world-state floor (hollow hand-edit cannot unblock).
|
|
997
|
+
overlap serialization is NEW deliberate behavior (legacy `ready` only advises):
|
|
998
|
+
cards sharing an allowed-file token, or overlapping a currently active worktree's
|
|
999
|
+
tokens, are held back so two agents never edit one file."""
|
|
872
1000
|
cards = _read_cards(os.path.join(root, "cards"))
|
|
873
1001
|
cycles = _dep_cycles(cards)
|
|
874
1002
|
# Only `todo` is buildable - cmd_ready's exact semantics (an invalid/blank status is
|
|
@@ -877,7 +1005,13 @@ def compile_cards(root, active_files=()):
|
|
|
877
1005
|
in_cycle = {n for cyc in cycles for n in cyc}
|
|
878
1006
|
deps_met, blocked = [], {}
|
|
879
1007
|
for c in sorted(todo, key=lambda x: x["id"]):
|
|
880
|
-
missing = [
|
|
1008
|
+
missing = []
|
|
1009
|
+
for d in c["deps"]:
|
|
1010
|
+
dep = cards.get(d, {})
|
|
1011
|
+
if dep.get("status") != "done":
|
|
1012
|
+
missing.append(d)
|
|
1013
|
+
elif not _card_done_evidence_ok(dep.get("path") or "", root):
|
|
1014
|
+
missing.append(d)
|
|
881
1015
|
if c["id"] in in_cycle:
|
|
882
1016
|
blocked[c["id"]] = {"reason": "dep cycle",
|
|
883
1017
|
"cycle": next("->".join(x) for x in cycles if c["id"] in x)}
|
|
@@ -39,7 +39,10 @@ for each todo card in card-number order:
|
|
|
39
39
|
4. flow.sh check C-NNN must PASS (mechanical) + gate-rules semantic check
|
|
40
40
|
[records: card-review, with the gate exit as the evidence]
|
|
41
41
|
5. merge to main in card order; deploy; VERIFY ON LIVE URL (merge != shipped)
|
|
42
|
-
6. paste world-state evidence into the card;
|
|
42
|
+
6. paste world-state evidence into the card; prefer **`/flow card done C-NNN`** (CLI-owned
|
|
43
|
+
flip + evidence multi-signal floor). Hand-edit `status: done` is allowed to coexist but
|
|
44
|
+
**must** re-run `flow.sh check C-NNN` before dependents are trusted — `ready`/graph
|
|
45
|
+
deps re-validate Evidence (hollow process prose cannot unblock parallel).
|
|
43
46
|
[records: card-verify-live, via '/flow card done C-NNN' (evidence gate = the done boundary)]
|
|
44
47
|
7. flow.sh harness story complete --id … --proof-source manual (or card_markdown_gate) + trace; log AUTO-LOG.md
|
|
45
48
|
8. remove worktree: /flow workspace remove card/C-NNN
|
|
@@ -25,14 +25,32 @@ rule text is enforceable by *a* judge," not "this build's self-review caught eve
|
|
|
25
25
|
|
|
26
26
|
## Fixtures and thresholds
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
Seven shipped fixtures (`skills/flow/eval/fixtures/`, manifest `skills/flow/eval/manifest.tsv`):
|
|
29
29
|
sound/hollow pairs for Stage 01 (Research — fabricated quotes, vague "users"), Stage 02 (Scope —
|
|
30
|
-
grade laundering, a real C quietly graded B), and the card gate
|
|
30
|
+
grade laundering, a real C quietly graded B), and the card gate:
|
|
31
|
+
- **fcda** — sound multi-signal Evidence (mechanical PASS, semantic PASS)
|
|
32
|
+
- **fcdb** — process-only hollow (mechanical **FAIL** after hollow-done floor; still listed for
|
|
33
|
+
historical FLAG text / offline inspection)
|
|
34
|
+
- **fcdc** — hollow-with-decoy multi-signal (mechanical PASS, semantic **FLAG** — the residual
|
|
35
|
+
attack class the mechanical floor does not claim to close)
|
|
36
|
+
|
|
31
37
|
Fixture content maps 1:1 to the failure modes already documented in `gate-rules.md`, not an
|
|
32
38
|
invented taxonomy. The v1 input set is **manifest-listed shipped fixtures only** — `eval` never
|
|
33
39
|
accepts a caller-supplied artifact path; widening that is a v2 change that reopens security
|
|
34
40
|
review (an unvetted artifact reaching an LLM judge is a different trust boundary).
|
|
35
41
|
|
|
42
|
+
### Scorecard policy (CI vs offline)
|
|
43
|
+
|
|
44
|
+
| Suite | Where | Blocks merge? |
|
|
45
|
+
|-------|-------|---------------|
|
|
46
|
+
| Mechanical done-evidence + auto-path + ready re-validate | `tests/run_all.sh` / GHA | **Yes** |
|
|
47
|
+
| Fixture structure / manifest paths | CI shell tests | **Yes** |
|
|
48
|
+
| LLM semantic `flow.sh eval` batch | Operator offline (billable) | **No** by default |
|
|
49
|
+
| Offline `eval --report` drift | Local | Advisory |
|
|
50
|
+
|
|
51
|
+
CI-measurable process-only catch rate: process-only fixtures must exit 1 from `flow.sh check`.
|
|
52
|
+
Billable LLM catch-rate is never a default CI gate (see `flow.sh` eval header).
|
|
53
|
+
|
|
36
54
|
Per fixture, N=3 judge calls (`--n`), majority vote: **healthy = hollow fixtures flagged at
|
|
37
55
|
≥2/3 majority, sound fixtures passed at majority**. More than 1/3 of a fixture's runs coming
|
|
38
56
|
back unparseable/timed-out makes that fixture's result `UNRELIABLE` (an infra failure, reported
|
|
@@ -43,14 +61,14 @@ separately from a real mismatch — never silently counted as a gate pass or fai
|
|
|
43
61
|
`eval` is **opt-in and billable** — it makes real `claude -p` API calls. Zero cost when the
|
|
44
62
|
`claude` CLI isn't on `PATH` (clean skip, exit 0). When present, exactly one minimal probe call
|
|
45
63
|
is made to confirm the CLI runs headless before any real judging starts; a probe failure means
|
|
46
|
-
one billable call was made, then a clean skip. A full default batch is
|
|
47
|
-
judge calls + the 1 probe =
|
|
64
|
+
one billable call was made, then a clean skip. A full default batch is 7 fixtures × N=3 = 21
|
|
65
|
+
judge calls + the 1 probe = 22 calls.
|
|
48
66
|
|
|
49
67
|
**Measured real cost** (this machine, 2026-07-10, default/unforced model, no `--bare` available
|
|
50
68
|
under an OAuth/subscription session — see the build's spike notes): roughly **$0.30–0.37 per
|
|
51
69
|
call**, driven almost entirely by this repo's large global `CLAUDE.md` + skill/agent/MCP
|
|
52
70
|
declarations loading as system-prompt context on every call (~50–60K cache-creation tokens),
|
|
53
|
-
not by the judging prompt itself. A full
|
|
71
|
+
not by the judging prompt itself. A full 22-call baseline batch is real money, order **$6–7**
|
|
54
72
|
on this setup — not the trivial "a token or two" a casual reader might assume from "opt-in".
|
|
55
73
|
Forcing a cheaper model (`--model haiku` on the underlying CLI, not currently plumbed as a flow
|
|
56
74
|
flag) cuts this roughly 5x, but changes what the eval measures (a cheaper, possibly weaker judge)
|
|
@@ -128,7 +146,7 @@ yet (the same "measure, don't assume" discipline that gated this build in the fi
|
|
|
128
146
|
platform: a signal arriving while the runner is blocked on a foreground judge call was not
|
|
129
147
|
observed to preempt that call promptly. Cleanup between fixtures/calls fires reliably; a
|
|
130
148
|
normal (uninterrupted) run always cleans up its temp files immediately.
|
|
131
|
-
- **Small fixture corpus (
|
|
149
|
+
- **Small fixture corpus (seven)**: enough to prove the mechanism and give a real number, not
|
|
132
150
|
enough for statistical confidence across every stage/failure-mode combination. Widening the
|
|
133
151
|
corpus is a natural v1.x follow-up, not a v1 blocker.
|
|
134
152
|
|
|
@@ -157,7 +175,7 @@ window; the mechanism was never confirmed at the time (rate-limit vs hook conten
|
|
|
157
175
|
aborts the batch with a distinct nonzero exit code (2) and NO `done` trailer written — the
|
|
158
176
|
junk batch is invisible to `--report`/drift, so it cannot poison the baseline. `--keep-going`
|
|
159
177
|
forces the full batch; document worst case (≤ N_fixtures × n × 2 + 1 probe ≈ 37 calls at
|
|
160
|
-
default
|
|
178
|
+
default 7 × 3).
|
|
161
179
|
|
|
162
180
|
### Rate-limit visibility (advisory, best-effort)
|
|
163
181
|
|
|
@@ -14,6 +14,8 @@ and used only to add color, never to pass a gate.
|
|
|
14
14
|
| Story proof | `flow.sh harness story verify <id>` -> `verify_command` exit 0 | "I wrote tests" |
|
|
15
15
|
| Build merged-safe | review green (adversarial) + `flow.sh check` pass | "the diff is small" |
|
|
16
16
|
| Card DONE | deploy ran + **live URL verified as a user** (world-state evidence pasted) | "merged" / "deploy succeeded" / "tests pass" |
|
|
17
|
+
| Card DONE (mechanical floor) | `flow.sh check` / `card done`: `## Evidence` multi-signal score ≥2 (URL + curl/command/test-log/path/DB/skill categories; denylist example.com etc.) | process-only prose (PR approvals, CI green, release notes alone); empty/placeholder |
|
|
18
|
+
| Dep ready / parallel | `ready` + graph deps-met: dep `status=done` **and** same Evidence multi-signal floor | hand-edit `status: done` with hollow Evidence |
|
|
17
19
|
| Contract not drifted | contract-test card asserts every endpoint exists in live `/openapi.json` with matching shape | "backend and UI agree" (they don't, silently) |
|
|
18
20
|
|
|
19
21
|
## Rules
|
|
@@ -26,6 +28,15 @@ and used only to add color, never to pass a gate.
|
|
|
26
28
|
4. **A red signal stops the run.** No "probably fine" override. Fix or open debt.
|
|
27
29
|
5. **Capture the proof.** Paste the real curl/URL/exit into the card `## Evidence` and the
|
|
28
30
|
harness `trace` — so the next session sees ground truth, not a claim.
|
|
31
|
+
6. **Mechanical multi-signal is a floor, not a ceiling.** A decoy URL + genuine positive
|
|
32
|
+
tokens (e.g. staging host + `PASS` log) can still score ≥2; that residual is for the
|
|
33
|
+
**offline semantic** layer (`flow.sh eval` / fcdc), never claimed closed by regex alone.
|
|
34
|
+
C-tokens are **not** matched inside URLs; `fail`/`failed` do not award C; denylist hosts
|
|
35
|
+
ignore `:port` spoofing. Durable `card_markdown_gate` only records that the markdown floor
|
|
36
|
+
passed — it is not stronger than that floor.
|
|
37
|
+
7. **Hand-edit `status: done` is not trusted for deps** until Evidence **and** Verify boxes
|
|
38
|
+
pass the same floor (`ready` re-validates Evidence + unchecked Verify + `[FILL]`; graph
|
|
39
|
+
executor mirrors).
|
|
29
40
|
|
|
30
41
|
## Bug-fix cards: prove the test was tied to the bug
|
|
31
42
|
When a card's job is fixing a bug/regression (not new behavior), a passing test is not yet
|
|
@@ -1171,8 +1171,17 @@ cmd_card_done() { # CLI-owned flip to done, gated by the SAME done-rules as '/fl
|
|
|
1171
1171
|
if [ -z "$file" ] || [ ! -f "$file" ]; then echo "FAIL: card not found for '$arg' (looked for ${file:-?})"; return 1; fi
|
|
1172
1172
|
local id orig; id="$(basename "$file" .md)"; FLOW_LOG_CARD="$id"; orig="$(card_status "$file")"
|
|
1173
1173
|
if [ -z "$orig" ]; then echo "FAIL: $id has no 'status:' line — run '/flow check $id' to see what's missing."; return 1; fi
|
|
1174
|
-
|
|
1174
|
+
# On INT/TERM after flip-to-done: restore todo and abort (fail-closed). Never continue
|
|
1175
|
+
# into durable complete after a partial restore.
|
|
1176
|
+
# shellcheck disable=SC2064
|
|
1177
|
+
trap '_set_card_status "'"$file"'" "todo" 2>/dev/null || true; trap - INT TERM; exit 130' INT TERM
|
|
1178
|
+
if ! _set_card_status "$file" done; then
|
|
1179
|
+
trap - INT TERM
|
|
1180
|
+
echo "FAIL: could not write status for $id"
|
|
1181
|
+
return 1
|
|
1182
|
+
fi
|
|
1175
1183
|
if cmd_check "$id"; then # cmd_check enforces evidence/verify AND records the durable done-trace
|
|
1184
|
+
trap - INT TERM
|
|
1176
1185
|
_inflight_clear "$id"
|
|
1177
1186
|
# card-verify-live: the done boundary is the EVIDENCE gate passing, not the merge
|
|
1178
1187
|
# (merge != shipped) - dependents unblock on card status, matching cmd_ready.
|
|
@@ -1180,9 +1189,11 @@ cmd_card_done() { # CLI-owned flip to done, gated by the SAME done-rules as '/fl
|
|
|
1180
1189
|
_graph_record "$id" card-verify-live --gate-exit 0 --card-status done
|
|
1181
1190
|
return 0
|
|
1182
1191
|
fi
|
|
1183
|
-
|
|
1192
|
+
trap - INT TERM
|
|
1193
|
+
# Always force todo on gate fail (even if orig was hollow done) — never leave hollow done.
|
|
1194
|
+
_set_card_status "$file" todo
|
|
1184
1195
|
echo
|
|
1185
|
-
echo "REVERTED: $id left at status '
|
|
1196
|
+
echo "REVERTED: $id left at status 'todo' — the done-gate above must pass first."
|
|
1186
1197
|
return 1
|
|
1187
1198
|
}
|
|
1188
1199
|
|
|
@@ -1226,6 +1237,131 @@ cmd_card() {
|
|
|
1226
1237
|
return 0
|
|
1227
1238
|
}
|
|
1228
1239
|
|
|
1240
|
+
# ---- done-evidence world-state signal (hollow-done trust floor; plan 260811) ----
|
|
1241
|
+
# When status=done, ## Evidence must score ≥2 distinct categories (A–F). Process-only
|
|
1242
|
+
# prose (PR/CI/release notes) without that floor fails. Decoy multi-signal may still
|
|
1243
|
+
# pass mechanically — residual for offline semantic eval (fcdc). Pure bash, no python.
|
|
1244
|
+
|
|
1245
|
+
_evidence_body() { # $1=card file → Evidence section body (non-empty lines)
|
|
1246
|
+
awk '/^## Evidence/{f=1; next} f && /^## /{f=0} f{print}' "$1" | tr -d '\r' | sed '/^[[:space:]]*$/d'
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
_evidence_is_empty() { # $1=evidence body; return 0 if empty/placeholder
|
|
1250
|
+
local ev="$1" ev_lc
|
|
1251
|
+
[ -z "$ev" ] && return 0
|
|
1252
|
+
ev_lc="$(printf '%s' "$ev" | tr '[:upper:]' '[:lower:]')"
|
|
1253
|
+
case "$ev_lc" in *"(empty until done)"*|"---"|"--") return 0 ;; esac
|
|
1254
|
+
return 1
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
# Host from first http(s) URL; empty if none. Normalize: lower, strip userinfo/port/trailing dots.
|
|
1258
|
+
_evidence_url_host() {
|
|
1259
|
+
local ev="$1" u host
|
|
1260
|
+
u="$(printf '%s\n' "$ev" | grep -oiE 'https?://[^[:space:]/]+' | head -1 | tr '[:upper:]' '[:lower:]')"
|
|
1261
|
+
[ -z "$u" ] && return 0
|
|
1262
|
+
host="${u#http://}"; host="${host#https://}"; host="${host%%/*}"
|
|
1263
|
+
# userinfo@host
|
|
1264
|
+
case "$host" in *@*) host="${host##*@}" ;; esac
|
|
1265
|
+
# [ipv6] or [ipv6]:port
|
|
1266
|
+
case "$host" in
|
|
1267
|
+
\[*\])
|
|
1268
|
+
host="$(printf '%s' "$host" | sed 's/^\(\[[^]]*\]\).*/\1/')"
|
|
1269
|
+
;;
|
|
1270
|
+
*)
|
|
1271
|
+
# hostname:port (single colon) — leave bare IPv6 alone (multiple colons)
|
|
1272
|
+
case "$host" in
|
|
1273
|
+
*:*)
|
|
1274
|
+
# if exactly one colon, treat as port
|
|
1275
|
+
case "${host#*:}" in *:*) : ;; *) host="${host%%:*}" ;; esac
|
|
1276
|
+
;;
|
|
1277
|
+
esac
|
|
1278
|
+
;;
|
|
1279
|
+
esac
|
|
1280
|
+
# trailing dots
|
|
1281
|
+
while [ "${host%.}" != "$host" ]; do host="${host%.}"; done
|
|
1282
|
+
printf '%s' "$host"
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
_evidence_url_host_ok() { # $1=host; 0=counts for category A
|
|
1286
|
+
local h="$1"
|
|
1287
|
+
[ -z "$h" ] && return 1
|
|
1288
|
+
h="$(printf '%s' "$h" | tr '[:upper:]' '[:lower:]')"
|
|
1289
|
+
case "$h" in
|
|
1290
|
+
example.com|www.example.com|example.org|www.example.org) return 1 ;;
|
|
1291
|
+
*.example.com|*.example.org) return 1 ;;
|
|
1292
|
+
localhost|127.0.0.1|0.0.0.0|'[::1]') return 1 ;; # need B/C — no A alone
|
|
1293
|
+
*.invalid|*.test|*.example|*.localhost) return 1 ;;
|
|
1294
|
+
esac
|
|
1295
|
+
case "$h" in invalid|test|example) return 1 ;; esac
|
|
1296
|
+
return 0
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
# Echo integer score 0..6 for distinct categories hit. $1=evidence $2=project root for path exists.
|
|
1300
|
+
_evidence_signal_score() {
|
|
1301
|
+
local ev="$1" root="${2:-.}"
|
|
1302
|
+
local score=0 ev_lc host ev_c
|
|
1303
|
+
ev_lc="$(printf '%s' "$ev" | tr '[:upper:]' '[:lower:]')"
|
|
1304
|
+
# Strip URLs before C-token matching so https://ok.dev/x does not award C.
|
|
1305
|
+
ev_c="$(printf '%s' "$ev_lc" | sed -E 's|https?://[^[:space:]]+||g')"
|
|
1306
|
+
|
|
1307
|
+
# A: non-denylist URL
|
|
1308
|
+
host="$(_evidence_url_host "$ev")"
|
|
1309
|
+
if _evidence_url_host_ok "$host"; then score=$((score + 1)); fi
|
|
1310
|
+
|
|
1311
|
+
# B: command / curl / exit / HTTP status / line-leading $ prompt
|
|
1312
|
+
if printf '%s' "$ev_lc" | grep -qE 'curl|(^|[[:space:]])\$[[:space:]]|exit 0|exit code|->[[:space:]]*200|http/[0-9]'; then
|
|
1313
|
+
score=$((score + 1))
|
|
1314
|
+
fi
|
|
1315
|
+
|
|
1316
|
+
# C: positive test-log tokens only (not fail/failed); never match inside stripped URLs
|
|
1317
|
+
if printf '%s' "$ev_c" | grep -qE '([^a-z]|^)(pass|passed|ok)([^a-z]|$)'; then
|
|
1318
|
+
score=$((score + 1))
|
|
1319
|
+
elif printf '%s' "$ev" | awk '
|
|
1320
|
+
/^[[:space:]]*```/ {fence=!fence; next}
|
|
1321
|
+
fence {
|
|
1322
|
+
low=tolower($0)
|
|
1323
|
+
if (low ~ /(^|[^a-z])(pass|passed|ok)([^a-z]|$)/) { print "yes"; exit }
|
|
1324
|
+
}
|
|
1325
|
+
' | grep -q yes; then
|
|
1326
|
+
# fence awards C only when body contains positive tokens (not bare 2-line blob)
|
|
1327
|
+
score=$((score + 1))
|
|
1328
|
+
fi
|
|
1329
|
+
|
|
1330
|
+
# D: repo-relative media/log path that exists
|
|
1331
|
+
local p
|
|
1332
|
+
for p in $(printf '%s\n' "$ev" | grep -oE '[A-Za-z0-9_./-]+\.(png|jpg|jpeg|webp|gif|log|txt)' | head -20); do
|
|
1333
|
+
case "$p" in /*|~*) continue ;; esac
|
|
1334
|
+
if [ -f "$root/$p" ] || [ -f "$p" ]; then score=$((score + 1)); break; fi
|
|
1335
|
+
done
|
|
1336
|
+
|
|
1337
|
+
# E: DB-ish row proof (no bare id=1; need rows=N / SELECT / sqlite / inserted <id>)
|
|
1338
|
+
if printf '%s' "$ev_lc" | grep -qE 'rows?=[0-9]+|select[[:space:]]+[a-z_*]|sqlite|inserted[[:space:]]+[a-z0-9]'; then
|
|
1339
|
+
score=$((score + 1))
|
|
1340
|
+
fi
|
|
1341
|
+
|
|
1342
|
+
# F: skill install path
|
|
1343
|
+
if printf '%s' "$ev" | grep -qE '~/.claude/skills|~/.codex/skills|~/.agents/skills|skills/flow'; then
|
|
1344
|
+
score=$((score + 1))
|
|
1345
|
+
fi
|
|
1346
|
+
|
|
1347
|
+
printf '%s' "$score"
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
# Done dep trust: status=done + multi-signal Evidence + no unchecked Verify + no [FILL].
|
|
1351
|
+
# Mirrors the done branch of cmd_check (ready/graph must not be weaker).
|
|
1352
|
+
_card_done_evidence_ok() { # $1=card file
|
|
1353
|
+
local file="$1" st ev score root
|
|
1354
|
+
st="$(card_status "$file")"
|
|
1355
|
+
[ "$st" = "done" ] || return 1
|
|
1356
|
+
if grep -q '\[FILL' "$file" 2>/dev/null; then return 1; fi
|
|
1357
|
+
if grep -qE '^[[:space:]]*- \[ \]' "$file" 2>/dev/null; then return 1; fi
|
|
1358
|
+
ev="$(_evidence_body "$file")"
|
|
1359
|
+
if _evidence_is_empty "$ev"; then return 1; fi
|
|
1360
|
+
root="${FLOW_PROJECT_ROOT:-$PWD}"
|
|
1361
|
+
score="$(_evidence_signal_score "$ev" "$root")"
|
|
1362
|
+
[ "$score" -ge 2 ]
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1229
1365
|
cmd_check() {
|
|
1230
1366
|
local arg="${1:-}"
|
|
1231
1367
|
if [ -z "$arg" ]; then echo "usage: /flow check C-NNN"; return 1; fi
|
|
@@ -1270,7 +1406,7 @@ cmd_check() {
|
|
|
1270
1406
|
fi
|
|
1271
1407
|
done
|
|
1272
1408
|
|
|
1273
|
-
# 5) if done: all Verify boxes checked AND Evidence is real world-state
|
|
1409
|
+
# 5) if done: all Verify boxes checked AND Evidence is real world-state (multi-signal floor)
|
|
1274
1410
|
if [ "$st" = "done" ]; then
|
|
1275
1411
|
local unchecked; unchecked="$(grep -nE '^[[:space:]]*- \[ \]' "$file" 2>/dev/null || true)"
|
|
1276
1412
|
if [ -n "$unchecked" ]; then
|
|
@@ -1279,16 +1415,20 @@ cmd_check() {
|
|
|
1279
1415
|
found=1
|
|
1280
1416
|
fi
|
|
1281
1417
|
# Evidence body: lines between the '## Evidence' header and the next '## ' heading (or EOF).
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
ev="$(awk '/^## Evidence/{f=1; next} f && /^## /{f=0} f{print}' "$file" | tr -d '\r' | sed '/^[[:space:]]*$/d')"
|
|
1286
|
-
ev_lc="$(printf '%s' "$ev" | tr '[:upper:]' '[:lower:]')"
|
|
1287
|
-
[ -z "$ev" ] && empty_ev=1
|
|
1288
|
-
case "$ev_lc" in *"(empty until done)"*|"---"|"--") empty_ev=1 ;; esac
|
|
1418
|
+
local ev empty_ev=0 score=0
|
|
1419
|
+
ev="$(_evidence_body "$file")"
|
|
1420
|
+
if _evidence_is_empty "$ev"; then empty_ev=1; fi
|
|
1289
1421
|
if [ "$empty_ev" -eq 1 ]; then
|
|
1290
1422
|
echo " [x] status is 'done' but ## Evidence is empty (paste world-state proof: URL/curl/DB row)"
|
|
1291
1423
|
found=1
|
|
1424
|
+
else
|
|
1425
|
+
score="$(_evidence_signal_score "$ev" "${FLOW_PROJECT_ROOT:-$PWD}")"
|
|
1426
|
+
if [ "$score" -lt 2 ]; then
|
|
1427
|
+
echo " [x] status is 'done' but ## Evidence has no world-state signal for type '$(get_project_type)' (score $score/2)"
|
|
1428
|
+
echo " (need ≥2 of: URL/curl-or-command/test-log/existing-path/DB-row/skill-path — not only PR/CI/process prose)"
|
|
1429
|
+
echo " hint: done means: $(done_def_for_type "$(get_project_type)")"
|
|
1430
|
+
found=1
|
|
1431
|
+
fi
|
|
1292
1432
|
fi
|
|
1293
1433
|
fi
|
|
1294
1434
|
|
|
@@ -1350,6 +1490,16 @@ cmd_project_type() {
|
|
|
1350
1490
|
fi
|
|
1351
1491
|
case "$arg" in
|
|
1352
1492
|
web|cli|library|skill)
|
|
1493
|
+
# After planning is complete AND an explicit type file exists, refuse silent
|
|
1494
|
+
# type flips unless FLOW_FORCE=1 (D6). Default-web with no file is not "locked".
|
|
1495
|
+
if planning_complete 2>/dev/null && [ -f "$PROJECT_TYPE_FILE" ]; then
|
|
1496
|
+
local cur; cur="$(get_project_type)"
|
|
1497
|
+
if [ -n "$cur" ] && [ "$cur" != "$arg" ] && [ "${FLOW_FORCE:-0}" != "1" ]; then
|
|
1498
|
+
echo "FAIL: project type is locked to '$cur' after planning completes (would change done-evidence lens)."
|
|
1499
|
+
echo " re-set with FLOW_FORCE=1 if intentional, and record a DEBT.md line for the change."
|
|
1500
|
+
return 1
|
|
1501
|
+
fi
|
|
1502
|
+
fi
|
|
1353
1503
|
printf '%s\n' "$arg" > "$PROJECT_TYPE_FILE"; _ignore_run_state
|
|
1354
1504
|
echo "PASS: project type set to '$arg'."
|
|
1355
1505
|
echo " done-evidence now means: $(done_def_for_type "$arg")"
|
|
@@ -1475,19 +1625,25 @@ cmd_ready() {
|
|
|
1475
1625
|
[ "$st" = "todo" ] || continue
|
|
1476
1626
|
deps="$(grep -m1 -E '^deps:' "$f" | sed 's/^deps:[[:space:]]*//' | tr -d '\r')"
|
|
1477
1627
|
ok=1
|
|
1478
|
-
# deps met if every referenced card is status done
|
|
1628
|
+
# deps met if every referenced card is status done AND Evidence passes world-state
|
|
1629
|
+
# signal floor (hand-edit done without multi-signal cannot unblock dependents).
|
|
1479
1630
|
for dep in $(printf '%s' "$deps" | grep -oiE 'C-[0-9]+' || true); do
|
|
1480
1631
|
local depnum depfile
|
|
1481
1632
|
depnum="${dep#C-}"; depnum="${depnum#c-}"
|
|
1482
1633
|
case "$depnum" in (*[!0-9]*) continue;; esac
|
|
1483
1634
|
depfile="$(printf '%s/C-%03d.md' "$CARDS_DIR" "$((10#$depnum))")"
|
|
1484
|
-
if [ ! -f "$depfile" ] || [ "$(card_status "$depfile")" != "done" ]; then
|
|
1635
|
+
if [ ! -f "$depfile" ] || [ "$(card_status "$depfile")" != "done" ]; then
|
|
1636
|
+
ok=0
|
|
1637
|
+
elif ! _card_done_evidence_ok "$depfile"; then
|
|
1638
|
+
ok=0
|
|
1639
|
+
echo " note: dep $(basename "$depfile" .md) is status=done but ## Evidence fails world-state signal (re-check or paste real proof)"
|
|
1640
|
+
fi
|
|
1485
1641
|
done
|
|
1486
1642
|
if [ "$ok" -eq 1 ]; then
|
|
1487
1643
|
echo " BUILDABLE $id (deps: ${deps:-none})"
|
|
1488
1644
|
_card_allowed_files "$f" | sed 's/^/ allowed: /'
|
|
1489
1645
|
else
|
|
1490
|
-
echo " blocked $id (deps
|
|
1646
|
+
echo " blocked $id (deps unmet or evidence floor failed: ${deps:-none})"
|
|
1491
1647
|
fi
|
|
1492
1648
|
done
|
|
1493
1649
|
return 0
|
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-
|
|
4
|
+
"fileCount": 84,
|
|
5
|
+
"syncedAt": "2026-08-11T06:03:43.844Z",
|
|
6
6
|
"fileList": [
|
|
7
7
|
"SKILL.md",
|
|
8
8
|
"_templates/00-idea.md",
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
"eval/fixtures/fcda/cards/C-001.md",
|
|
23
23
|
"eval/fixtures/fcdb/.flow/.gitkeep",
|
|
24
24
|
"eval/fixtures/fcdb/cards/C-001.md",
|
|
25
|
+
"eval/fixtures/fcdc/.flow/.gitkeep",
|
|
26
|
+
"eval/fixtures/fcdc/cards/C-001.md",
|
|
25
27
|
"eval/fixtures/routing/manifest.tsv",
|
|
26
28
|
"eval/fixtures/routing/states/state-any-midproject.txt",
|
|
27
29
|
"eval/fixtures/routing/states/state-cards-todo.txt",
|