@capagents/forgeqe 0.0.0-stage → 1.0.1

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 (220) hide show
  1. package/HOW_TO_USE.md +1639 -0
  2. package/README.md +338 -2
  3. package/bin/forgeqe.d.ts +3 -0
  4. package/bin/forgeqe.js +188 -0
  5. package/docs/ARCHITECTURE.md +408 -0
  6. package/docs/security-data-flow.html +1870 -0
  7. package/docs/user-guide.html +2094 -0
  8. package/evals/domain/access-role-change.yaml +37 -0
  9. package/evals/domain/banking-payment-transfer.yaml +41 -0
  10. package/evals/domain/gdpr-account-erasure.yaml +30 -0
  11. package/evals/domain/insurance-claim-fnol.yaml +30 -0
  12. package/evals/domain/insurance-policy-renewal.yaml +37 -0
  13. package/evals/domain/lending-loan-application.yaml +36 -0
  14. package/forgeqe-architecture.html +718 -0
  15. package/forgeqe.example.yaml +203 -0
  16. package/knowledge/README.md +18 -0
  17. package/knowledge/business-rules.md +10 -0
  18. package/knowledge/compliance.md +16 -0
  19. package/knowledge/environments.md +5 -0
  20. package/knowledge/glossary.md +5 -0
  21. package/knowledge/product.md +16 -0
  22. package/knowledge/roles.md +9 -0
  23. package/knowledge/test-data.md +7 -0
  24. package/llms.example.json +51 -0
  25. package/package.json +63 -4
  26. package/prompts/README.md +34 -0
  27. package/prompts/api-scenarios.md +21 -0
  28. package/prompts/api-tests.md +39 -0
  29. package/prompts/brain.md +166 -0
  30. package/prompts/browser-capture.md +7 -0
  31. package/prompts/browser-case-report.md +7 -0
  32. package/prompts/browser-checks.md +14 -0
  33. package/prompts/browser-efficiency.md +7 -0
  34. package/prompts/browser-error-recovery.md +15 -0
  35. package/prompts/browser-extra.md +5 -0
  36. package/prompts/browser-goal-verification.md +13 -0
  37. package/prompts/browser-skill-select.md +6 -0
  38. package/prompts/domain-rule.md +13 -0
  39. package/prompts/eval-judge.md +19 -0
  40. package/prompts/explore-charters.md +27 -0
  41. package/prompts/find-tests.md +28 -0
  42. package/prompts/heal.md +40 -0
  43. package/prompts/pr-review.md +29 -0
  44. package/prompts/skill-rule.md +6 -0
  45. package/prompts/test-agent.md +34 -0
  46. package/prompts/test-reviewer.md +30 -0
  47. package/prompts/test-rules.md +28 -0
  48. package/py/forgeqe_engine/__init__.py +3 -0
  49. package/py/forgeqe_engine/__main__.py +3 -0
  50. package/py/forgeqe_engine/brand.py +453 -0
  51. package/py/forgeqe_engine/brand_assets.py +6 -0
  52. package/py/forgeqe_engine/browser.py +196 -0
  53. package/py/forgeqe_engine/capture.py +538 -0
  54. package/py/forgeqe_engine/checks.py +98 -0
  55. package/py/forgeqe_engine/cli.py +188 -0
  56. package/py/forgeqe_engine/diagnostics.py +609 -0
  57. package/py/forgeqe_engine/llm.py +195 -0
  58. package/py/forgeqe_engine/logins.py +251 -0
  59. package/py/forgeqe_engine/loops.py +224 -0
  60. package/py/forgeqe_engine/memory.py +1004 -0
  61. package/py/forgeqe_engine/network.py +244 -0
  62. package/py/forgeqe_engine/prompts.py +20 -0
  63. package/py/forgeqe_engine/replay.py +240 -0
  64. package/py/forgeqe_engine/report.py +173 -0
  65. package/py/forgeqe_engine/service.py +1335 -0
  66. package/py/forgeqe_engine/settings.py +93 -0
  67. package/py/forgeqe_engine/skills.py +88 -0
  68. package/py/forgeqe_engine/summary.py +239 -0
  69. package/py/forgeqe_engine/urls.py +81 -0
  70. package/py/forgeqe_engine/workflow_yaml.py +689 -0
  71. package/skills/dotnet-nunit-playwright/SKILL.md +34 -0
  72. package/skills/dynamics-365/SKILL.md +21 -0
  73. package/skills/playwright-typescript/SKILL.md +35 -0
  74. package/skills/writing-skills/SKILL.md +30 -0
  75. package/src/api/plan.ts +246 -0
  76. package/src/api/run.ts +204 -0
  77. package/src/api/scenario.ts +502 -0
  78. package/src/api/schema.ts +170 -0
  79. package/src/api/spec.ts +624 -0
  80. package/src/api/store.ts +232 -0
  81. package/src/api/traffic.ts +281 -0
  82. package/src/api/types.ts +163 -0
  83. package/src/api/write.ts +272 -0
  84. package/src/auth/signin.ts +494 -0
  85. package/src/auth/store.ts +170 -0
  86. package/src/brain/index.ts +189 -0
  87. package/src/brain/prompt.ts +19 -0
  88. package/src/capabilities/docs.ts +101 -0
  89. package/src/capabilities/guide.ts +455 -0
  90. package/src/capabilities/registry.ts +343 -0
  91. package/src/capabilities/render.ts +106 -0
  92. package/src/capabilities/status.ts +137 -0
  93. package/src/card.ts +11 -0
  94. package/src/cases/ado.ts +325 -0
  95. package/src/cases/api.ts +117 -0
  96. package/src/cases/bugs.ts +223 -0
  97. package/src/cases/confluence.ts +111 -0
  98. package/src/cases/context.ts +223 -0
  99. package/src/cases/dashboards.ts +137 -0
  100. package/src/cases/failure.ts +97 -0
  101. package/src/cases/files.ts +295 -0
  102. package/src/cases/goal.ts +53 -0
  103. package/src/cases/http.ts +111 -0
  104. package/src/cases/index.ts +131 -0
  105. package/src/cases/jira.ts +122 -0
  106. package/src/cases/qtest.ts +108 -0
  107. package/src/cases/quality.ts +190 -0
  108. package/src/cases/rag.ts +100 -0
  109. package/src/cases/readiness.ts +284 -0
  110. package/src/cases/save.ts +149 -0
  111. package/src/cases/sprints.ts +148 -0
  112. package/src/cases/testrail.ts +176 -0
  113. package/src/cases/types.ts +107 -0
  114. package/src/cases/wiki.ts +105 -0
  115. package/src/cases/xray.ts +206 -0
  116. package/src/cases/zephyr.ts +153 -0
  117. package/src/childEnv.ts +86 -0
  118. package/src/cli/args.ts +114 -0
  119. package/src/cli/help.ts +167 -0
  120. package/src/cli/index.ts +1142 -0
  121. package/src/cli/terminal.ts +187 -0
  122. package/src/code/areas.ts +337 -0
  123. package/src/code/attention.ts +214 -0
  124. package/src/code/find.ts +540 -0
  125. package/src/code/handoff.ts +541 -0
  126. package/src/code/history.ts +170 -0
  127. package/src/code/impact.ts +231 -0
  128. package/src/code/index.ts +34 -0
  129. package/src/code/livebrowser.ts +67 -0
  130. package/src/code/loop.ts +603 -0
  131. package/src/code/notes.ts +75 -0
  132. package/src/code/profile.ts +226 -0
  133. package/src/code/prompt.ts +303 -0
  134. package/src/code/prreview.ts +649 -0
  135. package/src/code/prtests.ts +103 -0
  136. package/src/code/replay.ts +772 -0
  137. package/src/code/review.ts +202 -0
  138. package/src/code/runtime.ts +781 -0
  139. package/src/code/slices.ts +159 -0
  140. package/src/code/testrun.ts +187 -0
  141. package/src/code/traceability.ts +100 -0
  142. package/src/code/worktree.ts +153 -0
  143. package/src/commands.ts +101 -0
  144. package/src/config.ts +970 -0
  145. package/src/detail.ts +65 -0
  146. package/src/domain.ts +19 -0
  147. package/src/engine/bridge.ts +359 -0
  148. package/src/engine/mock.ts +102 -0
  149. package/src/engine/setup.ts +298 -0
  150. package/src/entra.ts +154 -0
  151. package/src/env.ts +24 -0
  152. package/src/evals/domain.ts +338 -0
  153. package/src/explore/charters.ts +151 -0
  154. package/src/explore/run.ts +280 -0
  155. package/src/explore/story.ts +106 -0
  156. package/src/guard.ts +51 -0
  157. package/src/heal/loop.ts +357 -0
  158. package/src/heal/prompt.ts +143 -0
  159. package/src/heal/report.ts +95 -0
  160. package/src/index.ts +12 -0
  161. package/src/init.ts +102 -0
  162. package/src/knowledge.ts +114 -0
  163. package/src/legacyEnv.ts +9 -0
  164. package/src/llm.ts +61 -0
  165. package/src/locale.ts +25 -0
  166. package/src/logins.ts +148 -0
  167. package/src/managed.ts +101 -0
  168. package/src/mcp/apitools.ts +335 -0
  169. package/src/mcp/captools.ts +25 -0
  170. package/src/mcp/casetools.ts +223 -0
  171. package/src/mcp/commands.ts +421 -0
  172. package/src/mcp/exploretools.ts +129 -0
  173. package/src/mcp/external.ts +172 -0
  174. package/src/mcp/git.ts +78 -0
  175. package/src/mcp/notifytools.ts +49 -0
  176. package/src/mcp/prtools.ts +261 -0
  177. package/src/mcp/qualitytools.ts +154 -0
  178. package/src/mcp/repotests.ts +286 -0
  179. package/src/mcp/server.ts +234 -0
  180. package/src/mcp/tools.ts +810 -0
  181. package/src/memory.ts +250 -0
  182. package/src/money.ts +39 -0
  183. package/src/monitor/alerts.ts +94 -0
  184. package/src/monitor/background.ts +68 -0
  185. package/src/monitor/run.ts +263 -0
  186. package/src/monitor/state.ts +105 -0
  187. package/src/notify/events.ts +144 -0
  188. package/src/notify/notice.ts +177 -0
  189. package/src/prices.ts +175 -0
  190. package/src/print.ts +54 -0
  191. package/src/privacy.ts +35 -0
  192. package/src/prompts.ts +165 -0
  193. package/src/quality/budgets.ts +249 -0
  194. package/src/quality/history.ts +149 -0
  195. package/src/quality/visual.ts +201 -0
  196. package/src/report/data.ts +475 -0
  197. package/src/report/html.ts +106 -0
  198. package/src/report/template.html +84 -0
  199. package/src/run.ts +243 -0
  200. package/src/sessionLog.ts +216 -0
  201. package/src/sessionLogins.ts +94 -0
  202. package/src/skills/registry.ts +217 -0
  203. package/src/skills/scaffold.ts +65 -0
  204. package/src/skills/select.ts +37 -0
  205. package/src/skills/view.ts +44 -0
  206. package/src/suite/report.ts +135 -0
  207. package/src/suite/run.ts +303 -0
  208. package/src/summary.ts +188 -0
  209. package/src/tools.ts +130 -0
  210. package/src/tui/App.tsx +983 -0
  211. package/src/tui/clipboard.ts +36 -0
  212. package/src/tui/components.tsx +263 -0
  213. package/src/tui/dialogs.ts +65 -0
  214. package/src/tui/main.tsx +20 -0
  215. package/src/tui/text.ts +28 -0
  216. package/src/tui/theme.ts +104 -0
  217. package/src/tui/transcript.tsx +344 -0
  218. package/src/types.ts +146 -0
  219. package/src/update.ts +311 -0
  220. package/src/usage.ts +158 -0
