@techierathore/techieflow 1.1.0 → 1.2.0

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 (106) 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/_smoke-test-policy.md +7 -5
  5. package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +2 -2
  6. package/.claude/commands/TechieFlow/tasks/amend-docs.md +1 -1
  7. package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +2 -2
  8. package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +4 -4
  9. package/.claude/commands/TechieFlow/tasks/develop-end-to-end.md +30 -0
  10. package/.claude/commands/TechieFlow/tasks/fix-issues.md +1 -0
  11. package/.claude/commands/TechieFlow/tasks/mockups.md +1 -1
  12. package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +2 -2
  13. package/.claude/commands/TechieFlow/tasks/verify-phase.md +12 -11
  14. package/.opencode/plugin/techieflow.js +15 -7
  15. package/.tfcore/agents/flow-master.md +2 -0
  16. package/.tfcore/agents/verifier.md +2 -2
  17. package/.tfcore/core-config.yaml +2 -1
  18. package/.tfcore/hooks/block-git.sh +25 -2
  19. package/.tfcore/hooks/guard-db.sh +106 -9
  20. package/.tfcore/hooks/guard-status-html.sh +5 -1
  21. package/.tfcore/hooks/guard-verify-deps.sh +16 -1
  22. package/.tfcore/hooks/yolo-allow.sh +27 -0
  23. package/.tfcore/standards/blazor-zoom.cs.txt +191 -0
  24. package/.tfcore/standards/coding-standards-dotnet.md +31 -3
  25. package/.tfcore/tasks/_develop-mode.md +34 -0
  26. package/.tfcore/tasks/_smoke-test-policy.md +7 -5
  27. package/.tfcore/tasks/_yolo-mode.md +2 -2
  28. package/.tfcore/tasks/amend-docs.md +1 -1
  29. package/.tfcore/tasks/day1-brownfield.md +2 -2
  30. package/.tfcore/tasks/day1-greenfield.md +4 -4
  31. package/.tfcore/tasks/develop-end-to-end.md +30 -0
  32. package/.tfcore/tasks/fix-issues.md +1 -0
  33. package/.tfcore/tasks/mockups.md +1 -1
  34. package/.tfcore/tasks/triage-and-fix.md +2 -2
  35. package/.tfcore/tasks/verify-phase.md +12 -11
  36. package/.tfcore/telemetry/SCHEMA.md +6 -2
  37. package/.tfcore/telemetry/tf-metrics.sh +19 -0
  38. package/.tfcore/templates/stack-defaults/dotnet.md +5 -1
  39. package/.tfcore/templates/v4custom/app-architecture-tmpl.md +5 -0
  40. package/.tfcore/templates/v4custom/app-brd-tmpl.md +2 -1
  41. package/.tfcore/templates/v4custom/app-checklist-tmpl.md +11 -0
  42. package/.tfcore/templates/v4custom/app-phases-tmpl.md +2 -1
  43. package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +4 -1
  44. package/.tfcore/templates/v4custom/html-render-shell.md +2 -0
  45. package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
  46. package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
  47. package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
  48. package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
  49. package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
  50. package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
  51. package/.tfcore/utils/tf-appium.mjs +179 -0
  52. package/.tfcore/utils/tf-brd-status.py +10 -1
  53. package/.tfcore/utils/tf-build-list.py +16 -1
  54. package/.tfcore/utils/tf-build.sh +75 -0
  55. package/.tfcore/utils/tf-checklist-edit.py +28 -3
  56. package/.tfcore/utils/tf-day1-files.py +55 -28
  57. package/.tfcore/utils/tf-develop.sh +264 -0
  58. package/.tfcore/utils/tf-devguide-list.py +6 -2
  59. package/.tfcore/utils/tf-doc-check.py +124 -21
  60. package/.tfcore/utils/tf-doc-tests.sh +40 -3
  61. package/.tfcore/utils/tf-emit.sh +40 -10
  62. package/.tfcore/utils/tf-fix-close.sh +13 -7
  63. package/.tfcore/utils/tf-maccatalyst-check.sh +92 -0
  64. package/.tfcore/utils/tf-menus.mjs +401 -0
  65. package/.tfcore/utils/tf-mockup-parity.mjs +361 -28
  66. package/.tfcore/utils/tf-mockup-parity.sh +14 -3
  67. package/.tfcore/utils/tf-owner-text.py +27 -5
  68. package/.tfcore/utils/tf-phase.sh +24 -4
  69. package/.tfcore/utils/tf-render-html.py +19 -3
  70. package/.tfcore/utils/tf-req-ids.py +36 -0
  71. package/.tfcore/utils/tf-stack-check.sh +3 -0
  72. package/.tfcore/utils/tf-status-facts.py +90 -15
  73. package/.tfcore/utils/tf-theme.mjs +189 -0
  74. package/.tfcore/utils/tf-triage.py +30 -4
  75. package/.tfcore/utils/tf-verify-boot.sh +296 -21
  76. package/.tfcore/utils/tf-verify-emit.sh +37 -17
  77. package/.tfcore/utils/tf-verify-list.py +219 -20
  78. package/.tfcore/utils/tf-verify-list.sh +3 -1
  79. package/.tfcore/utils/tf-verify-native.mjs +307 -0
  80. package/.tfcore/utils/tf-verify-screens.mjs +184 -20
  81. package/.tfcore/utils/tf-verify-screens.sh +17 -4
  82. package/.tfcore/utils/tf-verify-tests.sh +107 -17
  83. package/.tfcore/utils/tf-verify-verdict.py +118 -15
  84. package/.tfcore/utils/tf-verify-zoom.mjs +193 -0
  85. package/.tfcore/utils/tf-verify-zoom.sh +29 -0
  86. package/.tfcore/utils/tf-yolo-alias.sh +84 -0
  87. package/.tfcore/utils/tf-yolo-perms.py +135 -0
  88. package/.tfcore/utils/tf-yolo-start.sh +44 -0
  89. package/.tfcore/utils/tf-yolo.sh +31 -3
  90. package/.tfcore/utils/tf_feedback.py +86 -18
  91. package/.tfcore/utils/tf_stack_check.py +93 -0
  92. package/README.md +2 -2
  93. package/opencode.jsonc +6 -2
  94. package/package.json +1 -1
  95. package/scripts/install.mjs +14 -3
  96. package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
  97. package/.tfcore/utils/__pycache__/tf-codex-bind.cpython-312.pyc +0 -0
  98. package/.tfcore/utils/__pycache__/tf-codex-telemetry.cpython-312.pyc +0 -0
  99. package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
  100. package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
  101. package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
  102. package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
  103. package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
  104. package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
  105. package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
  106. package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
