@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,24 @@
1
+ #!/usr/bin/env bash
2
+ # tf-ci-repro.sh — reproduce a CI job here the way a fresh runner runs it (Lekhak TF-019, 2026-09-30).
3
+ #
4
+ # bash .tfcore/utils/tf-ci-repro.sh [<workflow.yml>] [--job <id>] [--list] [--keep] [--run-setup]
5
+ #
6
+ # Run from the repository root. A developer machine hides CI failures in its package caches and its
7
+ # build output: Lekhak's CI failed with NETSDK1112 because the win-x64 runtime pack was never
8
+ # downloaded, while the same commands passed locally where the pack was already cached. A local run
9
+ # with warm caches is therefore no proof that a CI failure is fixed. This script copies the repository
10
+ # without anything .gitignore ignores, to a folder outside it, points every package cache it knows
11
+ # (NuGet, npm, yarn, pnpm, pip, Go, Gradle, Maven) at an empty folder, and runs the job's `run:` steps
12
+ # in the runner's shell, stopping at the first failure. A Windows job runs Windows-side from WSL.
13
+ # `uses:` steps, steps that need a secret and steps that only install tools on the machine are
14
+ # skipped, each with the reason. --list prints the plan and runs nothing; --keep keeps the copy.
15
+ # Prints ONE verdict line last:
16
+ # PASS job <id>: <n> run step(s) passed on a clean copy with empty caches (…) — logs <dir> exit 0
17
+ # FAIL job <id>: step <n> "<name>" failed (exit N) with empty caches — <first error> — log <file> exit 1
18
+ # NOT-RUN <why: no workflow, several jobs, a macOS job off a Mac …> exit 2
19
+ # PyYAML is used when installed; without it (a stock Mac) a built-in reader takes the workflow.
20
+ # Logs: tests/.artifacts/ci-repro/<UTC time>/step-NN.log. The first run downloads every package, so
21
+ # it takes as long as a cold CI restore.
22
+ set -uo pipefail
23
+ case "${1:-}" in -h|--help) sed -n '2,21p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; esac
24
+ exec python3 "$(dirname "${BASH_SOURCE[0]}")/tf-ci-repro.py" "$@"
@@ -4,15 +4,17 @@
4
4
  bash .tfcore/utils/tf-day1-files.sh <App> [--size S|M|L] [--kind app|library] [--prefix obj|none] [--phase N]
5
5
  bash .tfcore/utils/tf-day1-files.sh --archive <file> [<file> ...]
6
6
 
7
- Default mode, for <App>:
8
- - .tfcore/core-config.yaml: customTechnicalDocuments (brd, architecture, codingStandards),
9
- devLoadAlwaysFiles, and appSize / appKind / appPhase when given.
7
+ Default mode, for <App>. Each flag does its own part and nothing else:
8
+ - --size, --kind, --phase set appSize, appKind, appPhase in .tfcore/core-config.yaml; --phase is the
9
+ phase being worked (handoff moves it on).
10
10
  - --size L also writes docs/<App>-Phases.md from app-phases-tmpl.md when it does not exist
11
11
  (the Large layout: docs/TechieFlow-Document-Schemas.md §2, §3.11). The agent fills the table.
12
- - --phase N sets appPhase, the phase being worked (handoff moves it on).
13
- - .editorconfig from app-editorconfig-tmpl.editorconfig (overwritten; machine config).
14
- - AGENTS.md and CLAUDE.md from their templates with {AppName} substituted and the
15
- field-prefix line resolved (--prefix, default obj). An existing copy is archived first.
12
+ - --prefix obj|none (day-1 stage 2) writes .editorconfig (overwritten; machine config), AGENTS.md and
13
+ CLAUDE.md from their templates with {AppName} substituted and the field-prefix line resolved, an
14
+ existing copy archived first; and adds the brd, architecture and codingStandards paths to
15
+ customTechnicalDocuments and devLoadAlwaysFiles where missing, keeping every entry already there.
16
+ Raising the size of a project with *amend-docs (--size L) once replaced Sevak's AGENTS.md and CLAUDE.md
17
+ with blank templates and cut its document list to three entries (Sevak TF-001). tests/regression sv_001.
16
18
  --archive: move each file (and its sibling .html) to docs/OldDocs/, date-suffixed when a
17
19
  file of that name is already there. Never asks; never writes a -v2 variant.
18
20
 
@@ -86,22 +88,45 @@ def set_key(text, key, value_lines):
86
88
  return "\n".join(out) + "\n"
87
89
 
88
90
 