@@ -0,0 +1,214 @@
1
+ import { spawnSync } from "node:child_process"
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs"
3
+ import path from "node:path"
4
+
5
+ import type { CodeReview } from "../types.ts"
6
+ import { jobChanges, type WorkTree } from "./worktree.ts"
7
+
8
+ /**
9
+ * When a coding job ends without a passing, approved test, what it wrote does not stay among the repository's tests,
10
+ * where it would fail every run, and is not thrown away either: it goes to a needs-attention folder next to the tests,
11
+ * with a README saying which test case it is for, why it stopped, and how to pick it up.
12
+ *
13
+ * - Files the job created are moved there, their relative imports rewritten so they still point at the same modules.
14
+ * - Edits the job made to files that were committed and unchanged before it are saved as changes.patch and reverted,
15
+ * so the repository's existing tests and page objects are as they were. Files the user had already changed are left.
16
+ * - The folder is kept out of builds and test runs: Playwright's config gets a testIgnore for it, TypeScript files a
17
+ * @ts-nocheck line, a Python folder a conftest.py that collects nothing, and files of compiled languages (C#, Java,
18
+ * Kotlin) a .txt suffix so the project does not compile them.
19
+ */
20
+
21
+ export const ATTENTION_DIR = "needs-attention"
22
+
23
+ export type AttentionInput = {
24
+ repo: string
25
+ tree: WorkTree
26
+ testFile?: string
27
+ command?: string
28
+ caseId?: string
29
+ title?: string
30
+ url?: string
31
+ /** Why the job stopped, in a sentence or two. */
32
+ why: string
33
+ /** The end of the last test run's output. */
34
+ output?: string
35
+ review?: CodeReview
36
+ }
37
+
38
+ export type Attention = { dir: string; files: string[]; patch?: string; readme: string }
39
+
40
+ const git = (repo: string, args: string[]) => spawnSync("git", args, { cwd: repo, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 })
41
+ const posix = (p: string) => p.split(path.sep).join("/")
42
+ const COMPILED = /\.(cs|java|kt|kts|scala|go|rs|swift|vb|fs)$/i
43
+ const SCRIPT = /\.(m|c)?[jt]sx?$/i
44
+
45
+ function slugOf(s: string): string {
46
+ return s.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "test"
47
+ }
48
+
49
+ /** The needs-attention folder for a job: beside the test's folder, one subfolder per test case. */
50
+ export function attentionDir(repo: string, testFile: string | undefined, caseId: string | undefined): string {
51
+ const base = testFile ? path.dirname(testFile) : "."
52
+ return posix(path.join(base, ATTENTION_DIR, slugOf(caseId || (testFile ? path.basename(testFile).replace(/\..*$/, "") : "unfinished-test"))))
53
+ }
54
+
55
+ /** Rewrite a moved script's relative imports so they reach the same modules from its new place. */
56
+ export function rewriteImports(source: string, fromDir: string, toDir: string, moved: Map<string, string>): string {
57
+ const fix = (spec: string) => {
58
+ if (!spec.startsWith(".")) return spec
59
+ const target = path.resolve(fromDir, spec)
60
+ // Another file this job created, moved alongside: by its new name, in the same folder.
61
+ for (const [oldAbs, newAbs] of moved) {
62
+ const stem = oldAbs.replace(/\.(m|c)?[jt]sx?$/i, "")
63
+ if (target === oldAbs || target === stem) return `./${path.basename(target === oldAbs ? newAbs : newAbs.replace(/\.(m|c)?[jt]sx?$/i, ""))}`
64
+ }
65
+ let rel = posix(path.relative(toDir, target))
66
+ if (!rel.startsWith(".")) rel = `./${rel}`
67
+ return rel
68
+ }
69
+ return source
70
+ .replace(/(\bfrom\s*|\bimport\s*\(\s*|\brequire\s*\(\s*|^\s*import\s+)(['"])(\.{1,2}\/[^'"]*)\2/gm, (_m, lead: string, q: string, spec: string) => `${lead}${q}${fix(spec)}${q}`)
71
+ }
72
+
73
+ /** Add testIgnore for the folder to the nearest Playwright config above it; false when there is none to change. */
74
+ export function ignoreInPlaywright(repo: string, dirAbs: string): string | null {
75
+ let dir = dirAbs
76
+ while (true) {
77
+ for (const name of ["playwright.config.ts", "playwright.config.js", "playwright.config.mjs", "playwright.config.cjs"]) {
78
+ const file = path.join(dir, name)
79
+ if (!existsSync(file)) continue
80
+ const text = readFileSync(file, "utf8")
81
+ if (text.includes(ATTENTION_DIR)) return null
82
+ const glob = `'**/${ATTENTION_DIR}/**'`
83
+ let next: string | null = null
84
+ if (/\btestIgnore\s*:\s*\[/.test(text)) next = text.replace(/\btestIgnore\s*:\s*\[/, (m) => `${m}${glob}, `)
85
+ else if (/\btestIgnore\s*:/.test(text)) next = null
86
+ else if (/defineConfig\(\s*\{/.test(text)) next = text.replace(/defineConfig\(\s*\{/, (m) => `${m}\n testIgnore: [${glob}],`)
87
+ else if (/module\.exports\s*=\s*\{/.test(text)) next = text.replace(/module\.exports\s*=\s*\{/, (m) => `${m}\n testIgnore: [${glob}],`)
88
+ if (!next) return null
89
+ writeFileSync(file, next)
90
+ return posix(path.relative(repo, file))
91
+ }
92
+ const up = path.dirname(dir)
93
+ if (up === dir || path.relative(repo, up).startsWith("..")) return null
94
+ dir = up
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Move what an unfinished job wrote into the needs-attention folder and write its README. Null outside git, where
100
+ * ForgeQE cannot tell the job's files from the user's, and when the job changed nothing.
101
+ */
102
+ export function moveToAttention(input: AttentionInput): Attention | null {
103
+ const { repo, tree } = input
104
+ const changes = jobChanges(tree)
105
+ if (!changes?.length) return null
106
+ const status = new Map<string, string>()
107
+ const st = git(repo, ["status", "--porcelain=v1", "-z", "-uall", "--no-renames"])
108
+ for (const rec of st.stdout.split("\0")) if (rec.length > 3) status.set(rec.slice(3), rec.slice(0, 2))
109
+ const created = changes.filter((rel) => !tree.entries.has(rel) && status.get(rel) === "??" && existsSync(path.join(repo, rel)))
110
+ const edited = changes.filter((rel) => !tree.entries.has(rel) && status.has(rel) && status.get(rel) !== "??" && existsSync(path.join(repo, rel)))
111
+ const kept = changes.filter((rel) => tree.entries.has(rel) && existsSync(path.join(repo, rel)))
112
+ if (!created.length && !edited.length) return null
113
+
114
+ const relDir = attentionDir(repo, input.testFile && created.includes(input.testFile) ? input.testFile : created[0] || input.testFile, input.caseId)
115
+ const dirAbs = path.join(repo, relDir)
116
+ mkdirSync(dirAbs, { recursive: true })
117
+
118
+ // Edits to committed files: kept as a patch, then reverted.
119
+ let patch: string | undefined
120
+ if (edited.length) {
121
+ const diff = git(repo, ["diff", "--no-color", "--", ...edited]).stdout
122
+ if (diff.trim()) {
123
+ patch = posix(path.join(relDir, "changes.patch"))
124
+ writeFileSync(path.join(repo, patch), diff)
125
+ git(repo, ["checkout", "--", ...edited])
126
+ }
127
+ }
128
+
129
+ // Created files, flat in the folder, with a suffix when two share a name.
130
+ const moved = new Map<string, string>()
131
+ const used = new Set<string>()
132
+ for (const rel of created) {
133
+ let name = path.basename(rel)
134
+ for (let n = 2; used.has(name); n++) name = path.basename(rel).replace(/^([^.]*)/, `$1-${n}`)
135
+ used.add(name)
136
+ moved.set(path.join(repo, rel), path.join(dirAbs, name))
137
+ }
138
+ const files: string[] = []
139
+ let python = false
140
+ for (const [oldAbs, newAbs] of moved) {
141
+ let text: string | null = null
142
+ try {
143
+ text = statSync(oldAbs).size < 2_000_000 ? readFileSync(oldAbs, "utf8") : null
144
+ } catch {}
145
+ let target = newAbs
146
+ if (text !== null && SCRIPT.test(oldAbs)) {
147
+ text = rewriteImports(text, path.dirname(oldAbs), dirAbs, moved)
148
+ if (/\.tsx?$/i.test(oldAbs) && !text.startsWith("// @ts-nocheck")) text = `// @ts-nocheck: unfinished test, see README.md in this folder\n${text}`
149
+ }
150
+ if (COMPILED.test(oldAbs)) target = `${newAbs}.txt`
151
+ if (/\.py$/i.test(oldAbs)) python = true
152
+ if (text !== null) {
153
+ writeFileSync(target, text)
154
+ rmSync(oldAbs, { force: true })
155
+ } else {
156
+ renameSync(oldAbs, target)
157
+ }
158
+ files.push(posix(path.relative(repo, target)))
159
+ }
160
+ if (python) writeFileSync(path.join(dirAbs, "conftest.py"), "# Unfinished tests: pytest collects nothing here. See README.md.\ncollect_ignore_glob = [\"*\"]\n")
161
+ const ignored = [...moved.keys()].some((f) => SCRIPT.test(f)) ? ignoreInPlaywright(repo, dirAbs) : null
162
+
163
+ const readme = posix(path.join(relDir, "README.md"))
164
+ const original = [...moved.keys()].map((f) => posix(path.relative(repo, f)))
165
+ writeFileSync(
166
+ path.join(repo, readme),
167
+ [
168
+ `# Needs attention: ${input.caseId ? `${input.caseId} ` : ""}${input.title || input.testFile || "unfinished test"}`,
169
+ "",
170
+ input.url ? `Test case: ${input.url}` : "",
171
+ `Written by ForgeQE's coding agent on ${new Date().toISOString().slice(0, 10)}. It did not end with a passing, approved test, so it is kept here, out of the test runs, instead of among the tests.`,
172
+ "",
173
+ "## Why it stopped",
174
+ "",
175
+ input.why.trim(),
176
+ ...(input.review && !input.review.approved
177
+ ? ["", "The reviewer's last verdict:", "", input.review.summary, ...input.review.issues.map((i) => `- ${i.file ? `${i.file}: ` : ""}${i.problem}${i.fix ? ` Fix: ${i.fix}` : ""}`)]
178
+ : []),
179
+ ...(input.output?.trim() ? ["", "The end of the last test run:", "", "```", input.output.trim().split("\n").slice(-30).join("\n"), "```"] : []),
180
+ "",
181
+ "## What is here",
182
+ "",
183
+ ...files.map((f, i) => `- ${path.basename(f)} (was ${original[i]})`),
184
+ ...(patch ? [`- changes.patch: the job's edits to existing files (${edited.join(", ")}), taken out of them; apply with git apply`] : []),
185
+ ...(kept.length ? ["", `Also changed, and left in place because they had uncommitted changes before the job: ${kept.join(", ")}.`] : []),
186
+ "",
187
+ "## Picking it up",
188
+ "",
189
+ `1. Move the files back to where they were${patch ? " and apply changes.patch (git apply)" : ""}.`,
190
+ `2. Fix what is described above, then run it${input.command ? `: ${input.command}` : "."}`,
191
+ "3. Or ask ForgeQE to finish it, naming this folder.",
192
+ "",
193
+ `This folder is kept out of test runs${ignored ? ` (testIgnore in ${ignored})` : python ? " (conftest.py)" : ""}${files.some((f) => f.endsWith(".txt")) ? "; compiled files have a .txt suffix so the project does not build them" : ""}.`,
194
+ ]
195
+ .filter((l, i, all) => l !== "" || all[i - 1] !== "")
196
+ .join("\n") + "\n",
197
+ )
198
+ writeIndex(path.dirname(dirAbs))
199
+ return { dir: relDir, files, ...(patch ? { patch } : {}), readme }
200
+ }
201
+
202
+ /** needs-attention/README.md: one line per unfinished test, from each subfolder's README title. */
203
+ function writeIndex(rootAbs: string) {
204
+ const entries = readdirSync(rootAbs, { withFileTypes: true })
205
+ .filter((e) => e.isDirectory() && existsSync(path.join(rootAbs, e.name, "README.md")))
206
+ .map((e) => {
207
+ const title = readFileSync(path.join(rootAbs, e.name, "README.md"), "utf8").split("\n")[0].replace(/^#\s*Needs attention:\s*/, "")
208
+ return `- [${e.name}](${e.name}/README.md): ${title}`
209
+ })
210
+ writeFileSync(
211
+ path.join(rootAbs, "README.md"),
212
+ ["# Tests that need attention", "", "ForgeQE's coding agent wrote these but did not finish them: each folder says why and how to pick it up. They are kept out of test runs.", "", ...entries, ""].join("\n"),
213
+ )
214
+ }