@techierathore/techieflow 1.0.3 → 1.1.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 (150) hide show
  1. package/.claude/commands/TechieFlow/agents/analyst.md +5 -8
  2. package/.claude/commands/TechieFlow/agents/architect.md +1 -10
  3. package/.claude/commands/TechieFlow/agents/flow-master.md +1 -0
  4. package/.claude/commands/TechieFlow/agents/verifier.md +1 -0
  5. package/.claude/commands/TechieFlow/tasks/_owner-language.md +27 -0
  6. package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +1 -1
  7. package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +4 -3
  8. package/.claude/commands/TechieFlow/tasks/amend-docs.md +6 -5
  9. package/.claude/commands/TechieFlow/tasks/build-phase.md +1 -1
  10. package/.claude/commands/TechieFlow/tasks/create-deep-research-prompt.md +4 -0
  11. package/.claude/commands/TechieFlow/tasks/create-doc.md +4 -0
  12. package/.claude/commands/TechieFlow/tasks/create-project-brief.md +22 -0
  13. package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +3 -3
  14. package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +3 -3
  15. package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +4 -0
  16. package/.claude/commands/TechieFlow/tasks/fix-issues.md +2 -2
  17. package/.claude/commands/TechieFlow/tasks/generate-html.md +7 -3
  18. package/.claude/commands/TechieFlow/tasks/handoff-phase.md +20 -18
  19. package/.claude/commands/TechieFlow/tasks/metrics-report.md +2 -0
  20. package/.claude/commands/TechieFlow/tasks/mockups.md +1 -1
  21. package/.claude/commands/TechieFlow/tasks/productguide.md +2 -2
  22. package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +1 -1
  23. package/.claude/commands/TechieFlow/tasks/triage-issues.md +2 -2
  24. package/.claude/commands/TechieFlow/tasks/verify-phase.md +1 -1
  25. package/.opencode/plugin/techieflow.js +181 -9
  26. package/.tfcore/agents/analyst.md +5 -8
  27. package/.tfcore/agents/architect.md +1 -10
  28. package/.tfcore/agents/flow-master.md +1 -0
  29. package/.tfcore/agents/verifier.md +1 -0
  30. package/.tfcore/hooks/block-git.sh +4 -2
  31. package/.tfcore/hooks/guard-build.sh +5 -1
  32. package/.tfcore/hooks/guard-db.sh +6 -4
  33. package/.tfcore/hooks/guard-metrics.sh +13 -1
  34. package/.tfcore/hooks/guard-status-html.sh +86 -2
  35. package/.tfcore/hooks/guard-verify-deps.sh +124 -0
  36. package/.tfcore/hooks/guard-verify.sh +8 -3
  37. package/.tfcore/hooks/sweep-artifacts.sh +46 -0
  38. package/.tfcore/routing.yaml +61 -0
  39. package/.tfcore/standards/owner-words.txt +23 -0
  40. package/.tfcore/tasks/_owner-language.md +27 -0
  41. package/.tfcore/tasks/_smoke-test-policy.md +1 -1
  42. package/.tfcore/tasks/_status-update-gate.md +4 -3
  43. package/.tfcore/tasks/amend-docs.md +6 -5
  44. package/.tfcore/tasks/build-phase.md +1 -1
  45. package/.tfcore/tasks/create-deep-research-prompt.md +4 -0
  46. package/.tfcore/tasks/create-doc.md +4 -0
  47. package/.tfcore/tasks/create-project-brief.md +22 -0
  48. package/.tfcore/tasks/day1-brownfield.md +3 -3
  49. package/.tfcore/tasks/day1-greenfield.md +3 -3
  50. package/.tfcore/tasks/facilitate-brainstorming-session.md +4 -0
  51. package/.tfcore/tasks/fix-issues.md +2 -2
  52. package/.tfcore/tasks/generate-html.md +7 -3
  53. package/.tfcore/tasks/handoff-phase.md +20 -18
  54. package/.tfcore/tasks/metrics-report.md +2 -0
  55. package/.tfcore/tasks/mockups.md +1 -1
  56. package/.tfcore/tasks/productguide.md +2 -2
  57. package/.tfcore/tasks/render-workflow-docs.md +1 -1
  58. package/.tfcore/tasks/triage-issues.md +2 -2
  59. package/.tfcore/tasks/verify-phase.md +1 -1
  60. package/.tfcore/telemetry/SCHEMA.md +61 -1
  61. package/.tfcore/telemetry/install-metrics.sh +3 -2
  62. package/.tfcore/telemetry/tf-metrics.sh +435 -21
  63. package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +6 -0
  64. package/.tfcore/templates/v4custom/app-brd-tmpl.md +5 -3
  65. package/.tfcore/templates/v4custom/app-brief-tmpl.md +65 -0
  66. package/.tfcore/templates/v4custom/app-decision-request-tmpl.md +85 -0
  67. package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +86 -31
  68. package/.tfcore/templates/v4custom/app-phase-brd-tmpl.md +94 -0
  69. package/.tfcore/templates/v4custom/app-phase-uidesign-tmpl.md +60 -0
  70. package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +4 -2
  71. package/.tfcore/templates/v4custom/build-subagent-prompt.md +2 -2
  72. package/.tfcore/templates/v4custom/html-render-shell.md +4 -2
  73. package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
  74. package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
  75. package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
  76. package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
  77. package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
  78. package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
  79. package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
  80. package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
  81. package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
  82. package/.tfcore/utils/__pycache__/tf-perf-grade.cpython-312.pyc +0 -0
  83. package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
  84. package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
  85. package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
  86. package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
  87. package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
  88. package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
  89. package/.tfcore/utils/tf-assets-browser.mjs +96 -0
  90. package/.tfcore/utils/tf-assets.sh +97 -12
  91. package/.tfcore/utils/tf-brd-status.py +14 -5
  92. package/.tfcore/utils/tf-build-list.py +99 -11
  93. package/.tfcore/utils/tf-build-list.sh +5 -3
  94. package/.tfcore/utils/tf-build.sh +88 -15
  95. package/.tfcore/utils/tf-ci-repro.py +685 -0
  96. package/.tfcore/utils/tf-ci-repro.sh +24 -0
  97. package/.tfcore/utils/tf-devguide-list.py +47 -22
  98. package/.tfcore/utils/tf-doc-check.py +242 -10
  99. package/.tfcore/utils/tf-doc-tests.sh +79 -0
  100. package/.tfcore/utils/tf-emit.sh +413 -19
  101. package/.tfcore/utils/tf-feedback.sh +13 -0
  102. package/.tfcore/utils/tf-fix-close.sh +113 -31
  103. package/.tfcore/utils/tf-gitignore-audit.sh +50 -3
  104. package/.tfcore/utils/tf-goal.sh +105 -18
  105. package/.tfcore/utils/tf-harness.sh +9 -4
  106. package/.tfcore/utils/tf-lock.sh +29 -0
  107. package/.tfcore/utils/tf-log-miss.py +34 -8
  108. package/.tfcore/utils/tf-login.mjs +167 -0
  109. package/.tfcore/utils/tf-misses-md.py +24 -21
  110. package/.tfcore/utils/tf-misses-md.sh +5 -4
  111. package/.tfcore/utils/tf-mockup-parity.mjs +427 -63
  112. package/.tfcore/utils/tf-mockup-parity.sh +14 -2
  113. package/.tfcore/utils/tf-model-pick.sh +475 -0
  114. package/.tfcore/utils/tf-owner-text.py +233 -0
  115. package/.tfcore/utils/tf-owner-text.sh +14 -0
  116. package/.tfcore/utils/tf-phase.sh +33 -1
  117. package/.tfcore/utils/tf-portable.sh +230 -0
  118. package/.tfcore/utils/tf-render-html.py +80 -2
  119. package/.tfcore/utils/tf-routing-bind.sh +87 -1
  120. package/.tfcore/utils/tf-routing.sh +7 -0
  121. package/.tfcore/utils/tf-selfcheck.sh +356 -0
  122. package/.tfcore/utils/tf-split-brd.py +17 -2
  123. package/.tfcore/utils/tf-status-evidence.sh +11 -9
  124. package/.tfcore/utils/tf-status-facts.py +133 -44
  125. package/.tfcore/utils/tf-triage.py +62 -16
  126. package/.tfcore/utils/tf-triage.sh +2 -0
  127. package/.tfcore/utils/tf-verify-boot.sh +263 -43
  128. package/.tfcore/utils/tf-verify-emit.sh +20 -0
  129. package/.tfcore/utils/tf-verify-env.sh +33 -4
  130. package/.tfcore/utils/tf-verify-list.py +75 -8
  131. package/.tfcore/utils/tf-verify-screens.mjs +254 -43
  132. package/.tfcore/utils/tf-verify-screens.sh +18 -4
  133. package/.tfcore/utils/tf-verify-tests.sh +272 -28
  134. package/.tfcore/utils/tf-verify-verdict.py +59 -11
  135. package/.tfcore/utils/tf_defect.py +27 -0
  136. package/.tfcore/utils/tf_feedback.py +209 -0
  137. package/.tfcore/utils/tf_roadmap.py +35 -0
  138. package/README.md +19 -5
  139. package/docs/TechieFlow-Installation.md +5 -3
  140. package/opencode.jsonc +24 -0
  141. package/package.json +1 -1
  142. package/scripts/install.mjs +4 -0
  143. package/.tfcore/templates/architecture-tmpl.yaml +0 -650
  144. package/.tfcore/templates/brownfield-architecture-tmpl.yaml +0 -476
  145. package/.tfcore/templates/competitor-analysis-tmpl.yaml +0 -306
  146. package/.tfcore/templates/front-end-architecture-tmpl.yaml +0 -218
  147. package/.tfcore/templates/fullstack-architecture-tmpl.yaml +0 -823
  148. package/.tfcore/templates/market-research-tmpl.yaml +0 -252
  149. package/.tfcore/templates/project-brief-tmpl.yaml +0 -221
  150. package/.tfcore/utils/techieflow-doc-template.md +0 -325