89
- def update_config(app, size, kind, phase):
91
+ def block_of(text, key):
92
+ """The indented lines under a top-level YAML key, [] when the key is absent or empty."""
93
+ lines, out, inside = text.splitlines(), [], False
94
+ for line in lines:
95
+ if re.match(rf"^{re.escape(key)}:", line):
96
+ inside = True
97
+ continue
98
+ if inside:
99
+ if line.startswith((" ", "\t")):
100
+ out.append(line)
101
+ elif line.strip():
102
+ break
103
+ return out
104
+
105
+
106
+ def add_documents(s, app):
107
+ """The day-1 document paths, added where missing; every entry already listed is kept."""
108
+ docs = block_of(s, "customTechnicalDocuments")
109
+ have = {m.group(1) for m in (re.match(r"^\s+([\w-]+):", l) for l in docs) if m}
110
+ for k, v in (("brd", f"docs/{app}-BRD.md"), ("architecture", f"docs/{app}-Architecture.md"),
111
+ ("codingStandards", f"docs/{app}-Coding-Standards.md")):
112
+ if k not in have:
113
+ docs.append(f" {k}: {v}")
114
+ s = set_key(s, "customTechnicalDocuments", ["customTechnicalDocuments:"] + docs)
115
+ files = block_of(s, "devLoadAlwaysFiles")
116
+ listed = {l.strip().lstrip("-").strip() for l in files}
117
+ for f in (f"docs/{app}-Coding-Standards.md", f"docs/{app}-Architecture.md"):
118
+ if f not in listed:
119
+ files.append(f" - {f}")
120
+ return set_key(s, "devLoadAlwaysFiles", ["devLoadAlwaysFiles:"] + files)
121
+
122
+
123
+ def update_config(app, size, kind, phase, documents):
90
124
  p = os.path.join(".tfcore", "core-config.yaml")
91
125
  if not os.path.isfile(p):
92
126
  die(f"{p} does not exist at its literal path; is the framework installed here?")
93
127
  s = read(p)
94
- s = set_key(s, "customTechnicalDocuments", [
95
- "customTechnicalDocuments:",
96
- f" brd: docs/{app}-BRD.md",
97
- f" architecture: docs/{app}-Architecture.md",
98
- f" codingStandards: docs/{app}-Coding-Standards.md",
99
- ])
100
- s = set_key(s, "devLoadAlwaysFiles", [
101
- "devLoadAlwaysFiles:",
102
- f" - docs/{app}-Coding-Standards.md",
103
- f" - docs/{app}-Architecture.md",
104
- ])
128
+ if documents:
129
+ s = add_documents(s, app)
105
130
  if size:
106
131
  s = set_key(s, "appSize", [f"appSize: {size}"])
107
132
  if kind:
@@ -109,9 +134,9 @@ def update_config(app, size, kind, phase):
109
134
  if phase:
110
135
  s = set_key(s, "appPhase", [f"appPhase: {phase}"])
111
136
  write(p, s)
112
- print(f"tf-day1-files: .tfcore/core-config.yaml updated (documents for {app}"
113
- + (f", appSize {size}" if size else "") + (f", appKind {kind}" if kind else "")
114
- + (f", appPhase {phase}" if phase else "") + ")")
137
+ done = ([f"documents for {app}"] if documents else []) + ([f"appSize {size}"] if size else []) \
138
+ + ([f"appKind {kind}"] if kind else []) + ([f"appPhase {phase}"] if phase else [])
139
+ print(f"tf-day1-files: .tfcore/core-config.yaml updated ({', '.join(done)})")
115
140
 
116
141
 
117
142
  def write_phases_doc(app):
@@ -161,8 +186,7 @@ def main(argv):
161
186
  app = argv[1]
162
187
  if not re.fullmatch(r"[A-Z][A-Za-z0-9]*", app):
163
188
  die(f"app name {app!r} must be PascalCase with no spaces")
164
- size = kind = phase = None
165
- prefix = "obj"
189
+ size = kind = phase = prefix = None
166
190
  args = argv[2:]
167
191
  i = 0
168
192
  while i < len(args):
@@ -190,12 +214,15 @@ def main(argv):
190
214
  i += 2
191
215
  else:
192
216
  die(f"unknown argument {a!r}")
193
- update_config(app, size, kind, phase)
217
+ if not (size or kind or phase or prefix):
218
+ die("nothing to do: give --size, --kind, --phase or --prefix")
219
+ update_config(app, size, kind, phase, documents=bool(prefix))
194
220
  if size == "L":
195
221
  write_phases_doc(app)
196
- write_from_template(app, "app-editorconfig-tmpl.editorconfig", ".editorconfig", None)
197
- write_from_template(app, "app-agents-md-tmpl.md", "AGENTS.md", prefix)
198
- write_from_template(app, "app-claude-md-tmpl.md", "CLAUDE.md", prefix)
222
+ if prefix: # day-1 stage 2 only: a size, kind or phase change never rewrites these (Sevak TF-001)
223
+ write_from_template(app, "app-editorconfig-tmpl.editorconfig", ".editorconfig", None)
224
+ write_from_template(app, "app-agents-md-tmpl.md", "AGENTS.md", prefix)
225
+ write_from_template(app, "app-claude-md-tmpl.md", "CLAUDE.md", prefix)
199
226
  return 0
200
227
 
