@techierathore/techieflow 1.0.2 → 1.0.4
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/.claude/commands/TechieFlow/agents/analyst.md +5 -13
- package/.claude/commands/TechieFlow/agents/architect.md +1 -16
- package/.claude/commands/TechieFlow/agents/flow-master.md +42 -215
- package/.claude/commands/TechieFlow/agents/verifier.md +24 -64
- package/.claude/commands/TechieFlow/tasks/_metrics-emit-gate.md +24 -117
- package/.claude/commands/TechieFlow/tasks/_owner-language.md +26 -0
- package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +20 -84
- package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +10 -74
- package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +20 -58
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +21 -143
- package/.claude/commands/TechieFlow/tasks/build-phase.md +14 -224
- package/.claude/commands/TechieFlow/tasks/create-deep-research-prompt.md +4 -0
- package/.claude/commands/TechieFlow/tasks/create-doc.md +4 -0
- package/.claude/commands/TechieFlow/tasks/create-project-brief.md +22 -0
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +31 -505
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +28 -213
- package/.claude/commands/TechieFlow/tasks/deploy-checklist.md +19 -0
- package/.claude/commands/TechieFlow/tasks/devguide.md +10 -219
- package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +4 -0
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +10 -129
- package/.claude/commands/TechieFlow/tasks/generate-html.md +5 -1
- package/.claude/commands/TechieFlow/tasks/handoff-phase.md +18 -16
- package/.claude/commands/TechieFlow/tasks/log-miss.md +19 -158
- package/.claude/commands/TechieFlow/tasks/metrics-report.md +3 -1
- package/.claude/commands/TechieFlow/tasks/mockups.md +12 -117
- package/.claude/commands/TechieFlow/tasks/refresh-status.md +9 -130
- package/.claude/commands/TechieFlow/tasks/split-brd.md +10 -126
- package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +14 -0
- package/.claude/commands/TechieFlow/tasks/triage-issues.md +11 -152
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +36 -572
- package/.opencode/plugin/techieflow.js +40 -13
- package/.tfcore/agents/analyst.md +5 -13
- package/.tfcore/agents/architect.md +1 -16
- package/.tfcore/agents/flow-master.md +42 -215
- package/.tfcore/agents/verifier.md +24 -64
- package/.tfcore/core-config.yaml +10 -0
- package/.tfcore/hooks/block-git.sh +4 -4
- package/.tfcore/hooks/guard-artifacts.sh +2 -2
- package/.tfcore/hooks/guard-build.sh +76 -0
- package/.tfcore/hooks/guard-db.sh +90 -0
- package/.tfcore/hooks/guard-metrics.sh +68 -0
- package/.tfcore/hooks/guard-status-html.sh +116 -71
- package/.tfcore/hooks/guard-status.sh +43 -16
- package/.tfcore/hooks/guard-verify-deps.sh +124 -0
- package/.tfcore/hooks/guard-verify.sh +70 -48
- package/.tfcore/hooks/sweep-artifacts.sh +2 -2
- package/.tfcore/install-manifest.yaml +0 -18
- package/.tfcore/routing.yaml +86 -14
- package/.tfcore/standards/coding-standards-core.md +61 -0
- package/.tfcore/standards/coding-standards-dotnet.md +93 -0
- package/.tfcore/tasks/_metrics-emit-gate.md +24 -117
- package/.tfcore/tasks/_owner-language.md +26 -0
- package/.tfcore/tasks/_smoke-test-policy.md +20 -84
- package/.tfcore/tasks/_status-update-gate.md +10 -74
- package/.tfcore/tasks/_yolo-mode.md +20 -58
- package/.tfcore/tasks/amend-docs.md +21 -143
- package/.tfcore/tasks/build-phase.md +14 -224
- package/.tfcore/tasks/create-deep-research-prompt.md +4 -0
- package/.tfcore/tasks/create-doc.md +4 -0
- package/.tfcore/tasks/create-project-brief.md +22 -0
- package/.tfcore/tasks/day1-brownfield.md +31 -505
- package/.tfcore/tasks/day1-greenfield.md +28 -213
- package/.tfcore/tasks/deploy-checklist.md +19 -0
- package/.tfcore/tasks/devguide.md +10 -219
- package/.tfcore/tasks/facilitate-brainstorming-session.md +4 -0
- package/.tfcore/tasks/fix-issues.md +10 -129
- package/.tfcore/tasks/generate-html.md +5 -1
- package/.tfcore/tasks/handoff-phase.md +18 -16
- package/.tfcore/tasks/log-miss.md +19 -158
- package/.tfcore/tasks/metrics-report.md +3 -1
- package/.tfcore/tasks/mockups.md +12 -117
- package/.tfcore/tasks/refresh-status.md +9 -130
- package/.tfcore/tasks/split-brd.md +10 -126
- package/.tfcore/tasks/triage-and-fix.md +14 -0
- package/.tfcore/tasks/triage-issues.md +11 -152
- package/.tfcore/tasks/verify-phase.md +36 -572
- package/.tfcore/telemetry/SCHEMA.md +100 -8
- package/.tfcore/telemetry/tf-metrics.sh +564 -32
- package/.tfcore/templates/stack-defaults/dotnet.md +35 -0
- package/.tfcore/templates/stack-questions.md +77 -0
- package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +3 -4
- package/.tfcore/templates/v4custom/app-architecture-tmpl.md +107 -115
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +108 -184
- package/.tfcore/templates/v4custom/app-brief-tmpl.md +65 -0
- package/.tfcore/templates/v4custom/app-checklist-tmpl.md +48 -73
- package/.tfcore/templates/v4custom/app-claude-md-tmpl.md +1 -1
- package/.tfcore/templates/v4custom/app-coding-standards-tmpl.md +52 -0
- package/.tfcore/templates/v4custom/app-decision-request-tmpl.md +85 -0
- package/.tfcore/templates/v4custom/app-deployment-checklist-tmpl.md +86 -0
- package/.tfcore/templates/v4custom/app-devguide-tmpl.md +69 -146
- package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +86 -31
- package/.tfcore/templates/v4custom/app-phases-tmpl.md +35 -0
- package/.tfcore/templates/v4custom/app-productguide-tmpl.md +38 -35
- package/.tfcore/templates/v4custom/app-project-status-tmpl.md +63 -73
- package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +62 -37
- package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +69 -54
- package/.tfcore/templates/v4custom/build-invocation-ladder.md +17 -180
- package/.tfcore/templates/v4custom/build-subagent-prompt.md +17 -0
- package/.tfcore/templates/v4custom/html-render-shell.md +1 -1
- package/.tfcore/templates/v4custom/metrics-report-template.md +2 -2
- package/.tfcore/user-guide.md +0 -23
- package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-codex-bind.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-codex-telemetry.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-perf-grade.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
- package/.tfcore/utils/tf-brd-status.py +176 -0
- package/.tfcore/utils/tf-brd-status.sh +19 -0
- package/.tfcore/utils/tf-build-list.py +211 -0
- package/.tfcore/utils/tf-build-list.sh +21 -0
- package/.tfcore/utils/tf-build.sh +143 -0
- package/.tfcore/utils/tf-cdp-relay.ps1 +64 -0
- package/.tfcore/utils/tf-checklist-edit.py +133 -0
- package/.tfcore/utils/tf-day1-files.py +208 -0
- package/.tfcore/utils/tf-day1-files.sh +20 -0
- package/.tfcore/utils/tf-devguide-list.py +249 -0
- package/.tfcore/utils/tf-devguide-list.sh +18 -0
- package/.tfcore/utils/tf-doc-check.py +1369 -0
- package/.tfcore/utils/tf-doc-check.sh +29 -0
- package/.tfcore/utils/tf-emit.sh +545 -18
- package/.tfcore/utils/tf-fix-close.sh +115 -0
- package/.tfcore/utils/tf-gitignore-audit.sh +50 -3
- package/.tfcore/utils/tf-goal.sh +284 -66
- package/.tfcore/utils/tf-harness.sh +4 -9
- package/.tfcore/utils/tf-log-miss.py +197 -0
- package/.tfcore/utils/tf-log-miss.sh +14 -0
- package/.tfcore/utils/tf-misses-md.py +208 -0
- package/.tfcore/utils/tf-misses-md.sh +14 -0
- package/.tfcore/utils/tf-mockups-locate.py +66 -0
- package/.tfcore/utils/tf-mockups-locate.sh +18 -0
- package/.tfcore/utils/tf-model-pick.sh +475 -0
- package/.tfcore/utils/tf-opencode-audit.py +23 -7
- package/.tfcore/utils/tf-perf-grade.py +97 -0
- package/.tfcore/utils/tf-perf-grade.sh +12 -0
- package/.tfcore/utils/tf-phase.sh +92 -0
- package/.tfcore/utils/tf-render-html.py +20 -0
- package/.tfcore/utils/tf-routing-bind.sh +97 -6
- package/.tfcore/utils/tf-routing.sh +13 -7
- package/.tfcore/utils/tf-selfcheck.sh +323 -0
- package/.tfcore/utils/tf-split-brd.py +309 -0
- package/.tfcore/utils/tf-split-brd.sh +22 -0
- package/.tfcore/utils/tf-status-evidence.sh +68 -0
- package/.tfcore/utils/tf-status-facts.py +358 -0
- package/.tfcore/utils/tf-status-facts.sh +20 -0
- package/.tfcore/utils/tf-triage.py +243 -0
- package/.tfcore/utils/tf-triage.sh +16 -0
- package/.tfcore/utils/tf-verify-boot.sh +250 -0
- package/.tfcore/utils/tf-verify-emit.sh +110 -0
- package/.tfcore/utils/tf-verify-env.sh +88 -0
- package/.tfcore/utils/tf-verify-list.py +270 -0
- package/.tfcore/utils/tf-verify-list.sh +13 -0
- package/.tfcore/utils/tf-verify-screens.mjs +355 -0
- package/.tfcore/utils/tf-verify-screens.sh +31 -0
- package/.tfcore/utils/tf-verify-tests.sh +166 -0
- package/.tfcore/utils/tf-verify-verdict.py +292 -0
- package/.tfcore/utils/tf-verify-verdict.sh +15 -0
- package/.tfcore/utils/tf-yolo.sh +59 -0
- package/README.md +120 -1145
- package/docs/TechieFlow-Installation.md +1 -5
- package/opencode.jsonc +37 -33
- package/package.json +1 -5
- package/scripts/install.mjs +56 -46
- package/.claude/commands/TechieFlow/tasks/advanced-elicitation.md +0 -117
- package/.claude/commands/TechieFlow/tasks/author-brd.md +0 -79
- package/.claude/commands/TechieFlow/tasks/document-project.md +0 -343
- package/.claude/commands/TechieFlow/tasks/execute-checklist.md +0 -86
- package/.claude/commands/TechieFlow/tasks/index-docs.md +0 -173
- package/.claude/commands/TechieFlow/tasks/kb-mode-interaction.md +0 -75
- package/.claude/commands/TechieFlow/tasks/shard-doc.md +0 -185
- package/.codex/config.toml +0 -46
- package/.codex/hooks.json +0 -63
- package/.codex/rules/techieflow.rules +0 -8
- package/.tfcore/hooks/codex-adapter.py +0 -248
- package/.tfcore/tasks/advanced-elicitation.md +0 -117
- package/.tfcore/tasks/author-brd.md +0 -79
- package/.tfcore/tasks/document-project.md +0 -343
- package/.tfcore/tasks/execute-checklist.md +0 -86
- package/.tfcore/tasks/index-docs.md +0 -173
- package/.tfcore/tasks/kb-mode-interaction.md +0 -75
- package/.tfcore/tasks/shard-doc.md +0 -185
- package/.tfcore/templates/architecture-tmpl.yaml +0 -650
- package/.tfcore/templates/brownfield-architecture-tmpl.yaml +0 -476
- package/.tfcore/templates/competitor-analysis-tmpl.yaml +0 -306
- package/.tfcore/templates/front-end-architecture-tmpl.yaml +0 -218
- package/.tfcore/templates/fullstack-architecture-tmpl.yaml +0 -823
- package/.tfcore/templates/market-research-tmpl.yaml +0 -252
- package/.tfcore/templates/project-brief-tmpl.yaml +0 -221
- package/.tfcore/utils/techieflow-doc-template.md +0 -325
- package/.tfcore/utils/tf-codex-bind.py +0 -196
- package/.tfcore/utils/tf-codex-telemetry.py +0 -60
- package/WORKFLOW.html +0 -1986
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tf-phase.sh — mark which TechieFlow command is running (Sitting 4b, 2026-09-05).
|
|
3
|
+
#
|
|
4
|
+
# bash .tfcore/utils/tf-phase.sh start <command> [App] # step 0 of every task: prints the start
|
|
5
|
+
# # time and writes the marker
|
|
6
|
+
# bash .tfcore/utils/tf-phase.sh show # prints the marker, or "none"
|
|
7
|
+
# bash .tfcore/utils/tf-phase.sh end # removes the marker
|
|
8
|
+
# bash .tfcore/utils/tf-phase.sh goal [App] # tf-goal.sh only: an unclaimed marker the first
|
|
9
|
+
# # command's `start` claims, keeping its started
|
|
10
|
+
#
|
|
11
|
+
# The marker is .tfcore/.session/phase.json:
|
|
12
|
+
# {"cmd":"build-phase","app":"MyApp","started":"2026-09-05T16:19:38Z"}
|
|
13
|
+
# Who reads it:
|
|
14
|
+
# - .tfcore/hooks/guard-db.sh: a migration runner is allowed only while the marker says
|
|
15
|
+
# build-phase or fix-issues (MISS-TechieFlow-20260905-09: day-1 patched a database).
|
|
16
|
+
# - tf-emit.sh: a run record that leaves `started` out gets it from here, so the start
|
|
17
|
+
# time is a measurement, never reconstructed.
|
|
18
|
+
# A marker older than 24 h is ignored by the readers (a killed session must not keep a
|
|
19
|
+
# grant alive). The next `start` overwrites; `end` is optional tidiness.
|
|
20
|
+
# Never committed: .tfcore/.session/ is git-ignored. Exit 0 always except a usage error (2).
|
|
21
|
+
set -u
|
|
22
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
23
|
+
DIR="$ROOT/.tfcore/.session"
|
|
24
|
+
FILE="$DIR/phase.json"
|
|
25
|
+
case "${1:-}" in
|
|
26
|
+
start)
|
|
27
|
+
CMD="${2:-}"
|
|
28
|
+
[[ -z "$CMD" ]] && { echo "tf-phase: usage: tf-phase.sh start <command> [App]" >&2; exit 2; }
|
|
29
|
+
CMD="${CMD#\*}"
|
|
30
|
+
APP="${3:-}"
|
|
31
|
+
NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
32
|
+
mkdir -p "$DIR"
|
|
33
|
+
# A supervisor (tf-goal.sh) writes an unclaimed marker {"cmd":"goal",…} when its first cycle
|
|
34
|
+
# starts. The first command of that run claims it and KEEPS its started: the run began when
|
|
35
|
+
# the harness began reading, not when the agent got round to step 0 (MISS-TechieFlow-20260905-20:
|
|
36
|
+
# a 38-minute stage 2 recorded a 27-second run because step 0 ran last).
|
|
37
|
+
if [[ -f "$FILE" ]] && grep -q '"cmd":"goal"' "$FILE" && [[ -z "$(find "$FILE" -mmin +1440 2>/dev/null)" ]]; then
|
|
38
|
+
NOW="$(sed -n 's/.*"started":"\([^"]*\)".*/\1/p' "$FILE")"
|
|
39
|
+
echo "tf-phase: started taken from the supervisor's marker ($NOW)" >&2
|
|
40
|
+
fi
|
|
41
|
+
printf '{"cmd":"%s","app":"%s","started":"%s"}\n' "$CMD" "$APP" "$NOW" > "$FILE"
|
|
42
|
+
echo "$NOW"
|
|
43
|
+
echo "tf-phase: $CMD${APP:+ $APP} started $NOW (marker .tfcore/.session/phase.json)" >&2
|
|
44
|
+
# The document-check baseline (Sitting 4c, 2026-09-06): the findings the checklists and the
|
|
45
|
+
# status file already carry when the command starts are recorded, so the gate and the Stop
|
|
46
|
+
# hook block only on findings this command introduces (Schemas §7.1 decision 8: old projects
|
|
47
|
+
# are repaired through *amend-docs, never by whichever command happens to run next).
|
|
48
|
+
if [[ -x "$ROOT/.tfcore/utils/tf-doc-check.sh" || -f "$ROOT/.tfcore/utils/tf-doc-check.sh" ]]; then
|
|
49
|
+
docs=(); for f in "$ROOT"/docs/*-Checklist.md "$ROOT"/PROJECT-STATUS.md; do [[ -f "$f" ]] && docs+=("$f"); done
|
|
50
|
+
if [[ ${#docs[@]} -gt 0 ]]; then
|
|
51
|
+
( cd "$ROOT" && bash .tfcore/utils/tf-doc-check.sh --root "$ROOT" --baseline-write "${docs[@]}" 2>/dev/null | tail -1 >&2 ) || true
|
|
52
|
+
fi
|
|
53
|
+
fi
|
|
54
|
+
# The framework's own scripts, against THIS project's real files, before the command
|
|
55
|
+
# does anything (2026-09-09). Nine framework defects reached TfLens by being met
|
|
56
|
+
# mid-build, and each cost a switch to the framework repo and back. Step 0 is the one
|
|
57
|
+
# place every command already passes through, so the check lives here and no task file
|
|
58
|
+
# has to mention it -- including commands added later.
|
|
59
|
+
#
|
|
60
|
+
# It NEVER blocks: a framework defect is reported and the command carries on, exactly
|
|
61
|
+
# as a library gap does. Set TF_SKIP_SELFCHECK=1 to skip it (the self-tests do).
|
|
62
|
+
if [[ "${TF_SKIP_SELFCHECK:-0}" != "1" && -f "$ROOT/.tfcore/utils/tf-selfcheck.sh" ]]; then
|
|
63
|
+
sc="$(cd "$ROOT" && bash .tfcore/utils/tf-selfcheck.sh 2>/dev/null)" || true
|
|
64
|
+
if grep -q '^FAIL' <<<"$sc"; then
|
|
65
|
+
echo "tf-phase: the framework is broken on this project — log these in the feedback file and carry on:" >&2
|
|
66
|
+
grep '^FAIL' <<<"$sc" >&2
|
|
67
|
+
echo "tf-phase: entries to paste: bash .tfcore/utils/tf-selfcheck.sh --report" >&2
|
|
68
|
+
else
|
|
69
|
+
echo "tf-phase: framework self-check clean" >&2
|
|
70
|
+
fi
|
|
71
|
+
fi
|
|
72
|
+
;;
|
|
73
|
+
goal)
|
|
74
|
+
# written by tf-goal.sh at the start of a run's first cycle; claimed by the first `start`
|
|
75
|
+
NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
76
|
+
mkdir -p "$DIR"
|
|
77
|
+
printf '{"cmd":"goal","app":"%s","started":"%s"}\n' "${2:-}" "$NOW" > "$FILE"
|
|
78
|
+
echo "$NOW"
|
|
79
|
+
;;
|
|
80
|
+
show)
|
|
81
|
+
if [[ -f "$FILE" ]]; then cat "$FILE"; else echo "none"; fi
|
|
82
|
+
;;
|
|
83
|
+
end)
|
|
84
|
+
rm -f "$FILE"
|
|
85
|
+
echo "tf-phase: marker removed"
|
|
86
|
+
;;
|
|
87
|
+
*)
|
|
88
|
+
sed -n '2,20p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
|
|
89
|
+
exit 2
|
|
90
|
+
;;
|
|
91
|
+
esac
|
|
92
|
+
exit 0
|
|
@@ -479,6 +479,22 @@ def is_requirements_checklist(base, raw):
|
|
|
479
479
|
or "SINGLE SOURCE OF TRUTH" in head)
|
|
480
480
|
|
|
481
481
|
|
|
482
|
+
def is_miss_list(base, raw):
|
|
483
|
+
"""Is this THE derived miss log, or an ordinary document about misses?
|
|
484
|
+
|
|
485
|
+
`docs/{AppName}-Misses.md` is rebuilt from `misses.jsonl` on every write and is
|
|
486
|
+
read by agents and by the owner in markdown. Rendering it produced an HTML copy
|
|
487
|
+
nobody opened, re-rendered on every miss, in every project — 20 of them across
|
|
488
|
+
the estate before this refusal existed (owner, 2026-09-08).
|
|
489
|
+
|
|
490
|
+
Identify the document, do not guess from the suffix, exactly as the checklist
|
|
491
|
+
rule does: the name must match AND the content must carry the generator's own
|
|
492
|
+
Source row. A hand-written note called `Release-Misses.md` has neither."""
|
|
493
|
+
if not re.search(r"-Misses\.md$", base, re.I):
|
|
494
|
+
return False
|
|
495
|
+
return "Rewritten by `tf-misses-md.sh`" in raw[:2000]
|
|
496
|
+
|
|
497
|
+
|
|
482
498
|
NEXT_CMD_H2 = re.compile(r"^##\s+Next command to run\s*$", re.I)
|
|
483
499
|
|
|
484
500
|
|
|
@@ -541,6 +557,10 @@ def render(md_path, css, head_js, body_js, out_dir=None):
|
|
|
541
557
|
raise Refused("%s is the requirements checklist — checklists are AI-agent "
|
|
542
558
|
"working documents and are NEVER rendered to HTML "
|
|
543
559
|
"(html-render-shell §0)." % base)
|
|
560
|
+
if is_miss_list(base, raw_probe):
|
|
561
|
+
raise Refused("%s is the derived miss log — it is read in markdown by agents "
|
|
562
|
+
"and by the owner, and is NEVER rendered to HTML "
|
|
563
|
+
"(html-render-shell §0)." % base)
|
|
544
564
|
|
|
545
565
|
raw = raw_probe
|
|
546
566
|
lines = raw.split("\n")
|
|
@@ -16,9 +16,6 @@
|
|
|
16
16
|
# model-only agent entries merge, command
|
|
17
17
|
# entries must be complete → they carry
|
|
18
18
|
# template+description+model)
|
|
19
|
-
# Codex:
|
|
20
|
-
# .codex/agents/*.toml + .agents/skills/techieflow-*/SKILL.md, generated
|
|
21
|
-
# by tf-codex-bind.py. Subagent model/effort is pinned when routing is on;
|
|
22
19
|
# skills remain thin loaders and do not claim to switch the main thread.
|
|
23
20
|
#
|
|
24
21
|
# When `enabled: false` (the default) every generated artifact is REMOVED, using
|
|
@@ -44,6 +41,63 @@ root = sys.argv[1]
|
|
|
44
41
|
ryaml = os.path.join(root, ".tfcore", "routing.yaml")
|
|
45
42
|
manifest_path = os.path.join(root, ".tfcore", ".session", "routing-bind.manifest")
|
|
46
43
|
|
|
44
|
+
# --- migration: a project's routing.yaml is never overwritten -------------
|
|
45
|
+
# `update-framework.sh` leaves a project's own routing.yaml alone on purpose, so a block added to
|
|
46
|
+
# the framework default after the project was scaffolded would never reach it. Both delivery
|
|
47
|
+
# routes run THIS script, so the migration lives here: a missing block is APPENDED, with the
|
|
48
|
+
# Claude aliases filled in (opus/sonnet/haiku exist on every account) and the OpenCode lines left
|
|
49
|
+
# empty, because guessing a model id for a provider this machine may not have would produce a
|
|
50
|
+
# fallback that fails at the moment it is needed. Existing lines are never touched, and a file
|
|
51
|
+
# that already has the block is left exactly as it is.
|
|
52
|
+
MIGRATIONS = [
|
|
53
|
+
("fallbacks:", """
|
|
54
|
+
# Which models this tier drops to while its own is limited, in order, comma-separated.
|
|
55
|
+
# Added by tf-routing-bind.sh. The opencode lines are OpenCode Go models
|
|
56
|
+
# (https://opencode.ai/docs/go); change them if this project uses a different provider —
|
|
57
|
+
# `opencode models` prints what this machine can reach.
|
|
58
|
+
# The "limited until <time>" record is kept in this project, at
|
|
59
|
+
# .tfcore/.session/model-cooldown.json. Uncomment to move it (relative to the project root, or
|
|
60
|
+
# absolute); several projects sharing one file learn about a limit from each other.
|
|
61
|
+
# cooldown_file: .tfcore/.session/model-cooldown.json
|
|
62
|
+
# Full explanation: docs/TechieFlow-Routing-Guide.md §9.
|
|
63
|
+
fallbacks:
|
|
64
|
+
frontier:
|
|
65
|
+
claude: opus, haiku
|
|
66
|
+
opencode: opencode-go/glm-5.3, opencode-go/kimi-k3, openai/gpt-5.6-sol
|
|
67
|
+
standard:
|
|
68
|
+
claude: haiku
|
|
69
|
+
opencode: opencode-go/mimo-v2.5, opencode-go/qwen3.8-max, openai/gpt-5.6-luna
|
|
70
|
+
economy:
|
|
71
|
+
claude: sonnet
|
|
72
|
+
opencode: opencode-go/deepseek-v4-flash, opencode-go/glm-5.3-flash, openai/gpt-5.6-luna
|
|
73
|
+
"""),
|
|
74
|
+
("billing:", """
|
|
75
|
+
# How each harness is paid for, so a cost figure knows what it means (SCHEMA.md §2.5b).
|
|
76
|
+
# subscription = a flat fee, marginal dollars are zero; metered = an API key, real money;
|
|
77
|
+
# local = no bill; auto = work it out per model from OpenCode's own sign-in file.
|
|
78
|
+
# Add a provider by name here to override the guess, e.g. " myserver: local".
|
|
79
|
+
billing:
|
|
80
|
+
claude: subscription
|
|
81
|
+
opencode: auto
|
|
82
|
+
"""),
|
|
83
|
+
]
|
|
84
|
+
try:
|
|
85
|
+
_txt = open(ryaml, encoding="utf-8").read()
|
|
86
|
+
_added = []
|
|
87
|
+
for key, block in MIGRATIONS:
|
|
88
|
+
if not re.search(r"^%s\s*$" % re.escape(key), _txt, re.M):
|
|
89
|
+
if not _txt.endswith("\n"):
|
|
90
|
+
_txt += "\n"
|
|
91
|
+
_txt += block
|
|
92
|
+
_added.append(key.rstrip(":"))
|
|
93
|
+
if _added:
|
|
94
|
+
with open(ryaml, "w", encoding="utf-8", newline="\n") as _fh:
|
|
95
|
+
_fh.write(_txt)
|
|
96
|
+
print("tf-routing-bind: added %s to .tfcore/routing.yaml (nothing existing was changed)"
|
|
97
|
+
% " and ".join(_added))
|
|
98
|
+
except FileNotFoundError:
|
|
99
|
+
pass
|
|
100
|
+
|
|
47
101
|
# --- parse routing.yaml (flat format — see its header) --------------------
|
|
48
102
|
cfg = {"enabled": False, "phases": {}, "subagents": {}, "tiers": {}, "effort": {}}
|
|
49
103
|
sect = tier = None
|
|
@@ -70,8 +124,37 @@ try:
|
|
|
70
124
|
except FileNotFoundError:
|
|
71
125
|
pass # no routing.yaml -> treat as disabled
|
|
72
126
|
|
|
127
|
+
# A tier resolves to its own model UNLESS that model is on cooldown — a usage limit recorded by
|
|
128
|
+
# tf-goal.sh in the per-machine file tf-model-pick.sh owns. Binding the fallback here is what stops
|
|
129
|
+
# the sub-agents failing behind a main agent that already moved (Routing-Guide §8, 2026-09-06).
|
|
130
|
+
_PICK = os.path.join(root, ".tfcore", "utils", "tf-model-pick.sh")
|
|
131
|
+
_picked = {}
|
|
132
|
+
|
|
133
|
+
|
|
73
134
|
def model_for(t, harness):
|
|
74
|
-
|
|
135
|
+
declared = cfg["tiers"].get(t, {}).get(harness)
|
|
136
|
+
if not declared or t == "inherit":
|
|
137
|
+
return declared
|
|
138
|
+
if (t, harness) not in _picked:
|
|
139
|
+
got = declared
|
|
140
|
+
if os.path.isfile(_PICK):
|
|
141
|
+
try:
|
|
142
|
+
import subprocess
|
|
143
|
+
out = subprocess.run(["bash", _PICK, "pick", t, harness],
|
|
144
|
+
capture_output=True, text=True, timeout=20)
|
|
145
|
+
cand = (out.stdout or "").strip().splitlines()
|
|
146
|
+
cand = cand[-1].strip() if cand else ""
|
|
147
|
+
if cand and cand != "inherit":
|
|
148
|
+
got = cand
|
|
149
|
+
elif cand == "inherit":
|
|
150
|
+
got = None # every model in the chain is limited: bind nothing, let it wait
|
|
151
|
+
except Exception:
|
|
152
|
+
pass
|
|
153
|
+
if got != declared:
|
|
154
|
+
print("tf-routing-bind: tier %s (%s) is on %s — %s is on cooldown"
|
|
155
|
+
% (t, harness, got or "no model: the whole chain is limited", declared))
|
|
156
|
+
_picked[(t, harness)] = got
|
|
157
|
+
return _picked[(t, harness)]
|
|
75
158
|
|
|
76
159
|
# --- remove previously generated artifacts (manifest-driven) --------------
|
|
77
160
|
removed = 0
|
|
@@ -109,9 +192,18 @@ def write(relpath, content):
|
|
|
109
192
|
|
|
110
193
|
MARK = "<!-- generated by tf-routing-bind.sh from .tfcore/routing.yaml — do not edit; edit routing.yaml and re-run -->"
|
|
111
194
|
|
|
195
|
+
# A phase whose task file no longer ships gets no binding, with a printed warning: the seven
|
|
196
|
+
# never-used commands were removed in Sitting 4c (2026-09-06) and routing.yaml is preserved per
|
|
197
|
+
# project, so a stale line there must never write a file reference OpenCode refuses to load
|
|
198
|
+
# (MISS-TechieFlow-20260906-27: OpenCode would not start on TechieBlog-oc).
|
|
199
|
+
for _ph in sorted(cfg["phases"]):
|
|
200
|
+
if not os.path.isfile(os.path.join(root, ".tfcore", "tasks", _ph + ".md")):
|
|
201
|
+
print("tf-routing-bind: phase %s has no task file .tfcore/tasks/%s.md; no binding written (remove the line from routing.yaml)" % (_ph, _ph))
|
|
202
|
+
del cfg["phases"][_ph]
|
|
203
|
+
|
|
112
204
|
# --- Claude Code: routed wrapper commands ---------------------------------
|
|
113
205
|
OWNER = {"day1-greenfield": "analyst", "day1-brownfield": "analyst",
|
|
114
|
-
"
|
|
206
|
+
"amend-docs": "analyst", "mockups": "analyst",
|
|
115
207
|
"split-brd": "analyst", "verify-phase": "verifier"}
|
|
116
208
|
for phase, t in sorted(cfg["phases"].items()):
|
|
117
209
|
m = model_for(t, "claude")
|
|
@@ -212,5 +304,4 @@ with open(manifest_path, "w", encoding="utf-8", newline="\n") as fh:
|
|
|
212
304
|
print("tf-routing-bind: routing enabled — %d artifact(s) generated (%d stale removed)"
|
|
213
305
|
% (len(generated), removed))
|
|
214
306
|
PY
|
|
215
|
-
python3 "$SELF_DIR/tf-codex-bind.py" "$ROOT" || echo "tf-routing-bind: warning: Codex bindings were not refreshed" >&2
|
|
216
307
|
exit 0
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
# bash .tfcore/utils/tf-routing.sh set-model <tier> <harness> <model>
|
|
9
9
|
# e.g. set-model economy opencode opencode-go/deepseek-v4-flash
|
|
10
10
|
# e.g. set-model frontier claude opus
|
|
11
|
-
# e.g. set-model standard
|
|
11
|
+
# e.g. set-model standard opencode openai/gpt-5.6-terra
|
|
12
12
|
# bash .tfcore/utils/tf-routing.sh bind re-generate bindings after editing routing.yaml by hand
|
|
13
13
|
# bash .tfcore/utils/tf-routing.sh set-escalation <phase> <attempts> <tier>
|
|
14
14
|
# e.g. set-escalation fix-issues 2 frontier
|
|
@@ -90,12 +90,12 @@ PY
|
|
|
90
90
|
set-model)
|
|
91
91
|
TIER="${2:-}"; HARNESS="${3:-}"; MODEL="${4:-}"
|
|
92
92
|
case "$TIER" in frontier|standard|economy) ;; *)
|
|
93
|
-
echo "usage: tf-routing.sh set-model <frontier|standard|economy> <claude|opencode
|
|
93
|
+
echo "usage: tf-routing.sh set-model <frontier|standard|economy> <claude|opencode> <model-id>" >&2; exit 2 ;;
|
|
94
94
|
esac
|
|
95
|
-
case "$HARNESS" in claude|opencode
|
|
96
|
-
echo "harness must be claude
|
|
95
|
+
case "$HARNESS" in claude|opencode) ;; *)
|
|
96
|
+
echo "harness must be claude or opencode" >&2; exit 2 ;;
|
|
97
97
|
esac
|
|
98
|
-
[[ -n "$MODEL" ]] || { echo "usage: tf-routing.sh set-model <tier> <claude|opencode
|
|
98
|
+
[[ -n "$MODEL" ]] || { echo "usage: tf-routing.sh set-model <tier> <claude|opencode> <model-id>" >&2; exit 2; }
|
|
99
99
|
python3 - "$RY" "$TIER" "$HARNESS" "$MODEL" <<'PY'
|
|
100
100
|
import sys
|
|
101
101
|
p, tier, harness, model = sys.argv[1:5]
|
|
@@ -199,7 +199,7 @@ print()
|
|
|
199
199
|
print("Tier models:")
|
|
200
200
|
for t in ("frontier", "standard", "economy"):
|
|
201
201
|
m = cfg["tiers"].get(t, {})
|
|
202
|
-
print(" %-9s claude: %-10s opencode:
|
|
202
|
+
print(" %-9s claude: %-10s opencode: %s" % (t, m.get("claude", "-"), m.get("opencode", "-")))
|
|
203
203
|
print()
|
|
204
204
|
print("Phases by tier:")
|
|
205
205
|
for t in ("frontier", "standard", "economy", "inherit"):
|
|
@@ -210,6 +210,13 @@ sub = ["%s=%s" % (k, v) for k, v in sorted(cfg["subagents"].items())]
|
|
|
210
210
|
if sub:
|
|
211
211
|
print("Subagents: " + ", ".join(sub))
|
|
212
212
|
print()
|
|
213
|
+
print("When a model is limited (docs/TechieFlow-Routing-Guide.md §9):")
|
|
214
|
+
print(" what each tier would run right now, and what is parked:")
|
|
215
|
+
print(" bash .tfcore/utils/tf-model-pick.sh status")
|
|
216
|
+
print(" the order a tier falls back in: ... chain <tier> <claude|opencode>")
|
|
217
|
+
print(" unpark by hand: ... clear <harness> [model]")
|
|
218
|
+
print(" The supervisor parks a model by itself on a usage limit; tf-goal.sh --no-fallback stops it.")
|
|
219
|
+
print()
|
|
213
220
|
print("Escalation (ADVISORY — you apply it when you launch; nothing switches a model mid-run):")
|
|
214
221
|
if cfg["escalation"]:
|
|
215
222
|
for ph in sorted(cfg["escalation"]):
|
|
@@ -226,7 +233,6 @@ if cfg["enabled"]:
|
|
|
226
233
|
print("Invoke routed phases as:")
|
|
227
234
|
print(" OpenCode: /techieflow:tasks:<phase> (same commands as always — model now pinned)")
|
|
228
235
|
print(" Claude Code: /tf:<phase> (new wrappers; old commands still work, unrouted)")
|
|
229
|
-
print(" Codex: $techieflow-<phase> (main-thread model inherited; delegated roles are pinned)")
|
|
230
236
|
print()
|
|
231
237
|
print("WHERE YOU SEE IT — opening the TUI looks UNCHANGED on purpose: your normal chat")
|
|
232
238
|
print("(the default 'build' agent) stays on YOUR selected model. Routing becomes visible when:")
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# TechieFlow — the framework's own scripts, run against THIS project's real files.
|
|
3
|
+
#
|
|
4
|
+
# bash .tfcore/utils/tf-selfcheck.sh [<repo>] [--report]
|
|
5
|
+
#
|
|
6
|
+
# WHY IT EXISTS. Nine framework defects reached TfLens in three weeks. Every one was
|
|
7
|
+
# found mid-build, in the application's repository, and could only be fixed in the
|
|
8
|
+
# framework's — so each cost a switch between repos, a feedback entry, a maintenance
|
|
9
|
+
# sitting and a re-deploy. The defects were not exotic: a checklist with a row that
|
|
10
|
+
# could never clear, a BRD carrying the anchors its own link checker demands, a metrics
|
|
11
|
+
# file holding the framework's own verdicts. They were simply never run against a real
|
|
12
|
+
# project's real files before the project ran them (MISS-TechieFlow-20260909-01).
|
|
13
|
+
#
|
|
14
|
+
# So this runs them here, first, and says which are broken ON THIS PROJECT. A FAIL is a
|
|
15
|
+
# FRAMEWORK defect, never the project's: nothing here grades the application.
|
|
16
|
+
#
|
|
17
|
+
# Run it before a build pass, or any time. It writes nothing, boots nothing, and never
|
|
18
|
+
# runs git. Python 3 standard library only.
|
|
19
|
+
#
|
|
20
|
+
# --report prints a ready-to-paste entry for the project's framework-feedback file, so
|
|
21
|
+
# filing costs one paste instead of a page.
|
|
22
|
+
#
|
|
23
|
+
# EXIT: 0 every framework script works here · 1 at least one is broken · 3 cannot run.
|
|
24
|
+
|
|
25
|
+
set -uo pipefail
|
|
26
|
+
REPO="."; REPORT=0
|
|
27
|
+
while [[ $# -gt 0 ]]; do
|
|
28
|
+
case "$1" in
|
|
29
|
+
--report) REPORT=1; shift ;;
|
|
30
|
+
-h|--help) sed -n '2,23p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
31
|
+
*) REPO="$1"; shift ;;
|
|
32
|
+
esac
|
|
33
|
+
done
|
|
34
|
+
[[ -d "$REPO" ]] || { echo "tf-selfcheck: not a directory: $REPO" >&2; exit 3; }
|
|
35
|
+
command -v python3 >/dev/null 2>&1 || { echo "tf-selfcheck: python3 required" >&2; exit 3; }
|
|
36
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
37
|
+
|
|
38
|
+
TF_REPO="$REPO" TF_UTILS="$HERE" TF_REPORT="$REPORT" python3 - <<'PY'
|
|
39
|
+
import json, os, re, subprocess, sys
|
|
40
|
+
|
|
41
|
+
REPO = os.path.abspath(os.environ["TF_REPO"])
|
|
42
|
+
UTILS = os.environ["TF_UTILS"]
|
|
43
|
+
TELEM = os.path.join(os.path.dirname(UTILS), "telemetry")
|
|
44
|
+
REPORT = os.environ["TF_REPORT"] == "1"
|
|
45
|
+
findings = [] # (script, what is wrong, the one-line fix hint)
|
|
46
|
+
lines = []
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def ok(script, msg):
|
|
50
|
+
lines.append("ok %-20s %s" % (script, msg))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def broken(script, msg, hint=""):
|
|
54
|
+
lines.append("FAIL %-20s %s" % (script, msg))
|
|
55
|
+
findings.append((script, msg, hint))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def skip(script, msg):
|
|
59
|
+
lines.append("-- %-20s %s" % (script, msg))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def run(cmd, cwd=REPO, timeout=120):
|
|
63
|
+
try:
|
|
64
|
+
p = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout)
|
|
65
|
+
return p.returncode, (p.stdout or "") + (p.stderr or "")
|
|
66
|
+
except Exception as e:
|
|
67
|
+
return 99, str(e)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def app_names():
|
|
71
|
+
"""Every application this repo holds a checklist for. The checklist is the framework's
|
|
72
|
+
single source of truth, so its name is the app's name — no config value is trusted,
|
|
73
|
+
because core-config.yaml is rsynced between projects and is the thing most likely to
|
|
74
|
+
be stale (the same reason tf-gitignore-audit detects the stack from the tree)."""
|
|
75
|
+
d = os.path.join(REPO, "docs")
|
|
76
|
+
out = []
|
|
77
|
+
if os.path.isdir(d):
|
|
78
|
+
for f in sorted(os.listdir(d)):
|
|
79
|
+
m = re.match(r"^(.+?)(?:-P(\d+))?-Checklist\.md$", f)
|
|
80
|
+
if m and "Deployment" not in f:
|
|
81
|
+
out.append((m.group(1), int(m.group(2) or 1), os.path.join(d, f)))
|
|
82
|
+
return out
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ---- 1. the deployed copy is intact ----------------------------------------------------
|
|
86
|
+
# A framework script that will not even parse is the cheapest possible thing to catch and
|
|
87
|
+
# the most expensive to meet mid-build.
|
|
88
|
+
# Parsed in-process and the shells checked in ONE bash, not one subprocess per file:
|
|
89
|
+
# 54 scripts meant 54 process launches and ten seconds, which is enough to make a check
|
|
90
|
+
# that should run at the top of every command feel like a tax and get skipped.
|
|
91
|
+
bad_parse, shells = [], []
|
|
92
|
+
for folder in (UTILS, TELEM):
|
|
93
|
+
if not os.path.isdir(folder):
|
|
94
|
+
continue
|
|
95
|
+
for f in sorted(os.listdir(folder)):
|
|
96
|
+
full = os.path.join(folder, f)
|
|
97
|
+
if f.endswith(".py"):
|
|
98
|
+
try:
|
|
99
|
+
import ast
|
|
100
|
+
ast.parse(open(full, encoding="utf-8", errors="replace").read(), filename=full)
|
|
101
|
+
except SyntaxError:
|
|
102
|
+
bad_parse.append(f)
|
|
103
|
+
elif f.endswith(".sh"):
|
|
104
|
+
shells.append(full)
|
|
105
|
+
if shells:
|
|
106
|
+
script = "for f in " + " ".join('"%s"' % x for x in shells) + \
|
|
107
|
+
'; do bash -n "$f" 2>/dev/null || echo "$f"; done'
|
|
108
|
+
rc, out = run(["bash", "-c", script])
|
|
109
|
+
bad_parse += [os.path.basename(x) for x in out.split() if x.strip()]
|
|
110
|
+
if bad_parse:
|
|
111
|
+
broken("deployed copy", "%d script(s) do not parse: %s" % (len(bad_parse), ", ".join(bad_parse[:6])),
|
|
112
|
+
"re-run update-framework.sh; if it persists the framework shipped a broken script")
|
|
113
|
+
else:
|
|
114
|
+
ok("deployed copy", "every framework script parses")
|
|
115
|
+
|
|
116
|
+
# ---- 2. the working list accounts for every checklist row ------------------------------
|
|
117
|
+
# TF-019: a row can sit at `Needs re-verify` forever, which pinned the mode and dropped
|
|
118
|
+
# every not-started row out of the list AND out of the counts, so a build pass could report
|
|
119
|
+
# a phase finished with rows at 0%. The arithmetic is the check: every row lands in exactly
|
|
120
|
+
# one of open / terminal / Blocked.
|
|
121
|
+
apps = app_names()
|
|
122
|
+
if not apps:
|
|
123
|
+
skip("tf-build-list", "no checklist in docs/ yet")
|
|
124
|
+
for app, phase, path in apps:
|
|
125
|
+
rc, out = run(["bash", os.path.join(UTILS, "tf-build-list.sh"), app] +
|
|
126
|
+
([] if phase <= 1 else ["--phase", str(phase)]))
|
|
127
|
+
label = "%s p%d" % (app, phase)
|
|
128
|
+
if rc not in (0, 2):
|
|
129
|
+
broken("tf-build-list", "%s: exited %d" % (label, rc), out.strip().splitlines()[0][:120] if out.strip() else "")
|
|
130
|
+
continue
|
|
131
|
+
m = re.search(r"Mode: (\w+) — (\d+) row\(s\) to build; (\d+) terminal, (\d+) Blocked, (\d+) total", out)
|
|
132
|
+
if not m:
|
|
133
|
+
if "has no REQ rows" in out:
|
|
134
|
+
skip("tf-build-list", "%s: checklist has no requirement rows yet" % label)
|
|
135
|
+
else:
|
|
136
|
+
broken("tf-build-list", "%s: no counts line to check" % label,
|
|
137
|
+
"the output shape changed; the arithmetic can no longer be verified")
|
|
138
|
+
continue
|
|
139
|
+
mode, work, term, blocked, total = m.group(1), *(int(x) for x in m.groups()[1:])
|
|
140
|
+
if work + term + blocked != total:
|
|
141
|
+
broken("tf-build-list",
|
|
142
|
+
"%s: %d rows are in no category (%d open + %d terminal + %d blocked ≠ %d total)"
|
|
143
|
+
% (label, total - work - term - blocked, work, term, blocked, total),
|
|
144
|
+
"a build pass trusting this list would leave those rows at 0%")
|
|
145
|
+
else:
|
|
146
|
+
ok("tf-build-list", "%s: %d rows, all accounted for (%s)" % (label, total, mode))
|
|
147
|
+
|
|
148
|
+
# ---- 3. the BRD this project actually wrote can be read --------------------------------
|
|
149
|
+
# TF-017: the ledger regex required **BRD-N** immediately after the bullet, and a real BRD
|
|
150
|
+
# carries an <a id> anchor there because §9 cross-links need one. It matched 0 of 179 items.
|
|
151
|
+
sys.path.insert(0, UTILS)
|
|
152
|
+
for app, phase, _cl in apps:
|
|
153
|
+
brd = os.path.join(REPO, "docs", "%s-BRD.md" % app if phase <= 1 else "%s-P%d-BRD.md" % (app, phase))
|
|
154
|
+
if not os.path.isfile(brd):
|
|
155
|
+
continue
|
|
156
|
+
text = open(brd, encoding="utf-8", errors="replace").read()
|
|
157
|
+
# A struck-through item (~~**BRD-3**~~ *(removed …)*) is deliberately not a live
|
|
158
|
+
# requirement, and the ledger is right to skip it. Counting it as unread would make
|
|
159
|
+
# this check cry wolf on every mature BRD -- and a check that cries wolf is the one
|
|
160
|
+
# that gets skimmed (TF-012 failed 8 of 10 screens identically and trained exactly that).
|
|
161
|
+
# UNIQUE ids, not occurrences: an id named again in a cross-reference is the same
|
|
162
|
+
# requirement, and counting mentions made every BRD look short by however many times
|
|
163
|
+
# it referred to itself.
|
|
164
|
+
written_ids = {m.group(1) for m in re.finditer(r"\*\*(BRD-\d+)\*\*", text)
|
|
165
|
+
if not re.search(r"~~\s*$", text[max(0, m.start() - 3):m.start()])}
|
|
166
|
+
written = len(written_ids)
|
|
167
|
+
try:
|
|
168
|
+
import importlib.util
|
|
169
|
+
spec = importlib.util.spec_from_file_location("_sb", os.path.join(UTILS, "tf-split-brd.py"))
|
|
170
|
+
mod = importlib.util.module_from_spec(spec)
|
|
171
|
+
spec.loader.exec_module(mod)
|
|
172
|
+
read = len({(i["id"] if isinstance(i, dict) else i[0]) for i in mod.ledger(text)})
|
|
173
|
+
except Exception as e:
|
|
174
|
+
broken("tf-split-brd", "%s: could not read the BRD at all (%s)" % (app, type(e).__name__),
|
|
175
|
+
"*amend-docs --add-missing cannot append checklist rows")
|
|
176
|
+
continue
|
|
177
|
+
if written and read < written:
|
|
178
|
+
broken("tf-split-brd", "%s: reads %d of %d BRD items" % (app, read, written),
|
|
179
|
+
"new requirements must be added to the checklist by hand until this is fixed")
|
|
180
|
+
elif written:
|
|
181
|
+
ok("tf-split-brd", "%s: reads all %d BRD items" % (app, written))
|
|
182
|
+
|
|
183
|
+
# ---- 4. the streams this project's figures are built on --------------------------------
|
|
184
|
+
# TF-015: a run whose `ended` precedes its `started` was stored with a negative or invented
|
|
185
|
+
# duration, and the two readers of the stream then disagreed about the same record.
|
|
186
|
+
runs_p = os.path.join(REPO, "docs", "metrics", "runs.jsonl")
|
|
187
|
+
if not os.path.isfile(runs_p):
|
|
188
|
+
skip("runs.jsonl", "no telemetry in this project yet")
|
|
189
|
+
else:
|
|
190
|
+
neg = impossible = total_r = badline = 0
|
|
191
|
+
for l in open(runs_p, encoding="utf-8", errors="replace"):
|
|
192
|
+
l = l.strip()
|
|
193
|
+
if not l:
|
|
194
|
+
continue
|
|
195
|
+
try:
|
|
196
|
+
r = json.loads(l)
|
|
197
|
+
except Exception:
|
|
198
|
+
badline += 1
|
|
199
|
+
continue
|
|
200
|
+
total_r += 1
|
|
201
|
+
d = r.get("duration_s")
|
|
202
|
+
if isinstance(d, (int, float)) and d < 0:
|
|
203
|
+
neg += 1
|
|
204
|
+
s, e = r.get("started"), r.get("ended")
|
|
205
|
+
if isinstance(s, str) and isinstance(e, str) and e < s:
|
|
206
|
+
impossible += 1
|
|
207
|
+
if badline:
|
|
208
|
+
broken("runs.jsonl", "%d line(s) are not valid JSON" % badline, "those records are invisible to every figure")
|
|
209
|
+
if impossible or neg:
|
|
210
|
+
# NOT a failure. The emitter refuses these now and the reader discards them and
|
|
211
|
+
# prints how many -- so nothing is wrong with the framework here, and nothing can
|
|
212
|
+
# be done about the records either: the stream is append-only and the true start
|
|
213
|
+
# times are gone. Reported once as history, because calling it broken on every
|
|
214
|
+
# command for the life of the project is how a check earns being skimmed.
|
|
215
|
+
skip("runs.jsonl", "%d old record(s) end before they start — discarded from every "
|
|
216
|
+
"duration figure, and the report says so" % impossible)
|
|
217
|
+
elif total_r:
|
|
218
|
+
ok("runs.jsonl", "%d run record(s), none impossible" % total_r)
|
|
219
|
+
|
|
220
|
+
# ---- 5. the framework's own verdicts are not counted as this project's -----------------
|
|
221
|
+
# TF-020: an FR verdict grades a framework requirement, not a screen, and pooling it into
|
|
222
|
+
# an application segment raises that segment's first-pass rate with nothing on the output
|
|
223
|
+
# to show it happened. SCHEMA §3 states the rule; the rollup has to apply it.
|
|
224
|
+
gates_p = os.path.join(REPO, "docs", "metrics", "gates.jsonl")
|
|
225
|
+
if not os.path.isfile(gates_p):
|
|
226
|
+
skip("tf-metrics", "no gate records in this project yet")
|
|
227
|
+
else:
|
|
228
|
+
has_fr = False
|
|
229
|
+
for l in open(gates_p, encoding="utf-8", errors="replace"):
|
|
230
|
+
if '"req_class"' in l and '"FR"' in l:
|
|
231
|
+
has_fr = True
|
|
232
|
+
break
|
|
233
|
+
rc, out = run(["bash", os.path.join(TELEM, "tf-metrics.sh"), "--rollup", REPO, "--json"])
|
|
234
|
+
try:
|
|
235
|
+
data = json.loads(out)
|
|
236
|
+
except Exception:
|
|
237
|
+
broken("tf-metrics", "--rollup did not return readable JSON (exit %d)" % rc,
|
|
238
|
+
"no figure this project publishes can be checked against the reference")
|
|
239
|
+
data = None
|
|
240
|
+
if data is not None:
|
|
241
|
+
segs = data.get("live") or {}
|
|
242
|
+
if has_fr and "framework-requirement" not in segs:
|
|
243
|
+
broken("tf-metrics", "framework verdicts are pooled into %s" % ", ".join(sorted(segs) or ["nothing"]),
|
|
244
|
+
"this project's first-pass rate is overstated")
|
|
245
|
+
else:
|
|
246
|
+
ok("tf-metrics", "rollup reads; %d segment(s), framework verdicts separate" % len(segs))
|
|
247
|
+
|
|
248
|
+
# ---- 6. amendments to the miss log still fold -------------------------------------------
|
|
249
|
+
# TF-006 and TF-018: a field added after a record was written can only be completed by an
|
|
250
|
+
# amendment, and an amendment the reader will not fold is a fact silently lost.
|
|
251
|
+
miss_p = os.path.join(REPO, "docs", "metrics", "misses.jsonl")
|
|
252
|
+
if not os.path.isfile(miss_p):
|
|
253
|
+
skip("misses.jsonl", "no miss records in this project yet")
|
|
254
|
+
else:
|
|
255
|
+
ids, amends, orphan = set(), 0, 0
|
|
256
|
+
for l in open(miss_p, encoding="utf-8", errors="replace"):
|
|
257
|
+
l = l.strip()
|
|
258
|
+
if not l:
|
|
259
|
+
continue
|
|
260
|
+
try:
|
|
261
|
+
r = json.loads(l)
|
|
262
|
+
except Exception:
|
|
263
|
+
continue
|
|
264
|
+
if r.get("kind") == "miss":
|
|
265
|
+
ids.add(r.get("miss_id"))
|
|
266
|
+
elif r.get("kind") == "miss-amend":
|
|
267
|
+
amends += 1
|
|
268
|
+
if r.get("miss_id") not in ids:
|
|
269
|
+
orphan += 1
|
|
270
|
+
if orphan:
|
|
271
|
+
broken("misses.jsonl", "%d amendment(s) name a miss that is not on this stream" % orphan,
|
|
272
|
+
"the information in them is not reaching any figure")
|
|
273
|
+
else:
|
|
274
|
+
ok("misses.jsonl", "%d miss(es), %d amendment(s), none orphaned" % (len(ids), amends))
|
|
275
|
+
|
|
276
|
+
# ---- 7. the checks that read this project's documents run at all -----------------------
|
|
277
|
+
for name, cmd in (("tf-doc-check", ["bash", os.path.join(UTILS, "tf-doc-check.sh")] +
|
|
278
|
+
(["--app", apps[0][0]] if apps else []) + ["--warn"]),
|
|
279
|
+
("tf-gitignore-audit", ["bash", os.path.join(UTILS, "tf-gitignore-audit.sh"), REPO, "--dry-run"])):
|
|
280
|
+
if name == "tf-doc-check" and not apps:
|
|
281
|
+
skip(name, "no application documents yet")
|
|
282
|
+
continue
|
|
283
|
+
rc, out = run(cmd)
|
|
284
|
+
if rc in (99,) or "Traceback" in out:
|
|
285
|
+
broken(name, "crashed on this project's files",
|
|
286
|
+
(out.strip().splitlines() or [""])[-1][:120])
|
|
287
|
+
else:
|
|
288
|
+
first = next((l for l in out.splitlines() if l.strip()), "")
|
|
289
|
+
ok(name, "runs (exit %d)" % rc)
|
|
290
|
+
|
|
291
|
+
# ---- output -----------------------------------------------------------------------------
|
|
292
|
+
proj = os.path.basename(REPO.rstrip(os.sep))
|
|
293
|
+
print("# tf-selfcheck — %s — the framework's own scripts against this project's real files" % proj)
|
|
294
|
+
for l in lines:
|
|
295
|
+
print(l)
|
|
296
|
+
print()
|
|
297
|
+
if not findings:
|
|
298
|
+
print("Every framework script works on this project. Nothing to file.")
|
|
299
|
+
raise SystemExit(0)
|
|
300
|
+
|
|
301
|
+
print("%d framework defect(s) found. These are the FRAMEWORK's, not this project's —" % len(findings))
|
|
302
|
+
print("nothing here graded the application. Do not work around them silently: file them,")
|
|
303
|
+
print("and carry on (a framework defect blocks a row only when it stops the work).")
|
|
304
|
+
if REPORT:
|
|
305
|
+
print()
|
|
306
|
+
print("--- paste into docs/%s-TechieFlow-Feedback.md ---" % proj)
|
|
307
|
+
for i, (script, msg, hint) in enumerate(findings, 1):
|
|
308
|
+
print()
|
|
309
|
+
print("## TF-xxx — %s: %s" % (script, msg))
|
|
310
|
+
print()
|
|
311
|
+
print("- **Severity:** major")
|
|
312
|
+
print("- **Blocks:** no")
|
|
313
|
+
print("- **Repro:** `bash .tfcore/utils/tf-selfcheck.sh` in this repository")
|
|
314
|
+
print("- **Expected:** the script works on this project's files")
|
|
315
|
+
print("- **Actual:** %s" % msg)
|
|
316
|
+
print("- **Encountered in:** tf-selfcheck, %s" % proj)
|
|
317
|
+
if hint:
|
|
318
|
+
print("- **Consequence:** %s" % hint)
|
|
319
|
+
print("- **Not fixable here** — `.tfcore/` is framework-owned and is replaced on the next update.")
|
|
320
|
+
else:
|
|
321
|
+
print("Run again with --report for entries you can paste into the feedback file.")
|
|
322
|
+
raise SystemExit(1)
|
|
323
|
+
PY
|