@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.
- package/.claude/commands/TechieFlow/agents/analyst.md +5 -8
- package/.claude/commands/TechieFlow/agents/architect.md +1 -10
- package/.claude/commands/TechieFlow/agents/flow-master.md +1 -0
- package/.claude/commands/TechieFlow/agents/verifier.md +1 -0
- package/.claude/commands/TechieFlow/tasks/_owner-language.md +27 -0
- package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +1 -1
- package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +4 -3
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +6 -5
- package/.claude/commands/TechieFlow/tasks/build-phase.md +1 -1
- package/.claude/commands/TechieFlow/tasks/create-deep-research-prompt.md +4 -0
- package/.claude/commands/TechieFlow/tasks/create-doc.md +4 -0
- package/.claude/commands/TechieFlow/tasks/create-project-brief.md +22 -0
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +3 -3
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +3 -3
- package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +4 -0
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +2 -2
- package/.claude/commands/TechieFlow/tasks/generate-html.md +7 -3
- package/.claude/commands/TechieFlow/tasks/handoff-phase.md +20 -18
- package/.claude/commands/TechieFlow/tasks/metrics-report.md +2 -0
- package/.claude/commands/TechieFlow/tasks/mockups.md +1 -1
- package/.claude/commands/TechieFlow/tasks/productguide.md +2 -2
- package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +1 -1
- package/.claude/commands/TechieFlow/tasks/triage-issues.md +2 -2
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +1 -1
- package/.opencode/plugin/techieflow.js +181 -9
- package/.tfcore/agents/analyst.md +5 -8
- package/.tfcore/agents/architect.md +1 -10
- package/.tfcore/agents/flow-master.md +1 -0
- package/.tfcore/agents/verifier.md +1 -0
- package/.tfcore/hooks/block-git.sh +4 -2
- package/.tfcore/hooks/guard-build.sh +5 -1
- package/.tfcore/hooks/guard-db.sh +6 -4
- package/.tfcore/hooks/guard-metrics.sh +13 -1
- package/.tfcore/hooks/guard-status-html.sh +86 -2
- package/.tfcore/hooks/guard-verify-deps.sh +124 -0
- package/.tfcore/hooks/guard-verify.sh +8 -3
- package/.tfcore/hooks/sweep-artifacts.sh +46 -0
- package/.tfcore/routing.yaml +61 -0
- package/.tfcore/standards/owner-words.txt +23 -0
- package/.tfcore/tasks/_owner-language.md +27 -0
- package/.tfcore/tasks/_smoke-test-policy.md +1 -1
- package/.tfcore/tasks/_status-update-gate.md +4 -3
- package/.tfcore/tasks/amend-docs.md +6 -5
- package/.tfcore/tasks/build-phase.md +1 -1
- package/.tfcore/tasks/create-deep-research-prompt.md +4 -0
- package/.tfcore/tasks/create-doc.md +4 -0
- package/.tfcore/tasks/create-project-brief.md +22 -0
- package/.tfcore/tasks/day1-brownfield.md +3 -3
- package/.tfcore/tasks/day1-greenfield.md +3 -3
- package/.tfcore/tasks/facilitate-brainstorming-session.md +4 -0
- package/.tfcore/tasks/fix-issues.md +2 -2
- package/.tfcore/tasks/generate-html.md +7 -3
- package/.tfcore/tasks/handoff-phase.md +20 -18
- package/.tfcore/tasks/metrics-report.md +2 -0
- package/.tfcore/tasks/mockups.md +1 -1
- package/.tfcore/tasks/productguide.md +2 -2
- package/.tfcore/tasks/render-workflow-docs.md +1 -1
- package/.tfcore/tasks/triage-issues.md +2 -2
- package/.tfcore/tasks/verify-phase.md +1 -1
- package/.tfcore/telemetry/SCHEMA.md +61 -1
- package/.tfcore/telemetry/install-metrics.sh +3 -2
- package/.tfcore/telemetry/tf-metrics.sh +435 -21
- package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +6 -0
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +5 -3
- package/.tfcore/templates/v4custom/app-brief-tmpl.md +65 -0
- package/.tfcore/templates/v4custom/app-decision-request-tmpl.md +85 -0
- package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +86 -31
- package/.tfcore/templates/v4custom/app-phase-brd-tmpl.md +94 -0
- package/.tfcore/templates/v4custom/app-phase-uidesign-tmpl.md +60 -0
- package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +4 -2
- package/.tfcore/templates/v4custom/build-subagent-prompt.md +2 -2
- package/.tfcore/templates/v4custom/html-render-shell.md +4 -2
- package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-day1-files.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-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-perf-grade.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-render-html.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-triage.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
- package/.tfcore/utils/tf-assets-browser.mjs +96 -0
- package/.tfcore/utils/tf-assets.sh +97 -12
- package/.tfcore/utils/tf-brd-status.py +14 -5
- package/.tfcore/utils/tf-build-list.py +99 -11
- package/.tfcore/utils/tf-build-list.sh +5 -3
- package/.tfcore/utils/tf-build.sh +88 -15
- package/.tfcore/utils/tf-ci-repro.py +685 -0
- package/.tfcore/utils/tf-ci-repro.sh +24 -0
- package/.tfcore/utils/tf-devguide-list.py +47 -22
- package/.tfcore/utils/tf-doc-check.py +242 -10
- package/.tfcore/utils/tf-doc-tests.sh +79 -0
- package/.tfcore/utils/tf-emit.sh +413 -19
- package/.tfcore/utils/tf-feedback.sh +13 -0
- package/.tfcore/utils/tf-fix-close.sh +113 -31
- package/.tfcore/utils/tf-gitignore-audit.sh +50 -3
- package/.tfcore/utils/tf-goal.sh +105 -18
- package/.tfcore/utils/tf-harness.sh +9 -4
- package/.tfcore/utils/tf-lock.sh +29 -0
- package/.tfcore/utils/tf-log-miss.py +34 -8
- package/.tfcore/utils/tf-login.mjs +167 -0
- package/.tfcore/utils/tf-misses-md.py +24 -21
- package/.tfcore/utils/tf-misses-md.sh +5 -4
- package/.tfcore/utils/tf-mockup-parity.mjs +427 -63
- package/.tfcore/utils/tf-mockup-parity.sh +14 -2
- package/.tfcore/utils/tf-model-pick.sh +475 -0
- package/.tfcore/utils/tf-owner-text.py +233 -0
- package/.tfcore/utils/tf-owner-text.sh +14 -0
- package/.tfcore/utils/tf-phase.sh +33 -1
- package/.tfcore/utils/tf-portable.sh +230 -0
- package/.tfcore/utils/tf-render-html.py +80 -2
- package/.tfcore/utils/tf-routing-bind.sh +87 -1
- package/.tfcore/utils/tf-routing.sh +7 -0
- package/.tfcore/utils/tf-selfcheck.sh +356 -0
- package/.tfcore/utils/tf-split-brd.py +17 -2
- package/.tfcore/utils/tf-status-evidence.sh +11 -9
- package/.tfcore/utils/tf-status-facts.py +133 -44
- package/.tfcore/utils/tf-triage.py +62 -16
- package/.tfcore/utils/tf-triage.sh +2 -0
- package/.tfcore/utils/tf-verify-boot.sh +263 -43
- package/.tfcore/utils/tf-verify-emit.sh +20 -0
- package/.tfcore/utils/tf-verify-env.sh +33 -4
- package/.tfcore/utils/tf-verify-list.py +75 -8
- package/.tfcore/utils/tf-verify-screens.mjs +254 -43
- package/.tfcore/utils/tf-verify-screens.sh +18 -4
- package/.tfcore/utils/tf-verify-tests.sh +272 -28
- package/.tfcore/utils/tf-verify-verdict.py +59 -11
- package/.tfcore/utils/tf_defect.py +27 -0
- package/.tfcore/utils/tf_feedback.py +209 -0
- package/.tfcore/utils/tf_roadmap.py +35 -0
- package/README.md +19 -5
- package/docs/TechieFlow-Installation.md +5 -3
- package/opencode.jsonc +24 -0
- package/package.json +1 -1
- package/scripts/install.mjs +4 -0
- package/.tfcore/templates/architecture-tmpl.yaml +0 -650
- package/.tfcore/templates/brownfield-architecture-tmpl.yaml +0 -476
- package/.tfcore/templates/competitor-analysis-tmpl.yaml +0 -306
- package/.tfcore/templates/front-end-architecture-tmpl.yaml +0 -218
- package/.tfcore/templates/fullstack-architecture-tmpl.yaml +0 -823
- package/.tfcore/templates/market-research-tmpl.yaml +0 -252
- package/.tfcore/templates/project-brief-tmpl.yaml +0 -221
- 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
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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)
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
8
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
10
|
-
# open row
|
|
11
|
-
#
|
|
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
|