@techierathore/techieflow 1.0.2 → 1.0.3
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 +0 -5
- package/.claude/commands/TechieFlow/agents/architect.md +0 -6
- package/.claude/commands/TechieFlow/agents/flow-master.md +41 -215
- package/.claude/commands/TechieFlow/agents/verifier.md +23 -64
- package/.claude/commands/TechieFlow/tasks/_metrics-emit-gate.md +24 -117
- 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 +20 -143
- package/.claude/commands/TechieFlow/tasks/build-phase.md +14 -224
- 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/fix-issues.md +10 -129
- package/.claude/commands/TechieFlow/tasks/log-miss.md +19 -158
- package/.claude/commands/TechieFlow/tasks/metrics-report.md +1 -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 +39 -12
- package/.tfcore/agents/analyst.md +0 -5
- package/.tfcore/agents/architect.md +0 -6
- package/.tfcore/agents/flow-master.md +41 -215
- package/.tfcore/agents/verifier.md +23 -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 +56 -0
- package/.tfcore/hooks/guard-status-html.sh +116 -71
- package/.tfcore/hooks/guard-status.sh +43 -16
- 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 +25 -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/_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 +20 -143
- package/.tfcore/tasks/build-phase.md +14 -224
- 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/fix-issues.md +10 -129
- package/.tfcore/tasks/log-miss.md +19 -158
- package/.tfcore/tasks/metrics-report.md +1 -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 +43 -7
- package/.tfcore/telemetry/tf-metrics.sh +152 -17
- package/.tfcore/templates/project-brief-tmpl.yaml +1 -1
- 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-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-deployment-checklist-tmpl.md +86 -0
- package/.tfcore/templates/v4custom/app-devguide-tmpl.md +69 -146
- 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/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 +191 -0
- package/.tfcore/utils/tf-build-list.sh +20 -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 +1267 -0
- package/.tfcore/utils/tf-doc-check.sh +29 -0
- package/.tfcore/utils/tf-emit.sh +203 -13
- package/.tfcore/utils/tf-fix-close.sh +83 -0
- package/.tfcore/utils/tf-goal.sh +193 -62
- 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 +211 -0
- package/.tfcore/utils/tf-misses-md.sh +13 -0
- package/.tfcore/utils/tf-mockups-locate.py +66 -0
- package/.tfcore/utils/tf-mockups-locate.sh +18 -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 +74 -0
- package/.tfcore/utils/tf-routing-bind.sh +10 -5
- package/.tfcore/utils/tf-routing.sh +6 -7
- package/.tfcore/utils/tf-split-brd.py +304 -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 +309 -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 +283 -0
- package/.tfcore/utils/tf-verify-screens.sh +22 -0
- package/.tfcore/utils/tf-verify-tests.sh +123 -0
- package/.tfcore/utils/tf-verify-verdict.py +283 -0
- package/.tfcore/utils/tf-verify-verdict.sh +15 -0
- package/.tfcore/utils/tf-yolo.sh +59 -0
- package/README.md +114 -1149
- package/docs/TechieFlow-Installation.md +1 -5
- package/opencode.jsonc +13 -33
- package/package.json +1 -5
- package/scripts/install.mjs +52 -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/utils/tf-codex-bind.py +0 -196
- package/.tfcore/utils/tf-codex-telemetry.py +0 -60
- package/WORKFLOW.html +0 -1986
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tf-status-evidence.sh — the evidence *refresh-status rebuilds PROJECT-STATUS from, after a
|
|
3
|
+
# session died before its status gate (Sitting 4b, 2026-09-06). Reads only; never git.
|
|
4
|
+
#
|
|
5
|
+
# bash .tfcore/utils/tf-status-evidence.sh <App> [--no-build] [--phase N]
|
|
6
|
+
#
|
|
7
|
+
# Prints, in this order:
|
|
8
|
+
# 1. what the stale PROJECT-STATUS.md claims (last_updated, current_phase, the next command)
|
|
9
|
+
# 2. files under src/ and tests/ changed after that file was last written (work the gate never saw)
|
|
10
|
+
# 3. checklist rows left at Implemented / In Progress / Needs re-verify / PARTIAL / FAIL
|
|
11
|
+
# (the fingerprint of a smoke that ran and a verify that did not)
|
|
12
|
+
# 4. the build verdict from tf-build.sh (skipped with --no-build)
|
|
13
|
+
# Then the agent decides the interruption signature and runs tf-status-facts.sh for the gate.
|
|
14
|
+
set -uo pipefail
|
|
15
|
+
APP="${1:-}"; [[ -z "$APP" ]] && { sed -n '2,12p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 2; }
|
|
16
|
+
shift
|
|
17
|
+
BUILD=1; PHASE=""
|
|
18
|
+
while [[ $# -gt 0 ]]; do
|
|
19
|
+
case "$1" in
|
|
20
|
+
--no-build) BUILD=0; shift ;;
|
|
21
|
+
--phase) PHASE="$2"; shift 2 ;;
|
|
22
|
+
*) shift ;;
|
|
23
|
+
esac
|
|
24
|
+
done
|
|
25
|
+
[[ -z "$PHASE" ]] && PHASE="$(grep -oE '^appPhase:\s*[0-9]+' .tfcore/core-config.yaml 2>/dev/null | grep -oE '[0-9]+' || echo 1)"
|
|
26
|
+
[[ "$PHASE" -le 1 ]] && CL="docs/${APP}-Checklist.md" || CL="docs/${APP}-P${PHASE}-Checklist.md"
|
|
27
|
+
ST="PROJECT-STATUS.md"
|
|
28
|
+
|
|
29
|
+
echo "# tf-status-evidence — $APP — $(date -u +%Y-%m-%dT%H:%M:%SZ) — phase $PHASE"
|
|
30
|
+
echo
|
|
31
|
+
echo "## 1. What the stale status file claims (a hypothesis, not a fact)"
|
|
32
|
+
if [[ -f "$ST" ]]; then
|
|
33
|
+
grep -E '^(last_updated|current_phase|last_verified_build|last_verified_date):' "$ST" | sed 's/^/- /'
|
|
34
|
+
awk '/^## Next command to run/{p=1;next} /^## /{p=0} p' "$ST" | grep -E '^\s*/' | sed 's/^\s*/- next: /' | head -2
|
|
35
|
+
echo "- written: $(date -u -r "$ST" +%Y-%m-%dT%H:%M:%SZ)"
|
|
36
|
+
else
|
|
37
|
+
echo "- no PROJECT-STATUS.md; treat as never written"
|
|
38
|
+
fi
|
|
39
|
+
echo
|
|
40
|
+
echo "## 2. Files changed after the status file was written (src/ and tests/, newest first)"
|
|
41
|
+
if [[ -f "$ST" ]]; then
|
|
42
|
+
mapfile -t NEWER < <(find src tests -type f -newer "$ST" \
|
|
43
|
+
-not -path '*/bin/*' -not -path '*/obj/*' -not -path '*/node_modules/*' -not -path '*/.artifacts/*' 2>/dev/null \
|
|
44
|
+
| xargs -r ls -t 2>/dev/null)
|
|
45
|
+
echo "- ${#NEWER[@]} file(s)"
|
|
46
|
+
printf -- '- %s\n' "${NEWER[@]:0:30}"
|
|
47
|
+
[[ ${#NEWER[@]} -gt 30 ]] && echo "- (${#NEWER[@]} in total; the rest with: find src tests -type f -newer PROJECT-STATUS.md)"
|
|
48
|
+
else
|
|
49
|
+
echo "- no status file to compare against"
|
|
50
|
+
fi
|
|
51
|
+
echo
|
|
52
|
+
echo "## 3. Checklist rows that look interrupted ($CL)"
|
|
53
|
+
if [[ -f "$CL" ]]; then
|
|
54
|
+
grep -E '^\s*\|\s*`?REQ-' "$CL" | awk -F'|' '{id=$2; st=$4; gsub(/^[ `*]+|[ `*]+$/,"",id); gsub(/^[ ]+|[ ]+$/,"",st); print id" — "st}' \
|
|
55
|
+
| grep -iE ' — (Implemented|In Progress|Needs re-verify|PARTIAL|FAIL)' | sed 's/^/- /' | head -40
|
|
56
|
+
n=$(grep -E '^\s*\|\s*`?REQ-' "$CL" | awk -F'|' '{print $4}' | grep -ciE 'Implemented|In Progress|Needs re-verify|PARTIAL|FAIL' || true)
|
|
57
|
+
[[ "$n" == "0" ]] && echo "- none: every row is terminal, Blocked or not started"
|
|
58
|
+
else
|
|
59
|
+
echo "- $CL does not exist"
|
|
60
|
+
fi
|
|
61
|
+
echo
|
|
62
|
+
echo "## 4. Build"
|
|
63
|
+
if [[ $BUILD -eq 1 ]]; then
|
|
64
|
+
bash "$(dirname "${BASH_SOURCE[0]}")/tf-build.sh" build 2>&1 | tail -3 | sed 's/^/- /'
|
|
65
|
+
else
|
|
66
|
+
echo "- skipped (--no-build)"
|
|
67
|
+
fi
|
|
68
|
+
exit 0
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""tf-status-facts.py — print the facts PROJECT-STATUS.md is written from.
|
|
3
|
+
|
|
4
|
+
bash .tfcore/utils/tf-status-facts.sh <App> ["<command label>"] [--phase N]
|
|
5
|
+
|
|
6
|
+
Reads docs/<App>-Checklist.md, docs/.last-verify.json, docs/metrics/gates.jsonl,
|
|
7
|
+
docs/<App>-*-Feedback.md and the current PROJECT-STATUS.md. On a Large project the
|
|
8
|
+
checklist is the one of the phase being worked: --phase N, else appPhase in
|
|
9
|
+
.tfcore/core-config.yaml (phase 1 is docs/<App>-Checklist.md, phase n is
|
|
10
|
+
docs/<App>-Pn-Checklist.md), and the phase line names it from docs/<App>-Phases.md. Prints, ready to copy
|
|
11
|
+
into the template (.tfcore/templates/v4custom/app-project-status-tmpl.md):
|
|
12
|
+
|
|
13
|
+
- the frontmatter values (last_updated, current_phase, last_verified_*)
|
|
14
|
+
- the Open requirements section (counts by status, at most ten named rows)
|
|
15
|
+
- the Verification log table with its new row (last five kept)
|
|
16
|
+
- the Library feedback summary lines
|
|
17
|
+
- the Next command to run, in both harness forms, and why
|
|
18
|
+
|
|
19
|
+
Writes nothing. The agent writes PROJECT-STATUS.md with the harness Write tool.
|
|
20
|
+
Python 3 standard library only. Exit 0 printed, 2 could not run.
|
|
21
|
+
"""
|
|
22
|
+
import datetime
|
|
23
|
+
import glob
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
import re
|
|
27
|
+
import sys
|
|
28
|
+
|
|
29
|
+
TERMINAL = {"verified", "done (pre-existing)", "n/a"}
|
|
30
|
+
PASS_THROUGH = {"blocked"} # a library gap; counts as closed for the ladder
|
|
31
|
+
BUILT = {"implemented", "needs re-verify"}
|
|
32
|
+
OWNER = {"owner-uat"} # only the owner can close it, from the UsageGuide test plan
|
|
33
|
+
# anything else (Not Started, In Progress, PARTIAL, FAIL, unknown) is not built yet
|
|
34
|
+
|
|
35
|
+
LADDER = ["Not Started", "In Progress", "PARTIAL", "FAIL", "Implemented", "Needs re-verify",
|
|
36
|
+
"Blocked", "Owner-UAT", "Verified", "Done (pre-existing)", "N/A"]
|
|
37
|
+
COUNT_ROWS = ["Not Started", "In Progress", "Implemented", "Needs re-verify", "Blocked"]
|
|
38
|
+
|
|
39
|
+
CC = "/TechieFlow:agents:"
|
|
40
|
+
OC = "/"
|
|
41
|
+
AGENT_OC = {"flow-master": "flow-master", "verifier": "flow-verifier", "analyst": "flow-analyst"}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def die(msg):
|
|
45
|
+
print(f"tf-status-facts: {msg}", file=sys.stderr)
|
|
46
|
+
sys.exit(2)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def norm_status(raw):
|
|
50
|
+
s = raw.strip().strip("`* ").strip()
|
|
51
|
+
low = s.lower()
|
|
52
|
+
for v in LADDER:
|
|
53
|
+
if low == v.lower() or low.startswith(v.lower() + " ("):
|
|
54
|
+
return v
|
|
55
|
+
return s or "?"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def read(path):
|
|
59
|
+
with open(path, encoding="utf-8", errors="replace") as f:
|
|
60
|
+
return f.read()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def checklist_rows(text):
|
|
64
|
+
rows = []
|
|
65
|
+
for line in text.splitlines():
|
|
66
|
+
if not re.match(r"^\s*\|\s*`?REQ-", line):
|
|
67
|
+
continue
|
|
68
|
+
cells = [c.strip() for c in line.strip().strip("|").split("|")]
|
|
69
|
+
if len(cells) < 3:
|
|
70
|
+
continue
|
|
71
|
+
rid = cells[0].strip("`* ")
|
|
72
|
+
rows.append({"id": rid, "name": cells[1].strip("`* "), "status": norm_status(cells[2])})
|
|
73
|
+
return rows
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def ladder_rank(status):
|
|
77
|
+
return LADDER.index(status) if status in LADDER else 0
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def last_verify(root, app):
|
|
81
|
+
"""(date, build_result) from docs/.last-verify.json and gates.jsonl; ('', 'not-run') if none."""
|
|
82
|
+
ledger = os.path.join(root, "docs", ".last-verify.json")
|
|
83
|
+
if not os.path.isfile(ledger):
|
|
84
|
+
return "", "not-run"
|
|
85
|
+
try:
|
|
86
|
+
data = json.loads(read(ledger).strip().splitlines()[-1])
|
|
87
|
+
except Exception:
|
|
88
|
+
return "", "not-run"
|
|
89
|
+
date = str(data.get("date", ""))[:10]
|
|
90
|
+
result = "PASS"
|
|
91
|
+
gates = os.path.join(root, "docs", "metrics", "gates.jsonl")
|
|
92
|
+
if date and os.path.isfile(gates):
|
|
93
|
+
for line in read(gates).splitlines():
|
|
94
|
+
try:
|
|
95
|
+
rec = json.loads(line)
|
|
96
|
+
except Exception:
|
|
97
|
+
continue
|
|
98
|
+
if rec.get("app") == app and str(rec.get("ts", ""))[:10] == date and rec.get("gate") == "build":
|
|
99
|
+
result = "FAIL"
|
|
100
|
+
break
|
|
101
|
+
return date, result
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def existing_log_rows(status_text):
|
|
105
|
+
m = re.search(r"(?ms)^## Verification log\s*\n(.*?)(?=^## |\Z)", status_text)
|
|
106
|
+
if not m:
|
|
107
|
+
return []
|
|
108
|
+
rows = []
|
|
109
|
+
for line in m.group(1).splitlines():
|
|
110
|
+
if re.match(r"^\s*\|", line) and not re.match(r"^\s*\|\s*-", line) and "Date" not in line.split("|")[1]:
|
|
111
|
+
rows.append(line.strip())
|
|
112
|
+
return rows
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def handoff_ran(log_rows):
|
|
116
|
+
return any("handoff" in r.lower() for r in log_rows)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def feedback_lines(root, app):
|
|
120
|
+
out = []
|
|
121
|
+
for path in sorted(glob.glob(os.path.join(root, "docs", f"{app}-*-Feedback.md"))):
|
|
122
|
+
lib = re.sub(rf"^{re.escape(app)}-(.+)-Feedback\.md$", r"\1", os.path.basename(path))
|
|
123
|
+
text = read(path)
|
|
124
|
+
entries = re.split(r"(?m)^###\s+", text)[1:]
|
|
125
|
+
open_n = 0
|
|
126
|
+
for e in entries:
|
|
127
|
+
closed = re.search(r"(?im)^\s*[-*]\s*\*\*(status|resolved|fixed)[^:]*:\*\*\s*(fixed|resolved|closed|done)", e)
|
|
128
|
+
if not closed:
|
|
129
|
+
open_n += 1
|
|
130
|
+
rel = os.path.relpath(path, root).replace(os.sep, "/")
|
|
131
|
+
out.append(f"- {lib}: {open_n} open of {len(entries)} — {rel}")
|
|
132
|
+
return out or ["- None"]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def config_phase(root):
|
|
136
|
+
p = os.path.join(root, ".tfcore", "core-config.yaml")
|
|
137
|
+
if os.path.isfile(p):
|
|
138
|
+
m = re.search(r"(?m)^appPhase:\s*(\d+)", read(p))
|
|
139
|
+
if m:
|
|
140
|
+
return int(m.group(1))
|
|
141
|
+
return 1
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def phase_prefix(app, phase):
|
|
145
|
+
return f"{app}-" if phase <= 1 else f"{app}-P{phase}-"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def phase_row(root, app, phase):
|
|
149
|
+
"""(name, total phases) from docs/<App>-Phases.md; ('', 0) when the project has no phases."""
|
|
150
|
+
p = os.path.join(root, "docs", f"{app}-Phases.md")
|
|
151
|
+
if not os.path.isfile(p):
|
|
152
|
+
return "", 0
|
|
153
|
+
m = re.search(r"(?ms)^##\s+(?:\d+[.)]\s*)?Phases[^\n]*\n(.*?)(?=^## |\Z)", read(p))
|
|
154
|
+
name, total = "", 0
|
|
155
|
+
for line in (m.group(1) if m else "").splitlines():
|
|
156
|
+
cells = [c.strip() for c in line.strip().strip("|").split("|")]
|
|
157
|
+
if len(cells) >= 2 and cells[0].strip("`* ").isdigit():
|
|
158
|
+
total += 1
|
|
159
|
+
if int(cells[0].strip("`* ")) == phase:
|
|
160
|
+
name = cells[1].strip("`* ")
|
|
161
|
+
return name, total
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def next_command(app, rows, log_rows, phase=1):
|
|
165
|
+
"""Returns (phase, qualifier, cc_line, oc_line, reason)."""
|
|
166
|
+
def ids(lst):
|
|
167
|
+
return ", ".join(r["id"] for r in lst[:6]) + (" …" if len(lst) > 6 else "")
|
|
168
|
+
|
|
169
|
+
if not rows:
|
|
170
|
+
if os.path.isfile(os.path.join(os.getcwd(), "docs", f"{phase_prefix(app, phase)}BRD.md")):
|
|
171
|
+
return ("Day-1", "stage 1 done, awaiting owner review",
|
|
172
|
+
f"{CC}analyst *day1-greenfield {app} --stage2", f"{OC}flow-analyst *day1-greenfield {app} --stage2",
|
|
173
|
+
"the BRD exists and no checklist; stage 2 runs on the owner's go-ahead")
|
|
174
|
+
return ("Day-1", "no documents yet",
|
|
175
|
+
f"{CC}analyst *day1-greenfield {app}", f"{OC}flow-analyst *day1-greenfield {app}",
|
|
176
|
+
"no BRD and no checklist exist; day-1 stage 1 has not run")
|
|
177
|
+
|
|
178
|
+
unbuilt = [r for r in rows if r["status"].lower() not in TERMINAL | PASS_THROUGH | BUILT | OWNER]
|
|
179
|
+
built = [r for r in rows if r["status"].lower() in BUILT]
|
|
180
|
+
owner = [r for r in rows if r["status"].lower() in OWNER]
|
|
181
|
+
total = len(rows)
|
|
182
|
+
closed = sum(1 for r in rows if r["status"].lower() in TERMINAL)
|
|
183
|
+
q = f"{closed} of {total} verified"
|
|
184
|
+
|
|
185
|
+
if unbuilt:
|
|
186
|
+
return ("Build", f"{len(unbuilt)} not built, {q}",
|
|
187
|
+
f"{CC}flow-master *build-phase {app}", f"{OC}flow-master *build-phase {app}",
|
|
188
|
+
f"{len(unbuilt)} rows are not built yet: {ids(unbuilt)}")
|
|
189
|
+
if built:
|
|
190
|
+
ui = [r for r in built if r["id"].startswith("REQ-UI")]
|
|
191
|
+
scope = "ui" if len(ui) == len(built) else ("functional" if not ui else "all")
|
|
192
|
+
return ("Verify", f"{len(built)} to verify, {q}",
|
|
193
|
+
f"{CC}verifier *verify {scope} {app}", f"{OC}flow-verifier *verify {scope} {app}",
|
|
194
|
+
f"{len(built)} rows are built and not verified: {ids(built)}")
|
|
195
|
+
if owner:
|
|
196
|
+
if handoff_ran(log_rows):
|
|
197
|
+
line = f"(owner-run) docs/{app}-UsageGuide.md — {ids(owner)}"
|
|
198
|
+
return ("UAT", f"{len(owner)} owner-run, {q}", line, line,
|
|
199
|
+
f"{len(owner)} rows are Owner-UAT and handoff has run; the owner closes them")
|
|
200
|
+
return ("Handoff", f"{len(owner)} owner-run, {q}",
|
|
201
|
+
f"{CC}flow-master *handoff-phase {app}", f"{OC}flow-master *handoff-phase {app}",
|
|
202
|
+
f"{len(owner)} rows are Owner-UAT and handoff has not run yet")
|
|
203
|
+
if handoff_ran(log_rows):
|
|
204
|
+
line = "(owner) set current_phase to Released after UAT — no agent command"
|
|
205
|
+
return ("UAT", f"handoff done, {q}", line, line,
|
|
206
|
+
"every row is terminal and handoff has run; waiting on the owner")
|
|
207
|
+
return ("Handoff", q,
|
|
208
|
+
f"{CC}flow-master *handoff-phase {app}", f"{OC}flow-master *handoff-phase {app}",
|
|
209
|
+
"every row is terminal and handoff has not run yet")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def main(argv):
|
|
213
|
+
if len(argv) < 2 or argv[1] in ("-h", "--help"):
|
|
214
|
+
print(__doc__)
|
|
215
|
+
return 0
|
|
216
|
+
app = argv[1]
|
|
217
|
+
args = argv[2:]
|
|
218
|
+
root = os.getcwd()
|
|
219
|
+
phase_n = None
|
|
220
|
+
if "--phase" in args:
|
|
221
|
+
i = args.index("--phase")
|
|
222
|
+
if i + 1 >= len(args) or not args[i + 1].isdigit() or int(args[i + 1]) < 1:
|
|
223
|
+
die("--phase needs a number from 1 upwards")
|
|
224
|
+
phase_n = int(args[i + 1])
|
|
225
|
+
del args[i:i + 2]
|
|
226
|
+
phase_n = phase_n or config_phase(root)
|
|
227
|
+
label = args[0] if args else "status"
|
|
228
|
+
today = datetime.date.today().isoformat()
|
|
229
|
+
|
|
230
|
+
cl_rel = f"docs/{phase_prefix(app, phase_n)}Checklist.md"
|
|
231
|
+
cl_path = os.path.join(root, cl_rel)
|
|
232
|
+
rows = checklist_rows(read(cl_path)) if os.path.isfile(cl_path) else []
|
|
233
|
+
st_path = os.path.join(root, "PROJECT-STATUS.md")
|
|
234
|
+
st_text = read(st_path) if os.path.isfile(st_path) else ""
|
|
235
|
+
log_rows = existing_log_rows(st_text)
|
|
236
|
+
vdate, vresult = last_verify(root, app)
|
|
237
|
+
phase, qual, cc, oc, reason = next_command(app, rows, log_rows, phase_n)
|
|
238
|
+
pname, ptotal = phase_row(root, app, phase_n)
|
|
239
|
+
if ptotal or phase_n > 1:
|
|
240
|
+
tag = f"Phase {phase_n} of {ptotal or '?'}" + (f" ({pname})" if pname else "")
|
|
241
|
+
phase = f"{tag} · {phase}"
|
|
242
|
+
reason = f"{reason}; working {cl_rel}"
|
|
243
|
+
|
|
244
|
+
total = len(rows)
|
|
245
|
+
closed = sum(1 for r in rows if r["status"].lower() in TERMINAL)
|
|
246
|
+
counts = {}
|
|
247
|
+
for r in rows:
|
|
248
|
+
counts[r["status"]] = counts.get(r["status"], 0) + 1
|
|
249
|
+
open_rows = sorted((r for r in rows if r["status"].lower() not in TERMINAL),
|
|
250
|
+
key=lambda r: (ladder_rank(r["status"]), r["id"]))
|
|
251
|
+
|
|
252
|
+
print(f"# tf-status-facts — {app} — {today} — copy into PROJECT-STATUS.md (Write tool)")
|
|
253
|
+
print()
|
|
254
|
+
print("## Frontmatter values")
|
|
255
|
+
print(f"last_updated: {today}")
|
|
256
|
+
print(f"current_phase: {phase} — {qual}")
|
|
257
|
+
print(f"last_verified_build: {vresult}")
|
|
258
|
+
print(f"last_verified_date: {vdate or 'never'}")
|
|
259
|
+
print()
|
|
260
|
+
print("## Next command to run")
|
|
261
|
+
print(f"Claude Code:\n```\n{cc}\n```\nOpenCode:\n```\n{oc}\n```")
|
|
262
|
+
print(f"Why: {reason}.")
|
|
263
|
+
print()
|
|
264
|
+
print("## Open requirements")
|
|
265
|
+
print("| Status | Count |\n|---|---|")
|
|
266
|
+
shown = set()
|
|
267
|
+
for s in COUNT_ROWS:
|
|
268
|
+
print(f"| {s} | {counts.get(s, 0)} |")
|
|
269
|
+
shown.add(s)
|
|
270
|
+
for s, n in sorted(counts.items(), key=lambda kv: ladder_rank(kv[0])):
|
|
271
|
+
if s not in shown and s.lower() not in TERMINAL:
|
|
272
|
+
print(f"| {s} | {n} |")
|
|
273
|
+
print()
|
|
274
|
+
if open_rows:
|
|
275
|
+
for r in open_rows[:10]:
|
|
276
|
+
print(f"- [ ] {r['id']} — {r['name']} ({r['status']})")
|
|
277
|
+
if len(open_rows) > 10:
|
|
278
|
+
print(f"- ({len(open_rows) - 10} more open rows in {cl_rel})")
|
|
279
|
+
else:
|
|
280
|
+
print("- None")
|
|
281
|
+
print()
|
|
282
|
+
print("## Verification log")
|
|
283
|
+
print("Last five passes; older passes live in `docs/metrics/gates.jsonl`.")
|
|
284
|
+
print()
|
|
285
|
+
print("| Date | Phase | Result | Status table |\n|---|---|---|---|")
|
|
286
|
+
new_row = f"| {today} | {label} | {closed}/{total} Verified | {cl_rel}#requirements-status |"
|
|
287
|
+
for r in (log_rows[-4:] if len(log_rows) > 4 else log_rows):
|
|
288
|
+
print(r)
|
|
289
|
+
print(new_row)
|
|
290
|
+
print()
|
|
291
|
+
print("## Library feedback summary")
|
|
292
|
+
for line in feedback_lines(root, app):
|
|
293
|
+
print(line)
|
|
294
|
+
print()
|
|
295
|
+
print("## You write these yourself")
|
|
296
|
+
print("- Where I am: at most 80 words, state not story." + (f" Name the phase: {phase.split(' · ')[0]}." if ptotal or phase_n > 1 else ""))
|
|
297
|
+
print("- Known blockers: one line each, or `- None`.")
|
|
298
|
+
print("- Standards compliance: `- Last check {date}: {n} findings, see the checklist Remarks.`")
|
|
299
|
+
print("- Deferred / future: parked ideas, one line each.")
|
|
300
|
+
return 0
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
if __name__ == "__main__":
|
|
304
|
+
try:
|
|
305
|
+
sys.exit(main(sys.argv))
|
|
306
|
+
except SystemExit:
|
|
307
|
+
raise
|
|
308
|
+
except Exception as e: # never a stack trace for the agent to wade through
|
|
309
|
+
die(str(e))
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tf-status-facts.sh — print the facts PROJECT-STATUS.md is written from.
|
|
3
|
+
#
|
|
4
|
+
# bash .tfcore/utils/tf-status-facts.sh MyApp "build-phase"
|
|
5
|
+
#
|
|
6
|
+
# Prints the frontmatter values, the next command in both harness forms, the
|
|
7
|
+
# Open requirements section, the Verification log with its new row, and the
|
|
8
|
+
# library feedback lines, all copy-ready for the template
|
|
9
|
+
# .tfcore/templates/v4custom/app-project-status-tmpl.md. Writes nothing:
|
|
10
|
+
# PROJECT-STATUS.md is written with the harness Write tool (guard-status.sh
|
|
11
|
+
# refuses a shell write). Step 1 of _status-update-gate.md.
|
|
12
|
+
#
|
|
13
|
+
# Thin wrapper over tf-status-facts.py. Python 3 standard library only.
|
|
14
|
+
set -uo pipefail
|
|
15
|
+
SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
17
|
+
echo "tf-status-facts: python3 is required (standard library only)." >&2
|
|
18
|
+
exit 2
|
|
19
|
+
fi
|
|
20
|
+
exec python3 "$SELF_DIR/tf-status-facts.py" "$@"
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""tf-triage.py — log reported bugs in the checklist and the telemetry (see tf-triage.sh).
|
|
3
|
+
|
|
4
|
+
bash .tfcore/utils/tf-triage.sh <App> demote <REQ> "<symptom>" [--kind layout|render-empty|data-logic|rag]
|
|
5
|
+
[--evidence <path>] [--source owner|production] [--why <why_missed>] [--sort <sort>]
|
|
6
|
+
bash .tfcore/utils/tf-triage.sh <App> new "<title>" "<When … on <screen>, then …>" [--prefix UI|FN|NFR|RAG]
|
|
7
|
+
[--section "<screen>"] [--evidence <path>] [--source owner|production] [--mockup <file>] [--sort <sort>]
|
|
8
|
+
bash .tfcore/utils/tf-triage.sh <App> note <REQ> "<could not reproduce: what was tried>"
|
|
9
|
+
bash .tfcore/utils/tf-triage.sh <App> close [--started <ISO>] [--verify-ran] [--cmd triage-issues|fix-issues]
|
|
10
|
+
|
|
11
|
+
demote the row goes to Needs re-verify (% capped at 75) with a dated Remark holding the symptom in the
|
|
12
|
+
reporter's words, the evidence path and the kind.
|
|
13
|
+
new a defect no row covers: the next free id in the prefix, a Not Started row, a detail entry with the
|
|
14
|
+
acceptance line and the marker BRD-pending (*amend-docs gives it its BRD item).
|
|
15
|
+
note a dated remark on the nearest row, no status change (could not reproduce).
|
|
16
|
+
close what triage owes the telemetry, from the actions above (tests/.artifacts/verify/triage.json):
|
|
17
|
+
one gate record per demoted or new row with gate "escaped" (no check caught it), one miss per row
|
|
18
|
+
with found_by owner or production, the symptom as `what` and the sort (FR-32; --sort on demote or
|
|
19
|
+
new, else weak-check for a demoted row, whose line existed and whose check let it through, and
|
|
20
|
+
spec for a new row, which the spec never had) (duplicates collapsed through
|
|
21
|
+
tf-emit.sh --open-miss; the origin run looked up with --origin-of), then the run record. It also
|
|
22
|
+
lists every file under src/, source/ or tests/ changed since the start: a triage that edited code
|
|
23
|
+
is reported and logged as an instruction-ignored miss (FR-28).
|
|
24
|
+
Exit 0 · 2 could not run.
|
|
25
|
+
"""
|
|
26
|
+
import datetime
|
|
27
|
+
import importlib.util
|
|
28
|
+
import json
|
|
29
|
+
import os
|
|
30
|
+
import subprocess
|
|
31
|
+
import sys
|
|
32
|
+
|
|
33
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
34
|
+
LOGP = os.path.join("tests", ".artifacts", "verify", "triage.json")
|
|
35
|
+
KIND_CLASS = {"layout": "overlap", "render-empty": "blank-data", "data-logic": "assert-fail", "rag": "other", None: "other"}
|
|
36
|
+
EMIT = os.path.join(HERE, "tf-emit.sh")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def die(m):
|
|
40
|
+
print(f"tf-triage: {m}", file=sys.stderr)
|
|
41
|
+
sys.exit(2)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def mod():
|
|
45
|
+
spec = importlib.util.spec_from_file_location("tf_checklist_edit", os.path.join(HERE, "tf-checklist-edit.py"))
|
|
46
|
+
m = importlib.util.module_from_spec(spec)
|
|
47
|
+
spec.loader.exec_module(m)
|
|
48
|
+
return m
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def cfg_phase():
|
|
52
|
+
p = os.path.join(".tfcore", "core-config.yaml")
|
|
53
|
+
if os.path.isfile(p):
|
|
54
|
+
import re
|
|
55
|
+
m = re.search(r"(?m)^appPhase:\s*(\d+)", open(p, encoding="utf-8").read())
|
|
56
|
+
if m:
|
|
57
|
+
return int(m.group(1))
|
|
58
|
+
return 1
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def checklist(app):
|
|
62
|
+
ph = cfg_phase()
|
|
63
|
+
p = os.path.join("docs", f"{app}-Checklist.md" if ph <= 1 else f"{app}-P{ph}-Checklist.md")
|
|
64
|
+
if not os.path.isfile(p):
|
|
65
|
+
die(f"{p} does not exist")
|
|
66
|
+
return p
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def log_load():
|
|
70
|
+
try:
|
|
71
|
+
return json.load(open(LOGP, encoding="utf-8"))
|
|
72
|
+
except Exception:
|
|
73
|
+
return {"actions": []}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def log_save(d):
|
|
77
|
+
os.makedirs(os.path.dirname(LOGP), exist_ok=True)
|
|
78
|
+
json.dump(d, open(LOGP, "w", encoding="utf-8"), indent=1)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def opt(argv, name, default=None):
|
|
82
|
+
return argv[argv.index(name) + 1] if name in argv and argv.index(name) + 1 < len(argv) else default
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
SORTS = ("spec", "unsaid", "weak-check", "ignored")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def sort_opt(argv, default):
|
|
89
|
+
s = opt(argv, "--sort", default)
|
|
90
|
+
if s not in SORTS:
|
|
91
|
+
die(f"--sort must be one of {', '.join(SORTS)}")
|
|
92
|
+
return s
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def q(*args):
|
|
96
|
+
p = subprocess.run(["bash", EMIT, *args], capture_output=True, text=True)
|
|
97
|
+
return p.stdout.strip()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def emit(stream, rec):
|
|
101
|
+
p = subprocess.run(["bash", EMIT, stream], input=json.dumps(rec), capture_output=True, text=True)
|
|
102
|
+
msg = (p.stdout + p.stderr).strip()
|
|
103
|
+
ok = p.returncode == 0 and "refus" not in msg.lower() and "error" not in msg.lower()
|
|
104
|
+
if not ok:
|
|
105
|
+
print(f" {stream}: not written — {msg.splitlines()[0][:160] if msg else 'no reason printed'}")
|
|
106
|
+
return ok
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def main(argv):
|
|
110
|
+
if len(argv) < 3 or argv[1] in ("-h", "--help"):
|
|
111
|
+
print(__doc__)
|
|
112
|
+
return 0 if len(argv) > 1 else 2
|
|
113
|
+
app, verb = argv[1], argv[2]
|
|
114
|
+
ce = mod()
|
|
115
|
+
cl = checklist(app)
|
|
116
|
+
log = log_load()
|
|
117
|
+
log.setdefault("app", app)
|
|
118
|
+
now = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
119
|
+
|
|
120
|
+
if verb == "demote":
|
|
121
|
+
if len(argv) < 5:
|
|
122
|
+
die("demote needs <REQ> \"<symptom>\"")
|
|
123
|
+
rid, symptom = argv[3].upper(), argv[4]
|
|
124
|
+
kind = opt(argv, "--kind"); ev = opt(argv, "--evidence"); src = opt(argv, "--source", "owner")
|
|
125
|
+
prior = ce.demote(cl, rid, symptom, kind=kind, evidence=ev, source=src, prefix="⚠ prod bug" if src == "production" else "⚠ UAT bug")
|
|
126
|
+
if prior is None:
|
|
127
|
+
die(f"{rid} is not a row of {cl}")
|
|
128
|
+
log["actions"].append({"verb": "demote", "req_id": rid, "symptom": symptom, "kind": kind, "evidence": ev, "source": src,
|
|
129
|
+
"why": opt(argv, "--why", "insufficient-verify-method"), "sort": sort_opt(argv, "weak-check"),
|
|
130
|
+
"prior": prior["status"], "ts": now})
|
|
131
|
+
log_save(log)
|
|
132
|
+
print(f"{rid}: {prior['status']} → Needs re-verify — {symptom}")
|
|
133
|
+
return 0
|
|
134
|
+
|
|
135
|
+
if verb == "new":
|
|
136
|
+
if len(argv) < 5:
|
|
137
|
+
die("new needs \"<title>\" \"<acceptance>\"")
|
|
138
|
+
title, acc = argv[3], argv[4]
|
|
139
|
+
prefix = opt(argv, "--prefix", "FN").upper(); section = opt(argv, "--section"); ev = opt(argv, "--evidence")
|
|
140
|
+
src = opt(argv, "--source", "owner"); mockup = opt(argv, "--mockup")
|
|
141
|
+
if prefix == "UI" and not mockup:
|
|
142
|
+
print("note: a UI row needs a mockup link; pass --mockup docs/mockups/<screen>.html or the checker will flag it")
|
|
143
|
+
rid = ce.add_row(cl, prefix, title, acc, section=section, mockup=mockup,
|
|
144
|
+
remark=f"logged from {'production' if src == 'production' else 'UAT'} {ce.TODAY}" + (f" (evidence: {ev})" if ev else ""))
|
|
145
|
+
log["actions"].append({"verb": "new", "req_id": rid, "symptom": title, "kind": None, "evidence": ev, "source": src,
|
|
146
|
+
"why": opt(argv, "--why", "missing-checklist-item"), "sort": sort_opt(argv, "spec"), "prior": None, "ts": now})
|
|
147
|
+
log_save(log)
|
|
148
|
+
print(f"{rid}: new Not Started row — {title}")
|
|
149
|
+
return 0
|
|
150
|
+
|
|
151
|
+
if verb == "note":
|
|
152
|
+
if len(argv) < 5:
|
|
153
|
+
die("note needs <REQ> \"<text>\"")
|
|
154
|
+
rid = argv[3].upper()
|
|
155
|
+
if ce.note(cl, rid, argv[4]) is None:
|
|
156
|
+
die(f"{rid} is not a row of {cl}")
|
|
157
|
+
log["actions"].append({"verb": "note", "req_id": rid, "symptom": argv[4], "ts": now})
|
|
158
|
+
log_save(log)
|
|
159
|
+
print(f"{rid}: noted — {argv[4]}")
|
|
160
|
+
return 0
|
|
161
|
+
|
|
162
|
+
if verb == "close":
|
|
163
|
+
started = opt(argv, "--started") or subprocess.run(["bash", os.path.join(HERE, "tf-phase.sh"), "show"], capture_output=True, text=True).stdout
|
|
164
|
+
if started.startswith("{"):
|
|
165
|
+
import re
|
|
166
|
+
m = re.search(r'"started":"([^"]*)"', started)
|
|
167
|
+
started = m.group(1) if m else ""
|
|
168
|
+
started = started.strip() if started and not started.startswith("none") else ""
|
|
169
|
+
cmd = opt(argv, "--cmd", "triage-issues")
|
|
170
|
+
# code untouched?
|
|
171
|
+
changed = []
|
|
172
|
+
if started:
|
|
173
|
+
t0 = datetime.datetime.strptime(started, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=datetime.timezone.utc).timestamp()
|
|
174
|
+
for top in ("src", "source", "tests"):
|
|
175
|
+
for d, dirs, files in os.walk(top):
|
|
176
|
+
dirs[:] = [x for x in dirs if x not in ("bin", "obj", "node_modules", ".artifacts", "TestResults")]
|
|
177
|
+
for f in files:
|
|
178
|
+
p = os.path.join(d, f)
|
|
179
|
+
try:
|
|
180
|
+
if os.path.getmtime(p) > t0:
|
|
181
|
+
changed.append(p.replace("\\", "/"))
|
|
182
|
+
except OSError:
|
|
183
|
+
pass
|
|
184
|
+
acts = [a for a in log.get("actions", []) if a["verb"] in ("demote", "new")]
|
|
185
|
+
gates = misses = 0
|
|
186
|
+
for a in acts:
|
|
187
|
+
rid = a["req_id"]; cls_req = rid.split("-")[1]
|
|
188
|
+
att = q("--next-attempt", rid) or "1"
|
|
189
|
+
fc = KIND_CLASS.get(a.get("kind"), "other")
|
|
190
|
+
rec = {"kind": "gate", "app": app, "run_id": started, "req_id": rid, "req_class": cls_req,
|
|
191
|
+
"attempt": int(att) if att.isdigit() else 1, "verdict": "Needs re-verify" if a["verb"] == "demote" else "FAIL",
|
|
192
|
+
"gate": "escaped", "gates_run": [], "failure_class": fc, "prior_verdict": a.get("prior")}
|
|
193
|
+
if emit("gates", rec):
|
|
194
|
+
gates += 1
|
|
195
|
+
mc = ("unspecified-gap" if a["verb"] == "new" else "regression" if (a.get("prior") or "").lower().startswith("verified") else "wrong-behaviour")
|
|
196
|
+
opened = q("--open-miss", rid)
|
|
197
|
+
if opened and opened.split()[-1] == mc:
|
|
198
|
+
print(f" {rid}: already open as {opened.split()[0]} ({mc}); no second miss")
|
|
199
|
+
continue
|
|
200
|
+
mid = q("--next-miss-id")
|
|
201
|
+
m = {"kind": "miss", "miss_id": mid, "req_id": rid, "req_class": cls_req, "miss_class": mc,
|
|
202
|
+
"artifact": "brd" if a["verb"] == "new" else "src", "severity": "major", "why_missed": a.get("why"),
|
|
203
|
+
"origin_phase": "day1-greenfield" if a["verb"] == "new" else "build-phase", "origin_agent": "analyst" if a["verb"] == "new" else "flow-master",
|
|
204
|
+
"found_by": a.get("source", "owner"), "found_phase": cmd, "found_gate": None, "found_run_id": started,
|
|
205
|
+
"failure_class": fc, "what": a["symptom"][:300], "sort": a.get("sort") or ("spec" if a["verb"] == "new" else "weak-check")}
|
|
206
|
+
if a["verb"] != "new":
|
|
207
|
+
o = q("--origin-of", rid).split()
|
|
208
|
+
if len(o) >= 3:
|
|
209
|
+
m["origin_run_id"], m["origin_phase"], m["origin_agent"] = o[0], o[1], o[2]
|
|
210
|
+
if emit("misses", m):
|
|
211
|
+
misses += 1
|
|
212
|
+
a["miss_id"] = mid
|
|
213
|
+
if changed and cmd == "triage-issues" and not log.get("code_miss_id"):
|
|
214
|
+
mid = q("--next-miss-id")
|
|
215
|
+
log["code_miss_id"] = mid
|
|
216
|
+
emit("misses", {"kind": "miss", "miss_id": mid, "req_id": None, "req_class": None, "miss_class": "other", "artifact": "src",
|
|
217
|
+
"severity": "major", "why_missed": "instruction-ignored", "origin_phase": "triage-issues", "origin_agent": "flow-master",
|
|
218
|
+
"found_by": "gate", "found_phase": "triage-issues", "found_gate": None, "found_run_id": started, "failure_class": "other",
|
|
219
|
+
"what": f"triage edited {len(changed)} file(s) under src, source or tests, which a triage never does: " + ", ".join(changed[:5]),
|
|
220
|
+
"sort": "ignored"})
|
|
221
|
+
print(f"WARNING: {len(changed)} file(s) changed under src/, source/ or tests/ during this triage (logged as {mid}): " + ", ".join(changed[:5]))
|
|
222
|
+
if cmd == "triage-issues":
|
|
223
|
+
rec = {"kind": "run", "app": app, "cmd": "triage-issues", "mode": None, "ended": now, "reqs_touched": [a["req_id"] for a in acts],
|
|
224
|
+
"reqs_count": len(acts), "subagents": [], "files_written": 1, "build_result": "not-run"}
|
|
225
|
+
if started:
|
|
226
|
+
rec["started"] = started
|
|
227
|
+
already = False # one run record per start: a close called twice must not count the triage twice
|
|
228
|
+
runs_path = os.path.join(q("--where") or "docs/metrics", "runs.jsonl")
|
|
229
|
+
if started and os.path.isfile(runs_path):
|
|
230
|
+
already = any('"cmd":"triage-issues"' in l and f'"started":"{started}"' in l for l in open(runs_path, encoding="utf-8", errors="replace"))
|
|
231
|
+
ok = True if already else emit("runs", rec)
|
|
232
|
+
else:
|
|
233
|
+
ok = None
|
|
234
|
+
log["closed"] = now
|
|
235
|
+
log_save(log)
|
|
236
|
+
print(f"triage: {len(acts)} row(s) logged — {gates} gate record(s) (escaped), {misses} miss(es)"
|
|
237
|
+
+ (f", run record {'written' if ok else 'not written'}" if ok is not None else "") + f"; code untouched: {'yes' if not changed else 'NO'}")
|
|
238
|
+
return 0
|
|
239
|
+
die(f"unknown verb {verb}")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
if __name__ == "__main__":
|
|
243
|
+
sys.exit(main(sys.argv))
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tf-triage.sh — log reported bugs in the checklist and the telemetry (Sitting 4c, 2026-09-06).
|
|
3
|
+
#
|
|
4
|
+
# bash .tfcore/utils/tf-triage.sh <App> demote <REQ> "<symptom>" [--kind layout|render-empty|data-logic|rag] [--evidence <path>] [--source owner|production] [--why <why_missed>]
|
|
5
|
+
# bash .tfcore/utils/tf-triage.sh <App> new "<title>" "<When … on <screen>, then …>" [--prefix UI|FN|NFR|RAG] [--section "<screen>"] [--evidence <path>] [--mockup <file>]
|
|
6
|
+
# bash .tfcore/utils/tf-triage.sh <App> note <REQ> "<could not reproduce: what was tried>"
|
|
7
|
+
# bash .tfcore/utils/tf-triage.sh <App> close [--started <ISO>] [--cmd triage-issues|fix-issues]
|
|
8
|
+
#
|
|
9
|
+
# demote: Needs re-verify with a dated Remark in the reporter's words. new: a Not Started row with its
|
|
10
|
+
# acceptance line and BRD-pending. note: a remark, no status change. close: the escaped gate records,
|
|
11
|
+
# one miss per row (found by the owner, the symptom as `what`, duplicates collapsed), the run record,
|
|
12
|
+
# and a warning plus a miss when code changed during the triage. Details: tf-triage.py.
|
|
13
|
+
set -u
|
|
14
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
15
|
+
command -v python3 >/dev/null 2>&1 || { echo "tf-triage: python3 is required" >&2; exit 2; }
|
|
16
|
+
exec python3 "$HERE/tf-triage.py" "$@"
|