@@ -0,0 +1,96 @@
1
+ // TechieFlow — the documents tf-assets.sh grades, fetched through a signed-in browser
2
+ // (AppManager TF-011, 2026-09-14). Driven by tf-assets.sh when --login-path is given; run that.
3
+ //
4
+ // tf-assets reads what a page DECLARES and fetches each asset. It fetched the page with a bare HTTP
5
+ // request, and an app whose sign-in lives in the page's own connection answers 401 to every bare
6
+ // request: "declared 0, graded 0" on every path. Here each path is opened in a browser tab that
7
+ // signed in through the page's form (tf-login.mjs), judged by what it draws, and its document — the
8
+ // head with every stylesheet and script it declares — handed back for grading. The assets themselves
9
+ // are still fetched one by one by tf-assets.sh, so a 404 stays observable.
10
+ //
11
+ // node tf-assets-browser.mjs --base URL --paths "/a,/b" --login-path /login --user U --password P
12
+ // [--storage-state file] [--cookie 'k=v'] --json-out <file>
13
+ // Writes {"login": {...}, "pages": {"/a": {"status": 401, "reached": "…", "signed_out": false,
14
+ // "url": "...", "html": "<!doctype…"}}}. Exit 0 always except a usage error (3).
15
+
16
+ import { chromium } from 'playwright';
17
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
18
+ import { dirname, resolve } from 'node:path';
19
+ import { signIn, reach } from './tf-login.mjs';
20
+
21
+ const argv = process.argv.slice(2);
22
+ const arg = (name, dflt = null) => { const i = argv.indexOf(name); return i >= 0 && i + 1 < argv.length ? argv[i + 1] : dflt; };
23
+ const BASE = (arg('--base') || '').replace(/\/$/, '');
24
+ const PATHS = (arg('--paths', '/') || '').split(',').map((p) => p.trim()).filter(Boolean).map((p) => (p.startsWith('/') ? p : '/' + p));
25
+ const OUT = arg('--json-out');
26
+ const COOKIE = arg('--cookie'); const STORAGE = arg('--storage-state');
27
+ const LOGIN_OPTS = { base: BASE, loginPath: arg('--login-path'), user: arg('--user'), password: arg('--password'), settle: 1500, renderWait: 5000 };
28
+ // Lekhak TF-006: a desktop head, attached over its DevTools port. Its pages and assets are served by
29
+ // the embedded browser itself, so no request from outside can fetch them: each path is opened by
30
+ // pushState, and every asset the document declares is fetched from inside the page, its status and
31
+ // size handed back beside the document ("fetched").
32
+ const CDP = arg('--cdp') || '';
33
+ if ((!BASE && !CDP) || !OUT) { console.error('usage: tf-assets-browser.mjs --base URL|--cdp URL --paths /a,/b [--login-path /login --user U --password P] --json-out file'); process.exit(3); }
34
+
35
+ if (CDP) {
36
+ let b, page;
37
+ try {
38
+ b = await chromium.connectOverCDP(CDP, { timeout: 15000 });
39
+ const c = b.contexts()[0]; page = c && c.pages()[0];
40
+ } catch (e) { page = null; }
41
+ mkdirSync(dirname(resolve(OUT)), { recursive: true });
42
+ if (!page) { writeFileSync(resolve(OUT), JSON.stringify({ unreachable: true, cdp: CDP })); process.exit(0); }
43
+ const pages = {};
44
+ let origin = '';
45
+ for (const p of PATHS) {
46
+ let status = 200, error = '', html = '', url = '', fetched = {};
47
+ try {
48
+ await page.evaluate((r) => { history.pushState({}, '', r); window.dispatchEvent(new PopStateEvent('popstate', { state: {} })); }, p);
49
+ await page.waitForTimeout(LOGIN_OPTS.settle);
50
+ url = page.url(); origin = origin || new URL(url).origin;
51
+ html = await page.content();
52
+ fetched = await page.evaluate(async () => {
53
+ const out = {};
54
+ const urls = [...document.querySelectorAll('link[href]')].filter((l) => /stylesheet|preload|modulepreload|icon/i.test(l.rel)).map((l) => l.href)
55
+ .concat([...document.querySelectorAll('script[src]')].map((s) => s.src));
56
+ for (const u of [...new Set(urls)]) {
57
+ if (!/^https?:/i.test(u)) continue;
58
+ try { const r = await fetch(u, { cache: 'no-store' }); const buf = await r.arrayBuffer(); out[u] = { status: r.status, bytes: buf.byteLength }; }
59
+ catch (e) { out[u] = { status: null, bytes: 0, error: String(e.message || e).slice(0, 120) }; }
60
+ }
61
+ return out;
62
+ });
63
+ } catch (e) { status = 0; error = e.message.split('\n')[0]; }
64
+ pages[p] = { status, document_status: status, url, reached: 'opened inside the attached app (pushState)', signed_out: false, error, html, fetched };
65
+ }
66
+ await b.close().catch(() => {}); // disconnects; the app keeps running
67
+ writeFileSync(resolve(OUT), JSON.stringify({ login: null, cdp: CDP, origin, pages }));
68
+ process.exit(0);
69
+ }
70
+
71
+ const browser = await chromium.launch();
72
+ const ctxOpts = { ignoreHTTPSErrors: true };
73
+ if (STORAGE && existsSync(STORAGE)) ctxOpts.storageState = STORAGE;
74
+ const ctx = await browser.newContext(ctxOpts);
75
+ if (COOKIE) {
76
+ const u = new URL(BASE);
77
+ for (const pair of COOKIE.split(';')) {
78
+ const [name, ...v] = pair.trim().split('=');
79
+ if (name && v.length) await ctx.addCookies([{ name: name.trim(), value: v.join('='), domain: u.hostname, path: '/' }]);
80
+ }
81
+ }
82
+ const page = await ctx.newPage();
83
+ const login = await signIn(page, LOGIN_OPTS);
84
+ const pages = {};
85
+ for (const p of PATHS) {
86
+ const nav = await reach(page, LOGIN_OPTS, p);
87
+ let html = '';
88
+ if (nav.status && nav.status < 400 && !nav.signedOut) {
89
+ try { html = await page.content(); } catch (e) { html = ''; }
90
+ }
91
+ pages[p] = { status: nav.status, document_status: nav.document_status ?? nav.status, url: nav.url || '',
92
+ reached: nav.reached || '', signed_out: !!nav.signedOut, error: nav.error || '', html };
93
+ }
94
+ await browser.close();
95
+ mkdirSync(dirname(resolve(OUT)), { recursive: true });
96
+ writeFileSync(resolve(OUT), JSON.stringify({ login, pages }));
@@ -3,6 +3,13 @@
3
3
  #
