@techierathore/techieflow 1.0.4 → 1.1.1
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/flow-master.md +2 -0
- package/.claude/commands/TechieFlow/agents/verifier.md +2 -2
- package/.claude/commands/TechieFlow/tasks/_develop-mode.md +34 -0
- package/.claude/commands/TechieFlow/tasks/_owner-language.md +1 -0
- package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +4 -3
- package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +2 -2
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +2 -2
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +3 -3
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +5 -5
- package/.claude/commands/TechieFlow/tasks/develop-end-to-end.md +30 -0
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +3 -2
- package/.claude/commands/TechieFlow/tasks/generate-html.md +2 -2
- package/.claude/commands/TechieFlow/tasks/handoff-phase.md +2 -2
- package/.claude/commands/TechieFlow/tasks/mockups.md +2 -2
- package/.claude/commands/TechieFlow/tasks/productguide.md +2 -2
- package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +1 -1
- package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +2 -2
- package/.claude/commands/TechieFlow/tasks/triage-issues.md +2 -2
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +12 -11
- package/.opencode/plugin/techieflow.js +194 -14
- package/.tfcore/agents/flow-master.md +2 -0
- package/.tfcore/agents/verifier.md +2 -2
- package/.tfcore/core-config.yaml +2 -1
- package/.tfcore/hooks/block-git.sh +4 -2
- package/.tfcore/hooks/guard-build.sh +5 -1
- package/.tfcore/hooks/guard-db.sh +112 -13
- package/.tfcore/hooks/guard-status-html.sh +86 -2
- package/.tfcore/hooks/guard-verify-deps.sh +16 -1
- package/.tfcore/hooks/guard-verify.sh +8 -3
- package/.tfcore/hooks/sweep-artifacts.sh +46 -0
- package/.tfcore/hooks/yolo-allow.sh +27 -0
- package/.tfcore/standards/blazor-zoom.cs.txt +191 -0
- package/.tfcore/standards/coding-standards-dotnet.md +31 -3
- package/.tfcore/standards/owner-words.txt +23 -0
- package/.tfcore/tasks/_develop-mode.md +34 -0
- package/.tfcore/tasks/_owner-language.md +1 -0
- package/.tfcore/tasks/_status-update-gate.md +4 -3
- package/.tfcore/tasks/_yolo-mode.md +2 -2
- package/.tfcore/tasks/amend-docs.md +2 -2
- package/.tfcore/tasks/day1-brownfield.md +3 -3
- package/.tfcore/tasks/day1-greenfield.md +5 -5
- package/.tfcore/tasks/develop-end-to-end.md +30 -0
- package/.tfcore/tasks/fix-issues.md +3 -2
- package/.tfcore/tasks/generate-html.md +2 -2
- package/.tfcore/tasks/handoff-phase.md +2 -2
- package/.tfcore/tasks/mockups.md +2 -2
- package/.tfcore/tasks/productguide.md +2 -2
- package/.tfcore/tasks/render-workflow-docs.md +1 -1
- package/.tfcore/tasks/triage-and-fix.md +2 -2
- package/.tfcore/tasks/triage-issues.md +2 -2
- package/.tfcore/tasks/verify-phase.md +12 -11
- package/.tfcore/telemetry/SCHEMA.md +10 -2
- package/.tfcore/telemetry/install-metrics.sh +3 -2
- package/.tfcore/telemetry/tf-metrics.sh +38 -2
- package/.tfcore/templates/stack-defaults/dotnet.md +3 -1
- package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +6 -0
- package/.tfcore/templates/v4custom/app-architecture-tmpl.md +5 -0
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +7 -4
- package/.tfcore/templates/v4custom/app-checklist-tmpl.md +11 -0
- package/.tfcore/templates/v4custom/app-phase-brd-tmpl.md +94 -0
- package/.tfcore/templates/v4custom/app-phase-uidesign-tmpl.md +60 -0
- package/.tfcore/templates/v4custom/app-phases-tmpl.md +2 -1
- package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +4 -2
- package/.tfcore/templates/v4custom/build-subagent-prompt.md +2 -2
- package/.tfcore/templates/v4custom/html-render-shell.md +6 -2
- 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-split-brd.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/tf-appium.mjs +179 -0
- package/.tfcore/utils/tf-assets-browser.mjs +96 -0
- package/.tfcore/utils/tf-assets.sh +97 -12
- package/.tfcore/utils/tf-brd-status.py +14 -5
- package/.tfcore/utils/tf-build-list.py +95 -12
- package/.tfcore/utils/tf-build-list.sh +2 -1
- package/.tfcore/utils/tf-build.sh +163 -15
- package/.tfcore/utils/tf-checklist-edit.py +21 -3
- package/.tfcore/utils/tf-ci-repro.py +685 -0
- package/.tfcore/utils/tf-ci-repro.sh +24 -0
- package/.tfcore/utils/tf-day1-files.py +55 -28
- package/.tfcore/utils/tf-develop.sh +264 -0
- package/.tfcore/utils/tf-devguide-list.py +53 -24
- package/.tfcore/utils/tf-doc-check.py +218 -26
- package/.tfcore/utils/tf-doc-tests.sh +116 -0
- package/.tfcore/utils/tf-emit.sh +132 -45
- package/.tfcore/utils/tf-feedback.sh +13 -0
- package/.tfcore/utils/tf-fix-close.sh +87 -31
- package/.tfcore/utils/tf-goal.sh +12 -12
- package/.tfcore/utils/tf-harness.sh +9 -4
- package/.tfcore/utils/tf-lock.sh +29 -0
- package/.tfcore/utils/tf-log-miss.py +34 -8
- package/.tfcore/utils/tf-login.mjs +167 -0
- package/.tfcore/utils/tf-maccatalyst-check.sh +92 -0
- package/.tfcore/utils/tf-menus.mjs +401 -0
- package/.tfcore/utils/tf-misses-md.py +10 -4
- package/.tfcore/utils/tf-mockup-parity.mjs +763 -71
- package/.tfcore/utils/tf-mockup-parity.sh +25 -2
- package/.tfcore/utils/tf-owner-text.py +233 -0
- package/.tfcore/utils/tf-owner-text.sh +14 -0
- package/.tfcore/utils/tf-phase.sh +38 -4
- package/.tfcore/utils/tf-portable.sh +230 -0
- package/.tfcore/utils/tf-render-html.py +79 -5
- package/.tfcore/utils/tf-req-ids.py +36 -0
- package/.tfcore/utils/tf-selfcheck.sh +39 -6
- package/.tfcore/utils/tf-split-brd.py +11 -1
- package/.tfcore/utils/tf-stack-check.sh +3 -0
- package/.tfcore/utils/tf-status-evidence.sh +11 -9
- package/.tfcore/utils/tf-status-facts.py +160 -45
- package/.tfcore/utils/tf-theme.mjs +189 -0
- package/.tfcore/utils/tf-triage.py +74 -17
- package/.tfcore/utils/tf-triage.sh +2 -0
- package/.tfcore/utils/tf-verify-boot.sh +529 -54
- package/.tfcore/utils/tf-verify-emit.sh +57 -17
- package/.tfcore/utils/tf-verify-env.sh +33 -4
- package/.tfcore/utils/tf-verify-list.py +290 -24
- package/.tfcore/utils/tf-verify-list.sh +3 -1
- package/.tfcore/utils/tf-verify-native.mjs +307 -0
- package/.tfcore/utils/tf-verify-screens.mjs +341 -44
- package/.tfcore/utils/tf-verify-screens.sh +23 -5
- package/.tfcore/utils/tf-verify-tests.sh +310 -19
- package/.tfcore/utils/tf-verify-verdict.py +163 -21
- package/.tfcore/utils/tf-verify-zoom.mjs +193 -0
- package/.tfcore/utils/tf-verify-zoom.sh +29 -0
- package/.tfcore/utils/tf-yolo-alias.sh +84 -0
- package/.tfcore/utils/tf-yolo-perms.py +135 -0
- package/.tfcore/utils/tf-yolo-start.sh +44 -0
- package/.tfcore/utils/tf-yolo.sh +31 -3
- package/.tfcore/utils/tf_defect.py +27 -0
- package/.tfcore/utils/tf_feedback.py +272 -0
- package/.tfcore/utils/tf_roadmap.py +35 -0
- package/.tfcore/utils/tf_stack_check.py +93 -0
- package/README.md +6 -2
- package/docs/TechieFlow-Installation.md +5 -3
- package/opencode.jsonc +6 -2
- package/package.json +1 -1
- package/scripts/install.mjs +13 -2
- package/.tfcore/utils/__pycache__/tf-brd-status.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-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-render-html.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tf-yolo-alias.sh — put the `tfyolo` command in this machine's shell profile, once (owner, 2026-10-14).
|
|
3
|
+
#
|
|
4
|
+
# bash .tfcore/utils/tf-yolo-alias.sh install add it to ~/.bashrc and/or ~/.zshrc (idempotent)
|
|
5
|
+
# bash .tfcore/utils/tf-yolo-alias.sh remove take it out again
|
|
6
|
+
# bash .tfcore/utils/tf-yolo-alias.sh status say where it is
|
|
7
|
+
#
|
|
8
|
+
# `tfyolo` is a small shell function, the same text in bash and zsh. Typed anywhere inside a
|
|
9
|
+
# TechieFlow project, it finds the project's .tfcore/utils/tf-yolo-start.sh above the current folder
|
|
10
|
+
# and runs it: YOLO on, Claude Code started in bypass mode in this terminal, YOLO off when the session
|
|
11
|
+
# ends. `tfyolo -c` continues the last conversation. It never opens or closes a window: exit the
|
|
12
|
+
# session you are in (/exit), then type it in the same terminal, or type it in another tab.
|
|
13
|
+
# The function lives between two marker lines and is replaced whole on a second install, so an update
|
|
14
|
+
# to it reaches the profile by running install again. Profiles: ~/.zshrc when the login shell is zsh
|
|
15
|
+
# or the file exists (macOS), ~/.bashrc when the login shell is bash or the file exists (WSL, Linux).
|
|
16
|
+
# A new terminal picks it up; an open one needs `source ~/.bashrc` (or ~/.zshrc). Exit 0; 2 bad use.
|
|
17
|
+
|
|
18
|
+
set -u
|
|
19
|
+
BEGIN="# >>> TechieFlow tfyolo >>>"
|
|
20
|
+
END="# <<< TechieFlow tfyolo <<<"
|
|
21
|
+
HOME_DIR="${TF_HOME:-$HOME}"
|
|
22
|
+
|
|
23
|
+
block() {
|
|
24
|
+
cat <<'FN'
|
|
25
|
+
# >>> TechieFlow tfyolo >>>
|
|
26
|
+
# Start Claude Code in bypass mode with TechieFlow YOLO on, in the project around this folder.
|
|
27
|
+
# `tfyolo -c` continues the last conversation. Added by .tfcore/utils/tf-yolo-alias.sh; remove with
|
|
28
|
+
# `bash .tfcore/utils/tf-yolo-alias.sh remove`.
|
|
29
|
+
tfyolo() {
|
|
30
|
+
_tf_d="$PWD"
|
|
31
|
+
while [ "$_tf_d" != "/" ] && [ ! -f "$_tf_d/.tfcore/utils/tf-yolo-start.sh" ]; do _tf_d="$(dirname "$_tf_d")"; done
|
|
32
|
+
if [ ! -f "$_tf_d/.tfcore/utils/tf-yolo-start.sh" ]; then
|
|
33
|
+
echo "tfyolo: no TechieFlow project around $PWD (no .tfcore/utils/tf-yolo-start.sh above it)" >&2
|
|
34
|
+
unset _tf_d; return 2
|
|
35
|
+
fi
|
|
36
|
+
bash "$_tf_d/.tfcore/utils/tf-yolo-start.sh" "$@"
|
|
37
|
+
_tf_rc=$?; unset _tf_d; return $_tf_rc
|
|
38
|
+
}
|
|
39
|
+
# <<< TechieFlow tfyolo <<<
|
|
40
|
+
FN
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
profiles() {
|
|
44
|
+
local sh; sh="$(basename "${SHELL:-}")"
|
|
45
|
+
[[ "$sh" == zsh || -f "$HOME_DIR/.zshrc" ]] && echo "$HOME_DIR/.zshrc"
|
|
46
|
+
[[ "$sh" == bash || -f "$HOME_DIR/.bashrc" ]] && echo "$HOME_DIR/.bashrc"
|
|
47
|
+
[[ "$sh" != zsh && "$sh" != bash && ! -f "$HOME_DIR/.zshrc" && ! -f "$HOME_DIR/.bashrc" ]] && echo "$HOME_DIR/.bashrc"
|
|
48
|
+
return 0
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
strip() { # the file without our block
|
|
52
|
+
awk -v b="$BEGIN" -v e="$END" '$0==b{skip=1;next} $0==e{skip=0;next} !skip' "$1"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
case "${1:-status}" in
|
|
56
|
+
install)
|
|
57
|
+
while IFS= read -r p; do
|
|
58
|
+
[[ -n "$p" ]] || continue
|
|
59
|
+
tmp="$p.tftmp.$$"
|
|
60
|
+
if [[ -f "$p" ]]; then strip "$p" > "$tmp"; else : > "$tmp"; fi
|
|
61
|
+
# one blank line before the block, never a growing run of them
|
|
62
|
+
[[ -s "$tmp" && -n "$(tail -c 1 "$tmp")" ]] && echo "" >> "$tmp"
|
|
63
|
+
block >> "$tmp"
|
|
64
|
+
cat "$tmp" > "$p"; rm -f "$tmp"
|
|
65
|
+
echo "tfyolo: installed in $p (open a new terminal, or: source $p)"
|
|
66
|
+
done < <(profiles)
|
|
67
|
+
;;
|
|
68
|
+
remove)
|
|
69
|
+
for p in "$HOME_DIR/.bashrc" "$HOME_DIR/.zshrc"; do
|
|
70
|
+
[[ -f "$p" ]] && grep -qxF "$BEGIN" "$p" || continue
|
|
71
|
+
tmp="$p.tftmp.$$"; strip "$p" > "$tmp"; cat "$tmp" > "$p"; rm -f "$tmp"
|
|
72
|
+
echo "tfyolo: removed from $p"
|
|
73
|
+
done
|
|
74
|
+
;;
|
|
75
|
+
status)
|
|
76
|
+
found=0
|
|
77
|
+
for p in "$HOME_DIR/.bashrc" "$HOME_DIR/.zshrc"; do
|
|
78
|
+
[[ -f "$p" ]] && grep -qxF "$BEGIN" "$p" && { echo "tfyolo: in $p"; found=1; }
|
|
79
|
+
done
|
|
80
|
+
[[ $found -eq 1 ]] || echo "tfyolo: not installed (bash .tfcore/utils/tf-yolo-alias.sh install)"
|
|
81
|
+
;;
|
|
82
|
+
*) echo "usage: tf-yolo-alias.sh install|remove|status" >&2; exit 2 ;;
|
|
83
|
+
esac
|
|
84
|
+
exit 0
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""tf-yolo-perms.py — switch OpenCode to never ask, and back (owner, 2026-10-14). Run by tf-yolo.sh.
|
|
3
|
+
|
|
4
|
+
python3 .tfcore/utils/tf-yolo-perms.py on <project-root>
|
|
5
|
+
python3 .tfcore/utils/tf-yolo-perms.py off <project-root>
|
|
6
|
+
|
|
7
|
+
on OpenCode: the config file it reads LAST (it reads opencode.json, opencode.jsonc,
|
|
8
|
+
.opencode/opencode.json, .opencode/opencode.jsonc, in that order, the last matching rule
|
|
9
|
+
winning; checked on OpenCode 2.0.27) is rewritten with every "ask" made "allow", every "deny"
|
|
10
|
+
kept in its place, and edit, webfetch, external_directory and doom_loop allowed, in the root map
|
|
11
|
+
and in every agent's. In a project that file is the framework's own .opencode/opencode.jsonc.
|
|
12
|
+
Its exact text is saved once, in .tfcore/.session/yolo-perms.json; a second `on` keeps the
|
|
13
|
+
first saving.
|
|
14
|
+
Claude Code is NOT switched here: it ignores permissions.defaultMode "bypassPermissions" in
|
|
15
|
+
project and local settings ("only policy/user/flag settings may grant bypass mode", Claude Code
|
|
16
|
+
2.1.299, 2026-10-14). Its side is the hook .tfcore/hooks/yolo-allow.sh, and for the prompts a
|
|
17
|
+
hook cannot clear (ask rules, safety checks), a session started with
|
|
18
|
+
`claude --permission-mode bypassPermissions` (tf-goal.sh always starts it so).
|
|
19
|
+
off Puts the OpenCode file back as it was (one the updater replaced meanwhile is left as the updater
|
|
20
|
+
wrote it) and removes the saving. Without a saving it does nothing.
|
|
21
|
+
Exit 0 always: YOLO never fails a phase.
|
|
22
|
+
"""
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import re
|
|
26
|
+
import sys
|
|
27
|
+
|
|
28
|
+
MARK = "// tf-yolo.sh on rewrote this file so nothing asks while YOLO is on; tf-yolo.sh off puts the original back"
|
|
29
|
+
OPEN_ALLOW = ("edit", "webfetch", "external_directory", "doom_loop")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def jsonc(text):
|
|
33
|
+
"""JSON with // and /* */ comments and trailing commas, as opencode.jsonc is written"""
|
|
34
|
+
out, i, n, s = [], 0, len(text), None
|
|
35
|
+
while i < n:
|
|
36
|
+
c = text[i]
|
|
37
|
+
if s:
|
|
38
|
+
out.append(c)
|
|
39
|
+
if c == "\\" and i + 1 < n:
|
|
40
|
+
out.append(text[i + 1]); i += 2; continue
|
|
41
|
+
if c == s:
|
|
42
|
+
s = None
|
|
43
|
+
elif c == '"':
|
|
44
|
+
s = c; out.append(c)
|
|
45
|
+
elif text.startswith("//", i):
|
|
46
|
+
while i < n and text[i] != "\n":
|
|
47
|
+
i += 1
|
|
48
|
+
continue
|
|
49
|
+
elif text.startswith("/*", i):
|
|
50
|
+
j = text.find("*/", i + 2)
|
|
51
|
+
i = n if j < 0 else j + 2
|
|
52
|
+
continue
|
|
53
|
+
else:
|
|
54
|
+
out.append(c)
|
|
55
|
+
i += 1
|
|
56
|
+
return json.loads(re.sub(r",(\s*[}\]])", r"\1", "".join(out)))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def never_ask(perm):
|
|
60
|
+
"""a permission map with every "ask" made "allow"; order (and so every deny's place) kept"""
|
|
61
|
+
if perm == "ask":
|
|
62
|
+
return "allow"
|
|
63
|
+
if isinstance(perm, dict):
|
|
64
|
+
out = {k: never_ask(v) for k, v in perm.items()}
|
|
65
|
+
for k in OPEN_ALLOW:
|
|
66
|
+
if k not in out or out[k] == "ask":
|
|
67
|
+
out[k] = "allow"
|
|
68
|
+
return out
|
|
69
|
+
return perm
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def read(p):
|
|
73
|
+
try:
|
|
74
|
+
with open(p, encoding="utf-8") as f:
|
|
75
|
+
return f.read()
|
|
76
|
+
except OSError:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def write(p, text):
|
|
81
|
+
os.makedirs(os.path.dirname(p), exist_ok=True)
|
|
82
|
+
with open(p, "w", encoding="utf-8") as f:
|
|
83
|
+
f.write(text)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def main(argv):
|
|
87
|
+
if len(argv) < 3 or argv[1] not in ("on", "off"):
|
|
88
|
+
print(__doc__)
|
|
89
|
+
return 0
|
|
90
|
+
verb, root = argv[1], argv[2]
|
|
91
|
+
saving = os.path.join(root, ".tfcore", ".session", "yolo-perms.json")
|
|
92
|
+
oc = next((os.path.join(root, *p) for p in ((".opencode", "opencode.jsonc"), (".opencode", "opencode.json"),
|
|
93
|
+
("opencode.jsonc",), ("opencode.json",))
|
|
94
|
+
if os.path.isfile(os.path.join(root, *p))), None)
|
|
95
|
+
|
|
96
|
+
if verb == "on":
|
|
97
|
+
if not os.path.isfile(saving):
|
|
98
|
+
write(saving, json.dumps({"opencode_file": oc, "opencode_text": read(oc) if oc else None}, indent=1))
|
|
99
|
+
# OpenCode
|
|
100
|
+
text = read(oc) if oc else None
|
|
101
|
+
if text is not None and MARK not in text:
|
|
102
|
+
try:
|
|
103
|
+
cfg = jsonc(text)
|
|
104
|
+
except ValueError:
|
|
105
|
+
cfg = None
|
|
106
|
+
if isinstance(cfg, dict):
|
|
107
|
+
cfg["permission"] = never_ask(cfg.get("permission") or {})
|
|
108
|
+
for a in (cfg.get("agent") or {}).values():
|
|
109
|
+
if isinstance(a, dict) and a.get("permission"):
|
|
110
|
+
a["permission"] = never_ask(a["permission"])
|
|
111
|
+
write(oc, MARK + "\n" + json.dumps(cfg, indent=2) + "\n")
|
|
112
|
+
print("permissions: OpenCode set to never ask; Claude Code: the yolo-allow hook")
|
|
113
|
+
return 0
|
|
114
|
+
|
|
115
|
+
# off
|
|
116
|
+
if not os.path.isfile(saving):
|
|
117
|
+
return 0
|
|
118
|
+
try:
|
|
119
|
+
before = json.loads(read(saving) or "{}")
|
|
120
|
+
except ValueError:
|
|
121
|
+
before = {}
|
|
122
|
+
oc_file, oc_text = before.get("opencode_file"), before.get("opencode_text")
|
|
123
|
+
if oc_file and oc_text is not None and MARK in (read(oc_file) or ""):
|
|
124
|
+
write(oc_file, oc_text)
|
|
125
|
+
os.remove(saving)
|
|
126
|
+
print("permissions: OpenCode config put back as it was")
|
|
127
|
+
return 0
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
if __name__ == "__main__":
|
|
131
|
+
try:
|
|
132
|
+
sys.exit(main(sys.argv))
|
|
133
|
+
except Exception as e: # never a veto
|
|
134
|
+
print(f"tf-yolo-perms: {e}", file=sys.stderr)
|
|
135
|
+
sys.exit(0)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tf-yolo-start.sh — what `tfyolo` runs: Claude Code in bypass mode with YOLO on (owner, 2026-10-14).
|
|
3
|
+
#
|
|
4
|
+
# tfyolo start a new Claude Code session here, nothing asks
|
|
5
|
+
# tfyolo -c the same, continuing the last conversation in this folder (claude --continue)
|
|
6
|
+
# tfyolo <claude args> any other claude arguments are passed through unchanged
|
|
7
|
+
#
|
|
8
|
+
# Why it exists: Claude Code drops its permission prompts only for a session STARTED in bypass mode
|
|
9
|
+
# (`claude --permission-mode bypassPermissions`). Project settings cannot grant that mode (Claude Code
|
|
10
|
+
# 2.1.299 ignores it there), and `*yolo` typed into an open session cannot switch it. So `tfyolo`
|
|
11
|
+
# turns YOLO on (tf-yolo.sh on: the git-write ban stays, OpenCode stops asking too), starts Claude in
|
|
12
|
+
# this terminal, and turns YOLO off again when that session ends, however it ends.
|
|
13
|
+
#
|
|
14
|
+
# It refuses outside a TechieFlow project: the hooks that keep git writes refused live in the
|
|
15
|
+
# project, and bypass mode without them would let a commit through.
|
|
16
|
+
# The `tfyolo` command itself is a shell function put in ~/.bashrc / ~/.zshrc by tf-yolo-alias.sh.
|
|
17
|
+
# Exit: claude's own exit code; 2 when it could not start.
|
|
18
|
+
|
|
19
|
+
set -u
|
|
20
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
21
|
+
ROOT="$(cd "$HERE/../.." && pwd)"
|
|
22
|
+
if [[ ! -f "$ROOT/.tfcore/hooks/block-git.sh" || ! -f "$ROOT/.claude/settings.json" ]]; then
|
|
23
|
+
echo "tfyolo: $ROOT is not a TechieFlow project with its Claude Code hooks (no .tfcore/hooks/block-git.sh or .claude/settings.json); not starting in bypass mode." >&2
|
|
24
|
+
exit 2
|
|
25
|
+
fi
|
|
26
|
+
grep -q 'block-git.sh' "$ROOT/.claude/settings.json" 2>/dev/null || {
|
|
27
|
+
echo "tfyolo: $ROOT/.claude/settings.json does not register block-git.sh, so git writes would not be refused; run update-framework.sh on this project first." >&2
|
|
28
|
+
exit 2
|
|
29
|
+
}
|
|
30
|
+
CLAUDE_BIN="${TF_CLAUDE_BIN:-$(command -v claude 2>/dev/null)}"
|
|
31
|
+
[[ -n "$CLAUDE_BIN" ]] || { echo "tfyolo: the claude command is not on PATH" >&2; exit 2; }
|
|
32
|
+
|
|
33
|
+
export CLAUDE_PROJECT_DIR="$ROOT"
|
|
34
|
+
bash "$HERE/tf-yolo.sh" on --source tfyolo >/dev/null
|
|
35
|
+
off() { CLAUDE_PROJECT_DIR="$ROOT" bash "$HERE/tf-yolo.sh" off >/dev/null 2>&1; }
|
|
36
|
+
trap 'off' EXIT
|
|
37
|
+
trap 'off; exit 130' INT TERM
|
|
38
|
+
|
|
39
|
+
echo "tfyolo: YOLO on, Claude Code starting in bypass mode in $ROOT (git writes stay refused; YOLO goes off when this session ends)"
|
|
40
|
+
cd "$ROOT" || exit 2
|
|
41
|
+
"$CLAUDE_BIN" --permission-mode bypassPermissions "$@"
|
|
42
|
+
rc=$?
|
|
43
|
+
echo "tfyolo: session ended; YOLO off"
|
|
44
|
+
exit $rc
|
package/.tfcore/utils/tf-yolo.sh
CHANGED
|
@@ -8,6 +8,26 @@
|
|
|
8
8
|
# bash .tfcore/utils/tf-yolo.sh done [complete|blocked] ["<summary>"] goal finished → write the sentinel
|
|
9
9
|
# the tf-goal.sh supervisor stops on
|
|
10
10
|
# bash .tfcore/utils/tf-yolo.sh clear-done remove the sentinel (supervisor does this)
|
|
11
|
+
# bash .tfcore/utils/tf-yolo.sh sync put the harness permissions back when
|
|
12
|
+
# the flag has gone or expired (hooks call it)
|
|
13
|
+
#
|
|
14
|
+
# YOLO NEVER ASKS (owner, 2026-10-14: "if it's still asking for permission, what's the purpose of
|
|
15
|
+
# YOLO mode?"). While the flag is live both harnesses stop asking, and `off`, `done` and an expired
|
|
16
|
+
# flag put them back:
|
|
17
|
+
# - Claude Code: the hook .tfcore/hooks/yolo-allow.sh approves every tool call, in the session
|
|
18
|
+
# already running. Claude Code still asks where a hook's allow is not enough — an ask rule or one
|
|
19
|
+
# of its own safety checks ("requires the full permission pipeline") — and it refuses bypass mode
|
|
20
|
+
# from project or local settings ("only policy/user/flag settings may grant bypass mode"; both
|
|
21
|
+
# seen in its debug log, 2.1.299, 2026-10-14). Those prompts go only in a session started with
|
|
22
|
+
# `claude --permission-mode bypassPermissions`, which tf-goal.sh always uses.
|
|
23
|
+
# - OpenCode: the config file it reads last (.opencode/opencode.jsonc in a project) is rewritten
|
|
24
|
+
# with each "ask" turned to "allow" and every deny kept, and edits, web fetches, folders outside
|
|
25
|
+
# the project and repeated calls allowed; `off` puts its exact text back. OpenCode reads it when
|
|
26
|
+
# a session starts; the plugin's permission.ask approves everything while the flag is live
|
|
27
|
+
# (OpenCode 1.x).
|
|
28
|
+
# Version-control WRITES stay refused in both: the deny rules are kept, and block-git.sh refuses
|
|
29
|
+
# them before any permission check. What each file held before is kept under .tfcore/.session/ and
|
|
30
|
+
# restored exactly.
|
|
11
31
|
#
|
|
12
32
|
# YOLO is ON when ANY of these holds (checked in this order by block-git.sh too):
|
|
13
33
|
# 1. env TF_YOLO=1 (tf-goal.sh exports it for the whole run)
|
|
@@ -54,6 +74,8 @@ flag_live() { # exit 0 = flag present AND not expired
|
|
|
54
74
|
[[ -n "$(find "$FLAG" -mmin "+$(( TTL_HOURS * 60 ))" 2>/dev/null)" ]] && return 1
|
|
55
75
|
return 0
|
|
56
76
|
}
|
|
77
|
+
PERM_PY="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/tf-yolo-perms.py"
|
|
78
|
+
perms() { python3 "$PERM_PY" "$1" "$ROOT" 2>/dev/null || true; } # on | off; never fails the caller
|
|
57
79
|
json_escape() { printf '%s' "$1" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))' 2>/dev/null || printf '"%s"' "${1//\"/\\\"}"; }
|
|
58
80
|
|
|
59
81
|
cmd="${1:-status}"; shift || true
|
|
@@ -74,11 +96,16 @@ case "$cmd" in
|
|
|
74
96
|
"$(now_utc)" "$(json_escape "$SOURCE")" "$(json_escape "$GOAL")" "$(json_escape "${TF_HARNESS:-claude-code}")"
|
|
75
97
|
} > "$FLAG" 2>/dev/null || true
|
|
76
98
|
rm -f "$DONE" 2>/dev/null || true
|
|
77
|
-
|
|
99
|
+
perms on
|
|
100
|
+
echo "YOLO: ON (source=$SOURCE) — no confirmations; OpenCode never asks (from its next session); Claude Code approves every call it can (its own safety checks still ask unless the session was started with: claude --permission-mode bypassPermissions); deletes + read-only git allowed; git WRITES still blocked; run to completion."
|
|
78
101
|
;;
|
|
79
102
|
off)
|
|
80
103
|
rm -f "$FLAG" 2>/dev/null || true
|
|
81
|
-
|
|
104
|
+
perms off
|
|
105
|
+
echo "YOLO: OFF — back to ask-on-delete, no-git, confirm-at-phase-boundaries; OpenCode's asks restored."
|
|
106
|
+
;;
|
|
107
|
+
sync)
|
|
108
|
+
if [[ "${TF_YOLO:-0}" == "1" ]] || flag_live; then :; else perms off; fi
|
|
82
109
|
;;
|
|
83
110
|
is-on)
|
|
84
111
|
if [[ "${TF_YOLO:-0}" == "1" ]] || flag_live; then exit 0; else exit 1; fi
|
|
@@ -166,13 +193,14 @@ PY2
|
|
|
166
193
|
# unprompted and read-only git open and nothing on screen saying so. The
|
|
167
194
|
# supervisor reads $DONE, never $FLAG, so clearing it here stops nothing.
|
|
168
195
|
rm -f "$FLAG" 2>/dev/null || true
|
|
196
|
+
perms off
|
|
169
197
|
echo "GOAL-DONE ($OUTCOME) recorded at $DONE — the supervisor (tf-goal.sh) will stop. YOLO flag cleared."
|
|
170
198
|
;;
|
|
171
199
|
clear-done)
|
|
172
200
|
rm -f "$DONE" 2>/dev/null || true
|
|
173
201
|
;;
|
|
174
202
|
*)
|
|
175
|
-
echo "usage: tf-yolo.sh on|off|status|is-on|done [complete|blocked] [summary]|clear-done" >&2
|
|
203
|
+
echo "usage: tf-yolo.sh on|off|status|is-on|done [complete|blocked] [summary]|clear-done|sync" >&2
|
|
176
204
|
;;
|
|
177
205
|
esac
|
|
178
206
|
exit 0
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""tf_defect.py — the defect marks the framework's own writers put in a checklist row's Remarks.
|
|
2
|
+
|
|
3
|
+
devguide.md writes `⚠ DevGuide`, tf-checklist-edit's demote `⚠ UAT bug` / `⚠ prod bug` / `⚠ miss`,
|
|
4
|
+
tf-verify-verdict one mark per failing check, the smoke policy `⚠ visual`, security findings
|
|
5
|
+
`⚠ SECURITY`. A bare ⚠ is not a defect: hand-written remarks use it for "not verifiable here".
|
|
6
|
+
|
|
7
|
+
Imported by tf-status-facts.py (TF-019), tf-build-list.py (TF-020) and tf-verify-verdict.py (TF-021).
|
|
8
|
+
tests/regression/run.sh am_019, am_020, am_021.
|
|
9
|
+
"""
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
# found by a verify check: a new verify grades it again, so its verdict replaces the old mark
|
|
13
|
+
VERIFY = r"acceptance|build|render|assets|visual|mockup-parity|perf"
|
|
14
|
+
# found by another step (guide, owner's testing, production, a miss, a review): no verify sees it
|
|
15
|
+
OTHER = r"DevGuide|UAT bug|prod bug|miss \d{4}-|SECURITY"
|
|
16
|
+
DEFECT = re.compile(rf"⚠\s*(?:{OTHER}|{VERIFY})\b")
|
|
17
|
+
UNSEEN = re.compile(rf"⚠\s*(?:{OTHER})\b")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def clauses(remarks, pattern=DEFECT):
|
|
21
|
+
"""The defect clauses of a Remarks cell: each runs from its ⚠ mark to the next ⚠ or the end."""
|
|
22
|
+
return [c.strip().rstrip(";").strip() for c in re.split(r"(?=⚠)", remarks or "") if pattern.match(c)]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def unseen(remarks):
|
|
26
|
+
"""The clauses a verify cannot see, so a verify must neither erase them nor pass over them."""
|
|
27
|
+
return clauses(remarks, UNSEEN)
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""tf_feedback.py — the one reader of a project's upstream feedback files (2026-09-11).
|
|
3
|
+
|
|
4
|
+
bash .tfcore/utils/tf-feedback.sh <App> every entry and its state
|
|
5
|
+
bash .tfcore/utils/tf-feedback.sh <App> --waiting fixed upstream, not yet re-checked here
|
|
6
|
+
bash .tfcore/utils/tf-feedback.sh <App> --close <ID> "<what you ran and what it showed>"
|
|
7
|
+
|
|
8
|
+
An entry is one of three things, and every script that reports on these files reads them here:
|
|
9
|
+
|
|
10
|
+
open nobody upstream has answered it
|
|
11
|
+
fixed the upstream team's reply ("## Resolution status (…, <date>)") lists it, or its own body
|
|
12
|
+
says "fixed upstream" — the fix exists, and this project has not yet re-checked it
|
|
13
|
+
closed this project re-checked it and marked it closed (✅, "Closed <date>", "will not fix")
|
|
14
|
+
|
|
15
|
+
Why one reader. Until 2026-09-11 three scripts read these files three ways, and the one that writes
|
|
16
|
+
PROJECT-STATUS counted every `###` heading as an entry and accepted only a closing format no file
|
|
17
|
+
used: TfLens's status read "TechieFlow: 62 open of 62" for a file of 27 entries with 16 fixed, and
|
|
18
|
+
the agent reported two fixed problems to the owner as open (MISS-TechieFlow-20260911-04).
|
|
19
|
+
|
|
20
|
+
--close writes one line under the entry's heading and nothing else. Python 3 standard library only.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import datetime
|
|
25
|
+
import glob
|
|
26
|
+
import os
|
|
27
|
+
import re
|
|
28
|
+
import sys
|
|
29
|
+
|
|
30
|
+
# a heading is one line: [ \t], never \s, which runs past the line break and makes the next line
|
|
31
|
+
# of a bare "## TF-013" its title — its closing line included (TF-028)
|
|
32
|
+
ENTRY_HEAD = re.compile(r"(?m)^(#{2,3})[ \t]+`?([A-Z][A-Z0-9]*(?:-[A-Z0-9]+)*-\d+)\b[` \t—–-]*(.*)$")
|
|
33
|
+
REPLY_HEAD = re.compile(r"(?mi)^([ \t]*>[ \t]*)?#{2,3}[ \t]+[^\n]*\b(?:resolution status|resolved|repl(?:y|ies) from)\b[^\n]*")
|
|
34
|
+
ANY_ID = re.compile(r"(?<![\w-])([A-Z][A-Z0-9]*(?:-[A-Z]+)*-\d{2,})(?!\d)")
|
|
35
|
+
FIXED_WORDS = re.compile(r"(?i)\b(fixed|resolved|corrected|closes|closed|wired)\b")
|
|
36
|
+
EVERYTHING = re.compile(r"(?i)\beverything else is (?:now )?fixed\b|\ball (?:the )?others? (?:are )?(?:now )?fixed\b")
|
|
37
|
+
# a project's own mark that it re-checked the fix, that it will never be fixed, or that the entry
|
|
38
|
+
# was folded into another
|
|
39
|
+
CLOSED = re.compile(r"(?i)(✅|\bclosed (?:on )?\d{4}-\d{2}-\d{2}|is \*\*closed\*\*|\bwill not fix\b|\bwont-fix\b)")
|
|
40
|
+
MERGED = re.compile(r"(?i)^\s*merged into\b")
|
|
41
|
+
FIXED_IN_BODY = re.compile(r"(?i)\bfixed upstream\b")
|
|
42
|
+
STILL_OPEN = re.compile(r"(?i)\bopen\b|\bnot (?:yet )?(?:fixed|resolved)\b|\bunfixed\b")
|
|
43
|
+
BLOCKS = re.compile(r"(?im)^\s*[-*]\s*\*\*Blocks:?\*\*:?\s*(yes|no)\b")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def ids_in(t):
|
|
47
|
+
"""Every id the text names, shorthand included: "TR-RAG-028/029/030" and "TR-RAG-031..034"
|
|
48
|
+
(or "031–034") name each id they cover, not only the first."""
|
|
49
|
+
out = []
|
|
50
|
+
for m in re.finditer(r"(?<![\w-])([A-Z][A-Z0-9]*(?:-[A-Z]+)*)-(\d{2,})(?!\d)((?:/\d{2,})+|(?:\.\.|…|–|—)\d{2,})?", t):
|
|
51
|
+
pre, n, tail = m.group(1), m.group(2), m.group(3) or ""
|
|
52
|
+
out.append(f"{pre}-{n}")
|
|
53
|
+
w = len(n)
|
|
54
|
+
if tail.startswith("/"):
|
|
55
|
+
out += [f"{pre}-{x.zfill(w)}" for x in tail.strip("/").split("/")]
|
|
56
|
+
elif tail:
|
|
57
|
+
hi = int(re.sub(r"^\D+", "", tail))
|
|
58
|
+
if int(n) < hi <= int(n) + 50:
|
|
59
|
+
out += [f"{pre}-{str(x).zfill(w)}" for x in range(int(n) + 1, hi + 1)]
|
|
60
|
+
return out
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def unquoted(t):
|
|
64
|
+
"""The text without fenced code, inline code and double-quoted phrases."""
|
|
65
|
+
t = re.sub(r"(?ms)^[ \t]*```.*?^[ \t]*```", "", t)
|
|
66
|
+
t = re.sub(r"`[^`\n]*`", "", t)
|
|
67
|
+
return re.sub(r"[\"“][^\"”\n]*[\"”]", "", t)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def entries(path):
|
|
71
|
+
"""-> [{id, title, state, since, blocks, start, end}] in file order."""
|
|
72
|
+
text = open(path, encoding="utf-8", errors="replace").read()
|
|
73
|
+
heads = list(ENTRY_HEAD.finditer(text))
|
|
74
|
+
out = {}
|
|
75
|
+
for i, m in enumerate(heads):
|
|
76
|
+
end = heads[i + 1].start() if i + 1 < len(heads) else len(text)
|
|
77
|
+
nxt = re.search(r"(?m)^#{1,%d}\s" % len(m.group(1)), text[m.end():end])
|
|
78
|
+
body = text[m.end(): m.end() + nxt.start()] if nxt else text[m.end():end]
|
|
79
|
+
# quoted or code text is somebody's words, not a mark: `fixed upstream` in a repro, a test name
|
|
80
|
+
head = unquoted(body[:2500])
|
|
81
|
+
title = unquoted(m.group(3))
|
|
82
|
+
# the mark is read in the heading as well (TechieRag TF-001: "— **fixed upstream 2026-10-03**")
|
|
83
|
+
state = ("closed" if CLOSED.search(head) or CLOSED.search(title) or MERGED.search(m.group(3))
|
|
84
|
+
else "fixed" if FIXED_IN_BODY.search(head) or FIXED_IN_BODY.search(title) else "open")
|
|
85
|
+
b = BLOCKS.search(body)
|
|
86
|
+
out.setdefault(m.group(2), {"id": m.group(2), "title": m.group(3).strip(" `"), "state": state,
|
|
87
|
+
"since": "", "blocks": b.group(1).lower() if b else "",
|
|
88
|
+
"start": m.start(), "end": m.end() + len(body)})
|
|
89
|
+
# the upstream team's replies, in the shapes they have actually been written in
|
|
90
|
+
for rid, date in replied_fixed(text, list(out)):
|
|
91
|
+
e = out.get(rid)
|
|
92
|
+
if e and e["state"] == "open":
|
|
93
|
+
e["state"] = "fixed"
|
|
94
|
+
if e and not e["since"] and date:
|
|
95
|
+
e["since"] = date
|
|
96
|
+
return list(out.values())
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def reply_blocks(text):
|
|
100
|
+
"""-> [(date, block text)]. A reply is a heading naming a resolution or a reply —
|
|
101
|
+
"## Resolution status (TechieFlow team, 2026-09-09)", "## Replies from …", or a quoted
|
|
102
|
+
"> ## ✅ RESOLVED LIBRARY-SIDE 2026-08-31" — and runs to the next heading of its level,
|
|
103
|
+
or, when quoted, to the end of the quote."""
|
|
104
|
+
out, seen = [], set()
|
|
105
|
+
date_of = lambda h: (re.search(r"\d{4}-\d{2}-\d{2}", h) or [""])[0]
|
|
106
|
+
for m in REPLY_HEAD.finditer(text):
|
|
107
|
+
if m.start() in seen:
|
|
108
|
+
continue
|
|
109
|
+
rest = text[m.end():]
|
|
110
|
+
if m.group(1): # quoted: the block is the run of '>' lines after the heading's own line
|
|
111
|
+
rest = rest[1:] if rest.startswith("\n") else rest
|
|
112
|
+
q = re.match(r"(?:[ \t]*>[^\n]*\n?)*", rest)
|
|
113
|
+
out.append((date_of(m.group(0)), re.sub(r"(?m)^[ \t]*>[ \t]?", "", q.group(0))))
|
|
114
|
+
continue
|
|
115
|
+
level = len(re.match(r"[ \t]*(#+)", m.group(0)).group(1))
|
|
116
|
+
nxt = re.search(r"(?m)^#{1,%d}\s" % level, rest)
|
|
117
|
+
section = rest[: nxt.start()] if nxt else rest
|
|
118
|
+
if level == 2:
|
|
119
|
+
# A "## Replies from …" section holds dated "### " blocks, written months apart. Each is its
|
|
120
|
+
# own reply: an "all others fixed" kept in an old one must not reach entries a newer block
|
|
121
|
+
# merely names (TechieRag TF-001: Sevak's TR-RAG-047 read fixed when TR-RAG-048 was replied to).
|
|
122
|
+
subs = list(re.finditer(r"(?m)^###[ \t]+[^\n]*", section))
|
|
123
|
+
out.append((date_of(m.group(0)), section[: subs[0].start()] if subs else section))
|
|
124
|
+
for j, sm in enumerate(subs):
|
|
125
|
+
end = subs[j + 1].start() if j + 1 < len(subs) else len(section)
|
|
126
|
+
seen.add(m.end() + sm.start())
|
|
127
|
+
# the heading line stays in: "### TF-025 — fixed 2026-09-19" names the entry it answers
|
|
128
|
+
out.append((date_of(sm.group(0)) or date_of(m.group(0)), section[sm.start(): end]))
|
|
129
|
+
else:
|
|
130
|
+
out.append((date_of(m.group(0)), section))
|
|
131
|
+
return out
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def replied_fixed(text, known):
|
|
135
|
+
"""-> [(id, date)] for every entry a reply says is fixed. Inside a reply, an item — a table
|
|
136
|
+
row, a bullet, or a paragraph — fixes the ids it names when it is a table row or says fixed,
|
|
137
|
+
resolved, corrected or closes; "everything else is fixed" covers every entry up to the highest
|
|
138
|
+
number the reply mentions (entries filed after it stay open)."""
|
|
139
|
+
out = []
|
|
140
|
+
for date, body in reply_blocks(text):
|
|
141
|
+
items = re.split(r"\n\s*\n|\n(?=\s*[-*]\s)|\n(?=\s*\|)", body)
|
|
142
|
+
mentioned = []
|
|
143
|
+
for it in items:
|
|
144
|
+
ids = ids_in(it)
|
|
145
|
+
mentioned += ids
|
|
146
|
+
if not ids:
|
|
147
|
+
continue
|
|
148
|
+
if it.lstrip().startswith("|"):
|
|
149
|
+
first = ids_in(it.split("|")[1] if it.count("|") > 1 else "")
|
|
150
|
+
out += [(i, date) for i in first]
|
|
151
|
+
elif FIXED_WORDS.search(unquoted(it)) and not EVERYTHING.search(unquoted(it)):
|
|
152
|
+
# "Open: TR-RAG-001, TR-RAG-002. All others fixed": the ids are the exceptions.
|
|
153
|
+
# Read clause by clause: "1 OPEN: TR-RAG-001 …; 2 FIXED: TR-RAG-005 …" fixes 005 only,
|
|
154
|
+
# and "TR-RAG-035 (minor, OPEN)" beside "TR-RAG-034 (minor, FIXED)" stays open.
|
|
155
|
+
# A "fixed" clause naming no id answers the clause before it: "TR-RAG-010: every
|
|
156
|
+
# assembly carries its version. Fixed in 1.1.2." And "TR-RAG-017/019 → BRD-126 (fixed);
|
|
157
|
+
# TR-RAG-020…022 → BRD-127" fixes 017 and 019 only.
|
|
158
|
+
prev = []
|
|
159
|
+
for cl in re.split(r"(?<=[.;])\s+|\s+·\s+", it):
|
|
160
|
+
own_ids = ids_in(cl)
|
|
161
|
+
bare = unquoted(cl)
|
|
162
|
+
if FIXED_WORDS.search(bare) and not STILL_OPEN.search(bare):
|
|
163
|
+
out += [(i, date) for i in (own_ids or prev)]
|
|
164
|
+
if own_ids:
|
|
165
|
+
prev = [] if STILL_OPEN.search(bare) else own_ids
|
|
166
|
+
for it in items:
|
|
167
|
+
# a quoted or code "all others fixed" is somebody's words being cited, not this reply's
|
|
168
|
+
# claim: "…only because of the July "All others fixed app-side" line" fixed 13 entries the
|
|
169
|
+
# same block listed as still open (TechieRag TF-002)
|
|
170
|
+
if EVERYTHING.search(unquoted(it)) and mentioned:
|
|
171
|
+
# bounded by the ids of its own item, which it never covers ("all others"); an item
|
|
172
|
+
# naming none takes the block's. A running log in one block once let a July "all
|
|
173
|
+
# others fixed" reach every entry its later lines named (TechieRag TF-001).
|
|
174
|
+
own = ids_in(it)
|
|
175
|
+
top = {}
|
|
176
|
+
for i in own or mentioned:
|
|
177
|
+
p, n = i.rsplit("-", 1)
|
|
178
|
+
top[p] = max(top.get(p, 0), int(n))
|
|
179
|
+
for k in known:
|
|
180
|
+
p, n = k.rsplit("-", 1)
|
|
181
|
+
if p in top and int(n) <= top[p] and k not in own:
|
|
182
|
+
out.append((k, date))
|
|
183
|
+
return out
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def files(root, app=None):
|
|
187
|
+
pat = f"{app}-*-Feedback.md" if app else "*-Feedback.md"
|
|
188
|
+
return sorted(glob.glob(os.path.join(root, "docs", pat)))
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def upstream_of(path, app=None):
|
|
192
|
+
base = os.path.basename(path)
|
|
193
|
+
m = re.match(rf"^{re.escape(app)}-(.+)-Feedback\.md$", base) if app else re.match(r"^[^-]+-(.+)-Feedback\.md$", base)
|
|
194
|
+
return m.group(1) if m else base
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def state_map(root):
|
|
198
|
+
"""Every entry id across the project's feedback files -> (state, "<file>, <date>")."""
|
|
199
|
+
out = {}
|
|
200
|
+
for f in files(root):
|
|
201
|
+
rel = os.path.relpath(f, root).replace(os.sep, "/")
|
|
202
|
+
for e in entries(f):
|
|
203
|
+
out[e["id"]] = (e["state"], f"{rel}" + (f", Resolution status {e['since']}" if e["since"] else ""))
|
|
204
|
+
return out
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def summary_line(path, app):
|
|
208
|
+
es = entries(path)
|
|
209
|
+
n = {s: [e["id"] for e in es if e["state"] == s] for s in ("open", "fixed", "closed")}
|
|
210
|
+
rel = "docs/" + os.path.basename(path)
|
|
211
|
+
parts = [f"{len(n['open'])} open"]
|
|
212
|
+
if n["fixed"]:
|
|
213
|
+
ids = ", ".join(n["fixed"][:3]) + (" …" if len(n["fixed"]) > 3 else "")
|
|
214
|
+
parts.append(f"{len(n['fixed'])} fixed upstream, not yet re-checked ({ids})")
|
|
215
|
+
parts.append(f"{len(n['closed'])} closed")
|
|
216
|
+
return f"- {upstream_of(path, app)}: " + " · ".join(parts) + f" — {rel}"
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def close(path, rid, evidence):
|
|
220
|
+
text = open(path, encoding="utf-8").read()
|
|
221
|
+
e = next((x for x in entries(path) if x["id"] == rid), None)
|
|
222
|
+
if not e:
|
|
223
|
+
return f"tf-feedback: no entry {rid} in {os.path.basename(path)}", 2
|
|
224
|
+
if e["state"] == "closed":
|
|
225
|
+
return f"tf-feedback: {rid} is already closed; nothing written", 0
|
|
226
|
+
if not evidence.strip():
|
|
227
|
+
return "tf-feedback: say what you ran and what it showed; a close with no evidence is refused", 2
|
|
228
|
+
line_end = text.index("\n", e["start"]) + 1
|
|
229
|
+
today = datetime.date.today().isoformat()
|
|
230
|
+
mark = f"\n> ✅ **Closed {today}** — re-checked here: {evidence.strip()}\n"
|
|
231
|
+
open(path, "w", encoding="utf-8").write(text[:line_end] + mark + text[line_end:])
|
|
232
|
+
return f"tf-feedback: {rid} closed in {os.path.basename(path)}", 0
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def main(argv):
|
|
236
|
+
if not argv or argv[0] in ("-h", "--help"):
|
|
237
|
+
print(__doc__)
|
|
238
|
+
return 0 if argv else 2
|
|
239
|
+
app, rest = argv[0], argv[1:]
|
|
240
|
+
root = os.getcwd()
|
|
241
|
+
fs = files(root, app)
|
|
242
|
+
if not fs:
|
|
243
|
+
print(f"tf-feedback: no docs/{app}-*-Feedback.md here")
|
|
244
|
+
return 0
|
|
245
|
+
if rest[:1] == ["--close"]:
|
|
246
|
+
if len(rest) < 3:
|
|
247
|
+
print('usage: tf-feedback.sh <App> --close <ID> "<what you ran and what it showed>"')
|
|
248
|
+
return 2
|
|
249
|
+
for f in fs:
|
|
250
|
+
if any(e["id"] == rest[1] for e in entries(f)):
|
|
251
|
+
msg, rc = close(f, rest[1], rest[2])
|
|
252
|
+
print(msg)
|
|
253
|
+
return rc
|
|
254
|
+
print(f"tf-feedback: no entry {rest[1]} in any docs/{app}-*-Feedback.md")
|
|
255
|
+
return 2
|
|
256
|
+
waiting = rest[:1] == ["--waiting"]
|
|
257
|
+
for f in fs:
|
|
258
|
+
es = entries(f)
|
|
259
|
+
if waiting:
|
|
260
|
+
for e in es:
|
|
261
|
+
if e["state"] == "fixed":
|
|
262
|
+
print(f"{e['id']} fixed upstream{' ' + e['since'] if e['since'] else ''}, waiting to be "
|
|
263
|
+
f"re-checked here — {os.path.basename(f)}")
|
|
264
|
+
continue
|
|
265
|
+
print(summary_line(f, app))
|
|
266
|
+
for e in es:
|
|
267
|
+
print(f" {e['id']:<12} {e['state']:<7} {e['since'] or '':<10} {e['title'][:80]}")
|
|
268
|
+
return 0
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
if __name__ == "__main__":
|
|
272
|
+
sys.exit(main(sys.argv[1:]))
|