@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.
- package/.claude/commands/TechieFlow/agents/flow-master.md +2 -0
- package/.claude/commands/TechieFlow/agents/verifier.md +2 -2
- package/.claude/commands/TechieFlow/tasks/_develop-mode.md +34 -0
- package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +7 -5
- package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +2 -2
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +1 -1
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +2 -2
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +4 -4
- package/.claude/commands/TechieFlow/tasks/develop-end-to-end.md +30 -0
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +1 -0
- package/.claude/commands/TechieFlow/tasks/mockups.md +1 -1
- package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +2 -2
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +12 -11
- package/.opencode/plugin/techieflow.js +15 -7
- package/.tfcore/agents/flow-master.md +2 -0
- package/.tfcore/agents/verifier.md +2 -2
- package/.tfcore/core-config.yaml +2 -1
- package/.tfcore/hooks/block-git.sh +25 -2
- package/.tfcore/hooks/guard-db.sh +106 -9
- package/.tfcore/hooks/guard-status-html.sh +5 -1
- package/.tfcore/hooks/guard-verify-deps.sh +16 -1
- package/.tfcore/hooks/yolo-allow.sh +27 -0
- package/.tfcore/standards/blazor-zoom.cs.txt +191 -0
- package/.tfcore/standards/coding-standards-dotnet.md +31 -3
- package/.tfcore/tasks/_develop-mode.md +34 -0
- package/.tfcore/tasks/_smoke-test-policy.md +7 -5
- package/.tfcore/tasks/_yolo-mode.md +2 -2
- package/.tfcore/tasks/amend-docs.md +1 -1
- package/.tfcore/tasks/day1-brownfield.md +2 -2
- package/.tfcore/tasks/day1-greenfield.md +4 -4
- package/.tfcore/tasks/develop-end-to-end.md +30 -0
- package/.tfcore/tasks/fix-issues.md +1 -0
- package/.tfcore/tasks/mockups.md +1 -1
- package/.tfcore/tasks/triage-and-fix.md +2 -2
- package/.tfcore/tasks/verify-phase.md +12 -11
- package/.tfcore/telemetry/SCHEMA.md +6 -2
- package/.tfcore/telemetry/tf-metrics.sh +19 -0
- package/.tfcore/templates/stack-defaults/dotnet.md +5 -1
- package/.tfcore/templates/v4custom/app-architecture-tmpl.md +5 -0
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +2 -1
- package/.tfcore/templates/v4custom/app-checklist-tmpl.md +11 -0
- package/.tfcore/templates/v4custom/app-phases-tmpl.md +2 -1
- package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +4 -1
- package/.tfcore/templates/v4custom/html-render-shell.md +2 -0
- package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/tf-appium.mjs +179 -0
- package/.tfcore/utils/tf-brd-status.py +10 -1
- package/.tfcore/utils/tf-build-list.py +16 -1
- package/.tfcore/utils/tf-build.sh +75 -0
- package/.tfcore/utils/tf-checklist-edit.py +28 -3
- package/.tfcore/utils/tf-day1-files.py +55 -28
- package/.tfcore/utils/tf-develop.sh +264 -0
- package/.tfcore/utils/tf-devguide-list.py +6 -2
- package/.tfcore/utils/tf-doc-check.py +124 -21
- package/.tfcore/utils/tf-doc-tests.sh +40 -3
- package/.tfcore/utils/tf-emit.sh +40 -10
- package/.tfcore/utils/tf-fix-close.sh +13 -7
- package/.tfcore/utils/tf-maccatalyst-check.sh +92 -0
- package/.tfcore/utils/tf-menus.mjs +401 -0
- package/.tfcore/utils/tf-mockup-parity.mjs +361 -28
- package/.tfcore/utils/tf-mockup-parity.sh +14 -3
- package/.tfcore/utils/tf-owner-text.py +27 -5
- package/.tfcore/utils/tf-phase.sh +24 -4
- package/.tfcore/utils/tf-render-html.py +19 -3
- package/.tfcore/utils/tf-req-ids.py +36 -0
- package/.tfcore/utils/tf-stack-check.sh +3 -0
- package/.tfcore/utils/tf-status-facts.py +90 -15
- package/.tfcore/utils/tf-theme.mjs +189 -0
- package/.tfcore/utils/tf-triage.py +30 -4
- package/.tfcore/utils/tf-verify-boot.sh +296 -21
- package/.tfcore/utils/tf-verify-emit.sh +37 -17
- package/.tfcore/utils/tf-verify-list.py +219 -20
- package/.tfcore/utils/tf-verify-list.sh +3 -1
- package/.tfcore/utils/tf-verify-native.mjs +307 -0
- package/.tfcore/utils/tf-verify-screens.mjs +184 -20
- package/.tfcore/utils/tf-verify-screens.sh +17 -4
- package/.tfcore/utils/tf-verify-tests.sh +107 -17
- package/.tfcore/utils/tf-verify-verdict.py +118 -15
- package/.tfcore/utils/tf-verify-zoom.mjs +193 -0
- package/.tfcore/utils/tf-verify-zoom.sh +29 -0
- package/.tfcore/utils/tf-yolo-alias.sh +84 -0
- package/.tfcore/utils/tf-yolo-perms.py +135 -0
- package/.tfcore/utils/tf-yolo-start.sh +44 -0
- package/.tfcore/utils/tf-yolo.sh +31 -3
- package/.tfcore/utils/tf_feedback.py +86 -18
- package/.tfcore/utils/tf_stack_check.py +93 -0
- package/README.md +2 -2
- package/opencode.jsonc +6 -2
- package/package.json +1 -1
- package/scripts/install.mjs +14 -3
- package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-codex-bind.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-codex-telemetry.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
|
@@ -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
|
-
|
|
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
|
|
76
|
-
|
|
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
|
|
9
|
-
|
|
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
|
-
- --
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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", "
|
|
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"
|