201
228
 
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env bash
2
+ # tf-develop.sh — *develop-end-to-end: a brief in, a greenfield app out, up to UAT, unattended (2026-10-08).
3
+ #
4
+ # bash .tfcore/utils/tf-develop.sh <app-dir> --app <App> --brief <file> [--harness claude|opencode]
5
+ # [--model <id>] [--tier standard] [--no-push] [--dry-run] [--detach]
6
+ # bash .tfcore/utils/tf-develop.sh <app-dir> --report # write the Build Report again from the saved state
7
+ # bash .tfcore/utils/tf-develop.sh <app-dir> --resume # continue a stopped run where it stopped
8
+ #
9
+ # The flow-master's *develop-end-to-end starts this; the owner never types it. It runs four phases, each
10
+ # one an unattended goal run (tf-goal.sh: no questions, survives usage limits by waiting for the reset
11
+ # and resuming the same session):
12
+ # day1 *day1-greenfield <App> BRD, Architecture, mockups — no owner review stop
13
+ # day1-2 *day1-greenfield <App> --stage2 checklist and the remaining documents
14
+ # build *build-phase <App> every row built, verified inline, FIX mode on failures
15
+ # handoff *handoff-phase <App> UsageGuide with its smoke checklist and test users: UAT-ready
16
+ # While it runs, .tfcore/.session/develop.json exists and every task follows .tfcore/tasks/_develop-mode.md.
17
+ #
18
+ # GIT — the one exception to "agents never write to git", for this command only. The agent still
19
+ # cannot commit: its git guard is unchanged. THIS script commits after each phase, and pushes, from
20
+ # its own shell, never through an agent's tool. When it ends, nothing is left switched on.
21
+ #
22
+ # METRICS — after every phase it writes docs/metrics/develop-report.json and docs/<App>-Build-Report.md:
23
+ # per phase the wall-clock time (limit waits included), the agent's working time and its tokens (summed
24
+ # from that phase's run records in docs/metrics/runs.jsonl), and the totals: "built in …".
25
+ #
26
+ # Exit 0 UAT-ready · 3 the agent declared a phase BLOCKED (the owner must act; listed in the report)
27
+ # · 4 a phase ran out of cycles · 5 the provider refused the model · 2 usage error.
28
+ set -uo pipefail
29
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
30
+ case "${1:-}" in
31
+ -h|--help) sed -n '2,27p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
32
+ "") sed -n '2,27p' "$0" | sed 's/^# \{0,1\}//'; exit 2 ;;
33
+ esac
34
+
35
+ DIR="$(cd "$1" 2>/dev/null && pwd)" || { echo "tf-develop: no such dir: $1" >&2; exit 2; }; shift
36
+ APP=""; BRIEF=""; HARNESS="claude"; MODEL=""; TIER="standard"; PUSH=1; DRY=0; RESUME=0; DETACH=0; REPORT_ONLY=0
37
+ ARGS_IN=("$@")
38
+ while [[ $# -gt 0 ]]; do
39
+ case "$1" in
40
+ --app) APP="${2:-}"; shift 2 ;;
41
+ --brief) BRIEF="${2:-}"; shift 2 ;;
42
+ --harness) HARNESS="${2:-}"; shift 2 ;;
43
+ --model) MODEL="${2:-}"; shift 2 ;;
44
+ --tier) TIER="${2:-}"; shift 2 ;;
45
+ --no-push) PUSH=0; shift ;;
46
+ --dry-run) DRY=1; shift ;;
47
+ --resume) RESUME=1; shift ;;
48
+ --detach) DETACH=1; shift ;;
49
+ --report) REPORT_ONLY=1; shift ;;
50
+ *) echo "tf-develop: unknown argument $1" >&2; exit 2 ;;
51
+ esac
52
+ done
53
+ # --detach: start this same run in its own session and return at once, so it outlives the agent session
54
+ # that started it (the flow-master's *develop-end-to-end), in a session of its own (tf_setsid, portable).
55
+ if [[ $DETACH -eq 1 ]]; then
56
+ mkdir -p "$DIR/.tfcore/.session"
57
+ rest=(); for a in "${ARGS_IN[@]}"; do [[ "$a" != --detach ]] && rest+=("$a"); done
58
+ source "$HERE/tf-portable.sh" # tf_setsid works on macOS too (a python3 or perl stand-in)
59
+ tf_setsid bash "$0" "$DIR" "${rest[@]}" > "$DIR/.tfcore/.session/develop.out" 2>&1 < /dev/null &
60
+ echo "tf-develop: started (pid $!) — follow .tfcore/.session/develop.log; the result is docs/<App>-Build-Report.md"
61
+ exit 0
62
+ fi
63
+ STATE="$DIR/.tfcore/.session/develop.json"
64
+ GOAL_SH="${TF_DEVELOP_GOAL_SH:-$DIR/.tfcore/utils/tf-goal.sh}" # tests point this at a fake
65
+ mkdir -p "$DIR/.tfcore/.session" "$DIR/docs/metrics"
66
+ ts() { date -u +%Y-%m-%dT%H:%M:%SZ; }
67
+ # The app folder must be the TOP of its own repository. A folder inside another repository answers
68
+ # rev-parse too, and `git add -A` there staged the whole outer repository (found by dev_001 on its first run).
69
+ own_repo() { local top; top="$(git -C "$DIR" rev-parse --show-toplevel 2>/dev/null)" || return 1
70
+ [[ -n "$top" && "$(cd "$top" && pwd -P)" == "$(cd "$DIR" && pwd -P)" ]]; }
71
+ log() { printf '[%s] tf-develop: %s\n' "$(ts)" "$*" | tee -a "$DIR/.tfcore/.session/develop.log" >&2; }
72
+
73
+ st() { python3 - "$STATE" "$@" <<'PY'
74
+ import json, sys
75
+ p, op, *a = sys.argv[1:]
76
+ try: d = json.load(open(p))
77
+ except Exception: d = {}
78
+ if op == "get": print(d.get(a[0], ""))
79
+ elif op == "init":
80
+ d = {"app": a[0], "brief": a[1], "harness": a[2], "model": a[3], "tier": a[4], "push": a[5] == "1", "started": a[6], "phases": []}
81
+ elif op == "done": print("yes" if any(x["name"] == a[0] and x["status"] == "done" for x in d.get("phases", [])) else "")
82
+ elif op == "attempts": print(sum(1 for x in d.get("phases", []) if x["name"] == a[0]))
83
+ elif op == "phase": # name status started ended exit commit pushed attempt — one entry per attempt, keyed by its number:
84
+ # a blocked attempt and its resume can start in the same second on a fast machine, and keying by the
85
+ # start made the second overwrite the first (dev_001 on the CI runner, 2026-10-09)
86
+ ph = d.get("phases", [])
87
+ new = dict(zip(("name", "status", "started", "ended", "goal_exit", "commit", "pushed", "attempt"), a))
88
+ hit = [i for i, x in enumerate(ph) if x["name"] == a[0] and str(x.get("attempt", "")) == a[7]]
89
+ if hit: ph[hit[0]] = new
90
+ else: ph.append(new)
91
+ d["phases"] = ph
92
+ if op in ("init", "phase"): json.dump(d, open(p, "w"), indent=1)
93
+ PY
94
+ }
95
+
96
+ if [[ $REPORT_ONLY -eq 1 ]]; then :
97
+ elif [[ $RESUME -eq 1 ]]; then
98
+ [[ -f "$STATE" ]] || { echo "tf-develop: --resume: no $STATE" >&2; exit 2; }
99
+ APP="$(st get app)"; BRIEF="$(st get brief)"; HARNESS="$(st get harness)"; MODEL="$(st get model)"; TIER="$(st get tier)"
100
+ [[ "$(st get push)" == "False" ]] && PUSH=0
101
+ else
102
+ [[ -n "$APP" && -n "$BRIEF" ]] || { echo "tf-develop: --app <App> and --brief <file> are required" >&2; exit 2; }
103
+ [[ "$APP" =~ ^[A-Z][A-Za-z0-9]*$ ]] || { echo "tf-develop: --app must be PascalCase with no spaces" >&2; exit 2; }
104
+ [[ -f "$BRIEF" ]] || { echo "tf-develop: brief not found: $BRIEF" >&2; exit 2; }
105
+ own_repo || { echo "tf-develop: $DIR is not a git repository of its own (a folder inside another repository does not count) — set it up first" >&2; exit 2; }
106
+ if [[ $PUSH -eq 1 && -z "$(git -C "$DIR" remote 2>/dev/null)" ]]; then echo "tf-develop: $DIR has no git remote to push to — add one, or pass --no-push" >&2; exit 2; fi
107
+ if [[ $DRY -eq 0 ]]; then
108
+ [[ "$(cd "$(dirname "$BRIEF")" && pwd)/$(basename "$BRIEF")" == "$DIR/docs/$APP-Brief.md" ]] || cp "$BRIEF" "$DIR/docs/$APP-Brief.md"
109
+ st init "$APP" "docs/$APP-Brief.md" "$HARNESS" "$MODEL" "$TIER" "$PUSH" "$(ts)"
110
+ fi
111
+ fi
112
+
113
+ MODE="Act as the flow-master (.tfcore/agents/flow-master.md). The command is *develop-end-to-end for $APP, in develop mode: read .tfcore/tasks/_develop-mode.md first and follow it in every task — no question to the owner, no stop for an owner review; every decision the owner would make is yours, written where that rule says. The brief is docs/$APP-Brief.md."
114
+ goal_of() {
115
+ case "$1" in
116
+ day1) echo "$MODE Run *day1-greenfield $APP, stage 1 (.tfcore/tasks/day1-greenfield.md), with the brief as the concept, all of it. When stage 1 is complete and rendered, the goal is met: do not stop for review and do not start stage 2." ;;
117
+ day1-2) echo "$MODE Run *day1-greenfield $APP --stage2. The goal is met when stage 2 is complete and its status gate has run." ;;
118
+ build) echo "$MODE Run *build-phase $APP (.tfcore/tasks/build-phase.md) on the whole phase checklist: every row built, the verifier chained inline, FIX mode on every failing row, until every row is terminal. Create and migrate the database the Architecture names, locally, as that rule says." ;;
119
+ handoff) echo "$MODE Run *handoff-phase $APP (.tfcore/tasks/handoff-phase.md): the UsageGuide with its smoke checklist and test users, the guides, the HTMLs, the status gate. The goal is met when the app is ready for the owner's UAT." ;;
120
+ esac
121
+ }
122
+
123
+ commit_phase() { # $1 phase → prints "<sha> <pushed yes|no|off>"
124
+ local sha="" pushed="off"
125
+ own_repo || { echo "none no"; return; } # never commit into a repository the app folder merely sits inside
126
+ if [[ -n "$(git -C "$DIR" status --porcelain 2>/dev/null)" ]]; then
127
+ git -C "$DIR" add -A >/dev/null 2>&1
128
+ git -C "$DIR" commit -q -m "TechieFlow develop-end-to-end: $1 done — $APP" -m "Written by tf-develop.sh after the $1 phase; per-phase time and tokens in docs/metrics/develop-report.json." >/dev/null 2>&1
129
+ fi
130
+ sha="$(git -C "$DIR" rev-parse --short HEAD 2>/dev/null)"
131
+ if [[ $PUSH -eq 1 ]]; then
132
+ if git -C "$DIR" rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1; then
133
+ git -C "$DIR" push -q >/dev/null 2>&1 && pushed=yes || pushed=no
134
+ else
135
+ git -C "$DIR" push -q -u "$(git -C "$DIR" remote | head -1)" HEAD >/dev/null 2>&1 && pushed=yes || pushed=no
136
+ fi
137
+ fi
138
+ echo "${sha:-none} $pushed"
139
+ }
140
+
141
+ report() { # writes docs/metrics/develop-report.json and docs/<App>-Build-Report.md from the state and runs.jsonl
142
+ python3 - "$STATE" "$DIR" <<'PY'
143
+ import datetime, json, os, sys
144
+ state, root = json.load(open(sys.argv[1])), sys.argv[2]
145
+ app = state["app"]
146
+ def t(s):
147
+ try: return datetime.datetime.strptime(s, "%Y-%m-%dT%H:%M:%SZ")
148
+ except Exception: return None
149
+ runs = []
150
+ try:
151
+ for line in open(os.path.join(root, "docs", "metrics", "runs.jsonl"), encoding="utf-8", errors="replace"):
152
+ try: r = json.loads(line)
153
+ except Exception: continue
154
+ if r.get("kind", "run") == "run" and r.get("app") in (None, app) and r.get("started"): runs.append(r)
155
+ except OSError: pass
156
+ voided = {(r.get("cmd"), r.get("started")) for r in runs if r.get("kind") == "run-void"}
157
+ def hm(s):
158
+ s = int(s or 0); return f"{s // 3600} h {s % 3600 // 60:02d} min" if s >= 3600 else f"{s // 60} min {s % 60:02d} s"
159
+ NAMES = {"day1": "Day 1, stage 1 (BRD, Architecture, mockups)", "day1-2": "Day 1, stage 2 (checklist, documents)",
160
+ "build": "Build and verify", "handoff": "Handoff (UsageGuide, UAT-ready)"}
161
+ out, tot = [], {"wall_s": 0, "work_s": 0, "tokens_in": 0, "tokens_out": 0, "tokens_cache_read": 0, "tokens_cache_write": 0, "runs": 0}
162
+ models = {}
163
+ # One row per phase, summing every attempt of it (a phase stopped "blocked" and resumed is run again; its
164
+ # first attempts' time and tokens are part of what the phase cost). A run record belongs to the attempt
165
+ # whose window holds its start, the window's first second included and its last excluded: a record that
166
+ # starts the second the previous phase ends belongs to the next one (the first proof run counted the
167
+ # build's 301,201 tokens under Day 1 stage 2).
168
+ order, attempts = [], {}
169
+ for x in state.get("phases", []):
170
+ if x["name"] not in attempts: order.append(x["name"])
171
+ attempts.setdefault(x["name"], []).append(x)
172
+ def inside(r, wins):
173
+ s = t(r.get("started"))
174
+ return s is not None and any(a <= s < b for a, b in wins)
175
+ for name in order:
176
+ ats = attempts[name]; p = ats[-1]
177
+ wins = [(t(x.get("started")), t(x.get("ended")) or datetime.datetime.utcnow()) for x in ats if t(x.get("started"))]
178
+ rs = [r for r in runs if (r.get("cmd"), r.get("started")) not in voided and inside(r, wins)]
179
+ # A record's tokens are measured over its own window of the session, so a command chained inside
180
+ # another (devguide inside handoff-phase) is counted twice if both are summed: a record lying wholly
181
+ # inside another is dropped, and working time is the union of the windows, not their sum.
182
+ span = lambda r: (t(r.get("started")), t(r.get("ended")) or t(r.get("started")))
183
+ rs = [r for i, r in enumerate(rs) if not any(
184
+ j != i and span(o)[0] and span(r)[0] and span(o)[0] <= span(r)[0] and span(r)[1] <= span(o)[1]
185
+ and (span(o) != span(r) or j < i) for j, o in enumerate(rs))]
186
+ iv = sorted(span(r) for r in rs if span(r)[0]); union, cur = 0, None
187
+ for a_, b_ in iv:
188
+ if cur and a_ <= cur[1]: cur = (cur[0], max(cur[1], b_))
189
+ else:
190
+ if cur: union += (cur[1] - cur[0]).total_seconds()
191
+ cur = (a_, b_)
192
+ if cur: union += (cur[1] - cur[0]).total_seconds()
193
+ row = {"phase": name, "title": NAMES.get(name, name), "status": p.get("status"), "attempts": len(ats),
194
+ "started": ats[0].get("started"), "ended": p.get("ended"),
195
+ "wall_s": int(sum((b - a).total_seconds() for a, b in wins)), "work_s": int(union),
196
+ "tokens_in": sum(int(r.get("tokens_in") or 0) for r in rs), "tokens_out": sum(int(r.get("tokens_out") or 0) for r in rs),
197
+ "tokens_cache_read": sum(int(r.get("tokens_cache_read") or 0) for r in rs),
198
+ "tokens_cache_write": sum(int(r.get("tokens_cache_write") or 0) for r in rs),
199
+ "runs": len(rs), "commands": sorted({r.get("cmd") for r in rs if r.get("cmd")}), "commit": p.get("commit"), "pushed": p.get("pushed")}
200
+ for r in rs:
201
+ for m, n in (r.get("model_tokens_out") or {r.get("model") or "unknown": r.get("tokens_out") or 0}).items():
202
+ models[m] = models.get(m, 0) + int(n or 0)
203
+ for k in tot: tot[k] += row[k]
204
+ out.append(row)
205
+ done = [p for p in out if p["status"] == "done"]
206
+ ready = len(done) == 4
207
+ starts = [t(p["started"]) for p in out if t(p["started"])]; ends = [t(p["ended"]) for p in out if t(p["ended"])]
208
+ span = int((max(ends) - min(starts)).total_seconds()) if starts and ends else 0
209
+ rep = {"app": app, "harness": state.get("harness"), "model": state.get("model") or None, "tier": state.get("tier"),
210
+ "started": state.get("started"), "status": "uat-ready" if ready else (out[-1]["status"] if out else "not-started"),
211
+ "elapsed_s": span, "totals": tot, "tokens_out_by_model": models, "phases": out,
212
+ "note": "elapsed_s is first phase start to last phase end, usage-limit waits included; work_s is the agent's own run time from its run records"}
213
+ json.dump(rep, open(os.path.join(root, "docs", "metrics", "develop-report.json"), "w"), indent=1)
214
+ L = [f"# {app} — Build Report", "", "| | |", "|---|---|",
215
+ f"| What | {app}, developed end to end from docs/{app}-Brief.md by *develop-end-to-end |",
216
+ f"| Result | {'Ready for UAT' if ready else 'Stopped at ' + (out[-1]['title'] if out else 'the start') + ' (' + rep['status'] + ')'} |",
217
+ f"| Built in | {hm(span)} from start to finish, of which the agent worked {hm(tot['work_s'])} |",
218
+ f"| Harness and model | {state.get('harness')}, {', '.join(sorted(models)) or state.get('model') or 'default'} |",
219
+ f"| Tokens | {tot['tokens_in']:,} in, {tot['tokens_out']:,} out, {tot['tokens_cache_read']:,} read from cache, {tot['tokens_cache_write']:,} written to cache |",
220
+ "", "## Phases", "", "| Phase | Result | Time | Agent working | Tokens out | Commit |", "|---|---|---|---|---|---|"]
221
+ for p in out:
222
+ L.append(f"| {p['title']}{' (' + str(p['attempts']) + ' attempts)' if p['attempts'] > 1 else ''} | {p['status']} | {hm(p['wall_s'])} | {hm(p['work_s'])} | {p['tokens_out']:,} | {p['commit'] or '—'}{'' if p['pushed'] in ('yes', 'off') else ' (not pushed)'} |")
223
+ L += ["", "Time is the clock from the phase's start to its end, summed over its attempts, waits for a usage limit to reset included. "
224
+ "Built in also counts the time between attempts, while a stopped run waited to be resumed. "
225
+ "Agent working is the sum of the phase's run records. The same figures, for a comparison harness, are in "
226
+ "`docs/metrics/develop-report.json`.", ""]
227
+ open(os.path.join(root, "docs", f"{app}-Build-Report.md"), "w", encoding="utf-8").write("\n".join(L))
228
+ print(f"report: {rep['status']}, {hm(span)} elapsed, {tot['tokens_out']:,} tokens out — docs/{app}-Build-Report.md")
229
+ PY
230
+ }
231
+
232
+ # No --model: the tier's routed model (.tfcore/routing.yaml), never the machine's own default. The proof
233
+ # run on OpenCode fell back to a local model server that was not running and failed every cycle.
234
+ if [[ -z "$MODEL" && -f "$DIR/.tfcore/utils/tf-model-pick.sh" ]]; then
235
+ MODEL="$(cd "$DIR" && bash .tfcore/utils/tf-model-pick.sh pick "$TIER" "$HARNESS" 2>/dev/null)"
236
+ [[ "$MODEL" == inherit ]] && MODEL=""
237
+ fi
238
+ # --report: write the report again from the saved state (the run's, or the finished run's), nothing else
239
+ if [[ $REPORT_ONLY -eq 1 ]]; then
240
+ [[ -f "$STATE" ]] || STATE="$DIR/.tfcore/.session/develop-done.json"
241
+ [[ -f "$STATE" ]] || { echo "tf-develop: --report: no develop run recorded here" >&2; exit 2; }
242
+ report; exit 0
243
+ fi
244
+ GOAL_ARGS=(--harness "$HARNESS" --tier "$TIER"); [[ -n "$MODEL" ]] && GOAL_ARGS+=(--model "$MODEL")
245
+ for phase in day1 day1-2 build handoff; do
246
+ if [[ $DRY -eq 1 ]]; then echo "$phase: bash $GOAL_SH ${GOAL_ARGS[*]} $DIR \"$(goal_of "$phase" | cut -c1-90)…\"; then commit$([[ $PUSH -eq 1 ]] && echo ' and push')"; continue; fi
247
+ [[ -n "$(st done "$phase")" ]] && { log "$phase already done — skipped"; continue; }
248
+ start="$(ts)"; log "$phase started"; attempt=$(( $(st attempts "$phase") + 1 ))
249
+ st phase "$phase" running "$start" "" "" "" "" "$attempt"
250
+ bash "$GOAL_SH" "${GOAL_ARGS[@]}" "$DIR" "$(goal_of "$phase")"; rc=$?
251
+ status=done; [[ $rc -eq 3 ]] && status=blocked; [[ $rc -ne 0 && $rc -ne 3 ]] && status="failed (exit $rc)"
252
+ end="$(ts)"
253
+ st phase "$phase" "$status" "$start" "$end" "$rc" "" "" "$attempt"
254
+ report >/dev/null # the phase's figures go into its own commit
255
+ read -r sha pushed <<<"$(commit_phase "$phase")"
256
+ st phase "$phase" "$status" "$start" "$end" "$rc" "$sha" "$pushed" "$attempt"
257
+ log "$phase $status — commit $sha, pushed $pushed"
258
+ if [[ $rc -ne 0 ]]; then report; commit_phase "report" >/dev/null; exit "$rc"; fi
259
+ done
260
+ [[ $DRY -eq 1 ]] && exit 0
261
+ report; read -r sha pushed <<<"$(commit_phase "build report")"
262
+ log "UAT-ready — final report commit $sha, pushed $pushed"
263
+ mv -f "$STATE" "$DIR/.tfcore/.session/develop-done.json" # develop mode ends with the run
264
+ exit 0
@@ -3,7 +3,7 @@
3
3
 
