@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 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
- # Stable (latest) — current: v0.1.0, ships skill v0.24.0
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
- > **Stable is on `latest`**: bare `npx @manhquy/flow-skill` (or `@0.1.x`) resolves the GA release.
23
- > Pin `@rc` only if you want the pre-release channel. See [SECURITY.md](./SECURITY.md).
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.1.0`).
30
- > Skill product version lives in `skills/flow/SKILL.md` (`metadata.version`, e.g. `0.24.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@rc --yes
35
+ npx @manhquy/flow-skill@latest --yes
38
36
 
39
37
  # Explicit targets
40
- npx @manhquy/flow-skill@rc --yes -t claude -t codex
41
- npx @manhquy/flow-skill@rc --yes -t claude,codex # comma form OK
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@rc --yes --all
42
+ npx @manhquy/flow-skill@latest --yes --all
45
43
 
46
44
  # Project scope (Claude only — see below)
47
- npx @manhquy/flow-skill@rc --yes --project --dir .
45
+ npx @manhquy/flow-skill@latest --yes --project --dir .
48
46
 
49
47
  # CI-friendly JSONL output
50
- npx @manhquy/flow-skill@rc --yes --all --dry-run --json
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.24.0`**: that is the **skill product** version, not the npm package version. Install the installer by its own version (bare / `@0.1.x` / `@rc`) — skill content version is printed by `--help` as `ships skill v…`.
101
- - **Installed package but agent has no `/flow`**: `npm i` does not run the installer. Run `npx @manhquy/flow-skill@rc` (or `npx flow-skill` if already installed) and restart the agent.
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.1.0","skillVersion":"0.24.0","dryRun":false,"scope":"global","targets":["claude","codex"]}
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
- # Stable (latest) — hiện tại: v0.1.0, ships skill v0.24.0
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
- > **Stable ở `latest`**: bare `npx @manhquy/flow-skill` (hoặc `@0.1.x`) lấy bản GA. Pin `@rc` nếu muốn kênh pre-release. Xem [SECURITY.md](./SECURITY.md).
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 (`npx @manhquy/flow-skill`). Hai trục version: package npm vs skill product (`ships skill v…` / `skillVersion` trong JSONL).
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@rc --yes
29
+ npx @manhquy/flow-skill@latest --yes
31
30
 
32
31
  # Target rõ ràng
33
- npx @manhquy/flow-skill@rc --yes -t claude -t codex
34
- npx @manhquy/flow-skill@rc --yes -t claude,codex # Dạng comma OK
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@rc --yes --all
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@rc --yes --project --dir .
39
+ npx @manhquy/flow-skill@latest --yes --project --dir .
41
40
 
42
41
  # JSONL cho CI
43
- npx @manhquy/flow-skill@rc --yes --all --dry-run --json
42
+ npx @manhquy/flow-skill@latest --yes --all --dry-run --json
44
43
  ```
45
44
 
46
45
  ## Targets
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manhquy/flow-skill",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Install the flow skill (buildflow gated build process) into your coding agent(s) via one npx command",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,7 +8,7 @@ keywords: [flow, buildflow, gate, build, ship, scope, prd, contract, card, deplo
8
8
  license: MIT
9
9
  metadata:
10
10
  author: flow-skill
11
- version: "0.25.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).
@@ -5,3 +5,4 @@ f02a 02-scope flow/02-scope.md PASS
5
5
  f02b 02-scope flow/02-scope.md FLAG
6
6
  fcda card cards/C-001.md PASS
7
7
  fcdb card cards/C-001.md FLAG
8
+ fcdc card cards/C-001.md FLAG
@@ -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(os.path.join(cards_dir, name), encoding="utf-8", errors="replace") as fh:
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 (status of each dep == done);
869
- overlap serialization is NEW deliberate behavior (legacy `ready` only advises,
870
- flow.sh:1302): cards sharing an allowed-file token, or overlapping a currently
871
- active worktree's tokens, are held back so two agents never edit one file."""
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 = [d for d in c["deps"] if cards.get(d, {}).get("status") != "done"]
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; status: done
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
- Six shipped fixtures (`skills/flow/eval/fixtures/`, manifest `skills/flow/eval/manifest.tsv`):
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 ("merge ≈ shipped" evidence).
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 6 fixtures × N=3 = 18
47
- judge calls + the 1 probe = 19 calls.
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 19-call baseline batch is real money, order **$6–7**
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 (six)**: enough to prove the mechanism and give a real number, not
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 6 × 3).
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
- if ! _set_card_status "$file" done; then echo "FAIL: could not write status for $id"; return 1; fi
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
- _set_card_status "$file" "${orig:-todo}" # NOT done — never leave a hollow 'done'
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 '${orig:-todo}' — the done-gate above must pass first."
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
- # Bounded awk avoids counting a stray later '## Evidence' as content. tr consumes all stdin
1283
- # (no SIGPIPE); case-glob instead of 'grep -q' which closes the pipe early.
1284
- local ev ev_lc empty_ev=0
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 (normalize C-1 / C-001 to canonical path)
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 ok=0; fi
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 not all done: ${deps:-none})"
1646
+ echo " blocked $id (deps unmet or evidence floor failed: ${deps:-none})"
1491
1647
  fi
1492
1648
  done
1493
1649
  return 0
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "source": "../skills/flow",
3
3
  "destRelative": "skills/flow",
4
- "fileCount": 82,
5
- "syncedAt": "2026-07-27T03:21:41.774Z",
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",