4
4
  # bash .tfcore/utils/tf-assets.sh --base http://localhost:5099 \
5
5
  # --paths "/,/login,/export" --cookie 'AuthCookie=<value>'
6
+ # An app whose sign-in lives in the page and sets no cookie (AppManager TF-011):
7
+ # --login-path /login --user <u> --password <p> (or --storage-state <file>)
8
+ # opens each path in a browser that signed in through the form and grades the
9
+ # document it drew; `pages[].reached` says how a 401 page was reached.
10
+ # A desktop head (Lekhak TF-006): --cdp http://<host>:9223 in place of --base attaches to the app,
11
+ # opens each path by pushState and fetches every declared asset from inside the page, because
12
+ # nothing outside the embedded browser can reach the files it serves.
6
13
  #
7
14
  # WHAT IT ASKS, and it is the only question no other gate asks:
8
15
  #
@@ -50,8 +57,10 @@
50
57
 
51
58
  set -uo pipefail
52
59
 
60
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
53
61
  BASE=""; PATHS="/"; TIMEOUT=20; LABEL=""; OUT=""; EXTERNAL=0; MINBYTES=1
54
62
  HDRS="" # newline-separated "K: V" pairs (--header, repeatable; --cookie is sugar)
63
+ COOKIE=""; LOGIN_PATH=""; USER=""; PASS=""; STORAGE=""; CDP=""
55
64
  while [[ $# -gt 0 ]]; do
56
65
  case "$1" in
57
66
  --base) BASE="${2:-}"; shift 2 ;;
@@ -62,18 +71,44 @@ while [[ $# -gt 0 ]]; do
62
71
  --min-bytes) MINBYTES="${2:-}"; shift 2 ;;
63
72
  --include-external) EXTERNAL=1; shift ;;
64
73
  --header) HDRS="${HDRS}${2:-}"$'\n'; shift 2 ;;