4
4
  bash .tfcore/utils/tf-devguide-list.sh <App> [--phase N] [--update]
5
5
 
6
- Reads appKind and appPhase from .tfcore/core-config.yaml and prints:
6
+ Reads appKind (or metrics.project_type) and appPhase from .tfcore/core-config.yaml and prints:
7
7
  app every routed page found in the code (route, file), matched to the phase's
8
8
  UIDesign screens and to the roles in the UsageGuide's Test users table;
9
9
  UIDesign screens with no code (a defect), routes with no UIDesign screen
@@ -50,9 +50,16 @@ def cfg(key, default):
50
50
  PRUNE = {"bin", "obj", "node_modules", ".git", ".artifacts", "OldDocs", "dist", ".tfcore", ".claude", ".opencode", "packages", "TestResults"}
51
51
 
52
52
 
53
+ # The folders a project keeps its code in, in the order tf-verify-boot.sh looks (Lekhak TF-014: Lekhak's
54
+ # is source/, and --update counted only src/).
55
+ CODE_ROOTS = ("src", "source")
56
+
57
+
53
58
  def walk(root, exts, skip_samples=False):
54
59
  for d, dirs, files in os.walk(root):
55
- dirs[:] = [x for x in dirs if x not in PRUNE] # prune in place: never descend into build output
60
+ # prune in place: never descend into build output, nor into any hidden folder (.tfbuild, .vs,
61
+ # .playwright …), which holds tool output, never a page (Lekhak TF-014)
62
+ dirs[:] = [x for x in dirs if x not in PRUNE and not x.startswith(".")]
56
63
  dd = d.replace("\\", "/") + "/"