@@ -211,11 +211,26 @@ def main(argv):
211
211
  if any(r["id"].upper().startswith("REQ-UI-") for r in work):
212
212
  builder["UI"], why = ui_builder(app)
213
213
  print(f"UI rows go to: {builder['UI']} — {why}")
214
+ # A defect on a screen goes to one builder. Split by builder, the UI and the backend clusters of one
215
+ # page both carried its mockup defect, both edited the same files at once, and one left a component
216
+ # half-written that broke every other builder's boot for an hour (Chatur TF-004). Such a page is
217
+ # one cluster, built by the UI builder when one of its UI rows carries the defect.
218
+ cls_of = {r["id"]: re.match(r"REQ-(UI|FN|RAG|NFR)-", r["id"]).group(1) for r in work}
219
+ sec_of = {r["id"]: ent.get(r["id"].upper(), ("Other", "", "", ""))[0] for r in work}
220
+ fixed = {}
221
+ for r in work:
222
+ s, cls = sec_of[r["id"]], cls_of[r["id"]]
223
+ if r["defects"] and cls in ("UI", "FN") and (cls == "UI" or s not in fixed):
224
+ fixed[s] = builder[cls]
214
225
  for r in work:
215
226
  sec, acc, mock, brd = ent.get(r["id"].upper(), ("Other", "", "", ""))
216
227
  cls = re.match(r"REQ-(UI|FN|RAG|NFR)-", r["id"]).group(1)
217
- key = ("Non-functional" if cls == "NFR" else "RAG" if cls == "RAG" else sec, builder[cls])
228
+ who = fixed[sec] if cls in ("UI", "FN") and sec in fixed else builder[cls]
229
+ key = ("Non-functional" if cls == "NFR" else "RAG" if cls == "RAG" else sec, who)
218
230
  clusters.setdefault(key, []).append((r, acc, mock, brd))
