@techierathore/techieflow 1.0.2 → 1.0.3
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 +0 -5
- package/.claude/commands/TechieFlow/agents/architect.md +0 -6
- package/.claude/commands/TechieFlow/agents/flow-master.md +41 -215
- package/.claude/commands/TechieFlow/agents/verifier.md +23 -64
- package/.claude/commands/TechieFlow/tasks/_metrics-emit-gate.md +24 -117
- package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +20 -84
- package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +10 -74
- package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +20 -58
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +20 -143
- package/.claude/commands/TechieFlow/tasks/build-phase.md +14 -224
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +31 -505
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +28 -213
- package/.claude/commands/TechieFlow/tasks/deploy-checklist.md +19 -0
- package/.claude/commands/TechieFlow/tasks/devguide.md +10 -219
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +10 -129
- package/.claude/commands/TechieFlow/tasks/log-miss.md +19 -158
- package/.claude/commands/TechieFlow/tasks/metrics-report.md +1 -1
- package/.claude/commands/TechieFlow/tasks/mockups.md +12 -117
- package/.claude/commands/TechieFlow/tasks/refresh-status.md +9 -130
- package/.claude/commands/TechieFlow/tasks/split-brd.md +10 -126
- package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +14 -0
- package/.claude/commands/TechieFlow/tasks/triage-issues.md +11 -152
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +36 -572
- package/.opencode/plugin/techieflow.js +39 -12
- package/.tfcore/agents/analyst.md +0 -5
- package/.tfcore/agents/architect.md +0 -6
- package/.tfcore/agents/flow-master.md +41 -215
- package/.tfcore/agents/verifier.md +23 -64
- package/.tfcore/core-config.yaml +10 -0
- package/.tfcore/hooks/block-git.sh +4 -4
- package/.tfcore/hooks/guard-artifacts.sh +2 -2
- package/.tfcore/hooks/guard-build.sh +76 -0
- package/.tfcore/hooks/guard-db.sh +90 -0
- package/.tfcore/hooks/guard-metrics.sh +56 -0
- package/.tfcore/hooks/guard-status-html.sh +116 -71
- package/.tfcore/hooks/guard-status.sh +43 -16
- package/.tfcore/hooks/guard-verify.sh +70 -48
- package/.tfcore/hooks/sweep-artifacts.sh +2 -2
- package/.tfcore/install-manifest.yaml +0 -18
- package/.tfcore/routing.yaml +25 -14
- package/.tfcore/standards/coding-standards-core.md +61 -0
- package/.tfcore/standards/coding-standards-dotnet.md +93 -0
- package/.tfcore/tasks/_metrics-emit-gate.md +24 -117
- package/.tfcore/tasks/_smoke-test-policy.md +20 -84
- package/.tfcore/tasks/_status-update-gate.md +10 -74
- package/.tfcore/tasks/_yolo-mode.md +20 -58
- package/.tfcore/tasks/amend-docs.md +20 -143
- package/.tfcore/tasks/build-phase.md +14 -224
- package/.tfcore/tasks/day1-brownfield.md +31 -505
- package/.tfcore/tasks/day1-greenfield.md +28 -213
- package/.tfcore/tasks/deploy-checklist.md +19 -0
- package/.tfcore/tasks/devguide.md +10 -219
- package/.tfcore/tasks/fix-issues.md +10 -129
- package/.tfcore/tasks/log-miss.md +19 -158
- package/.tfcore/tasks/metrics-report.md +1 -1
- package/.tfcore/tasks/mockups.md +12 -117
- package/.tfcore/tasks/refresh-status.md +9 -130
- package/.tfcore/tasks/split-brd.md +10 -126
- package/.tfcore/tasks/triage-and-fix.md +14 -0
- package/.tfcore/tasks/triage-issues.md +11 -152
- package/.tfcore/tasks/verify-phase.md +36 -572
- package/.tfcore/telemetry/SCHEMA.md +43 -7
- package/.tfcore/telemetry/tf-metrics.sh +152 -17
- package/.tfcore/templates/project-brief-tmpl.yaml +1 -1
- package/.tfcore/templates/stack-defaults/dotnet.md +35 -0
- package/.tfcore/templates/stack-questions.md +77 -0
- package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +3 -4
- package/.tfcore/templates/v4custom/app-architecture-tmpl.md +107 -115
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +108 -184
- package/.tfcore/templates/v4custom/app-checklist-tmpl.md +48 -73
- package/.tfcore/templates/v4custom/app-claude-md-tmpl.md +1 -1
- package/.tfcore/templates/v4custom/app-coding-standards-tmpl.md +52 -0
- package/.tfcore/templates/v4custom/app-deployment-checklist-tmpl.md +86 -0
- package/.tfcore/templates/v4custom/app-devguide-tmpl.md +69 -146
- package/.tfcore/templates/v4custom/app-phases-tmpl.md +35 -0
- package/.tfcore/templates/v4custom/app-productguide-tmpl.md +38 -35
- package/.tfcore/templates/v4custom/app-project-status-tmpl.md +63 -73
- package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +62 -37
- package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +69 -54
- package/.tfcore/templates/v4custom/build-invocation-ladder.md +17 -180
- package/.tfcore/templates/v4custom/build-subagent-prompt.md +17 -0
- package/.tfcore/templates/v4custom/metrics-report-template.md +2 -2
- package/.tfcore/user-guide.md +0 -23
- 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-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-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-brd-status.py +176 -0
- package/.tfcore/utils/tf-brd-status.sh +19 -0
- package/.tfcore/utils/tf-build-list.py +191 -0
- package/.tfcore/utils/tf-build-list.sh +20 -0
- package/.tfcore/utils/tf-build.sh +143 -0
- package/.tfcore/utils/tf-cdp-relay.ps1 +64 -0
- package/.tfcore/utils/tf-checklist-edit.py +133 -0
- package/.tfcore/utils/tf-day1-files.py +208 -0
- package/.tfcore/utils/tf-day1-files.sh +20 -0
- package/.tfcore/utils/tf-devguide-list.py +249 -0
- package/.tfcore/utils/tf-devguide-list.sh +18 -0
- package/.tfcore/utils/tf-doc-check.py +1267 -0
- package/.tfcore/utils/tf-doc-check.sh +29 -0
- package/.tfcore/utils/tf-emit.sh +203 -13
- package/.tfcore/utils/tf-fix-close.sh +83 -0
- package/.tfcore/utils/tf-goal.sh +193 -62
- package/.tfcore/utils/tf-harness.sh +4 -9
- package/.tfcore/utils/tf-log-miss.py +197 -0
- package/.tfcore/utils/tf-log-miss.sh +14 -0
- package/.tfcore/utils/tf-misses-md.py +211 -0
- package/.tfcore/utils/tf-misses-md.sh +13 -0
- package/.tfcore/utils/tf-mockups-locate.py +66 -0
- package/.tfcore/utils/tf-mockups-locate.sh +18 -0
- package/.tfcore/utils/tf-opencode-audit.py +23 -7
- package/.tfcore/utils/tf-perf-grade.py +97 -0
- package/.tfcore/utils/tf-perf-grade.sh +12 -0
- package/.tfcore/utils/tf-phase.sh +74 -0
- package/.tfcore/utils/tf-routing-bind.sh +10 -5
- package/.tfcore/utils/tf-routing.sh +6 -7
- package/.tfcore/utils/tf-split-brd.py +304 -0
- package/.tfcore/utils/tf-split-brd.sh +22 -0
- package/.tfcore/utils/tf-status-evidence.sh +68 -0
- package/.tfcore/utils/tf-status-facts.py +309 -0
- package/.tfcore/utils/tf-status-facts.sh +20 -0
- package/.tfcore/utils/tf-triage.py +243 -0
- package/.tfcore/utils/tf-triage.sh +16 -0
- package/.tfcore/utils/tf-verify-boot.sh +250 -0
- package/.tfcore/utils/tf-verify-emit.sh +110 -0
- package/.tfcore/utils/tf-verify-env.sh +88 -0
- package/.tfcore/utils/tf-verify-list.py +270 -0
- package/.tfcore/utils/tf-verify-list.sh +13 -0
- package/.tfcore/utils/tf-verify-screens.mjs +283 -0
- package/.tfcore/utils/tf-verify-screens.sh +22 -0
- package/.tfcore/utils/tf-verify-tests.sh +123 -0
- package/.tfcore/utils/tf-verify-verdict.py +283 -0
- package/.tfcore/utils/tf-verify-verdict.sh +15 -0
- package/.tfcore/utils/tf-yolo.sh +59 -0
- package/README.md +114 -1149
- package/docs/TechieFlow-Installation.md +1 -5
- package/opencode.jsonc +13 -33
- package/package.json +1 -5
- package/scripts/install.mjs +52 -46
- package/.claude/commands/TechieFlow/tasks/advanced-elicitation.md +0 -117
- package/.claude/commands/TechieFlow/tasks/author-brd.md +0 -79
- package/.claude/commands/TechieFlow/tasks/document-project.md +0 -343
- package/.claude/commands/TechieFlow/tasks/execute-checklist.md +0 -86
- package/.claude/commands/TechieFlow/tasks/index-docs.md +0 -173
- package/.claude/commands/TechieFlow/tasks/kb-mode-interaction.md +0 -75
- package/.claude/commands/TechieFlow/tasks/shard-doc.md +0 -185
- package/.codex/config.toml +0 -46
- package/.codex/hooks.json +0 -63
- package/.codex/rules/techieflow.rules +0 -8
- package/.tfcore/hooks/codex-adapter.py +0 -248
- package/.tfcore/tasks/advanced-elicitation.md +0 -117
- package/.tfcore/tasks/author-brd.md +0 -79
- package/.tfcore/tasks/document-project.md +0 -343
- package/.tfcore/tasks/execute-checklist.md +0 -86
- package/.tfcore/tasks/index-docs.md +0 -173
- package/.tfcore/tasks/kb-mode-interaction.md +0 -75
- package/.tfcore/tasks/shard-doc.md +0 -185
- package/.tfcore/utils/tf-codex-bind.py +0 -196
- package/.tfcore/utils/tf-codex-telemetry.py +0 -60
- package/WORKFLOW.html +0 -1986
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tf-doc-check.sh — check TechieFlow human documents against their template schemas.
|
|
3
|
+
#
|
|
4
|
+
# bash .tfcore/utils/tf-doc-check.sh docs/MyApp-BRD.md [more.md ...]
|
|
5
|
+
# bash .tfcore/utils/tf-doc-check.sh --app MyApp # every document of the app + PROJECT-STATUS.md
|
|
6
|
+
# bash .tfcore/utils/tf-doc-check.sh --app MyApp --warn # report mode: findings as WARN, exit 0
|
|
7
|
+
# bash .tfcore/utils/tf-doc-check.sh --size Medium docs/MyApp-BRD.md
|
|
8
|
+
#
|
|
9
|
+
# Each template under .tfcore/templates/v4custom/ opens with a `<!-- tf-schema -->`
|
|
10
|
+
# block (required sections in order, budgets per size, row rules). This script
|
|
11
|
+
# prints one line per problem — FAIL blocks the phase, WARN does not — and exits
|
|
12
|
+
# 0 when nothing FAILs, 1 when something does, 2 when it could not run.
|
|
13
|
+
#
|
|
14
|
+
# Run by the status gate (_status-update-gate.md) on every human document a
|
|
15
|
+
# command wrote, before the HTML render. Readable version of the rules:
|
|
16
|
+
# docs/TechieFlow-Document-Schemas.md.
|
|
17
|
+
#
|
|
18
|
+
# Thin wrapper over tf-doc-check.py, matching the tf-render-html.sh idiom.
|
|
19
|
+
# Python 3 standard library only.
|
|
20
|
+
set -uo pipefail
|
|
21
|
+
|
|
22
|
+
SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
23
|
+
|
|
24
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
25
|
+
echo "tf-doc-check: python3 is required (standard library only)." >&2
|
|
26
|
+
exit 2
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
exec python3 "$SELF_DIR/tf-doc-check.py" "$@"
|
package/.tfcore/utils/tf-emit.sh
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
# tf-emit.sh --amend MISS-App-20260828-01 why_missed missing-checklist-item
|
|
20
20
|
# # fills a field that is still null on a miss already on
|
|
21
21
|
# # the stream (SCHEMA.md §5.5.7). Never overwrites.
|
|
22
|
+
# tf-emit.sh --origin-of REQ-UI-014 # prints "<started> <cmd> <agent>" of the last build or fix
|
|
23
|
+
# # run that touched the row, nothing when there is none
|
|
22
24
|
# tf-emit.sh --where # prints the resolved docs/metrics dir
|
|
23
25
|
#
|
|
24
26
|
# STREAMS: runs | gates | sessions | commits | misses (anything else is dropped)
|
|
@@ -131,6 +133,39 @@ PY
|
|
|
131
133
|
exit 0
|
|
132
134
|
fi
|
|
133
135
|
|
|
136
|
+
# --- read helper: which run caused a row's defect ------------------------
|
|
137
|
+
# Prints "<started> <cmd> <agent>" for the most recent non-backfilled build-phase
|
|
138
|
+
# or fix-issues run whose reqs_touched holds the REQ, nothing when there is none
|
|
139
|
+
# (the caller then leaves origin_run_id out and the record is marked inferred).
|
|
140
|
+
# Used by tf-log-miss.sh and tf-triage.sh (Sitting 4c, 2026-09-06).
|
|
141
|
+
if [[ "$1" == "--origin-of" ]]; then
|
|
142
|
+
REQ="$2"
|
|
143
|
+
[[ -n "$REQ" ]] || exit 0
|
|
144
|
+
python3 - "$MET_DIR/runs.jsonl" "$REQ" 2>"$TF_ERR" <<'PY2' || true
|
|
145
|
+
import json, sys
|
|
146
|
+
path, req = sys.argv[1], sys.argv[2].upper()
|
|
147
|
+
best = None
|
|
148
|
+
try:
|
|
149
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
150
|
+
for line in fh:
|
|
151
|
+
try:
|
|
152
|
+
r = json.loads(line)
|
|
153
|
+
except Exception:
|
|
154
|
+
continue
|
|
155
|
+
if r.get("kind") != "run" or r.get("backfilled") or r.get("cmd") not in ("build-phase", "fix-issues"):
|
|
156
|
+
continue
|
|
157
|
+
if req in [str(x).upper() for x in (r.get("reqs_touched") or [])]:
|
|
158
|
+
if best is None or r.get("started", "") >= best.get("started", ""):
|
|
159
|
+
best = r
|
|
160
|
+
except FileNotFoundError:
|
|
161
|
+
pass
|
|
162
|
+
if best:
|
|
163
|
+
subs = best.get("subagents") or []
|
|
164
|
+
print(best.get("started", ""), best.get("cmd"), subs[0] if subs else "flow-master")
|
|
165
|
+
PY2
|
|
166
|
+
exit 0
|
|
167
|
+
fi
|
|
168
|
+
|
|
134
169
|
# --- read helper: next RUN attempt number for a cmd + REQ set ------------
|
|
135
170
|
# run attempt = 1 + count of prior NON-BACKFILLED `run` records with the same
|
|
136
171
|
# cmd whose reqs_touched intersects the given REQ IDs. Same counting rule that
|
|
@@ -170,9 +205,10 @@ fi
|
|
|
170
205
|
# `miss-fix`, and a collision would silently merge two defects into one lifecycle.
|
|
171
206
|
if [[ "$1" == "--next-miss-id" ]]; then
|
|
172
207
|
python3 - "$MET_DIR/misses.jsonl" "$ROOT" 2>"$TF_ERR" <<'PY' || true
|
|
173
|
-
import datetime, glob, json, os, sys
|
|
208
|
+
import datetime, glob, json, os, re, sys
|
|
174
209
|
path, root = sys.argv[1], sys.argv[2]
|
|
175
|
-
hits = glob.glob(os.path.join(root, "docs", "*-Checklist.md"))
|
|
210
|
+
hits = [h for h in glob.glob(os.path.join(root, "docs", "*-Checklist.md"))
|
|
211
|
+
if not re.search(r"-(Deployment|P\d+)-Checklist\.md$", h)] # only the phase-1 work list names the app
|
|
176
212
|
app = (os.path.basename(hits[0])[: -len("-Checklist.md")] if len(hits) == 1
|
|
177
213
|
else os.path.basename(os.path.abspath(root)))
|
|
178
214
|
day = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%d")
|
|
@@ -304,6 +340,9 @@ AMENDABLE = {
|
|
|
304
340
|
"why_missed": ("missing-checklist-item", "insufficient-verify-method",
|
|
305
341
|
"code-audit-limitation", "ambiguous-acceptance",
|
|
306
342
|
"dependency-not-declared", "instruction-ignored", "other"),
|
|
343
|
+
# whose gap it was, the four-question sort (FR-32, Session 5): a judgement a reader can
|
|
344
|
+
# still make later, so a miss logged before the field existed can be sorted now.
|
|
345
|
+
"sort": ("spec", "unsaid", "weak-check", "ignored"),
|
|
307
346
|
}
|
|
308
347
|
if fld not in AMENDABLE:
|
|
309
348
|
print("REFUSED %s is not an amendable field (SCHEMA.md §5.5.7)" % fld); raise SystemExit(0)
|
|
@@ -360,7 +399,7 @@ esac
|
|
|
360
399
|
# The program is captured into a variable rather than fed on stdin — a heredoc
|
|
361
400
|
# on `python3 -` would REPLACE the caller's piped JSON and silently drop every event.
|
|
362
401
|
TF_PROG="$(cat <<'PY'
|
|
363
|
-
import json, os, sys, datetime, glob, re
|
|
402
|
+
import json, os, sys, datetime, glob, re, time
|
|
364
403
|
|
|
365
404
|
met_dir, stream, root = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
366
405
|
dbg = os.environ.get("TF_METRICS_DEBUG")
|
|
@@ -456,11 +495,8 @@ def _detect_harness():
|
|
|
456
495
|
# the OTHER harness inherits that harness's markers (e.g. OpenCode started
|
|
457
496
|
# from a Claude Code bash still carries CLAUDE_PROJECT_DIR).
|
|
458
497
|
tf = os.environ.get("TF_HARNESS")
|
|
459
|
-
if tf in ("claude-code", "opencode"
|
|
498
|
+
if tf in ("claude-code", "opencode"):
|
|
460
499
|
return tf
|
|
461
|
-
for k in ("CODEX_THREAD_ID", "CODEX_SESSION_ID"):
|
|
462
|
-
if os.environ.get(k):
|
|
463
|
-
return "codex"
|
|
464
500
|
for k in ("CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT", "CLAUDE_CODE_SESSION_ID",
|
|
465
501
|
"CLAUDE_PROJECT_DIR"):
|
|
466
502
|
if os.environ.get(k):
|
|
@@ -488,8 +524,6 @@ def _detect_harness():
|
|
|
488
524
|
name, ppid = os.path.basename(parts[0]), int(parts[1])
|
|
489
525
|
if name and "opencode" in name.lower():
|
|
490
526
|
return "opencode"
|
|
491
|
-
if name and "codex" in name.lower():
|
|
492
|
-
return "codex"
|
|
493
527
|
if name and "claude" in name.lower():
|
|
494
528
|
return "claude-code"
|
|
495
529
|
pid = ppid
|
|
@@ -505,7 +539,11 @@ except Exception:
|
|
|
505
539
|
# app name — inferred from the one checklist, else the repo directory name
|
|
506
540
|
def _app_name():
|
|
507
541
|
try:
|
|
508
|
-
|
|
542
|
+
# The Deployment Checklist and a Large project's phase-n checklists share the suffix;
|
|
543
|
+
# only <App>-Checklist.md names the app, else a copy such as TfLens-oc was recorded
|
|
544
|
+
# under its folder name (MISS-TechieFlow-20260906-04).
|
|
545
|
+
hits = [h for h in glob.glob(os.path.join(root, "docs", "*-Checklist.md"))
|
|
546
|
+
if not re.search(r"-(Deployment|P\d+)-Checklist\.md$", h)]
|
|
509
547
|
if len(hits) == 1:
|
|
510
548
|
return os.path.basename(hits[0])[: -len("-Checklist.md")]
|
|
511
549
|
except Exception:
|
|
@@ -524,6 +562,54 @@ def enrich(rec):
|
|
|
524
562
|
DECISIONS.md 2026-08-20)."""
|
|
525
563
|
rec.setdefault("v", 1)
|
|
526
564
|
rec.setdefault("ts", NOW)
|
|
565
|
+
# A run record says whether YOLO was on (D-12, FR-35; Sitting 4c, 2026-09-06): the task
|
|
566
|
+
# never has to remember it. TF_YOLO=1 (the supervisor) or a live .tfcore/.session/yolo.json.
|
|
567
|
+
if rec.get("kind") == "run" and "yolo" not in rec:
|
|
568
|
+
on = os.environ.get("TF_YOLO") == "1"
|
|
569
|
+
if not on:
|
|
570
|
+
try:
|
|
571
|
+
fl = os.path.join(root, ".tfcore", ".session", "yolo.json")
|
|
572
|
+
on = os.path.isfile(fl) and (time.time() - os.path.getmtime(fl)) < 24 * 3600
|
|
573
|
+
except Exception:
|
|
574
|
+
on = False
|
|
575
|
+
rec["yolo"] = bool(on)
|
|
576
|
+
# A run record without `started` takes it from the command marker written by
|
|
577
|
+
# tf-phase.sh start (step 0 of every task), when the marker names the same command.
|
|
578
|
+
if rec.get("kind") == "run" and not rec.get("started"):
|
|
579
|
+
try:
|
|
580
|
+
with open(os.path.join(root, ".tfcore", ".session", "phase.json"), encoding="utf-8") as fh:
|
|
581
|
+
mk = json.load(fh) or {}
|
|
582
|
+
if mk.get("started") and (not rec.get("cmd") or mk.get("cmd") == rec.get("cmd")):
|
|
583
|
+
rec["started"] = mk["started"]
|
|
584
|
+
rec.setdefault("ended", NOW)
|
|
585
|
+
sys.stdout.write("tf-emit: started taken from the command marker (%s)\n" % mk["started"])
|
|
586
|
+
except Exception:
|
|
587
|
+
pass
|
|
588
|
+
# A run's `ended` is when the record is written, never a guess: an ended in the
|
|
589
|
+
# future (more than a minute past now) or before started is replaced with now and
|
|
590
|
+
# duration_s recomputed (MISS-TechieFlow-20260905-11: a day-1 run wrote 17:05 at 16:40).
|
|
591
|
+
# The same rule the other way round: a run record with NO `ended` was silently
|
|
592
|
+
# accepted and could never be costed — the effort-per-phase figure just lost it
|
|
593
|
+
# (MISS-TechieFlow-20260907-09, the session-6 record itself). `ended` is when the
|
|
594
|
+
# record is written, so an absent one is filled in, never left empty.
|
|
595
|
+
if rec.get("kind") == "run" and not isinstance(rec.get("ended"), str):
|
|
596
|
+
rec["ended"] = NOW
|
|
597
|
+
if rec.get("kind") == "run" and isinstance(rec.get("ended"), str):
|
|
598
|
+
t_end, t_now = _iso_ms(rec["ended"]), _iso_ms(NOW)
|
|
599
|
+
t_start = _iso_ms(rec["started"]) if isinstance(rec.get("started"), str) else None
|
|
600
|
+
if t_end is not None and t_now is not None and (t_end > t_now + 60_000 or (t_start is not None and t_end < t_start)):
|
|
601
|
+
sys.stdout.write("tf-emit: ended %s is not a measurement (it lies %s); set to now, %s\n"
|
|
602
|
+
% (rec["ended"], "in the future" if t_end > t_now else "before started", NOW))
|
|
603
|
+
rec["ended"] = NOW
|
|
604
|
+
if t_start is not None and "duration_s" in rec:
|
|
605
|
+
rec["duration_s"] = max(0, (t_now - t_start) // 1000)
|
|
606
|
+
# A run record that omits duration_s gets it from started and ended (the devguide and
|
|
607
|
+
# refresh-status records of 2026-09-06 had none; MISS-TechieFlow-20260906-10).
|
|
608
|
+
if rec.get("kind") == "run" and "duration_s" not in rec and rec.get("started") and rec.get("ended"):
|
|
609
|
+
try:
|
|
610
|
+
rec["duration_s"] = max(0, (_iso_ms(rec["ended"]) - _iso_ms(rec["started"])) // 1000)
|
|
611
|
+
except Exception:
|
|
612
|
+
pass
|
|
527
613
|
if "project_type" not in rec:
|
|
528
614
|
if PTYPE:
|
|
529
615
|
rec["project_type"] = PTYPE
|
|
@@ -531,7 +617,10 @@ def enrich(rec):
|
|
|
531
617
|
# Default, but NEVER silently — the report labels these unclassified.
|
|
532
618
|
rec["project_type"] = "app"
|
|
533
619
|
rec["project_type_inferred"] = True
|
|
534
|
-
|
|
620
|
+
if HARNESS:
|
|
621
|
+
rec["harness"] = HARNESS # detected, never declared (SCHEMA.md §1)
|
|
622
|
+
else:
|
|
623
|
+
rec.setdefault("harness", None)
|
|
535
624
|
rec.setdefault("app", APP)
|
|
536
625
|
return rec
|
|
537
626
|
|
|
@@ -771,7 +860,7 @@ def enrich_run(rec):
|
|
|
771
860
|
tier = ROUTING["phases"].get(cmd)
|
|
772
861
|
if tier and tier != "inherit":
|
|
773
862
|
rec["tier"] = tier
|
|
774
|
-
key = {"claude-code": "claude", "opencode": "opencode"
|
|
863
|
+
key = {"claude-code": "claude", "opencode": "opencode"}.get(HARNESS)
|
|
775
864
|
tm = ROUTING["tiers"].get(tier, {}).get(key) if key else None
|
|
776
865
|
if tm:
|
|
777
866
|
rec["tier_model"] = tm
|
|
@@ -891,6 +980,7 @@ _AMENDABLE = {
|
|
|
891
980
|
"why_missed": ("missing-checklist-item", "insufficient-verify-method",
|
|
892
981
|
"code-audit-limitation", "ambiguous-acceptance",
|
|
893
982
|
"dependency-not-declared", "instruction-ignored", "other"),
|
|
983
|
+
"sort": ("spec", "unsaid", "weak-check", "ignored"),
|
|
894
984
|
}
|
|
895
985
|
|
|
896
986
|
def _amend_ok(rec):
|
|
@@ -952,6 +1042,14 @@ _MISS_ENUMS = {
|
|
|
952
1042
|
"why_missed": ("missing-checklist-item", "insufficient-verify-method",
|
|
953
1043
|
"code-audit-limitation", "ambiguous-acceptance",
|
|
954
1044
|
"dependency-not-declared", "instruction-ignored", "other"),
|
|
1045
|
+
# whose gap (FR-32, Session 5 2026-09-07): spec = the app's spec did not say it (fix the
|
|
1046
|
+
# checklist line) · unsaid = the framework never said it (a requirement line plus a check) ·
|
|
1047
|
+
# weak-check = a check existed and did not catch it (fix the check) · ignored = it was written
|
|
1048
|
+
# and not followed (a hook, or delete the rule)
|
|
1049
|
+
"sort": ("spec", "unsaid", "weak-check", "ignored"),
|
|
1050
|
+
}
|
|
1051
|
+
_REVIEW_ENUMS = {
|
|
1052
|
+
"phase": ("day1-review", "build-review", "verify-review", "handoff-review"),
|
|
955
1053
|
}
|
|
956
1054
|
_FIX_ENUMS = {
|
|
957
1055
|
"verdict_after": ("Verified", "Needs re-verify", "FAIL", "deferred", "wont-fix"),
|
|
@@ -975,6 +1073,72 @@ def _enums_ok(rec, table):
|
|
|
975
1073
|
return True
|
|
976
1074
|
|
|
977
1075
|
|
|
1076
|
+
# --- shape check (added 2026-09-05, reset Sitting 4a) ------------------------
|
|
1077
|
+
# A record may carry only the fields SCHEMA.md defines for its kind (plus the
|
|
1078
|
+
# ones this script injects). A `miss` must carry a miss_id from --next-miss-id.
|
|
1079
|
+
# Anything else is REFUSED on stdout and never appended, the same choice as
|
|
1080
|
+
# _enums_ok(): a field nobody defined is a field no report can read.
|
|
1081
|
+
_COMMON = {"v", "ts", "kind", "app", "project_type", "project_type_inferred", "harness", "backfilled"}
|
|
1082
|
+
_ALLOWED = {
|
|
1083
|
+
("runs", "run"): {"cmd", "mode", "yolo", "started", "ended", "duration_s", "reqs_touched", "reqs_count",
|
|
1084
|
+
"subagents", "files_written", "build_result", "tier", "tier_model", "model", "models",
|
|
1085
|
+
"routed", "tokens_in", "tokens_out", "tokens_cache_read", "tokens_cache_write",
|
|
1086
|
+
"cost_usd", "tokens_scope", "attempt", "subagent_runs", "tokens_out_subagents",
|
|
1087
|
+
"model_tokens_out", "session_id", "usage_start_event_ts", "usage_end_event_ts"},
|
|
1088
|
+
("gates", "gate"): {"run_id", "req_id", "req_class", "attempt", "verdict", "gate", "gates_run",
|
|
1089
|
+
"failure_class", "prior_verdict", "proof", "escaped", "phase", "result", "detail", "at",
|
|
1090
|
+
"tier", "tier_model", "model", "models", "tokens_in", "tokens_out", "tokens_cache_read",
|
|
1091
|
+
"tokens_cache_write", "cost_usd", "tokens_scope", "model_tokens_out", "routed",
|
|
1092
|
+
"subagent_runs", "tokens_out_subagents", "session_id"},
|
|
1093
|
+
("sessions", "session"): {"session_id", "model", "duration_s", "input_tokens", "output_tokens",
|
|
1094
|
+
"cache_read_tokens", "cache_creation_tokens", "cost_usd", "children_sessions"},
|
|
1095
|
+
("commits", "commit"): {"sha", "files", "insertions", "deletions", "subject_prefix", "branch"},
|
|
1096
|
+
("misses", "miss"): {"miss_id", "req_id", "req_class", "miss_class", "artifact", "severity", "origin_phase",
|
|
1097
|
+
"origin_agent", "origin_run_id", "origin_model", "origin_harness", "origin_confidence",
|
|
1098
|
+
"why_missed", "found_by", "found_phase", "found_gate", "found_run_id", "failure_class",
|
|
1099
|
+
"what", "sort"},
|
|
1100
|
+
("misses", "miss-fix"): {"miss_id", "req_id", "fix_run_id", "fix_cmd", "fix_attempt", "verdict_after",
|
|
1101
|
+
"reopened", "cost_attribution", "tokens_in", "tokens_out", "tokens_cache_read",
|
|
1102
|
+
"tokens_cache_write", "cost_usd", "tokens_scope", "model"},
|
|
1103
|
+
("misses", "miss-amend"): {"miss_id", "field", "value"},
|
|
1104
|
+
# an owner review of a phase's output (FR-36, built 2026-09-06): how many corrections were
|
|
1105
|
+
# given, what producing the reviewed output cost, what applying the corrections cost. The two
|
|
1106
|
+
# costs are copied from the runs named by reviewed_run_id and correction_run_id (their `started`).
|
|
1107
|
+
("misses", "review"): {"phase", "reviewed_run_id", "correction_run_id", "corrections", "what",
|
|
1108
|
+
"tokens_produce", "tokens_correct", "cost_produce_usd", "cost_correct_usd",
|
|
1109
|
+
"model_produce", "model_correct"},
|
|
1110
|
+
}
|
|
1111
|
+
# Fields the emitter derives for runs/gates: a caller's value is discarded.
|
|
1112
|
+
_DERIVED = ("harness", "tier", "tier_model", "model", "models", "model_tokens_out", "routed",
|
|
1113
|
+
"tokens_in", "tokens_out", "tokens_cache_read", "tokens_cache_write", "cost_usd",
|
|
1114
|
+
"tokens_scope", "subagent_runs", "tokens_out_subagents")
|
|
1115
|
+
|
|
1116
|
+
|
|
1117
|
+
def _shape_ok(rec):
|
|
1118
|
+
kind = rec.get("kind")
|
|
1119
|
+
if not isinstance(rec, dict) or not kind:
|
|
1120
|
+
sys.stdout.write("tf-emit: REFUSED — record has no \"kind\" (SCHEMA.md §1). Nothing was appended.\n")
|
|
1121
|
+
return False
|
|
1122
|
+
allowed = _ALLOWED.get((stream, kind))
|
|
1123
|
+
if allowed is None:
|
|
1124
|
+
sys.stdout.write("tf-emit: REFUSED — kind %r is not a record kind of the %s stream. Nothing was appended.\n" % (kind, stream))
|
|
1125
|
+
return False
|
|
1126
|
+
unknown = sorted(set(rec) - allowed - _COMMON)
|
|
1127
|
+
if unknown:
|
|
1128
|
+
sys.stdout.write("tf-emit: REFUSED — field(s) not in SCHEMA.md for a %s record: %s. Add the field to the schema first, or drop it. Nothing was appended.\n" % (kind, ", ".join(unknown)))
|
|
1129
|
+
return False
|
|
1130
|
+
if kind == "run" and rec.get("build_result") not in (None, "pass", "fail", "not-run"):
|
|
1131
|
+
sys.stdout.write("tf-emit: REFUSED — build_result must be pass, fail or not-run (SCHEMA.md §2); got %r. Put the detail in the checklist Remarks or Known blockers, not here. Nothing was appended.\n" % (rec.get("build_result"),))
|
|
1132
|
+
return False
|
|
1133
|
+
if kind == "miss" and not rec.get("miss_id"):
|
|
1134
|
+
sys.stdout.write("tf-emit: REFUSED — a miss record needs a miss_id; get one with: bash .tfcore/utils/tf-emit.sh --next-miss-id. Nothing was appended.\n")
|
|
1135
|
+
return False
|
|
1136
|
+
if stream in ("runs", "gates") and not rec.get("backfilled"):
|
|
1137
|
+
for f in _DERIVED:
|
|
1138
|
+
rec.pop(f, None)
|
|
1139
|
+
return True
|
|
1140
|
+
|
|
1141
|
+
|
|
978
1142
|
def enrich_miss(rec):
|
|
979
1143
|
if stream != "misses":
|
|
980
1144
|
return rec
|
|
@@ -984,6 +1148,21 @@ def enrich_miss(rec):
|
|
|
984
1148
|
return None
|
|
985
1149
|
if rec.get("kind") == "miss-fix" and not _enums_ok(rec, _FIX_ENUMS):
|
|
986
1150
|
return None
|
|
1151
|
+
if rec.get("kind") == "review":
|
|
1152
|
+
if not _enums_ok(rec, _REVIEW_ENUMS):
|
|
1153
|
+
return None
|
|
1154
|
+
if not isinstance(rec.get("corrections"), int) or rec["corrections"] < 0:
|
|
1155
|
+
sys.stdout.write("tf-emit: REFUSED — a review record needs \"corrections\", a whole number of corrections the owner gave. Nothing was appended.\n")
|
|
1156
|
+
return None
|
|
1157
|
+
for f in ("tokens_produce", "tokens_correct", "cost_produce_usd", "cost_correct_usd", "model_produce", "model_correct"):
|
|
1158
|
+
rec.pop(f, None) # derived, never a caller's figure
|
|
1159
|
+
runs = _runs_by_start()
|
|
1160
|
+
for side, key in (("produce", "reviewed_run_id"), ("correct", "correction_run_id")):
|
|
1161
|
+
src = runs.get(rec.get(key)) if rec.get(key) else None
|
|
1162
|
+
rec["tokens_" + side] = src.get("tokens_out") if src else None
|
|
1163
|
+
rec["cost_" + side + "_usd"] = src.get("cost_usd") if src else None
|
|
1164
|
+
rec["model_" + side] = src.get("model") if src else None
|
|
1165
|
+
return rec
|
|
987
1166
|
try:
|
|
988
1167
|
kind = rec.get("kind")
|
|
989
1168
|
runs = _runs_by_start()
|
|
@@ -1003,9 +1182,11 @@ def enrich_miss(rec):
|
|
|
1003
1182
|
rec["origin_harness"] = None
|
|
1004
1183
|
elif kind == "miss-fix":
|
|
1005
1184
|
src = runs.get(rec.get("fix_run_id")) if rec.get("fix_run_id") else None
|
|
1185
|
+
for f in _COST_FIELDS:
|
|
1186
|
+
rec.pop(f, None) # a caller's cost figure is never kept
|
|
1006
1187
|
if src:
|
|
1007
1188
|
for f in _COST_FIELDS:
|
|
1008
|
-
if f in src
|
|
1189
|
+
if f in src:
|
|
1009
1190
|
rec[f] = src[f]
|
|
1010
1191
|
if "cost_attribution" not in rec:
|
|
1011
1192
|
touched = src.get("reqs_touched") or []
|
|
@@ -1048,6 +1229,8 @@ def enrich_miss(rec):
|
|
|
1048
1229
|
# .gitattributes block pins to LF. These files are LF on every platform.
|
|
1049
1230
|
lines = []
|
|
1050
1231
|
for rec in records:
|
|
1232
|
+
if not _shape_ok(rec):
|
|
1233
|
+
continue
|
|
1051
1234
|
out = enrich_miss(enrich_run(stamp_attempt(enrich(rec))))
|
|
1052
1235
|
if out is None: # a refused miss-amend (§5.5.7) — dropped, never appended
|
|
1053
1236
|
continue
|
|
@@ -1072,4 +1255,11 @@ PY
|
|
|
1072
1255
|
)"
|
|
1073
1256
|
python3 -c "$TF_PROG" "$MET_DIR" "$STREAM" "$ROOT" 2>"$TF_ERR"
|
|
1074
1257
|
|
|
1258
|
+
# The readable miss list beside the record (FR-31, Session 5 2026-09-07): after any write to the
|
|
1259
|
+
# misses stream, docs/<App>-Misses.md and its HTML are rebuilt from the stream, so the file is
|
|
1260
|
+
# never older than the record. Best effort, like everything else here.
|
|
1261
|
+
if [[ "$STREAM" == "misses" ]]; then
|
|
1262
|
+
python3 "$(dirname "${BASH_SOURCE[0]}")/tf-misses-md.py" --root "$ROOT" --quiet 2>"$TF_ERR" || true
|
|
1263
|
+
fi
|
|
1264
|
+
|
|
1075
1265
|
exit 0
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tf-fix-close.sh — the telemetry a fix owes when it lands (Sitting 4c, 2026-09-06; D-15).
|
|
3
|
+
#
|
|
4
|
+
# bash .tfcore/utils/tf-fix-close.sh <App> [--started <ISO>] [--reqs REQ-UI-009,REQ-FN-014]
|
|
5
|
+
# [--subagents trblazeui,builder] [--build pass|fail|not-run] [--files N]
|
|
6
|
+
#
|
|
7
|
+
# Emits, in this order, through tf-emit.sh only:
|
|
8
|
+
# 1. the fix-issues run record (cmd fix-issues, mode fix, the rows touched) — first, because the
|
|
9
|
+
# miss-fix records below carry no numbers of their own: the emitter copies this run's token window
|
|
10
|
+
# 2. one miss-fix per row that has an open miss, with verdict_after taken from the verify ledger
|
|
11
|
+
# docs/.last-verify.json written by the verify the fix chained, mapped to a checklist status (PASS →
|
|
12
|
+
# Verified; FAIL → FAIL; any other failing verdict or an absent row → Needs re-verify). A row with no open miss is listed,
|
|
13
|
+
# not invented: a fix on a defect nobody logged is a triage gap, so open it first with tf-triage.sh
|
|
14
|
+
# demote or tf-log-miss.sh, then re-run this.
|
|
15
|
+
# --reqs defaults to every row in the ledger. Telemetry never blocks: refusals are printed, exit 0.
|
|
16
|
+
set -u
|
|
17
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
18
|
+
[[ $# -ge 1 && "$1" != "-h" && "$1" != "--help" ]] || { sed -n '2,17p' "$0" | sed 's/^# \{0,1\}//'; exit 3; }
|
|
19
|
+
APP="$1"; shift
|
|
20
|
+
STARTED=""; REQS=""; SUBS=""; BUILD="pass"; FILES=""
|
|
21
|
+
while [[ $# -gt 0 ]]; do
|
|
22
|
+
case "$1" in
|
|
23
|
+
--started) STARTED="${2:-}"; shift 2 ;;
|
|
24
|
+
--reqs) REQS="${2:-}"; shift 2 ;;
|
|
25
|
+
--subagents) SUBS="${2:-}"; shift 2 ;;
|
|
26
|
+
--build) BUILD="${2:-}"; shift 2 ;;
|
|
27
|
+
--files) FILES="${2:-}"; shift 2 ;;
|
|
28
|
+
*) echo "tf-fix-close: unknown argument $1" >&2; exit 3 ;;
|
|
29
|
+
esac
|
|
30
|
+
done
|
|
31
|
+
[[ -z "$STARTED" ]] && STARTED="$(bash "$HERE/tf-phase.sh" show 2>/dev/null | sed -n 's/.*"started":"\([^"]*\)".*/\1/p')"
|
|
32
|
+
TF_APP="$APP" TF_STARTED="$STARTED" TF_REQS="$REQS" TF_SUBS="$SUBS" TF_BUILD="$BUILD" TF_FILES="$FILES" TF_EMIT="$HERE/tf-emit.sh" python3 - <<'PY'
|
|
33
|
+
import datetime, json, os, subprocess
|
|
34
|
+
EMIT = os.environ["TF_EMIT"]; app = os.environ["TF_APP"]; started = os.environ["TF_STARTED"]
|
|
35
|
+
now = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
36
|
+
def q(*a): return subprocess.run(["bash", EMIT, *a], capture_output=True, text=True).stdout.strip()
|
|
37
|
+
def emit(stream, rec):
|
|
38
|
+
p = subprocess.run(["bash", EMIT, stream], input=json.dumps(rec), capture_output=True, text=True)
|
|
39
|
+
msg = (p.stdout + p.stderr).strip(); ok = p.returncode == 0 and "refus" not in msg.lower() and "error" not in msg.lower()
|
|
40
|
+
if not ok: print(f" {stream}: not written — {msg.splitlines()[0][:160] if msg else 'no reason printed'}")
|
|
41
|
+
return ok
|
|
42
|
+
ledger = {}
|
|
43
|
+
try:
|
|
44
|
+
ledger = json.load(open(os.path.join("docs", ".last-verify.json"), encoding="utf-8"))
|
|
45
|
+
except Exception:
|
|
46
|
+
pass
|
|
47
|
+
rows = ledger.get("rows") or {}
|
|
48
|
+
reqs = [r.strip().upper() for r in os.environ["TF_REQS"].split(",") if r.strip()] or sorted(rows)
|
|
49
|
+
subs = [s.strip() for s in os.environ["TF_SUBS"].split(",") if s.strip()]
|
|
50
|
+
files = os.environ["TF_FILES"]
|
|
51
|
+
run = {"kind": "run", "app": app, "cmd": "fix-issues", "mode": "fix", "ended": now, "reqs_touched": reqs, "reqs_count": len(reqs),
|
|
52
|
+
"subagents": subs, "files_written": int(files) if files.isdigit() else len(reqs), "build_result": os.environ["TF_BUILD"]}
|
|
53
|
+
if started: run["started"] = started
|
|
54
|
+
# one run record per start: a close called twice must not count the fix twice
|
|
55
|
+
runs_path = os.path.join(q("--where") or "docs/metrics", "runs.jsonl")
|
|
56
|
+
already = False
|
|
57
|
+
if started and os.path.isfile(runs_path):
|
|
58
|
+
for line in open(runs_path, encoding="utf-8", errors="replace"):
|
|
59
|
+
if '"cmd":"fix-issues"' in line and f'"started":"{started}"' in line:
|
|
60
|
+
already = True
|
|
61
|
+
break
|
|
62
|
+
ok = True if already else emit("runs", run)
|
|
63
|
+
if already:
|
|
64
|
+
print(f"fix-issues: run record for start {started} already exists; not written again")
|
|
65
|
+
closed, none = 0, []
|
|
66
|
+
for rid in reqs:
|
|
67
|
+
opened = q("--open-miss", rid)
|
|
68
|
+
if not opened:
|
|
69
|
+
none.append(rid); continue
|
|
70
|
+
mid = opened.split()[0]
|
|
71
|
+
fa = q("--next-fix-attempt", mid) or "1"
|
|
72
|
+
v = rows.get(rid)
|
|
73
|
+
# verdict_after takes a checklist status (SCHEMA §5.5): the ledger's verdict is mapped, never passed raw
|
|
74
|
+
# (MISS-TechieFlow-20260906-25: RENDER-FAIL was refused and twenty miss-fix records never landed)
|
|
75
|
+
after = ("Verified" if v == "PASS" else "FAIL" if v in ("FAIL", "BUILD-FAIL") else "Needs re-verify")
|
|
76
|
+
rec = {"kind": "miss-fix", "miss_id": mid, "req_id": rid, "fix_cmd": "fix-issues", "fix_attempt": int(fa) if fa.isdigit() else 1,
|
|
77
|
+
"verdict_after": after, "reopened": False}
|
|
78
|
+
if started: rec["fix_run_id"] = started
|
|
79
|
+
if emit("misses", rec): closed += 1
|
|
80
|
+
print(f"fix-issues: run record {'already there' if already else 'written' if ok else 'not written'}; {closed} miss-fix record(s) with the verifier's verdict"
|
|
81
|
+
+ (f"; no open miss on {', '.join(none)} (log it first, then re-run)" if none else ""))
|
|
82
|
+
PY
|
|
83
|
+
exit 0
|