57
64
  if any(s in dd for s in SKIP):
58
65
  continue
@@ -60,20 +67,28 @@ def walk(root, exts, skip_samples=False):
60
67
  continue
61
68
  for f in files:
62
69
  if f.lower().endswith(exts):
63
- yield os.path.join(d, f).replace("\\", "/").lstrip("./")
70
+ p = os.path.join(d, f).replace("\\", "/")
71
+ # only a leading "./" goes: lstrip("./") also ate the dot of a hidden folder, and
72
+ # .tfbuild/… became tfbuild/…, which does not exist (Lekhak TF-014)
73
+ yield p[2:] if p.startswith("./") else p
74
+
75
+
76
+ def code_files(exts, skip_samples=True):
77
+ """Every file with these extensions under the project's code folders (src/, source/)."""
78
+ for r in CODE_ROOTS:
79
+ if os.path.isdir(r):
80
+ yield from walk(r, exts, skip_samples=skip_samples)
64
81
 
65
82
 
66
83
  def roles_from_usageguide(app):
67
- p = os.path.join("docs", f"{app}-UsageGuide.md")
68
- roles = []
69
- if os.path.isfile(p):
70
- m = re.search(r"(?ms)^##\s+(?:\d+[.)]\s*)?Test users[^\n]*\n(.*?)(?=^## |\Z)", read(p))
71
- if m:
72
- for line in m.group(1).splitlines():
73
- c = [x.strip() for x in line.strip().strip("|").split("|")]
74
- if len(c) >= 4 and c[0].strip("`* ").isdigit():
75
- roles.append((c[1].strip("`* "), c[3].strip("`* ")))
76
- return roles
84
+ # the guide may be {App}-UsageGuide.md or {App}-Usage-Guide.md, its heading ## or ###, and its
85
+ # table the template's numbered shape or one read by its header names (AppManager TF-009):
86
+ # the same reader tf-verify-list.py uses
87
+ import importlib.util
88
+ spec = importlib.util.spec_from_file_location("tf_verify_list", os.path.join(os.path.dirname(os.path.abspath(__file__)), "tf-verify-list.py"))
89
+ vl = importlib.util.module_from_spec(spec)
90
+ spec.loader.exec_module(vl)
91
+ return [(u["user"], u["role"]) for u in vl.test_users(vl.usage_guide(app))]
77
92
 