65
- --cookie) HDRS="${HDRS}Cookie: ${2:-}"$'\n'; shift 2 ;;
66
- -h|--help) sed -n '2,48p' "$0"; exit 0 ;;
74
+ --cookie) HDRS="${HDRS}Cookie: ${2:-}"$'\n'; COOKIE="${2:-}"; shift 2 ;;
75
+ # a sign-in the page keeps for itself sets no cookie (AppManager TF-011): the pages are then
76
+ # opened in a browser that signed in through the form, the same options tf-verify-screens takes
77
+ --login-path) LOGIN_PATH="${2:-}"; shift 2 ;;
78
+ --user) USER="${2:-}"; shift 2 ;;
79
+ --password) PASS="${2:-}"; shift 2 ;;
80
+ --storage-state) STORAGE="${2:-}"; shift 2 ;;
81
+ --cdp) CDP="${2:-}"; shift 2 ;; # a desktop head over its DevTools port (Lekhak TF-006)
82
+ -h|--help) sed -n '2,56p' "$0"; exit 0 ;;
67
83
  *) echo "unknown argument: $1" >&2; exit 3 ;;
68
84
  esac
69
85
  done
70
86
 
71
- [[ -n "$BASE" ]] || { printf '%s\n' \
72
- "usage: tf-assets.sh --base URL [--paths /,/login] [--cookie 'k=v'] [--include-external]" \
87
+ [[ -n "$BASE" || -n "$CDP" ]] || { printf '%s\n' \
88
+ "usage: tf-assets.sh --base URL|--cdp URL [--paths /,/login] [--cookie 'k=v'] [--include-external]" \
89
+ " [--login-path /login --user U --password P] [--storage-state file]" \
73
90
  " [--json-out tests/.artifacts/assets/REQ-NFR-015.json]" >&2; exit 3; }
74
91
  command -v python3 >/dev/null 2>&1 || { echo '{"status":"no-python3"}'; exit 3; }
75
92
 
76
- TF_BASE="$BASE" TF_PATHS="$PATHS" TF_TIMEOUT="$TIMEOUT" TF_LABEL="$LABEL" TF_OUT="$OUT" \
93
+ # ---- the documents, through a signed-in browser when a sign-in is given (TF-011) --------------
94
+ DOCS=""
95
+ if [[ -n "$CDP" ]]; then
96
+ if ! command -v node >/dev/null 2>&1 || ! node -e "import('playwright').then(()=>process.exit(0)).catch(()=>process.exit(1))" 2>/dev/null; then
97
+ echo '{"status":"no-playwright","note":"--cdp needs node and playwright (verify-phase §1 installs the verify environment)"}'; exit 3
98
+ fi
99
+ DOCS="$(mktemp "${TMPDIR:-/tmp}/tmp.XXXXXXXXXX")" && mv "$DOCS" "$DOCS.json" && DOCS="$DOCS.json"
100
+ node "$HERE/tf-assets-browser.mjs" --cdp "$CDP" --paths "$PATHS" --json-out "$DOCS" >&2 || { echo '{"status":"browser-failed"}'; rm -f "$DOCS"; exit 3; }
101
+ elif [[ -n "$LOGIN_PATH" || -n "$STORAGE" ]]; then
102
+ if ! command -v node >/dev/null 2>&1 || ! node -e "import('playwright').then(()=>process.exit(0)).catch(()=>process.exit(1))" 2>/dev/null; then
103
+ echo '{"status":"no-playwright","note":"--login-path needs node and playwright (verify-phase §1 installs the verify environment)"}'; exit 3
104
+ fi
105
+ DOCS="$(mktemp "${TMPDIR:-/tmp}/tmp.XXXXXXXXXX")" && mv "$DOCS" "$DOCS.json" && DOCS="$DOCS.json" # only GNU mktemp takes a suffix option
106
+ node "$HERE/tf-assets-browser.mjs" --base "$BASE" --paths "$PATHS" --json-out "$DOCS" \
107
+ ${LOGIN_PATH:+--login-path "$LOGIN_PATH"} ${USER:+--user "$USER"} ${PASS:+--password "$PASS"} \
108
+ ${STORAGE:+--storage-state "$STORAGE"} ${COOKIE:+--cookie "$COOKIE"} >&2 || { echo '{"status":"browser-failed"}'; rm -f "$DOCS"; exit 3; }
109
+ fi
110
+
111
+ TF_CDP="$CDP" TF_BASE="$BASE" TF_PATHS="$PATHS" TF_TIMEOUT="$TIMEOUT" TF_LABEL="$LABEL" TF_OUT="$OUT" TF_DOCS="$DOCS" \
77
112
  TF_HEADERS="$HDRS" TF_EXTERNAL="$EXTERNAL" TF_MINBYTES="$MINBYTES" python3 - <<'PY'
78
113
  import html.parser, json, os, ssl, sys, urllib.error, urllib.request
79
114
  from urllib.parse import urljoin, urlsplit
@@ -85,6 +120,34 @@ LABEL = os.environ.get("TF_LABEL") or None
85
120
  OUT = os.environ.get("TF_OUT") or None
86
121
  EXTERNAL = os.environ.get("TF_EXTERNAL") == "1"
87
122
  MINBYTES = int(os.environ.get("TF_MINBYTES") or 1)
123
+ # the documents a signed-in browser fetched, by path (TF-011); empty when none was asked for
124
+ DOCS = {}
125
+ LOGIN = None
126
+ if os.environ.get("TF_DOCS"):
127
+ try:
128
+ _d = json.load(open(os.environ["TF_DOCS"], encoding="utf-8"))
129
+ DOCS, LOGIN = _d.get("pages", {}), _d.get("login")
130
+ except Exception:
131
+ DOCS = {}
132
+ try:
133
+ os.remove(os.environ["TF_DOCS"])
134
+ except Exception:
135
+ pass
136
+ # an attached desktop head (TF-006): the assets were fetched inside the page, by URL
137
+ CDP = os.environ.get("TF_CDP") or ""
138
+ FETCHED = {}
139
+ if CDP:
140
+ try:
141
+ _d
142
+ except NameError:
143
+ _d = {}
144
+ if _d.get("unreachable") or not DOCS:
145
+ print(json.dumps({"status": "unreachable", "base": CDP, "label": LABEL,
146
+ "note": "the desktop head's DevTools port did not answer — a `build` gate problem, not an assets result"}, indent=2))
147
+ raise SystemExit(2)
148
+ BASE = (_d.get("origin") or "").rstrip("/")
149
+ for _pg in DOCS.values():
150
+ FETCHED.update(_pg.get("fetched") or {})
88
151
 
89
152
  HEADERS = []
90
153
  for line in (os.environ.get("TF_HEADERS") or "").splitlines():
@@ -145,6 +208,11 @@ class AssetParser(html.parser.HTMLParser):
145
208
  def fetch(url, want_body=True):
146
209
  """Return (status, nbytes, error). Never raises — a transport failure is a
147
210
  finding, not a crash, and the caller needs every path's result."""
211
+ if CDP:
212
+ f = FETCHED.get(url)
213
+ if f is None:
214
+ return None, 0, "not fetched inside the attached app", b""
215
+ return f.get("status"), f.get("bytes", 0), f.get("error"), b""
148
216
  req = urllib.request.Request(url, headers={"User-Agent": UA})
149
217
  for k, v in HEADERS:
150
218
  req.add_header(k, v)
@@ -193,7 +261,7 @@ def fetch_page(url):
193
261
 
194
262
 
195
263
  # --- reachability first: an unreachable base is a `build` problem, not an assets one
196
- probe_status, _, probe_err = fetch_page(BASE + "/")
264
+ probe_status, _, probe_err = (200, b"", None) if CDP else fetch_page(BASE + "/")
197
265
  if probe_status is None:
198
266
  print(json.dumps({"status": "unreachable", "base": BASE, "error": probe_err,
199
267
  "label": LABEL, "note": "app not reachable — this is a `build` "
@@ -207,7 +275,23 @@ graded_assets = 0
207
275
 
208
276
  for p in PATHS:
209
277
  page_url = BASE + (p if p.startswith("/") else "/" + p)
210
- status, body, err = fetch_page(page_url)
278
+ extra = {}
279
+ doc = DOCS.get(p if p.startswith("/") else "/" + p)
280
+ if doc is not None:
281
+ # the document as the signed-in browser drew it (TF-011). A page that stayed signed out is
282
+ # an auth wall: nothing of it is graded, like a redirect to the sign-in page.
283
+ if doc.get("signed_out"):
284
+ redirected += 1
285
+ pages.append({"path": p, "status": doc.get("document_status"), "assets": [], "declared": 0,
286
+ "graded": 0, "failed": 0, "redirected": True, "auth_wall": True, "url": doc.get("url")})
287
+ continue
288
+ status, body, err = (doc.get("status") or None), (doc.get("html") or "").encode("utf-8"), (doc.get("error") or None)
289
+ if status and status < 400 and body:
290
+ status = 200
291
+ if doc.get("reached"):
292
+ extra = {"document_status": doc.get("document_status"), "reached": doc["reached"]}
293
+ else:
294
+ status, body, err = fetch_page(page_url)
211
295
  if status is None:
212
296
  pages.append({"path": p, "status": None, "error": err, "assets": [],
213
297
  "declared": 0, "graded": 0, "failed": 0})
@@ -269,16 +353,16 @@ for p in PATHS:
269
353
 
270
354
  pages.append({"path": p, "status": status, "declared": len(rows),
271
355
  "graded": sum(1 for r in rows if r.get("graded")),
272
- "failed": failed, "assets": rows})
356
+ "failed": failed, "assets": rows, **extra})
273
357
 
274
358
  # Every path turned away = nothing was graded. Reporting "0 findings" there would be
275
359
  # a PASS on a screen the harness never saw — the same false green tf-perf.sh's exit 4
276
360
  # exists to prevent, and the same shape as TF-011's ungradeable-looks-clean failure.
277
361
  if PATHS and redirected == len(PATHS):
278
- out = {"status": "redirected", "base": BASE, "label": LABEL, "pages": pages,
279
- "findings": [], "note": "every path answered 3xx — nothing was graded. "
280
- "Present a session with --cookie / --header and re-run. Grade the REQ "
281
- "ASSETS-UNMEASURED (auth wall); never record this as a pass."}
362
+ out = {"status": "redirected", "base": BASE, "label": LABEL, "pages": pages, "login": LOGIN,
363
+ "findings": [], "note": "every path answered 3xx or stayed signed out — nothing was graded. "
364
+ "Present a session with --cookie / --header, or --login-path/--user/--password, and re-run. "
365
+ "Grade the REQ ASSETS-UNMEASURED (auth wall); never record this as a pass."}
282
366
  print(json.dumps(out, indent=2))
283
367
  if OUT:
284
368
  os.makedirs(os.path.dirname(os.path.abspath(OUT)), exist_ok=True)
@@ -294,6 +378,7 @@ out = {
294
378
  "findings_n": len(findings),
295
379
  "findings": findings,
296
380
  "pages": pages,
381
+ "login": LOGIN,
297
382
  "include_external": EXTERNAL,
298
383
  # An assets run that graded nothing is UNGRADEABLE, not clean. Said out loud in
299
384
  # the payload so a consumer cannot read `findings_n: 0` as coverage.
@@ -16,8 +16,9 @@ docs/<App>-BRD.md (Screen | Requirements | Verified | Open | Status) with a
16
16
  re-renders docs/<App>-BRD.html through tf-render-html.sh unless --no-render.
17
17
 
18
18
  Status per screen: Done (every row terminal), Planned (no row started),
19
- Partial (some terminal, some open), In progress (started, none terminal).
20
- Terminal = Verified, Done (pre-existing), N/A. Blocked is counted as open.
19
+ Partial (some terminal, some open), In progress (started, none terminal), Not applicable
20
+ (every row N/A). Terminal = Verified, Done (pre-existing). An N/A row is left out of the counts,
21
+ never counted as verified (AppManager TF-018). Blocked is counted as open.
21
22
 
22
23
  Python 3 standard library only. Exit 0 written or nothing to do, 2 could not run.
23
24
  """
@@ -27,7 +28,9 @@ import re
27
28
  import subprocess
28
29
  import sys
29
30
 
30
- TERMINAL = {"verified", "done (pre-existing)", "n/a"}
31
+ # norm() drops a bracketed tail, so "Done (pre-existing)" arrives as "done" and was counted open (AppManager TF-003)
32
+ TERMINAL = {"verified", "done", "done (pre-existing)"}
33
+ NA = {"n/a"}
31
34
  NOT_STARTED = {"not started"}
32
35
 
33
36
 
@@ -80,6 +83,8 @@ def table(rows, screen_of):
80
83
  if s not in per:
81
84
  per[s] = {"n": 0, "v": 0, "ns": 0}
82
85
  order.append(s)
86
+ if status in NA:
87
+ continue
83
88
  per[s]["n"] += 1
84
89
  per[s]["v"] += 1 if status in TERMINAL else 0
85
90
  per[s]["ns"] += 1 if status in NOT_STARTED else 0
@@ -87,7 +92,9 @@ def table(rows, screen_of):
87
92
  for s in order:
88
93
  d = per[s]
89
94
  open_n = d["n"] - d["v"]
90
- if open_n == 0:
95
+ if d["n"] == 0:
96
+ st = "Not applicable"
97
+ elif open_n == 0:
91
98
  st = "Done"
92
99
  elif d["ns"] == d["n"]:
93
100
  st = "Planned"
@@ -153,7 +160,9 @@ def main(argv):
153
160
  with open(brd, "w", encoding="utf-8") as f:
154
161
  f.write(new_text)
155
162
  verified = sum(1 for _, s in rows if s in TERMINAL)
156
- print(f"tf-brd-status: {brd_rel} Development status {how}: {screens} screens, {verified} of {len(rows)} requirements verified")
163
+ na = sum(1 for _, s in rows if s in NA)
164
+ print(f"tf-brd-status: {brd_rel} Development status {how}: {screens} screens, {verified} of {len(rows) - na} "
165
+ f"requirements verified" + (f", {na} not applicable" if na else ""))
157
166
  if render:
158
167
  script = os.path.join(root, ".tfcore", "utils", "tf-render-html.sh")
159
168
  if os.path.isfile(script):
@@ -4,11 +4,14 @@
4
4
  bash .tfcore/utils/tf-build-list.sh <App> [--phase N] [--prompts]
5
5
 
6
6
  Reads the phase's checklist (appPhase in core-config.yaml, or --phase) and prints:
7
- - the mode: FIX (rows FAIL / PARTIAL / In Progress / Needs re-verify exist), FRESH
8
- (every open row), or NOTHING (every row terminal or Blocked)
7
+ - the mode: FIX (rows FAIL / PARTIAL / In Progress / Needs re-verify exist, and they
8
+ come FIRST in the list), FRESH (nothing failing), or NOTHING (every row terminal or
9
+ Blocked). The working list is every open row in both FIX and FRESH: the mode says
10
+ what to build first, never what to leave out. A roadmap row (tf_roadmap.py) is not
11
+ open: it is counted and named apart, never built.
9
12
  - the working list, grouped into clusters by the checklist's page section, each
10
- naming its builder from the row prefix: REQ-UI -> trblazeui, REQ-RAG -> techierag,
11
- REQ-FN / REQ-NFR -> builder
13
+ naming its builder from the row prefix: REQ-UI -> trblazeui when the project uses
14
+ TrBlazeUI (else builder), REQ-RAG -> techierag, REQ-FN / REQ-NFR -> builder
12
15
  - with --prompts, one ready sub-agent prompt per cluster, filled from
13
16
  .tfcore/templates/v4custom/build-subagent-prompt.md with the rows, their acceptance
14
17
  lines, their mockups and the two standing rules.
@@ -18,7 +21,9 @@ import os
18
21
  import re
19
22
  import sys
20
23
 
21
- TERMINAL = {"verified", "done (pre-existing)", "n/a"}
24
+ # norm() drops a bracketed tail ("Verified (2026-09-01)"), so "Done (pre-existing)" arrives as "done":
25
+ # only the bracketed spelling was listed, and AppManager's 18 finished rows went on the working list (TF-003)
26
+ TERMINAL = {"verified", "done", "done (pre-existing)", "n/a"}
22
27
  FIX = {"fail", "partial", "in progress", "needs re-verify"}
23
28
  BLOCKED = {"blocked"}
24
29
  BUILDER = {"UI": "trblazeui", "RAG": "techierag", "FN": "builder", "NFR": "builder"}
@@ -49,6 +54,44 @@ def norm(s):
49
54
  return re.sub(r"\s*\(.*$", "", s).strip()
50
55
 
51
56
 
57
+ def ui_builder(app):
58
+ """-> (builder, why) for the UI clusters: trblazeui only when the project uses TrBlazeUI.
59
+
60
+ The framework gives every project `.trblazeui/`, whether it uses the library or not, so that
61
+ folder says nothing: AppManager, which does not use it, had its UI rows sent to the trblazeui
62
+ sub-agent (TF-008). The project's own files decide: a TrBlazeUI package or project reference in
63
+ a .csproj or .props. Before any such file exists (a first build), the Architecture document
64
+ naming it decides. tests/regression/run.sh am_008."""
65
+ skip = {"bin", "obj", "node_modules", ".git", ".tfcore", ".claude", ".opencode", ".trblazeui",
66
+ ".techierag", ".artifacts"}
67
+ seen = 0
68
+ for root, dirs, files in os.walk("."):
69
+ dirs[:] = sorted(d for d in dirs if d not in skip)
70
+ for fn in sorted(files):
71
+ if not fn.endswith((".csproj", ".props")):
72
+ continue
73
+ seen += 1
74
+ p = os.path.join(root, fn)
75
+ if re.search(r'Include="[^"]*\bTrBlazeUI[\w.]*(?:\.csproj)?"', read(p), re.I):
76
+ return "trblazeui", f"{os.path.relpath(p)} references TrBlazeUI"
77
+ if seen:
78
+ return "builder", f"none of the {seen} .csproj/.props file(s) references TrBlazeUI"
79
+ arch = os.path.join("docs", f"{app}-Architecture.md")
80
+ if os.path.isfile(arch) and re.search(r"\bTrBlazeUI\b", read(arch)):
81
+ return "trblazeui", f"no project file yet; {arch} names TrBlazeUI"
82
+ return "builder", "no project file yet, and the Architecture document does not name TrBlazeUI"
83
+
84
+
85
+ def usage_guide(app):
86
+ """The guide the project really has, UsageGuide or Usage-Guide: tf-verify-list.py's reader. The prompt
87
+ named the first spelling only, a file AppManager does not have (TF-022). tests/regression/run.sh am_022."""
88
+ import importlib.util
89
+ spec = importlib.util.spec_from_file_location("tf_verify_list", os.path.join(os.path.dirname(os.path.abspath(__file__)), "tf-verify-list.py"))
90
+ vl = importlib.util.module_from_spec(spec)
91
+ spec.loader.exec_module(vl)
92
+ return vl.usage_guide(app)
93
+
94
+
52
95
  def rows_of(text):
53
96
  rows = []
54
97
  for line in text.splitlines():
@@ -110,20 +153,53 @@ def main(argv):
110
153
  if not rows:
111
154
  die(f"{cl} has no REQ rows")
112
155
  ent = entries_of(text)
156
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
157
+ import tf_roadmap
158
+ import tf_defect
159
+ marked = tf_roadmap.ids(text)
160
+ roadmap = [r for r in rows if r["id"].upper() in marked and r["status"] not in TERMINAL | BLOCKED]
113
161
 
114
- fix_rows = [r for r in rows if r["status"] in FIX]
115
- open_rows = [r for r in rows if r["status"] not in TERMINAL | BLOCKED]
162
+ fix_rows = [r for r in rows if r["status"] in FIX and r not in roadmap]
163
+ open_rows = [r for r in rows if r["status"] not in TERMINAL | BLOCKED and r not in roadmap]
164
+ # Repair before new work, but never INSTEAD of it. A row can sit at `Needs re-verify`
165
+ # permanently -- TfLens REQ-FN-067 and -070 need a repository that emits events.ndjson
166
+ # and none exists, so every honest verify run re-confirms that gate rather than clearing
167
+ # it. FIX used to win outright, which pinned the mode forever: the twelve rows a later
168
+ # *amend-docs added were never once printed, and a pass that trusted the working list
169
+ # would have met build-phase's ending condition with twelve requirements at 0%.
170
+ # So the working list is every open row, and FIX only says which ones come first.
171
+ # tests/regression/run.sh tf_019.
116
172
  if fix_rows:
117
- mode, work = "FIX", fix_rows
173
+ rest = [r for r in open_rows if r not in fix_rows]
174
+ mode, work = "FIX", fix_rows + rest
118
175
  elif open_rows:
119
176
  mode, work = "FRESH", open_rows
120
177
  else:
121
178
  mode, work = "NOTHING", []
179
+ # A row in FIX mode is on the list for a defect its acceptance line rarely names; a builder given
180
+ # only that line finds the row working and fixes nothing (AppManager TF-020). The defect rides with
181
+ # the row: its ⚠ clauses, else a FAIL/PARTIAL row's whole Remarks. tests/regression/run.sh am_020.
182
+ for r in rows:
183
+ r["defects"] = (tf_defect.clauses(r["remarks"]) or ([r["remarks"]] if r["status"] in {"fail", "partial"}
184
+ and r["remarks"] else [])) if r in fix_rows else []
122
185
  terminal = sum(1 for r in rows if r["status"] in TERMINAL)
123
186
  blocked = [r for r in rows if r["status"] in BLOCKED]
124
187
  print(f"# tf-build-list — {app} — {cl} — phase {phase}")
125
- print(f"Mode: {mode} — {len(work)} row(s) to build; {terminal} terminal, {len(blocked)} Blocked, {len(rows)} total"
188
+ print(f"Mode: {mode} — {len(work)} row(s) to build; {terminal} terminal, {len(blocked)} Blocked, "
189
+ f"{len(roadmap)} roadmap, {len(rows)} total"
126
190
  + (f" — {', '.join(r['id'] for r in work[:8])}{' …' if len(work) > 8 else ''}" if work else ""))
191
+ # Every row is in exactly one of the five counts, and the arithmetic is printed so a
192
+ # reader can check it rather than trust it. A row missing from all of them is the
193
+ # failure this line exists to make impossible.
194
+ if len(work) + terminal + len(blocked) + len(roadmap) != len(rows):
195
+ print(f"⚠ {len(rows) - len(work) - terminal - len(blocked) - len(roadmap)} row(s) are in no category — "
196
+ "the checklist carries a status this script does not know; fix the row or the script")
197
+ if roadmap:
198
+ print("Roadmap, not in this phase's scope (not built; the owner moves them with *amend-docs): "
199
+ + ", ".join(r["id"] for r in roadmap))
200
+ if fix_rows and len(work) > len(fix_rows):
201
+ print(f"Order: {len(fix_rows)} failing row(s) first ({', '.join(r['id'] for r in fix_rows)}), "
202
+ f"then {len(work) - len(fix_rows)} not yet started")
127
203
  if blocked:
128
204
  print("Blocked (library gaps, pass through unless the feedback entry is resolved): " + ", ".join(r["id"] for r in blocked))
129
205
  if not work:
@@ -131,10 +207,14 @@ def main(argv):
131
207
  return 0
132
208
 
133
209
  clusters = {}
210
+ builder = dict(BUILDER)
211
+ if any(r["id"].upper().startswith("REQ-UI-") for r in work):
212
+ builder["UI"], why = ui_builder(app)
213
+ print(f"UI rows go to: {builder['UI']} — {why}")
134
214
  for r in work:
135
215
  sec, acc, mock, brd = ent.get(r["id"].upper(), ("Other", "", "", ""))
136
216
  cls = re.match(r"REQ-(UI|FN|RAG|NFR)-", r["id"]).group(1)
137
- key = ("Non-functional" if cls == "NFR" else "RAG" if cls == "RAG" else sec, BUILDER[cls])
217
+ key = ("Non-functional" if cls == "NFR" else "RAG" if cls == "RAG" else sec, builder[cls])
138
218
  clusters.setdefault(key, []).append((r, acc, mock, brd))
139
219
  print()
140
220
  print("## Clusters (one sub-agent each; spawn them all in one turn)")
@@ -154,6 +234,8 @@ def main(argv):
154
234
  print(f"- {r['id']} [{label}/{builder}] {r['title']} — {r['status']}{todo}")
155
235
  if acc:
156
236
  print(f" Acceptance: {acc}")
237
+ for x in r["defects"]:
238
+ print(f" Defect: {x}")
157
239
  if mock:
158
240
  print(f" Mockup: {mock}")
159
241
  if not prompts:
@@ -164,18 +246,24 @@ def main(argv):
164
246
  die(f"template {TPL} does not exist at its literal path")
165
247
  tpl = read(TPL)
166
248
  tpl = re.sub(r"<!--.*?-->\s*", "", tpl, flags=re.S)
249
+ guide = usage_guide(app)
167
250
  for label, sec, builder, items in named:
168
251
  lines = []
169
252
  for r, acc, mock, brd in items:
170
253
  lines.append(f"- {r['id']} — {r['title']}" + (f" ({brd})" if brd else ""))
171
254
  lines.append(f" Acceptance: {acc or 'MISSING — refuse the row and report it'}")
255
+ lines.extend(f" Defect: {x}" for x in r["defects"])
172
256
  if mock:
173
257
  lines.append(f" Mockup: {mock}")
258
+ if any(r["defects"] for r, *_ in items):
259
+ lines.append("\nA row with a Defect line is on this list for that defect. Fix every one at the file and "
260
+ "line it names, keep the acceptance line true, and replace the row's Remarks: a row that "
261
+ "already passes its acceptance line is not done while its defect stands.")
174
262
  mocks = sorted({m for _r, _a, m, _b in items if m})
175
263
  body = (tpl.replace("{App}", app).replace("{Cluster}", label).replace("{Builder}", builder)
176
264
  .replace("{Section}", sec).replace("{Rows}", "\n".join(lines))
177
265
  .replace("{Mockups}", ", ".join(mocks) if mocks else "none (not a UI cluster)")
178
- .replace("{Checklist}", cl))
266
+ .replace("{Checklist}", cl).replace("{UsageGuide}", guide))
179
267
  print()
180
268
  print(f"## Prompt for cluster {label} [{builder}]")
181
269
  print(body.strip())
@@ -6,9 +6,11 @@
6
6
  # bash .tfcore/utils/tf-build-list.sh MyApp --phase 2 # a later phase of a Large project
7
7
  #
8
8
  # Reads the phase's checklist (appPhase in core-config.yaml unless --phase). Rows FAIL /
9
- # PARTIAL / In Progress / Needs re-verify make it FIX mode over exactly those rows; else every
10
- # open row is FRESH; Blocked rows pass through. Clusters follow the checklist's page sections,
11
- # each with its builder from the prefix (UI -> trblazeui, RAG -> techierag, FN/NFR -> builder).
9
+ # PARTIAL / In Progress / Needs re-verify make it FIX mode, which puts those rows FIRST and
10
+ # then lists every other open row; FRESH is the same list when nothing is failing; Blocked rows
11
+ # pass through. Clusters follow the checklist's page sections,
12
+ # each with its builder from the prefix (UI -> trblazeui when the project references TrBlazeUI,
13
+ # else builder; RAG -> techierag; FN/NFR -> builder).
12
14
  # Prompts are filled from .tfcore/templates/v4custom/build-subagent-prompt.md.
13
15
  # Writes nothing. Thin wrapper over tf-build-list.py; Python 3 stdlib only.
14
16
  set -uo pipefail