@techierathore/techieflow 1.0.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 (151) hide show
  1. package/.claude/commands/TechieFlow/agents/analyst.md +97 -0
  2. package/.claude/commands/TechieFlow/agents/architect.md +84 -0
  3. package/.claude/commands/TechieFlow/agents/flow-master.md +217 -0
  4. package/.claude/commands/TechieFlow/agents/verifier.md +72 -0
  5. package/.claude/commands/TechieFlow/tasks/_metrics-emit-gate.md +138 -0
  6. package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +98 -0
  7. package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +78 -0
  8. package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +77 -0
  9. package/.claude/commands/TechieFlow/tasks/advanced-elicitation.md +117 -0
  10. package/.claude/commands/TechieFlow/tasks/amend-docs.md +149 -0
  11. package/.claude/commands/TechieFlow/tasks/author-brd.md +79 -0
  12. package/.claude/commands/TechieFlow/tasks/build-phase.md +231 -0
  13. package/.claude/commands/TechieFlow/tasks/create-deep-research-prompt.md +278 -0
  14. package/.claude/commands/TechieFlow/tasks/create-doc.md +101 -0
  15. package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +507 -0
  16. package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +224 -0
  17. package/.claude/commands/TechieFlow/tasks/devguide.md +224 -0
  18. package/.claude/commands/TechieFlow/tasks/document-project.md +343 -0
  19. package/.claude/commands/TechieFlow/tasks/execute-checklist.md +86 -0
  20. package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +136 -0
  21. package/.claude/commands/TechieFlow/tasks/fix-issues.md +134 -0
  22. package/.claude/commands/TechieFlow/tasks/generate-html.md +115 -0
  23. package/.claude/commands/TechieFlow/tasks/handoff-phase.md +126 -0
  24. package/.claude/commands/TechieFlow/tasks/index-docs.md +173 -0
  25. package/.claude/commands/TechieFlow/tasks/kb-mode-interaction.md +75 -0
  26. package/.claude/commands/TechieFlow/tasks/log-miss.md +160 -0
  27. package/.claude/commands/TechieFlow/tasks/metrics-report.md +143 -0
  28. package/.claude/commands/TechieFlow/tasks/mockups.md +122 -0
  29. package/.claude/commands/TechieFlow/tasks/productguide.md +105 -0
  30. package/.claude/commands/TechieFlow/tasks/refresh-status.md +135 -0
  31. package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +117 -0
  32. package/.claude/commands/TechieFlow/tasks/shard-doc.md +185 -0
  33. package/.claude/commands/TechieFlow/tasks/split-brd.md +131 -0
  34. package/.claude/commands/TechieFlow/tasks/triage-issues.md +157 -0
  35. package/.claude/commands/TechieFlow/tasks/verify-phase.md +579 -0
  36. package/.claude/commands/generate-html.md +124 -0
  37. package/.codex/config.toml +46 -0
  38. package/.codex/hooks.json +63 -0
  39. package/.codex/rules/techieflow.rules +8 -0
  40. package/.opencode/command/generate-html.md +129 -0
  41. package/.opencode/plugin/techieflow.js +350 -0
  42. package/.tfcore/TOKEN-GUIDE.md +59 -0
  43. package/.tfcore/agents/analyst.md +97 -0
  44. package/.tfcore/agents/architect.md +84 -0
  45. package/.tfcore/agents/flow-master.md +217 -0
  46. package/.tfcore/agents/verifier.md +72 -0
  47. package/.tfcore/checklists/architect-checklist.md +438 -0
  48. package/.tfcore/core-config.yaml +47 -0
  49. package/.tfcore/data/brainstorming-techniques.md +36 -0
  50. package/.tfcore/data/elicitation-methods.md +154 -0
  51. package/.tfcore/data/techieflow-kb.md +807 -0
  52. package/.tfcore/data/technical-preferences.md +3 -0
  53. package/.tfcore/enhanced-ide-development-workflow.md +248 -0
  54. package/.tfcore/hooks/block-git.sh +301 -0
  55. package/.tfcore/hooks/codex-adapter.py +248 -0
  56. package/.tfcore/hooks/guard-artifacts.sh +101 -0
  57. package/.tfcore/hooks/guard-status-html.sh +102 -0
  58. package/.tfcore/hooks/guard-status.sh +124 -0
  59. package/.tfcore/hooks/guard-verify.sh +108 -0
  60. package/.tfcore/hooks/metrics-session.sh +129 -0
  61. package/.tfcore/hooks/session-pointer.sh +52 -0
  62. package/.tfcore/hooks/sweep-artifacts.sh +198 -0
  63. package/.tfcore/install-manifest.yaml +231 -0
  64. package/.tfcore/routing.yaml +105 -0
  65. package/.tfcore/tasks/_metrics-emit-gate.md +138 -0
  66. package/.tfcore/tasks/_smoke-test-policy.md +98 -0
  67. package/.tfcore/tasks/_status-update-gate.md +78 -0
  68. package/.tfcore/tasks/_yolo-mode.md +77 -0
  69. package/.tfcore/tasks/advanced-elicitation.md +117 -0
  70. package/.tfcore/tasks/amend-docs.md +149 -0
  71. package/.tfcore/tasks/author-brd.md +79 -0
  72. package/.tfcore/tasks/build-phase.md +231 -0
  73. package/.tfcore/tasks/create-deep-research-prompt.md +278 -0
  74. package/.tfcore/tasks/create-doc.md +101 -0
  75. package/.tfcore/tasks/day1-brownfield.md +507 -0
  76. package/.tfcore/tasks/day1-greenfield.md +224 -0
  77. package/.tfcore/tasks/devguide.md +224 -0
  78. package/.tfcore/tasks/document-project.md +343 -0
  79. package/.tfcore/tasks/execute-checklist.md +86 -0
  80. package/.tfcore/tasks/facilitate-brainstorming-session.md +136 -0
  81. package/.tfcore/tasks/fix-issues.md +134 -0
  82. package/.tfcore/tasks/generate-html.md +115 -0
  83. package/.tfcore/tasks/handoff-phase.md +126 -0
  84. package/.tfcore/tasks/index-docs.md +173 -0
  85. package/.tfcore/tasks/kb-mode-interaction.md +75 -0
  86. package/.tfcore/tasks/log-miss.md +160 -0
  87. package/.tfcore/tasks/metrics-report.md +143 -0
  88. package/.tfcore/tasks/mockups.md +122 -0
  89. package/.tfcore/tasks/productguide.md +105 -0
  90. package/.tfcore/tasks/refresh-status.md +135 -0
  91. package/.tfcore/tasks/render-workflow-docs.md +117 -0
  92. package/.tfcore/tasks/shard-doc.md +185 -0
  93. package/.tfcore/tasks/split-brd.md +131 -0
  94. package/.tfcore/tasks/triage-issues.md +157 -0
  95. package/.tfcore/tasks/verify-phase.md +579 -0
  96. package/.tfcore/telemetry/SCHEMA.md +678 -0
  97. package/.tfcore/telemetry/install-metrics.sh +390 -0
  98. package/.tfcore/telemetry/pre-commit +94 -0
  99. package/.tfcore/telemetry/tf-metrics.sh +1415 -0
  100. package/.tfcore/templates/architecture-tmpl.yaml +650 -0
  101. package/.tfcore/templates/brainstorming-output-tmpl.yaml +156 -0
  102. package/.tfcore/templates/brownfield-architecture-tmpl.yaml +476 -0
  103. package/.tfcore/templates/competitor-analysis-tmpl.yaml +306 -0
  104. package/.tfcore/templates/front-end-architecture-tmpl.yaml +218 -0
  105. package/.tfcore/templates/fullstack-architecture-tmpl.yaml +823 -0
  106. package/.tfcore/templates/market-research-tmpl.yaml +252 -0
  107. package/.tfcore/templates/project-brief-tmpl.yaml +221 -0
  108. package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +61 -0
  109. package/.tfcore/templates/v4custom/app-architecture-tmpl.md +127 -0
  110. package/.tfcore/templates/v4custom/app-brd-tmpl.md +204 -0
  111. package/.tfcore/templates/v4custom/app-checklist-tmpl.md +90 -0
  112. package/.tfcore/templates/v4custom/app-claude-md-tmpl.md +33 -0
  113. package/.tfcore/templates/v4custom/app-devguide-tmpl.md +167 -0
  114. package/.tfcore/templates/v4custom/app-editorconfig-tmpl.editorconfig +44 -0
  115. package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +31 -0
  116. package/.tfcore/templates/v4custom/app-productguide-tmpl.md +54 -0
  117. package/.tfcore/templates/v4custom/app-project-status-tmpl.md +98 -0
  118. package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +48 -0
  119. package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +67 -0
  120. package/.tfcore/templates/v4custom/build-invocation-ladder.md +190 -0
  121. package/.tfcore/templates/v4custom/html-render-shell.md +502 -0
  122. package/.tfcore/templates/v4custom/metrics-report-template.md +273 -0
  123. package/.tfcore/templates/v4custom/opencode-operating-contract.md +56 -0
  124. package/.tfcore/user-guide.md +633 -0
  125. package/.tfcore/utils/techieflow-doc-template.md +325 -0
  126. package/.tfcore/utils/tf-assets.sh +310 -0
  127. package/.tfcore/utils/tf-codex-bind.py +196 -0
  128. package/.tfcore/utils/tf-codex-telemetry.py +60 -0
  129. package/.tfcore/utils/tf-emit.sh +1075 -0
  130. package/.tfcore/utils/tf-gitignore-audit.sh +281 -0
  131. package/.tfcore/utils/tf-goal.sh +421 -0
  132. package/.tfcore/utils/tf-harness.sh +160 -0
  133. package/.tfcore/utils/tf-mockup-parity.mjs +555 -0
  134. package/.tfcore/utils/tf-mockup-parity.sh +65 -0
  135. package/.tfcore/utils/tf-opencode-audit.py +126 -0
  136. package/.tfcore/utils/tf-perf.sh +241 -0
  137. package/.tfcore/utils/tf-render-html.py +715 -0
  138. package/.tfcore/utils/tf-render-html.sh +28 -0
  139. package/.tfcore/utils/tf-routing-bind.sh +216 -0
  140. package/.tfcore/utils/tf-routing.sh +247 -0
  141. package/.tfcore/utils/tf-yolo.sh +119 -0
  142. package/.tfcore/working-in-the-brownfield.md +606 -0
  143. package/LICENSE +201 -0
  144. package/README.md +1212 -0
  145. package/WORKFLOW.html +1986 -0
  146. package/docs/TechieFlow-Installation.md +299 -0
  147. package/opencode.jsonc +929 -0
  148. package/package.json +60 -0
  149. package/scripts/install.mjs +992 -0
  150. package/scripts/test-install.mjs +505 -0
  151. package/scripts/validate.mjs +155 -0