78
93
 
79
94
  def screens_from_uidesign(app, phase):
@@ -88,9 +103,9 @@ def screens_from_uidesign(app, phase):
88
103
 
89
104
  def routes_in_code():
90
105
  found = []
91
- for f in walk(".", (".razor", ".cshtml", ".cs", ".ts", ".tsx", ".js", ".jsx", ".vue")):
92
- if "/" not in f:
93
- continue # a top-level file is never a page
106
+ for f in walk(".", (".razor", ".cshtml", ".cs", ".ts", ".tsx", ".js", ".jsx", ".vue"), skip_samples=True):
107
+ if "/" not in f or re.search(r"\.(spec|test)\.\w+$", f):
108
+ continue # a top-level file is never a page; a test's `path:` is a screenshot, not a route (TfLens TF-050)
94
109
  txt = read(f)
95
110
  for ext_re, pat, what in ROUTE_PATTERNS:
96
111
  if re.search(ext_re, f, re.I):
@@ -136,12 +151,16 @@ def main(argv):
136
151
  app = argv[1]
137
152
  update = "--update" in argv
138
153
  phase = int(argv[argv.index("--phase") + 1]) if "--phase" in argv and argv.index("--phase") + 1 < len(argv) else int(cfg("appPhase", "1"))
139
- kind = cfg("appKind", "app").lower()
154
+ kind = cfg("appKind", "").lower()
155
+ if not kind: # metrics.project_type: library says it when appKind is not set (TrBlazeUI TF-005)
156
+ pt = re.search(r"(?m)^\s+project_type:\s*(\S+)", read(os.path.join(".tfcore", "core-config.yaml"))) \
157
+ if os.path.isfile(os.path.join(".tfcore", "core-config.yaml")) else None
158
+ kind = pt.group(1).strip("'\"").lower() if pt else "app"
140
159
  if "--kind" in argv and argv.index("--kind") + 1 < len(argv):
