@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.
Files changed (148) hide show
  1. package/.claude/commands/TechieFlow/agents/flow-master.md +2 -0
  2. package/.claude/commands/TechieFlow/agents/verifier.md +2 -2
  3. package/.claude/commands/TechieFlow/tasks/_develop-mode.md +34 -0
  4. package/.claude/commands/TechieFlow/tasks/_owner-language.md +1 -0
  5. package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +4 -3
  6. package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +2 -2
  7. package/.claude/commands/TechieFlow/tasks/amend-docs.md +2 -2
  8. package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +3 -3
  9. package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +5 -5
  10. package/.claude/commands/TechieFlow/tasks/develop-end-to-end.md +30 -0
  11. package/.claude/commands/TechieFlow/tasks/fix-issues.md +3 -2
  12. package/.claude/commands/TechieFlow/tasks/generate-html.md +2 -2
  13. package/.claude/commands/TechieFlow/tasks/handoff-phase.md +2 -2
  14. package/.claude/commands/TechieFlow/tasks/mockups.md +2 -2
  15. package/.claude/commands/TechieFlow/tasks/productguide.md +2 -2
  16. package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +1 -1
  17. package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +2 -2
  18. package/.claude/commands/TechieFlow/tasks/triage-issues.md +2 -2
  19. package/.claude/commands/TechieFlow/tasks/verify-phase.md +12 -11
  20. package/.opencode/plugin/techieflow.js +194 -14
  21. package/.tfcore/agents/flow-master.md +2 -0
  22. package/.tfcore/agents/verifier.md +2 -2
  23. package/.tfcore/core-config.yaml +2 -1
  24. package/.tfcore/hooks/block-git.sh +4 -2
  25. package/.tfcore/hooks/guard-build.sh +5 -1
  26. package/.tfcore/hooks/guard-db.sh +112 -13
  27. package/.tfcore/hooks/guard-status-html.sh +86 -2
  28. package/.tfcore/hooks/guard-verify-deps.sh +16 -1
  29. package/.tfcore/hooks/guard-verify.sh +8 -3
  30. package/.tfcore/hooks/sweep-artifacts.sh +46 -0
  31. package/.tfcore/hooks/yolo-allow.sh +27 -0
  32. package/.tfcore/standards/blazor-zoom.cs.txt +191 -0
  33. package/.tfcore/standards/coding-standards-dotnet.md +31 -3
  34. package/.tfcore/standards/owner-words.txt +23 -0
  35. package/.tfcore/tasks/_develop-mode.md +34 -0
  36. package/.tfcore/tasks/_owner-language.md +1 -0
  37. package/.tfcore/tasks/_status-update-gate.md +4 -3
  38. package/.tfcore/tasks/_yolo-mode.md +2 -2
  39. package/.tfcore/tasks/amend-docs.md +2 -2
  40. package/.tfcore/tasks/day1-brownfield.md +3 -3
  41. package/.tfcore/tasks/day1-greenfield.md +5 -5
  42. package/.tfcore/tasks/develop-end-to-end.md +30 -0
  43. package/.tfcore/tasks/fix-issues.md +3 -2
  44. package/.tfcore/tasks/generate-html.md +2 -2
  45. package/.tfcore/tasks/handoff-phase.md +2 -2
  46. package/.tfcore/tasks/mockups.md +2 -2
  47. package/.tfcore/tasks/productguide.md +2 -2
  48. package/.tfcore/tasks/render-workflow-docs.md +1 -1
  49. package/.tfcore/tasks/triage-and-fix.md +2 -2
  50. package/.tfcore/tasks/triage-issues.md +2 -2
  51. package/.tfcore/tasks/verify-phase.md +12 -11
  52. package/.tfcore/telemetry/SCHEMA.md +10 -2
  53. package/.tfcore/telemetry/install-metrics.sh +3 -2
  54. package/.tfcore/telemetry/tf-metrics.sh +38 -2
  55. package/.tfcore/templates/stack-defaults/dotnet.md +3 -1
  56. package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +6 -0
  57. package/.tfcore/templates/v4custom/app-architecture-tmpl.md +5 -0
  58. package/.tfcore/templates/v4custom/app-brd-tmpl.md +7 -4
  59. package/.tfcore/templates/v4custom/app-checklist-tmpl.md +11 -0
  60. package/.tfcore/templates/v4custom/app-phase-brd-tmpl.md +94 -0
  61. package/.tfcore/templates/v4custom/app-phase-uidesign-tmpl.md +60 -0
  62. package/.tfcore/templates/v4custom/app-phases-tmpl.md +2 -1
  63. package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +4 -2
  64. package/.tfcore/templates/v4custom/build-subagent-prompt.md +2 -2
  65. package/.tfcore/templates/v4custom/html-render-shell.md +6 -2
  66. package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
  67. package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
  68. package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
  69. package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
  70. package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
  71. package/.tfcore/utils/tf-appium.mjs +179 -0
  72. package/.tfcore/utils/tf-assets-browser.mjs +96 -0
  73. package/.tfcore/utils/tf-assets.sh +97 -12
  74. package/.tfcore/utils/tf-brd-status.py +14 -5
  75. package/.tfcore/utils/tf-build-list.py +95 -12
  76. package/.tfcore/utils/tf-build-list.sh +2 -1
  77. package/.tfcore/utils/tf-build.sh +163 -15
  78. package/.tfcore/utils/tf-checklist-edit.py +21 -3
  79. package/.tfcore/utils/tf-ci-repro.py +685 -0
  80. package/.tfcore/utils/tf-ci-repro.sh +24 -0
  81. package/.tfcore/utils/tf-day1-files.py +55 -28
  82. package/.tfcore/utils/tf-develop.sh +264 -0
  83. package/.tfcore/utils/tf-devguide-list.py +53 -24
  84. package/.tfcore/utils/tf-doc-check.py +218 -26
  85. package/.tfcore/utils/tf-doc-tests.sh +116 -0
  86. package/.tfcore/utils/tf-emit.sh +132 -45
  87. package/.tfcore/utils/tf-feedback.sh +13 -0
  88. package/.tfcore/utils/tf-fix-close.sh +87 -31
  89. package/.tfcore/utils/tf-goal.sh +12 -12
  90. package/.tfcore/utils/tf-harness.sh +9 -4
  91. package/.tfcore/utils/tf-lock.sh +29 -0
  92. package/.tfcore/utils/tf-log-miss.py +34 -8
  93. package/.tfcore/utils/tf-login.mjs +167 -0
  94. package/.tfcore/utils/tf-maccatalyst-check.sh +92 -0
  95. package/.tfcore/utils/tf-menus.mjs +401 -0
  96. package/.tfcore/utils/tf-misses-md.py +10 -4
  97. package/.tfcore/utils/tf-mockup-parity.mjs +763 -71
  98. package/.tfcore/utils/tf-mockup-parity.sh +25 -2
  99. package/.tfcore/utils/tf-owner-text.py +233 -0
  100. package/.tfcore/utils/tf-owner-text.sh +14 -0
  101. package/.tfcore/utils/tf-phase.sh +38 -4
  102. package/.tfcore/utils/tf-portable.sh +230 -0
  103. package/.tfcore/utils/tf-render-html.py +79 -5
  104. package/.tfcore/utils/tf-req-ids.py +36 -0
  105. package/.tfcore/utils/tf-selfcheck.sh +39 -6
  106. package/.tfcore/utils/tf-split-brd.py +11 -1
  107. package/.tfcore/utils/tf-stack-check.sh +3 -0
  108. package/.tfcore/utils/tf-status-evidence.sh +11 -9
  109. package/.tfcore/utils/tf-status-facts.py +160 -45
  110. package/.tfcore/utils/tf-theme.mjs +189 -0
  111. package/.tfcore/utils/tf-triage.py +74 -17
  112. package/.tfcore/utils/tf-triage.sh +2 -0
  113. package/.tfcore/utils/tf-verify-boot.sh +529 -54
  114. package/.tfcore/utils/tf-verify-emit.sh +57 -17
  115. package/.tfcore/utils/tf-verify-env.sh +33 -4
  116. package/.tfcore/utils/tf-verify-list.py +290 -24
  117. package/.tfcore/utils/tf-verify-list.sh +3 -1
  118. package/.tfcore/utils/tf-verify-native.mjs +307 -0
  119. package/.tfcore/utils/tf-verify-screens.mjs +341 -44
  120. package/.tfcore/utils/tf-verify-screens.sh +23 -5
  121. package/.tfcore/utils/tf-verify-tests.sh +310 -19
  122. package/.tfcore/utils/tf-verify-verdict.py +163 -21
  123. package/.tfcore/utils/tf-verify-zoom.mjs +193 -0
  124. package/.tfcore/utils/tf-verify-zoom.sh +29 -0
  125. package/.tfcore/utils/tf-yolo-alias.sh +84 -0
  126. package/.tfcore/utils/tf-yolo-perms.py +135 -0
  127. package/.tfcore/utils/tf-yolo-start.sh +44 -0
  128. package/.tfcore/utils/tf-yolo.sh +31 -3
  129. package/.tfcore/utils/tf_defect.py +27 -0
  130. package/.tfcore/utils/tf_feedback.py +272 -0
  131. package/.tfcore/utils/tf_roadmap.py +35 -0
  132. package/.tfcore/utils/tf_stack_check.py +93 -0
  133. package/README.md +6 -2
  134. package/docs/TechieFlow-Installation.md +5 -3
  135. package/opencode.jsonc +6 -2
  136. package/package.json +1 -1
  137. package/scripts/install.mjs +13 -2
  138. package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
  139. package/.tfcore/utils/__pycache__/tf-codex-bind.cpython-312.pyc +0 -0
  140. package/.tfcore/utils/__pycache__/tf-codex-telemetry.cpython-312.pyc +0 -0
  141. package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
  142. package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
  143. package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
  144. package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
  145. package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
  146. package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
  147. package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
  148. 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
@@ -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
- echo "YOLO: ON (source=$SOURCE) — no confirmations; deletes + read-only git allowed; git WRITES still blocked; run to completion."
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
- echo "YOLO: OFF — back to ask-on-delete, no-git, confirm-at-phase-boundaries."
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:]))