231
+ for s in sorted(fixed):
232
+ if builder["UI"] != builder["FN"] and {"UI", "FN"} <= {cls_of[r["id"]] for r in work if sec_of[r["id"]] == s}:
233
+ print(f"One builder for {s}: it carries a defect, so its UI and backend rows go to {fixed[s]} together")
219
234
  print()
220
235
  print("## Clusters (one sub-agent each; spawn them all in one turn)")
221
236
  letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
@@ -108,6 +108,47 @@ if [[ "$MODE" == "probe" ]]; then
108
108
  fi
109
109
  [[ -z "$TARGET" && "$MODE" != "run" ]] && { echo "NOT-RUN no .sln, .slnx or .csproj here; name the target"; exit 2; }
110
110
 
111
+ # ---- test on a solution: one test project at a time (TechieRag TF-004) ----------------------------
112
+ # `dotnet test <solution>` runs every test project at once. On a 7.8 GB WSL machine TechieRag's local-
113
+ # model tests took the memory while TechieRag.Tests walked folders, and a test that passes alone failed
114
+ # with "Cannot allocate memory", twice in one day. Each test project now runs on its own, through this
115
+ # same script, and the verdicts are merged into one line naming one joined log: FAIL if any failed
116
+ # (naming which), else NOT-RUN if any could not run, else PASS. TF_TEST_TOGETHER=1 runs them together.
117
+ if [[ "$MODE" == "test" && "${TF_TEST_TOGETHER:-0}" != "1" && "$TARGET" == *.sln* ]]; then
118
+ tests=()
119
+ for p in ${projects[@]+"${projects[@]}"}; do
120
+ f="$p"; [[ -f "$f" ]] || f="$base/$p"
121
+ [[ -f "$f" ]] || continue
122
+ grep -qiE 'Microsoft\.NET\.Test\.Sdk|<IsTestProject>[[:space:]]*true|"(xunit|xunit\.v3|NUnit|MSTest(\.TestFramework)?|TUnit)"|Include="(xunit|xunit\.v3|NUnit|MSTest|TUnit)' "$f" && tests+=("$f")
123
+ done
124
+ if [[ ${#tests[@]} -gt 1 ]]; then
125
+ if [[ $PRINT -eq 1 ]]; then
126
+ for t in "${tests[@]}"; do echo "dotnet test $t ${EXTRA[*]:-}"; done; exit 0
127
+ fi
128
+ mkdir -p tests/.artifacts/build
129
+ JOINED="tests/.artifacts/build/$(date -u +%Y%m%dT%H%M%SZ)-test-split-$$.log"; : > "$JOINED"
130
+ fails=(); notrun=(); passed=0; lines=(); pass=()
131
+ [[ ${#EXTRA[@]} -gt 0 ]] && pass=(-- "${EXTRA[@]}")
132
+ for t in "${tests[@]}"; do
133
+ out="$(TF_TEST_TOGETHER=1 bash "${BASH_SOURCE[0]}" test "$t" ${pass[@]+"${pass[@]}"} 2>&1)"
134
+ v="$(grep -m1 -E '^(PASS|FAIL|NOT-RUN)' <<<"$out")"
135
+ lg="$(sed -n 's/.*log \(tests\/\.artifacts\/build\/[^ ;]*\).*/\1/p' <<<"$v" | head -1)"
136
+ { echo "### test project: $t"; [[ -n "$lg" && -f "$lg" ]] && cat "$lg" || echo "$out"; } >> "$JOINED"
137
+ lines+=(" $(basename "$t" .csproj): $v")
138
+ case "$v" in PASS*) passed=$((passed+1)) ;; FAIL*) fails+=("$(basename "$t" .csproj)") ;; *) notrun+=("$(basename "$t" .csproj)") ;; esac
139
+ done
140
+ if [[ ${#fails[@]} -gt 0 ]]; then
141
+ echo "FAIL tests failed in ${fails[*]} (${#tests[@]} test projects, each run on its own; $passed passed); log $JOINED"; rc=1
142
+ elif [[ ${#notrun[@]} -gt 0 ]]; then
143
+ echo "NOT-RUN ${notrun[*]} could not run (${#tests[@]} test projects, each run on its own; $passed passed); log $JOINED"; rc=2
144
+ else
145
+ echo "PASS test on $PLATFORM — ${#tests[@]} test projects, each run on its own, all passed; log $JOINED"; rc=0
146
+ fi
147
+ printf '%s\n' "${lines[@]}"
148
+ exit $rc
149
+ fi
150
+ fi
151
+
111
152
  # ---- the dotnet verb -------------------------------------------------------------------
112
153
  verb="$MODE"
113
154
  args=("$verb")
@@ -162,6 +203,26 @@ cross_side() { # side: clear what another side built with its own paths in it, t
162
203
  }
163
204
  WRONG_RUNG='NETSDK1178|Microsoft\.(iOS|Android|MacCatalyst|tvOS)\.Sdk|Workload ID|not recognized|WindowsAppSDK|command not found|No such file or directory|is not recognized as an internal or external command|The term .* is not recognized|workload.*not installed|Inadequate permissions'
164
205
 
206
+ # A Mac Catalyst build after Platforms/MacCatalyst/Info.plist (or an .entitlements file) changed: the
207
+ # incremental build refreshed the DLLs and kept the .app's old Info.plist, so a fix that lives there
208
+ # looked as if it had failed (Lekhak TF-026: the scene manifest was in the source and not in the
209
+ # bundle). When such a file is newer than the bundle, that target's bin/ and obj/ are cleared first.
210
+ mac_tfm="$(printf '%s\n' ${EXTRA[@]+"${EXTRA[@]}"} | grep -oE '^net[0-9.]+-maccatalyst[0-9.]*$' | head -1)"
211
+ if [[ -n "$mac_tfm" && "$MODE" == build && "$TARGET" == *.csproj ]]; then
212
+ pd="$(dirname "$TARGET")"
213
+ for cfgdir in "$pd"/bin/*/"$mac_tfm"; do
214
+ [[ -d "$cfgdir" ]] || continue
215
+ plist="$(ls -t "$cfgdir"/*.app/Contents/Info.plist "$cfgdir"/*/*.app/Contents/Info.plist 2>/dev/null | head -1)"
216
+ [[ -n "$plist" ]] || continue
217
+ newer="$(find "$pd/Platforms/MacCatalyst" -maxdepth 1 \( -name '*.plist' -o -name '*.entitlements' \) -newer "$plist" 2>/dev/null | head -1)"
218
+ if [[ -n "$newer" ]]; then
219
+ cfg="$(basename "$(dirname "$cfgdir")")"
220
+ rm -rf "$cfgdir" "$pd/obj/$cfg/$mac_tfm"
221
+ echo "note $newer is newer than the built app ($plist); cleared bin/$cfg/$mac_tfm and obj/$cfg/$mac_tfm so the bundle is built again"
222
+ fi
223
+ done
224
+ fi
225
+
165
226
  tried=()
166
227
  n=0
167
228
  for r in "${rungs[@]}"; do
@@ -183,6 +244,20 @@ for r in "${rungs[@]}"; do
183
244
  [[ "$rc" != "0" ]] && grep -qE "$LOCKED" <<<"$seg" || break
184
245
  [[ $attempt -lt ${TF_BUILD_LOCK_RETRIES:-2} ]] && sleep "${TF_BUILD_LOCK_WAIT:-15}"
185
246
  done
247
+ # A Mac Catalyst SDK names the one Xcode it was made for and refuses a newer one ("requires Xcode
248
+ # 26.6. The current version of Xcode is 27.0"): not the code, not the rung, so it fell through to
249
+ # NOT-RUN, while tf-verify-boot.sh built the same head with the check off (Lekhak TF-032). The same
250
+ # rung runs once more with the check off, and a note line says so before the verdict.
251
+ if [[ "$rc" != "0" && "$PLATFORM" == macos ]] && grep -qE 'requires Xcode [0-9.]+[.] The current version of Xcode is' <<<"$seg" \
252
+ && [[ " ${cmd[*]} " != *ValidateXcodeVersion* ]]; then
253
+ xnote="$(grep -oE 'requires Xcode [0-9.]+[.] The current version of Xcode is [0-9.]+' <<<"$seg" | head -1 | sed 's/[.]$//')"
254
+ echo "note the Mac Catalyst SDK $xnote; built again with -p:ValidateXcodeVersion=false (xcode-check=off)"
255
+ cmd+=(-p:ValidateXcodeVersion=false)
256
+ from=$(( $(wc -l < "$LOG") + 1 ))
257
+ { echo "### rung $n, Xcode check off: ${cmd[*]}"; "${cmd[@]}" 2>&1; echo "### exit $?"; } >> "$LOG" 2>&1
258
+ rc="$(tail -1 "$LOG" | sed 's/### exit //')"
259
+ seg="$(tail -n +"$from" "$LOG")"
260
+ fi
186
261
  if [[ "$rc" == "0" ]]; then
187
262
  warns="$(grep -cE 'warning [A-Z]+[0-9]+' <<<"$seg" || true)"
188
263
  echo "PASS $MODE on $PLATFORM via $label (rung $n) — $warns warning line(s); log $LOG"
@@ -72,8 +72,33 @@ def set_cells(path, rid, status=None, pct=None, remark=None, append_remark=None)
72
72
  return prior
73
73
 
74
74
 
75
- def next_id(text, prefix):
76
- ids = [int(m.group(1)) for m in re.finditer(rf"REQ-{prefix}-(\d{{3}})", text)]
75
+ def siblings(path):
76
+ """every checklist of the same application: docs/<App>-Checklist.md and docs/<App>-P<N>-Checklist.md"""
77
+ d, f = os.path.split(path)
78
+ m = re.match(r"^(.+?)(?:-P\d+)?-Checklist\.md$", f)
79
+ if not m:
80
+ return []
81
+ app = re.escape(m.group(1))
82
+ try:
83
+ names = os.listdir(d or ".")
84
+ except OSError:
85
+ return []
86
+ return [os.path.join(d, n) for n in sorted(names) if n != f and re.match(rf"^{app}(?:-P\d+)?-Checklist\.md$", n)]
87
+
88
+
89
+ def streams(path):
90
+ """the telemetry streams beside the checklists: an id a miss or gate record carries is taken even
91
+ after its row was renamed, since a record cannot be amended (TechieRag TF-005)"""
92
+ d = os.path.join(os.path.dirname(path) or ".", "metrics")
93
+ return [os.path.join(d, n) for n in ("gates.jsonl", "misses.jsonl", "runs.jsonl") if os.path.isfile(os.path.join(d, n))]
94
+
95
+
96
+ def next_id(text, prefix, others=()):
97
+ """one past the highest id of that prefix in this checklist AND every other checklist of the app:
98
+ counting this one alone gave REQ-UI-055 twice, once in phase 1 and once in phase 3 (Chatur TF-011)"""
99
+ ids = []
100
+ for t in (text, *(read(o) for o in others)):
101
+ ids += [int(m.group(1)) for m in re.finditer(rf"REQ-{prefix}-(\d{{3,}})", t)]
77
102
  return f"REQ-{prefix}-{(max(ids) + 1 if ids else 1):03d}"
78
103
 
79
104
 
@@ -81,7 +106,7 @@ def add_row(path, prefix, title, acceptance, section=None, remark=None, status="
81
106
  """append a row to the Requirements Status table and a detail entry under `section`
82
107
  (a new section at the end when it does not exist); returns the new id"""
83
108
  text = read(path)
84
- rid = next_id(text, prefix)
109
+ rid = next_id(text, prefix, siblings(path) + streams(path))
85
110
  nl = "\r\n" if "\r\n" in text else "\n"
86
111
  lines = text.split(nl)
87
112
  # the table: after the last REQ row
@@ -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
@@ -151,7 +151,11 @@ def main(argv):
151
151
  app = argv[1]
152
152
  update = "--update" in argv
153
153
  phase = int(argv[argv.index("--phase") + 1]) if "--phase" in argv and argv.index("--phase") + 1 < len(argv) else int(cfg("appPhase", "1"))
154
- 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"
155
159
  if "--kind" in argv and argv.index("--kind") + 1 < len(argv):
156
160
  kind = argv[argv.index("--kind") + 1].lower() # override for a project whose config lacks it
157
161
  sub = "app"