141
160
  kind = argv[argv.index("--kind") + 1].lower() # override for a project whose config lacks it
142
161
  sub = "app"
143
162
  if kind.startswith("lib"):
144
- razors = [f for f in walk("src", (".razor",), skip_samples=True)] if os.path.isdir("src") else []
163
+ razors = list(code_files((".razor",)))
145
164
  sub = "ui-library" if razors else "service-library"
146
165
  print(f"# tf-devguide-list — {app} — kind {sub} — phase {phase}")
147
166
  print()
@@ -184,9 +203,9 @@ def main(argv):
184
203
  for k in sorted(endpoints):
185
204
  print(f"- {k} — {', '.join(sorted(set(endpoints[k]))[:2])}")
186
205
  elif sub == "ui-library":
187
- comps = [f for f in walk("src", (".razor",), skip_samples=True)]
206
+ comps = list(code_files((".razor",)))
188
207
  if not comps:
189
- print("NOTHING: no component under src/. Build first.")
208
+ print("NOTHING: no component under src/ or source/. Build first.")
190
209
  return 0
191
210
  samples = sample_files((".razor", ".cshtml", ".html"))
192
211
  stxt = {f: read(f) for f in samples}
@@ -196,9 +215,9 @@ def main(argv):
196
215
  shown = [s for s, t in stxt.items() if re.search(rf"<{re.escape(name)}\b", t)]