@@ -0,0 +1,281 @@
1
+ #!/usr/bin/env bash
2
+ # TechieFlow — build-output ignore audit. Run by the scaffolds and by
3
+ # update-framework.sh on every refresh; safe to run by hand at any time.
4
+ #
5
+ # bash .tfcore/utils/tf-gitignore-audit.sh [<repo>] [--fix] [--dry-run]
6
+ #
7
+ # WHAT IT ASKS, and it is two questions rather than one:
8
+ #
9
+ # 1. Does .gitignore carry build-output rules for THE STACK THIS REPO IS
10
+ # ACTUALLY WRITTEN IN?
11
+ # 2. Is that build output already TRACKED?
12
+ #
13
+ # The second half is the half that is usually forgotten, and it is the half that
14
+ # matters: **a tracked file is never ignored, no matter what the ignore file
15
+ # says.** Adding the rule later fixes nothing on its own.
16
+ #
17
+ # WHY IT EXISTS (TfLens TF-007 companion 1, located 2026-08-29). The day-1 scaffold
18
+ # generated a complete, careful `.gitignore` — `.tfcore/`, `.claude/`,
19
+ # `node_modules/`, `tests/.artifacts/`, `playwright-report/`, `logs/` — every
20
+ # section framework-managed and labelled as such. It contained **no `bin/`, no
21
+ # `obj/`, and no rule of any kind for the stack the project was written in**, in a
22
+ # repository whose `core-config.yaml` and four `.csproj` files said .NET throughout.
23
+ #
24
+ # The consequence was mechanical and immediate: the first build produced build
25
+ # output, and a commit named — with some irony — "Updated git ignore" swept **1,041**
26
+ # build-output files into the index. Four later commits reached **1,962**.
27
+ #
28
+ # That is not merely untidy. Those files carry the static-web-assets manifest, whose
29
+ # content roots are MACHINE-ABSOLUTE: `/mnt/c/…` + `/home/<user>/.nuget/…` after a
30
+ # WSL build, `C:\…` + `C:\Users\<user>\.nuget\…` after a Windows build. Committing
31
+ # them ships one machine's absolute paths to another — a plausible route to exactly
32
+ # the asset 404 that TF-007 itself is about.
33
+ #
34
+ # WHOSE FAULT IT IS, stated plainly because it was first stated wrongly. The agent
35
+ # that ran day-1 generated that file and did not read it, having just chosen the
36
+ # stack and written the solution itself. **That agent is responsible.** This script
37
+ # makes the mistake harder to make — which is worth doing precisely because it is so
38
+ # easy to make — but a generator's omission is never a defence for the agent
39
+ # operating the generator.
40
+ #
41
+ # NO GIT. This script contains no git command and never will: `_metrics-emit-gate.md`
42
+ # constraint 1 reserves that for `tf-metrics.sh` alone, and `block-git.sh` is right to
43
+ # block it. The tracked-file check reads `.git/index` directly — a binary file with a
44
+ # documented format, and reading a file is not a git operation. Remediation is PRINTED
45
+ # for the owner to run, never executed.
46
+ #
47
+ # Exit codes: 0 clean (or --fix applied) · 1 rules missing · 2 build output is TRACKED
48
+ # (owner action required) · 3 both.
49
+
50
+ set -uo pipefail
51
+
52
+ REPO="."; FIX=0; DRY=0
53
+ while [[ $# -gt 0 ]]; do
54
+ case "$1" in
55
+ --fix) FIX=1; shift ;;
56
+ --dry-run) DRY=1; shift ;;
57
+ -h|--help) sed -n '2,45p' "$0"; exit 0 ;;
58
+ *) REPO="$1"; shift ;;
59
+ esac
60
+ done
61
+ [[ -d "$REPO" ]] || { echo "tf-gitignore-audit: not a directory: $REPO" >&2; exit 3; }
62
+ command -v python3 >/dev/null 2>&1 || { echo "tf-gitignore-audit: python3 required" >&2; exit 0; }
63
+
64
+ TF_REPO="$REPO" TF_FIX="$FIX" TF_DRY="$DRY" python3 - <<'PY'
65
+ import os, struct, sys
66
+
67
+ REPO = os.path.abspath(os.environ["TF_REPO"])
68
+ FIX = os.environ["TF_FIX"] == "1"
69
+ DRY = os.environ["TF_DRY"] == "1"
70
+
71
+ # --- the stacks, and what each one leaves behind ---------------------------
72
+ # Detected FROM THE TREE, never from a config value: a scaffold copies
73
+ # core-config.yaml between projects, and the day-1 tasks that write it are the very
74
+ # ones this audit exists to catch. The file system is the fact.
75
+ STACKS = [
76
+ ("dotnet", (".csproj", ".fsproj", ".vbproj", ".sln", ".slnx"),
77
+ ["bin/", "obj/", "*.user", "TestResults/"]),
78
+ ("node", ("package.json",), ["dist/", "build/", ".next/", ".nuxt/", ".turbo/"]),
79
+ ("python", ("pyproject.toml", "setup.py", "requirements.txt"),
80
+ ["__pycache__/", "*.py[cod]", ".venv/", "venv/", ".pytest_cache/", "*.egg-info/"]),
81
+ ("go", ("go.mod",), ["/bin/", "*.test"]),
82
+ ("rust", ("Cargo.toml",), ["target/"]),
83
+ ("java", ("pom.xml", "build.gradle", "build.gradle.kts"), ["target/", "build/", ".gradle/"]),
84
+ ]
85
+
86
+ # `package.json` alone does NOT make a repo a Node project, and this exception is
87
+ # load-bearing rather than fussy: verify-phase §1 runs `npm init -y` in EVERY repo
88
+ # to provision Playwright, so a bare package.json sits in essentially every .NET
89
+ # project the framework has ever touched. Treating that as a Node stack would append
90
+ # `dist/`, `build/`, `.next/`, `.nuxt/`, `.turbo/` to 19 repos that build none of
91
+ # them — noise in a file the owner reads, and `build/` is a plausible real directory
92
+ # name to shadow by accident.
93
+ #
94
+ # So Node is claimed only on evidence of an actual build: a `build` script, or a
95
+ # bundler/framework config on disk. This was caught by the dry run before the
96
+ # propagation, which is what dry runs are for.
97
+ BUNDLER_CONFIGS = ("next.config.js", "next.config.mjs", "next.config.ts",
98
+ "nuxt.config.js", "nuxt.config.ts", "vite.config.js",
99
+ "vite.config.ts", "webpack.config.js", "angular.json",
100
+ "svelte.config.js", "rollup.config.js", "gatsby-config.js",
101
+ "remix.config.js", "astro.config.mjs")
102
+
103
+
104
+ def _is_real_node(root, files):
105
+ if any(f in BUNDLER_CONFIGS for f in files):
106
+ return True
107
+ pj = os.path.join(root, "package.json")
108
+ try:
109
+ import json as _json
110
+ data = _json.load(open(pj, encoding="utf-8"))
111
+ except Exception:
112
+ return False
113
+ scripts = data.get("scripts") or {}
114
+ # `npm init -y` writes exactly one script: a `test` stub that echoes and exits 1.
115
+ return any(k in scripts for k in ("build", "dist", "bundle", "compile"))
116
+
117
+ SKIP_DIRS = {".git", "node_modules", ".tfcore", "bin", "obj", "target", "dist",
118
+ "build", ".venv", "venv", "__pycache__", ".next"}
119
+
120
+
121
+ def detect():
122
+ found = set()
123
+ for root, dirs, files in os.walk(REPO):
124
+ dirs[:] = [d for d in dirs if d not in SKIP_DIRS and not d.startswith(".")]
125
+ if root.count(os.sep) - REPO.count(os.sep) > 3:
126
+ dirs[:] = []
127
+ for name, markers, _ in STACKS:
128
+ for f in files:
129
+ if f in markers or any(f.endswith(m) for m in markers if m.startswith(".")):
130
+ if name == "node" and not _is_real_node(root, files):
131
+ continue # a Playwright-only package.json is not a Node app
132
+ found.add(name)
133
+ return found
134
+
135
+
136
+ def gitignore_lines():
137
+ p = os.path.join(REPO, ".gitignore")
138
+ try:
139
+ return [l.strip().replace("\r", "") for l in open(p, encoding="utf-8", errors="replace")]
140
+ except FileNotFoundError:
141
+ return []
142
+
143
+
144
+ def covered(rule, lines):
145
+ """Is this rule already expressed? Match tolerantly — an owner may legitimately
146
+ have written `**/bin/`, `/bin`, or `bin` — and NEVER rewrite existing content."""
147
+ core = rule.strip("/*").rstrip("/")
148
+ for l in lines:
149
+ if not l or l.startswith("#"):
150
+ continue
151
+ c = l.lstrip("!").strip().replace("**/", "").strip("/")
152
+ if c == core or c == rule.strip("/") or c.rstrip("/") == core:
153
+ return True
154
+ return False
155
+
156
+
157
+ def tracked_paths():
158
+ """Read .git/index directly. Returns the set of tracked paths, or None when the
159
+ index cannot be read (a bare repo, a worktree file, a version we do not know) —
160
+ None means 'not checked', never 'nothing tracked'."""
161
+ gitdir = os.path.join(REPO, ".git")
162
+ if os.path.isfile(gitdir): # a worktree: .git is a pointer file
163
+ try:
164
+ for line in open(gitdir, encoding="utf-8"):
165
+ if line.startswith("gitdir:"):
166
+ gd = line.split(":", 1)[1].strip()
167
+ gitdir = gd if os.path.isabs(gd) else os.path.join(REPO, gd)
168
+ break
169
+ except Exception:
170
+ return None
171
+ idx = os.path.join(gitdir, "index")
172
+ try:
173
+ raw = open(idx, "rb").read()
174
+ except Exception:
175
+ return None
176
+ if len(raw) < 12 or raw[:4] != b"DIRC":
177
+ return None
178
+ ver, n = struct.unpack(">II", raw[4:12])
179
+ if ver not in (2, 3, 4):
180
+ return None
181
+ if ver == 4:
182
+ return None # path-compressed; not worth guessing
183
+ out, pos = set(), 12
184
+ try:
185
+ for _ in range(n):
186
+ start = pos
187
+ pos += 62 # fixed entry header
188
+ end = raw.index(b"\x00", pos)
189
+ out.add(raw[pos:end].decode("utf-8", "replace"))
190
+ pos = end + 1
191
+ pos = start + ((pos - start + 7) // 8) * 8 # 8-byte padding
192
+ except Exception:
193
+ return None
194
+ return out
195
+
196
+
197
+ def matches(path, rule):
198
+ core = rule.strip("/*").rstrip("/")
199
+ if rule.startswith("*."):
200
+ return path.endswith(rule[1:])
201
+ if "[" in rule:
202
+ return path.endswith((".pyc", ".pyo", ".pyd"))
203
+ parts = path.split("/")
204
+ return core in parts[:-1] or parts[-1] == core
205
+
206
+
207
+ stacks = detect()
208
+ lines = gitignore_lines()
209
+ wanted = []
210
+ for name, _, rules in STACKS:
211
+ if name in stacks:
212
+ wanted += [(name, r) for r in rules]
213
+
214
+ missing = [(s, r) for s, r in wanted if not covered(r, lines)]
215
+
216
+ tracked = tracked_paths()
217
+ offenders = {}
218
+ if tracked is not None:
219
+ for s, r in wanted:
220
+ hits = [p for p in tracked if matches(p, r)]
221
+ if hits:
222
+ offenders[r] = hits
223
+
224
+ label = ", ".join(sorted(stacks)) or "none detected"
225
+ print(" gitignore audit — stack: %s" % label)
226
+
227
+ rc = 0
228
+ if not stacks:
229
+ print(" no application stack detected yet (docs-only repo, or day-1 has not run)")
230
+ elif not missing:
231
+ print(" build-output rules present for every detected stack")
232
+ else:
233
+ rc |= 1
234
+ names = " ".join(r for _, r in missing)
235
+ if FIX and not DRY:
236
+ with open(os.path.join(REPO, ".gitignore"), "a", encoding="utf-8", newline="\n") as fh:
237
+ fh.write("\n# Build output — %s (managed by scaffold/update-framework.sh)\n" % label)
238
+ fh.write("\n".join(r for _, r in missing) + "\n")
239
+ print(" ADDED build-output rules: %s" % names)
240
+ rc &= ~1
241
+ elif DRY:
242
+ print(" WOULD add build-output rules: %s" % names)
243
+ else:
244
+ print(" ⚠ MISSING build-output rules: %s" % names)
245
+ print(" add them with: bash .tfcore/utils/tf-gitignore-audit.sh . --fix")
246
+
247
+ if tracked is None:
248
+ print(" tracked-file check skipped (no readable .git/index) — rules alone do not")
249
+ print(" prove build output is untracked; re-run where the index is readable")
250
+ elif offenders:
251
+ rc |= 2
252
+ total = sum(len(v) for v in offenders.values())
253
+ print(" ⚠ %d build-output file(s) are ALREADY TRACKED. An ignore rule does NOT" % total)
254
+ print(" untrack a file — until these are removed from the index, every rule")
255
+ print(" above is inert and one machine's absolute paths keep shipping to the next.")
256
+ for r, hits in sorted(offenders.items()):
257
+ ex = sorted(hits)[:3]
258
+ print(" %-14s %4d file(s) e.g. %s" % (r, len(hits), ", ".join(ex)))
259
+ print(" OWNER ACTION (this script never runs git, and never will):")
260
+ # The path printed must be the OUTPUT directory itself, never an ancestor:
261
+ # `git rm -r --cached src/App` would untrack the source alongside the artefacts,
262
+ # which is a far worse outcome than the problem being fixed.
263
+ targets = set()
264
+ for r, hits in sorted(offenders.items()):
265
+ core = r.strip("/*").rstrip("/")
266
+ for h in hits:
267
+ parts = h.split("/")
268
+ if core in parts[:-1]:
269
+ targets.add("/".join(parts[:parts.index(core) + 1]))
270
+ else:
271
+ targets.add(h) # a file rule (*.user, *.test)
272
+ for t in sorted(targets)[:12]:
273
+ print(" git rm -r --cached '%s'" % t)
274
+ if len(targets) > 12:
275
+ print(" ... and %d more path(s); see the full list above" % (len(targets) - 12))
276
+ print(" then commit. The working files stay on disk; only the index entries go.")
277
+ else:
278
+ print(" no build output is tracked")
279
+
280
+ raise SystemExit(rc)
281
+ PY