197
216
  print(f"- {os.path.dirname(f).split('/')[-1]} / {name} — {f} — " + (f"shown in {shown[0]}" + (f" (+{len(shown)-1})" if len(shown) > 1 else "") if shown else "NO SAMPLE shows it: a sample gap to log"))
198
217
  else:
199
- files = [f for f in walk("src", (".cs", ".ts", ".py", ".go", ".java"), skip_samples=True)]
218
+ files = list(code_files((".cs", ".ts", ".py", ".go", ".java")))
200
219
  if not files:
201
- print("NOTHING: no source under src/. Build first.")
220
+ print("NOTHING: no source under src/ or source/. Build first.")
202
221
  return 0
203
222
  items = []
204
223
  for f in files:
@@ -222,6 +241,16 @@ def main(argv):
222
241
  print()
223
242
  if ver is None:
224
243
  print(f"## --update: no existing guide at {gp} (or no \"Verified on\"); everything is new")
244
+ # a guide kept under another name or folder (Lekhak TF-014: docs/devguides/, one file per
245
+ # role) is named, so it is read and carried over, never silently rewritten from scratch
246
+ import glob
247
+ base = os.path.basename(gp)[:-len(".md")]
248
+ others = sorted(f for f in glob.glob(os.path.join("docs", "**", base + "*.md"), recursive=True)
249
+ if os.path.normpath(f) != os.path.normpath(gp))
250
+ if others:
251
+ print(f"- {len(others)} guide file(s) under another name: " + ", ".join(f.replace(os.sep, "/") for f in others[:8])
252
+ + (" …" if len(others) > 8 else ""))
253
+ print(f" read them and carry every entry that still matches the code into {gp}; do not start from nothing")
225
254
  else:
226
255
  print(f"## --update: existing guide {gp}, Verified on {ver}, {len(entries)} entries")
227
256
  try:
@@ -230,8 +259,8 @@ def main(argv):
230
259
  except Exception:
231
260
  cutoff = None
232
261
  if cutoff:
233
- changed = [f for f in walk(".", (".razor", ".cshtml", ".cs", ".ts", ".tsx", ".js", ".py"), skip_samples=False)
234
- if f.startswith(("src/",)) and os.path.getmtime(f) > cutoff]
262
+ changed = [f for f in code_files((".razor", ".cshtml", ".cs", ".ts", ".tsx", ".js", ".py"), skip_samples=False)
263
+ if os.path.getmtime(f) > cutoff]
235
264
  print(f"- {len(changed)} source file(s) changed since then; remap the entries they serve, keep the rest verbatim")
236
265
  for f in changed[:30]:
237
266
  print(f" - {f}")