@agjs/tsforge 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/bin/tsforge.js +2 -0
  2. package/package.json +35 -0
  3. package/src/agent/agent.constants.ts +382 -0
  4. package/src/agent/agent.types.ts +34 -0
  5. package/src/agent/index.ts +4 -0
  6. package/src/agent/model-agent.ts +297 -0
  7. package/src/agent/tool-repair.ts +194 -0
  8. package/src/agent/tools.ts +190 -0
  9. package/src/browser/checks.ts +96 -0
  10. package/src/browser/index.ts +8 -0
  11. package/src/browser/oracle.ts +303 -0
  12. package/src/classify.ts +48 -0
  13. package/src/cli.ts +1333 -0
  14. package/src/config/config.constants.ts +9 -0
  15. package/src/config/flags.ts +32 -0
  16. package/src/config/index.ts +8 -0
  17. package/src/config/tsforge-config.ts +301 -0
  18. package/src/constitution/baseline.ts +257 -0
  19. package/src/detect-gate.ts +498 -0
  20. package/src/eval/eval.types.ts +36 -0
  21. package/src/eval/index.ts +3 -0
  22. package/src/eval/judge.ts +62 -0
  23. package/src/eval/score.ts +39 -0
  24. package/src/files/create.ts +22 -0
  25. package/src/files/edit.ts +193 -0
  26. package/src/files/files.constants.ts +11 -0
  27. package/src/files/files.types.ts +81 -0
  28. package/src/files/hashline-format.ts +110 -0
  29. package/src/files/hashline.ts +689 -0
  30. package/src/files/index.ts +19 -0
  31. package/src/index.ts +8 -0
  32. package/src/inference/index.ts +6 -0
  33. package/src/inference/inference.constants.ts +34 -0
  34. package/src/inference/inference.types.ts +123 -0
  35. package/src/inference/openai-compatible.ts +113 -0
  36. package/src/inference/stream-guard.ts +161 -0
  37. package/src/inference/stream.ts +370 -0
  38. package/src/inference/transport.ts +78 -0
  39. package/src/inference/wire.ts +0 -0
  40. package/src/lib/fs/fs.ts +126 -0
  41. package/src/lib/fs/fs.types.ts +5 -0
  42. package/src/lib/fs/index.ts +3 -0
  43. package/src/lib/fs/process.ts +146 -0
  44. package/src/lib/guards/guards.ts +9 -0
  45. package/src/lib/guards/index.ts +1 -0
  46. package/src/lib/json/index.ts +1 -0
  47. package/src/lib/json/json.ts +12 -0
  48. package/src/lib/scope/index.ts +2 -0
  49. package/src/lib/scope/scope.constants.ts +3 -0
  50. package/src/lib/scope/scope.ts +40 -0
  51. package/src/loop/astgrep-fix.ts +228 -0
  52. package/src/loop/feedback/feedback.ts +138 -0
  53. package/src/loop/feedback/index.ts +8 -0
  54. package/src/loop/feedback/meta-rule-docs.ts +41 -0
  55. package/src/loop/feedback/meta-rule-feedback.ts +61 -0
  56. package/src/loop/feedback/rule-docs.generated.json +112 -0
  57. package/src/loop/feedback/rule-docs.ts +342 -0
  58. package/src/loop/index.ts +19 -0
  59. package/src/loop/loop.constants.ts +68 -0
  60. package/src/loop/loop.types.ts +99 -0
  61. package/src/loop/prompt/index.ts +2 -0
  62. package/src/loop/prompt/project-map.ts +69 -0
  63. package/src/loop/prompt/prompt.ts +107 -0
  64. package/src/loop/quality.ts +174 -0
  65. package/src/loop/rule-docs.generated.json +367 -0
  66. package/src/loop/run-spec.ts +88 -0
  67. package/src/loop/run.ts +400 -0
  68. package/src/loop/session.ts +1410 -0
  69. package/src/loop/tools/add-dependency.ts +71 -0
  70. package/src/loop/tools/condense.ts +498 -0
  71. package/src/loop/tools/edit-hashline.ts +80 -0
  72. package/src/loop/tools/execute-tool.ts +80 -0
  73. package/src/loop/tools/file-ops.ts +323 -0
  74. package/src/loop/tools/index.ts +2 -0
  75. package/src/loop/tools/lsp-ops.ts +222 -0
  76. package/src/loop/tools/scaffold-routes.ts +68 -0
  77. package/src/loop/tools/scaffold-ui.ts +62 -0
  78. package/src/loop/tools/scaffold-web.ts +35 -0
  79. package/src/loop/tools/tool-context.ts +126 -0
  80. package/src/loop/ttsr-defaults.ts +53 -0
  81. package/src/loop/ttsr.ts +322 -0
  82. package/src/loop/turn.ts +856 -0
  83. package/src/lsp/index.ts +2 -0
  84. package/src/lsp/lsp.types.ts +56 -0
  85. package/src/lsp/service.ts +500 -0
  86. package/src/meta-rules/context.ts +195 -0
  87. package/src/meta-rules/index.ts +9 -0
  88. package/src/meta-rules/meta-rules.types.ts +47 -0
  89. package/src/meta-rules/parsers/package-json-parser.ts +51 -0
  90. package/src/meta-rules/registry.ts +37 -0
  91. package/src/meta-rules/rules/ci/workflow-actions-pinned.ts +59 -0
  92. package/src/meta-rules/rules/ci/workflow-runner-pinned.ts +57 -0
  93. package/src/meta-rules/rules/ci/workflow-timeout-required.ts +114 -0
  94. package/src/meta-rules/rules/config/tsconfig-paths-exist.ts +117 -0
  95. package/src/meta-rules/rules/config/tsconfig-strict.ts +91 -0
  96. package/src/meta-rules/rules/source-text/no-eslint-disable-comments.ts +34 -0
  97. package/src/meta-rules/rules/source-text/no-ts-suppressions.ts +38 -0
  98. package/src/meta-rules/rules/supply-chain/no-overlapping-libs.ts +57 -0
  99. package/src/meta-rules/rules/supply-chain/package-exact-deps.ts +55 -0
  100. package/src/meta-rules/rules/testing/test-sibling-required.ts +110 -0
  101. package/src/meta-rules/runner.ts +64 -0
  102. package/src/models-config.ts +196 -0
  103. package/src/render/ansi.ts +289 -0
  104. package/src/render/banner.ts +113 -0
  105. package/src/render/box.ts +134 -0
  106. package/src/render/index.ts +7 -0
  107. package/src/render/markdown.ts +123 -0
  108. package/src/render/render.types.ts +21 -0
  109. package/src/render/stream-markdown.ts +128 -0
  110. package/src/render/style.ts +26 -0
  111. package/src/rule-packs/bullmq/index.ts +39 -0
  112. package/src/rule-packs/bullmq/rules/index.ts +7 -0
  113. package/src/rule-packs/bullmq/rules/job-name-must-be-constant.ts +141 -0
  114. package/src/rule-packs/bullmq/rules/job-options-must-set-attempts.ts +174 -0
  115. package/src/rule-packs/bullmq/rules/no-blocking-concurrency-zero.ts +103 -0
  116. package/src/rule-packs/bullmq/rules/queue-options-must-set-removeoncomplete.ts +130 -0
  117. package/src/rule-packs/bullmq/rules/queue-options-must-set-removeonfail.ts +130 -0
  118. package/src/rule-packs/bullmq/rules/worker-must-implement-close.ts +182 -0
  119. package/src/rule-packs/bullmq/rules/worker-must-listen-failed.ts +140 -0
  120. package/src/rule-packs/bullmq/utils.ts +334 -0
  121. package/src/rule-packs/code-flow/index.ts +25 -0
  122. package/src/rule-packs/code-flow/rules/index.ts +3 -0
  123. package/src/rule-packs/code-flow/rules/no-bare-date-now.ts +138 -0
  124. package/src/rule-packs/code-flow/rules/no-template-trim-empty-ternary.ts +87 -0
  125. package/src/rule-packs/code-flow/rules/prefer-early-return.ts +80 -0
  126. package/src/rule-packs/code-flow/utils/prefer-early-return.ts +132 -0
  127. package/src/rule-packs/comment-hygiene/index.ts +25 -0
  128. package/src/rule-packs/comment-hygiene/rules/index.ts +3 -0
  129. package/src/rule-packs/comment-hygiene/rules/no-historical-comments.ts +102 -0
  130. package/src/rule-packs/comment-hygiene/rules/no-narration-comments.ts +83 -0
  131. package/src/rule-packs/comment-hygiene/rules/no-pr-reference-comments.ts +90 -0
  132. package/src/rule-packs/create-rule.ts +9 -0
  133. package/src/rule-packs/drizzle/index.ts +41 -0
  134. package/src/rule-packs/drizzle/rules/account-scoped-tables-require-where.ts +371 -0
  135. package/src/rule-packs/drizzle/rules/index.ts +8 -0
  136. package/src/rule-packs/drizzle/rules/no-nested-db-transaction.ts +127 -0
  137. package/src/rule-packs/drizzle/rules/no-raw-sql-outside-allowlist.ts +100 -0
  138. package/src/rule-packs/drizzle/rules/relations-must-cover-fks.ts +209 -0
  139. package/src/rule-packs/drizzle/rules/schema-files-must-not-import-driver.ts +127 -0
  140. package/src/rule-packs/drizzle/rules/schema-files-must-only-export-schema.ts +149 -0
  141. package/src/rule-packs/drizzle/rules/tables-must-have-timestamps.ts +312 -0
  142. package/src/rule-packs/drizzle/rules/timestamp-must-specify-mode.ts +166 -0
  143. package/src/rule-packs/drizzle/utils.ts +115 -0
  144. package/src/rule-packs/elysia/index.ts +43 -0
  145. package/src/rule-packs/elysia/rules/consistent-status-via-set.ts +69 -0
  146. package/src/rule-packs/elysia/rules/no-decorate-state-collision.ts +276 -0
  147. package/src/rule-packs/elysia/rules/no-separate-model-interfaces.ts +144 -0
  148. package/src/rule-packs/elysia/rules/prefer-destructured-context.ts +155 -0
  149. package/src/rule-packs/elysia/rules/prefer-direct-return.ts +176 -0
  150. package/src/rule-packs/elysia/rules/prefer-static-services.ts +159 -0
  151. package/src/rule-packs/elysia/rules/prefer-throw-status.ts +151 -0
  152. package/src/rule-packs/elysia/rules/require-hooks-before-routes.ts +209 -0
  153. package/src/rule-packs/elysia/rules/require-plugin-name.ts +107 -0
  154. package/src/rule-packs/elysia/utils/elysiaChain.ts +306 -0
  155. package/src/rule-packs/env-access/index.ts +23 -0
  156. package/src/rule-packs/env-access/rules/index.ts +2 -0
  157. package/src/rule-packs/env-access/rules/no-direct-process-env.ts +133 -0
  158. package/src/rule-packs/env-access/rules/no-process-exit.ts +95 -0
  159. package/src/rule-packs/i18n-keys/index.ts +19 -0
  160. package/src/rule-packs/i18n-keys/rules/static-translation-key-exists.ts +173 -0
  161. package/src/rule-packs/index.ts +139 -0
  162. package/src/rule-packs/jwt-cookies/index.ts +25 -0
  163. package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-be-httponly.ts +150 -0
  164. package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-be-secure-in-prod.ts +149 -0
  165. package/src/rule-packs/jwt-cookies/rules/bcrypt-rounds-min.ts +195 -0
  166. package/src/rule-packs/jwt-cookies/utils.ts +188 -0
  167. package/src/rule-packs/oauth-security/index.ts +25 -0
  168. package/src/rule-packs/oauth-security/rules/pkce-required-for-oidc.ts +296 -0
  169. package/src/rule-packs/oauth-security/rules/state-must-be-redis-backed.ts +193 -0
  170. package/src/rule-packs/oauth-security/rules/state-ttl-bounded.ts +219 -0
  171. package/src/rule-packs/oauth-security/utils.ts +127 -0
  172. package/src/rule-packs/react-component-architecture/index.ts +35 -0
  173. package/src/rule-packs/react-component-architecture/rules/component-folder-structure.ts +123 -0
  174. package/src/rule-packs/react-component-architecture/rules/forwardref-display-name.ts +93 -0
  175. package/src/rule-packs/react-component-architecture/rules/index-must-reexport-default.ts +123 -0
  176. package/src/rule-packs/react-component-architecture/rules/max-hooks-per-file.ts +122 -0
  177. package/src/rule-packs/react-component-architecture/rules/no-cross-feature-imports.ts +170 -0
  178. package/src/rule-packs/react-component-architecture/rules/no-inline-jsx-functions.ts +66 -0
  179. package/src/rule-packs/react-component-architecture/utils.ts +47 -0
  180. package/src/rule-packs/rule-packs.types.ts +18 -0
  181. package/src/rule-packs/structured-logging/index.ts +26 -0
  182. package/src/rule-packs/structured-logging/rules/mask-pii-fields.ts +221 -0
  183. package/src/rule-packs/structured-logging/rules/no-error-stringify.ts +217 -0
  184. package/src/rule-packs/structured-logging/rules/require-event-field.ts +136 -0
  185. package/src/rule-packs/structured-logging/utils/logger.ts +104 -0
  186. package/src/rule-packs/tanstack-query/index.ts +20 -0
  187. package/src/rule-packs/tanstack-query/rules/prefix-query-key-must-use-set-queries-data.ts +321 -0
  188. package/src/rule-packs/test-conventions/index.ts +23 -0
  189. package/src/rule-packs/test-conventions/rules/index.ts +2 -0
  190. package/src/rule-packs/test-conventions/rules/no-focused-tests.ts +170 -0
  191. package/src/rule-packs/test-conventions/rules/test-file-mirrors-source.ts +127 -0
  192. package/src/rule-packs/utils.ts +142 -0
  193. package/src/session-store.ts +359 -0
  194. package/src/spec/generate-tests.ts +213 -0
  195. package/src/spec/index.ts +5 -0
  196. package/src/spec/parse.ts +152 -0
  197. package/src/spec/review-tests.ts +162 -0
  198. package/src/spec/spec.constants.ts +13 -0
  199. package/src/spec/spec.types.ts +79 -0
  200. package/src/stack-detection/detect.ts +246 -0
  201. package/src/stack-detection/index.ts +3 -0
  202. package/src/stack-detection/packs.ts +174 -0
  203. package/src/stack-detection/stack-detection.types.ts +47 -0
  204. package/src/validate/accept.ts +49 -0
  205. package/src/validate/errors.ts +35 -0
  206. package/src/validate/index.ts +12 -0
  207. package/src/validate/parse.ts +148 -0
  208. package/src/validate/run-tests.ts +59 -0
  209. package/src/validate/validate.ts +40 -0
  210. package/src/validate/validate.types.ts +52 -0
  211. package/src/web-components.ts +638 -0
  212. package/src/web-coverage.ts +89 -0
  213. package/src/web-routes.ts +151 -0
  214. package/src/web-templates.ts +1011 -0
  215. package/strict.eslint.config.mjs +84 -0
  216. package/strict.web.eslint.config.mjs +185 -0
@@ -0,0 +1,61 @@
1
+ import type { IMetaRuleViolation } from "../../meta-rules";
2
+ import { META_RULE_DOCS } from "./meta-rule-docs";
3
+
4
+ /** Cap rendered meta-rule violations so error sets can't wall the model. */
5
+ const META_RULE_FEEDBACK_MAX = 10;
6
+
7
+ /**
8
+ * Render meta-rule violations as `<file>: <message> (<ruleId>)` with severity tag.
9
+ * Each violation gets its doc's "how" line appended if available.
10
+ */
11
+ export function renderMetaViolations(
12
+ violations: readonly IMetaRuleViolation[]
13
+ ): string {
14
+ const shown = violations.slice(0, META_RULE_FEEDBACK_MAX);
15
+ const rendered: string[] = [];
16
+
17
+ for (const v of shown) {
18
+ const severity = v.severity === "error" ? "[ERROR]" : "[WARN]";
19
+ const head = `- ${v.file}: ${v.message} (${v.ruleId}) ${severity}`;
20
+
21
+ const doc = META_RULE_DOCS[v.ruleId];
22
+ const docLine = doc !== undefined ? `\n 💡 ${doc}` : "";
23
+
24
+ rendered.push(`${head}${docLine}`);
25
+ }
26
+
27
+ const more =
28
+ violations.length > META_RULE_FEEDBACK_MAX
29
+ ? `\n… and ${String(violations.length - META_RULE_FEEDBACK_MAX)} more project structure violations.`
30
+ : "";
31
+
32
+ return rendered.join("\n") + more;
33
+ }
34
+
35
+ /**
36
+ * Extract unique meta-rule docs from violations for a help block.
37
+ * Format: `ruleId: <doc>\n`
38
+ */
39
+ export function metaRuleHelp(
40
+ violations: readonly IMetaRuleViolation[]
41
+ ): string {
42
+ const seen = new Set<string>();
43
+ const blocks: string[] = [];
44
+
45
+ for (const v of violations) {
46
+ if (seen.has(v.ruleId)) {
47
+ continue;
48
+ }
49
+
50
+ const doc = META_RULE_DOCS[v.ruleId];
51
+
52
+ if (doc === undefined) {
53
+ continue;
54
+ }
55
+
56
+ seen.add(v.ruleId);
57
+ blocks.push(`${v.ruleId}: ${doc}`);
58
+ }
59
+
60
+ return blocks.length > 0 ? blocks.map((b) => ` ${b}`).join("\n") : "";
61
+ }
@@ -0,0 +1,112 @@
1
+ {
2
+ "@typescript-eslint/no-explicit-any": {
3
+ "what": "Disallow the `any` type.",
4
+ "bad": "const age: any = 'seventeen';",
5
+ "good": "const age: number = 17;"
6
+ },
7
+ "@typescript-eslint/no-unsafe-argument": {
8
+ "what": "Disallow calling a function with a value with type `any`.",
9
+ "bad": "declare function foo(arg1: string, arg2: number, arg3: string): void;\n\nconst anyTyped = 1 as any;\n\nfoo(...anyTyped);\nfoo(anyTyped, 1, 'a');\n\nconst anyArray: any[] = [];",
10
+ "good": "declare function foo(arg1: string, arg2: number, arg3: string): void;\n\nfoo('a', 1, 'b');\n\nconst tuple1 = ['a', 1, 'b'] as const;\nfoo(...tuple1);\n\ndeclare function bar(arg1: string, arg2: number, ...rest: string[]): void;"
11
+ },
12
+ "@typescript-eslint/no-unsafe-assignment": {
13
+ "what": "Disallow assigning a value with type `any` to variables and properties.",
14
+ "bad": "const x = 1 as any,\n y = 1 as any;\nconst [x] = 1 as any;\nconst [x] = [] as any[];\nconst [x] = [1 as any];\n[x] = [1] as [any];\n\nfunction foo(a = 1 as any) {}",
15
+ "good": "const x = 1,\n y = 1;\nconst [x] = [1];\n[x] = [1] as [number];\n\nfunction foo(a = 1) {}\nclass Foo {\n constructor(private a = 1) {}"
16
+ },
17
+ "@typescript-eslint/no-unsafe-call": {
18
+ "what": "Disallow calling a value with type `any`.",
19
+ "bad": "declare const anyVar: any;\ndeclare const nestedAny: { prop: any };\n\nanyVar();\nanyVar.a.b();\n\nnestedAny.prop();\nnestedAny.prop['a']();",
20
+ "good": "declare const typedVar: () => void;\ndeclare const typedNested: { prop: { a: () => void } };\n\ntypedVar();\ntypedNested.prop.a();\n\n(() => {})();\n"
21
+ },
22
+ "@typescript-eslint/no-unsafe-member-access": {
23
+ "what": "Disallow member access on a value with type `any`.",
24
+ "bad": "declare const anyVar: any;\ndeclare const nestedAny: { prop: any };\n\nanyVar.a;\nanyVar.a.b;\nanyVar['a'];\nanyVar['a']['b'];\n",
25
+ "good": "declare const properlyTyped: { prop: { a: string } };\n\nproperlyTyped.prop.a;\nproperlyTyped.prop['a'];\n\nconst key = 'a';\nproperlyTyped.prop[key];\n"
26
+ },
27
+ "@typescript-eslint/no-unsafe-return": {
28
+ "what": "Disallow returning a value with type `any` from a function.",
29
+ "bad": "function foo1() {\n return 1 as any;\n}\nfunction foo2() {\n return Object.create(null);\n}\nconst foo3 = () => {\n return 1 as any;",
30
+ "good": "function foo1() {\n return 1;\n}\nfunction foo2() {\n return Object.create(null) as Record<string, unknown>;\n}\n\nconst foo3 = () => [];"
31
+ },
32
+ "@typescript-eslint/no-non-null-assertion": {
33
+ "what": "Disallow non-null assertions using the `!` postfix operator.",
34
+ "bad": "interface Example {\n property?: string;\n}\n\ndeclare const example: Example;\nconst includesBaz = example.property!.includes('baz');",
35
+ "good": "interface Example {\n property?: string;\n}\n\ndeclare const example: Example;\nconst includesBaz = example.property?.includes('baz') ?? false;"
36
+ },
37
+ "@typescript-eslint/restrict-plus-operands": {
38
+ "what": "Require both operands of addition to be the same type and be `bigint`, `number`, or `string`.",
39
+ "bad": "let foo = 1n + 1;\nlet fn = (a: string, b: never) => a + b;",
40
+ "good": "let foo = 1n + 1n;\nlet fn = (a: string, b: string) => a + b;"
41
+ },
42
+ "@typescript-eslint/restrict-template-expressions": {
43
+ "what": "Enforce template literal expressions to be of `string` type.",
44
+ "bad": "const arg1 = [1, 2];\nconst msg1 = `arg1 = ${arg1}`;\n\nconst arg2 = { name: 'Foo' };\nconst msg2 = `arg2 = ${arg2 || null}`;",
45
+ "good": "const arg = 'foo';\nconst msg1 = `arg = ${arg}`;\nconst msg2 = `arg = ${arg || 'default'}`;\n\nconst stringWithKindProp: string & { _kind?: 'MyString' } = 'foo';\nconst msg3 = `stringWithKindProp = ${stringWithKindProp}`;"
46
+ },
47
+ "@typescript-eslint/no-floating-promises": {
48
+ "what": "Require Promise-like statements to be handled appropriately.",
49
+ "bad": "const promise = new Promise((resolve, reject) => resolve('value'));\npromise;\n\nasync function returnsPromise() {\n return 'value';\n}\nreturnsPromise().then(() => {});\n",
50
+ "good": "const promise = new Promise((resolve, reject) => resolve('value'));\nawait promise;\n\nasync function returnsPromise() {\n return 'value';\n}\n\nvoid returnsPromise();"
51
+ },
52
+ "@typescript-eslint/await-thenable": {
53
+ "what": "Disallow awaiting a value that is not a Thenable.",
54
+ "bad": "await 'value';\n\nconst createValue = () => 'value';\nawait createValue();",
55
+ "good": "await Promise.resolve('value');\n\nconst createValue = async () => 'value';\nawait createValue();"
56
+ },
57
+ "@typescript-eslint/no-for-in-array": {
58
+ "what": "Disallow iterating over an array with a for-in loop.",
59
+ "bad": "declare const array: string[];\n\nfor (const i in array) {\n console.log(array[i]);\n}\n\nfor (const i in array) {\n console.log(i, array[i]);",
60
+ "good": "declare const array: string[];\n\nfor (const value of array) {\n console.log(value);\n}\n\nfor (let i = 0; i < array.length; i += 1) {\n console.log(i, array[i]);"
61
+ },
62
+ "@typescript-eslint/prefer-nullish-coalescing": {
63
+ "what": "Enforce using the nullish coalescing operator instead of logical assignments or chaining.",
64
+ "bad": "declare const a: string | null;\ndeclare const b: string | null;\n\nconst c = a || b;\n\ndeclare let foo: { a: string } | null;\ndeclare function makeFoo(): { a: string };\n",
65
+ "good": "declare const a: string | null;\ndeclare const b: string | null;\n\nconst c = a ?? b;\n\ndeclare let foo: { a: string } | null;\ndeclare function makeFoo(): { a: string };\n"
66
+ },
67
+ "@typescript-eslint/prefer-optional-chain": {
68
+ "what": "Enforce using concise optional chain expressions instead of chained logical ands, negated logical ors, or empty objects.",
69
+ "bad": "foo && foo.a && foo.a.b && foo.a.b.c;\nfoo && foo['a'] && foo['a'].b && foo['a'].b.c;\nfoo && foo.a && foo.a.b && foo.a.b.method && foo.a.b.method();\n\n// With empty objects\n(((foo || {}).a || {}).b || {}).c;\n(((foo || {})['a'] || {}).b || {}).c;\n",
70
+ "good": "foo?.a?.b?.c;\nfoo?.['a']?.b?.c;\nfoo?.a?.b?.method?.();\n\nfoo?.a?.b?.c?.d?.e;\n\n!foo?.bar;\n!foo?.[bar];"
71
+ },
72
+ "@typescript-eslint/no-unnecessary-condition": {
73
+ "what": "Disallow conditionals where the type is always truthy or always falsy.",
74
+ "bad": "function head<T>(items: T[]) {\n // items can never be nullable, so this is unnecessary\n if (items) {\n return items[0].toUpperCase();\n }\n}\n\nfunction foo(arg: 'bar' | 'baz') {",
75
+ "good": "function head<T>(items: T[]) {\n // Necessary, since items.length might be 0\n if (items.length) {\n return items[0].toUpperCase();\n }\n}\n\nfunction foo(arg: string) {"
76
+ },
77
+ "@typescript-eslint/no-unnecessary-type-assertion": {
78
+ "what": "Disallow type assertions that do not change the type of an expression.",
79
+ "bad": "const foo = 3;\nconst bar = foo!;",
80
+ "good": "const foo = <number>3;"
81
+ },
82
+ "@typescript-eslint/switch-exhaustiveness-check": {
83
+ "what": "Require switch-case statements to be exhaustive.",
84
+ "bad": "type Day =\n | 'Monday'\n | 'Tuesday'\n | 'Wednesday'\n | 'Thursday'\n | 'Friday'\n | 'Saturday'\n | 'Sunday';",
85
+ "good": "type Day =\n | 'Monday'\n | 'Tuesday'\n | 'Wednesday'\n | 'Thursday'\n | 'Friday'\n | 'Saturday'\n | 'Sunday';"
86
+ },
87
+ "@typescript-eslint/no-base-to-string": {
88
+ "what": "Require `.toString()` and `.toLocaleString()` to only be called on objects which provide useful information when stringified.",
89
+ "bad": "// Passing an object or class instance to string concatenation:\n'' + {};\n\nclass MyClass {}\nconst value = new MyClass();\nvalue + '';\n\n// Interpolation and manual .toString() and `toLocaleString()` calls too:",
90
+ "good": "// These types all have useful .toString() and `toLocaleString()` methods\n'Text' + true;\n`Value: ${123}`;\n`Arrays too: ${[1, 2, 3]}`;\n(() => {}).toString();\nString(42);\n(() => {}).toLocaleString();\n"
91
+ },
92
+ "@typescript-eslint/require-await": {
93
+ "what": "Disallow async functions which do not return promises and have no `await` expression.",
94
+ "bad": "async function returnNumber() {\n return 1;\n}\n\nasync function* asyncGenerator() {\n yield 1;\n}\n",
95
+ "good": "function returnNumber() {\n return 1;\n}\n\nfunction* syncGenerator() {\n yield 1;\n}\n"
96
+ },
97
+ "@typescript-eslint/no-confusing-void-expression": {
98
+ "what": "Require expressions of type void to appear in statement position.",
99
+ "bad": "// somebody forgot that `alert` doesn't return anything\nconst response = alert('Are you sure?');\nconsole.log(alert('Are you sure?'));\n\n// it's not obvious whether the chained promise will contain the response (fixable)\npromise.then(value => window.postMessage(value));\n\n// it looks like we are returning the result of `console.error` (fixable)",
100
+ "good": "// just a regular void function in a statement position\nalert('Hello, world!');\n\n// this function returns a boolean value so it's ok\nconst response = confirm('Are you sure?');\nconsole.log(confirm('Are you sure?'));\n\n// now it's obvious that `postMessage` doesn't return any response"
101
+ },
102
+ "@typescript-eslint/no-redundant-type-constituents": {
103
+ "what": "Disallow members of unions and intersections that do nothing or override type information.",
104
+ "bad": "type UnionAny = any | 'foo';\ntype UnionUnknown = unknown | 'foo';\ntype UnionNever = never | 'foo';\n\ntype UnionBooleanLiteral = boolean | false;\ntype UnionNumberLiteral = number | 1;\ntype UnionStringLiteral = string | 'foo';\n",
105
+ "good": "type UnionAny = any;\ntype UnionUnknown = unknown;\ntype UnionNever = never;\n\ntype UnionBooleanLiteral = boolean;\ntype UnionNumberLiteral = number;\ntype UnionStringLiteral = string;\n"
106
+ },
107
+ "@typescript-eslint/prefer-reduce-type-parameter": {
108
+ "what": "Enforce using type parameter when calling `Array#reduce` instead of using a type assertion.",
109
+ "bad": "[1, 2, 3].reduce((arr, num) => arr.concat(num * 2), [] as number[]);\n\n['a', 'b'].reduce(\n (accumulator, name) => ({\n ...accumulator,\n [name]: true,\n }),\n {} as Record<string, boolean>,",
110
+ "good": "[1, 2, 3].reduce<number[]>((arr, num) => arr.concat(num * 2), []);\n\n['a', 'b'].reduce<Record<string, boolean>>(\n (accumulator, name) => ({\n ...accumulator,\n [name]: true,\n }),\n {},"
111
+ }
112
+ }
@@ -0,0 +1,342 @@
1
+ import type { ErrorSet } from "../../validate";
2
+ import generatedJson from "./rule-docs.generated.json";
3
+
4
+ export interface IRuleDoc {
5
+ /** One-line statement of what the rule requires. */
6
+ what: string;
7
+ /** A minimal example that VIOLATES the rule. */
8
+ bad: string;
9
+ /** The corrected version that satisfies it. */
10
+ good: string;
11
+ }
12
+
13
+ /**
14
+ * Auto-fetched docs (eslint/typescript-eslint rules) built offline by
15
+ * `scripts/build-rule-docs.ts` from the rules' own source. Curated entries
16
+ * below take precedence; this fills coverage for everything else.
17
+ */
18
+ const GENERATED: Record<string, IRuleDoc> = generatedJson;
19
+
20
+ /**
21
+ * Curated documentation for the rules our gate actually enforces — each with a
22
+ * before/after, the way a human resolves a lint/type error. Keyed by the exact
23
+ * `rule` the validators emit: TS diagnostic codes (`TS2532`) and eslint rule
24
+ * ids (`@typescript-eslint/...`). Surfacing the rule's own bad→good next to the
25
+ * failure beats making the model re-derive the fix from scratch.
26
+ */
27
+ const RULE_DOCS: Record<string, IRuleDoc> = {
28
+ TS2532: {
29
+ what: "Indexed access is `T | undefined` (noUncheckedIndexedAccess). Bind and guard before use; never `!`.",
30
+ bad: "total += arr[i];",
31
+ good: "const x = arr[i]; if (x === undefined) { continue; } total += x;",
32
+ },
33
+ TS18048: {
34
+ what: "Value is possibly `undefined`. Guard it before use.",
35
+ bad: "return obj.maybe.length;",
36
+ good: "const v = obj.maybe; if (v === undefined) { return 0; } return v.length;",
37
+ },
38
+ TS2322: {
39
+ what: "Type is not assignable to the target type — fix the value or the annotation, don't widen to `any`.",
40
+ bad: "const n: number = readLine();",
41
+ good: "const n: number = Number(readLine());",
42
+ },
43
+ TS2307: {
44
+ what: "Module not found — the package isn't installed. Install it first (greenfield dirs have no node_modules); add its `@types/*` if it ships no types.",
45
+ bad: "import { render } from 'react-dom';",
46
+ good: "run: bun add react react-dom @types/react @types/react-dom",
47
+ },
48
+ "@typescript-eslint/no-unsafe-return": {
49
+ what: "Don't return a value typed `any` — narrow it to a real type before returning.",
50
+ bad: "function f() { return JSON.parse(s); }",
51
+ good: "const v: unknown = JSON.parse(s); if (typeof v === 'number') { return v; } return 0;",
52
+ },
53
+ "@typescript-eslint/no-unsafe-assignment": {
54
+ what: "Don't assign an `any` to a typed target — type the source.",
55
+ bad: "const xs = data.map((a, b) => a + b);",
56
+ good: "const xs: number[] = data.map((a: number, b: number) => a + b);",
57
+ },
58
+ "@typescript-eslint/no-unsafe-member-access": {
59
+ what: "Don't access members off an `any`. Narrow to a known type first.",
60
+ bad: "return res.body.id;",
61
+ good: "const body: unknown = res.body; if (isRecord(body)) { return body.id; }",
62
+ },
63
+ "@typescript-eslint/restrict-plus-operands": {
64
+ what: "`+` operands must each be number or string — an `any`/`undefined` is leaking in; type/guard it.",
65
+ bad: "const sum = a + b; // a or b is any | undefined",
66
+ good: "const sum: number = (a ?? 0) + (b ?? 0); // with a, b: number",
67
+ },
68
+ "@typescript-eslint/no-explicit-any": {
69
+ what: "No `any`. Use a real type, or `unknown` + a type guard.",
70
+ bad: "function parse(x: any) {}",
71
+ good: "function parse(x: unknown) { if (typeof x === 'string') { /* ... */ } }",
72
+ },
73
+ "@typescript-eslint/no-non-null-assertion": {
74
+ what: "No `!`. Guard the value instead.",
75
+ bad: "const first = arr[0]!;",
76
+ good: "const first = arr[0]; if (first === undefined) { return; }",
77
+ },
78
+ "@typescript-eslint/consistent-type-assertions": {
79
+ what: "No `as` casts. Use a type guard or `satisfies`.",
80
+ bad: "const u = json as IUser;",
81
+ good: "if (isUser(json)) { const u = json; /* narrowed */ }",
82
+ },
83
+ "@typescript-eslint/strict-boolean-expressions": {
84
+ what: "Conditions must be explicit booleans — no truthy strings/numbers/nullables.",
85
+ bad: "if (name) {}",
86
+ good: "if (name !== undefined && name.length > 0) {}",
87
+ },
88
+ "@typescript-eslint/naming-convention": {
89
+ what: "Interfaces are PascalCase with an `I` prefix.",
90
+ bad: "interface User {}",
91
+ good: "interface IUser {}",
92
+ },
93
+ "@typescript-eslint/no-floating-promises": {
94
+ what: "A promise must be awaited or explicitly voided.",
95
+ bad: "doAsync();",
96
+ good: "await doAsync(); // or: void doAsync();",
97
+ },
98
+ "prefer-const": {
99
+ what: "Use `const` for never-reassigned bindings.",
100
+ bad: "let x = 1;",
101
+ good: "const x = 1;",
102
+ },
103
+ eqeqeq: {
104
+ what: "Use `===`/`!==`.",
105
+ bad: "if (a == b) {}",
106
+ good: "if (a === b) {}",
107
+ },
108
+ "prefer-template": {
109
+ what: "Build strings with template literals, not `+` concatenation.",
110
+ bad: 'const s = "$" + dollars + "." + cents;',
111
+ good: "const s = `$${dollars}.${cents}`;",
112
+ },
113
+ "@typescript-eslint/no-inferrable-types": {
114
+ what: "Drop the type annotation when the initializer makes it obvious — let TS infer.",
115
+ bad: "const negative: boolean = cents < 0;",
116
+ good: "const negative = cents < 0;",
117
+ },
118
+
119
+ // React / hooks idioms — same failure-keyed mechanism as the eslint/TS rules,
120
+ // extended to framework rules the React gate enforces. Injected ONLY when the
121
+ // gate names the rule (never a standing wall of framework advice).
122
+ "react-hooks/rules-of-hooks": {
123
+ what: "Hooks run unconditionally at the top of the component — never inside a condition, loop, or after an early return.",
124
+ bad: "if (open) { const [x, setX] = useState(0); }",
125
+ good: "const [x, setX] = useState(0); if (open) { /* use x */ }",
126
+ },
127
+ "react-hooks/exhaustive-deps": {
128
+ what: "A hook's dependency array must list every reactive value it reads; add the missing dep (or memoize/move it).",
129
+ bad: "useEffect(() => { send(query); }, []);",
130
+ good: "useEffect(() => { send(query); }, [query]);",
131
+ },
132
+ "react/jsx-key": {
133
+ what: "Each element in a list needs a stable, unique `key` — an id, not the array index.",
134
+ bad: "items.map((it) => <li>{it.text}</li>)",
135
+ good: "items.map((it) => <li key={it.id}>{it.text}</li>)",
136
+ },
137
+ "react/no-array-index-key": {
138
+ what: "Don't use the array index as `key` — it breaks element identity when the list reorders or filters. Use a stable id.",
139
+ bad: "items.map((it, i) => <li key={i}>{it.text}</li>)",
140
+ good: "items.map((it) => <li key={it.id}>{it.text}</li>)",
141
+ },
142
+ };
143
+
144
+ /**
145
+ * Strict-TypeScript idiom traps: valid JavaScript the model habitually writes
146
+ * that trips the gate in a way the rule MESSAGE alone doesn't explain — the
147
+ * failure fires at the use-site, not where the bad value was created. Matched
148
+ * against the editable file's SOURCE (not just the errored line), and gated on
149
+ * the error set looking like this trap's failure, so the hint is precise and
150
+ * never fires spuriously on a clean run.
151
+ *
152
+ * Seeded from a real, repeated `money` failure: `new Array(n).fill(x)` is typed
153
+ * `any[]` under strict, so the model fixed it, reintroduced it, and fixed it
154
+ * again across separate turns.
155
+ */
156
+ interface IIdiomTrap {
157
+ /** Pattern in the editable source that signals the trap is present. */
158
+ inSource: RegExp;
159
+ /** Tested against each error's `rule + message`; the hint only shows on a match. */
160
+ relevant: RegExp;
161
+ /** The targeted fix, shown when both conditions hold. */
162
+ hint: string;
163
+ }
164
+
165
+ const IDIOM_TRAPS: readonly IIdiomTrap[] = [
166
+ {
167
+ inSource: /new\s+Array\s*\([^)]*\)\s*\.fill\(/,
168
+ relevant: /unsafe|no-explicit-any|\bany\b/i,
169
+ hint: "`new Array(n).fill(x)` is typed `any[]` under strict TypeScript, so every element read off it is `any`. Use `Array.from({ length: n }, () => x)` — it's typed `T[]`.",
170
+ },
171
+ ];
172
+
173
+ /**
174
+ * Idiom hints for traps whose pattern appears in the given source AND whose
175
+ * signature matches the current errors. `sources` are the editable files'
176
+ * contents; `errors` are the gate failures.
177
+ */
178
+ export function idiomHints(
179
+ sources: readonly string[],
180
+ errors: ErrorSet
181
+ ): string {
182
+ const errText = errors.map((e) => `${e.rule ?? ""} ${e.message}`).join("\n");
183
+ const hints = new Set<string>();
184
+
185
+ for (const trap of IDIOM_TRAPS) {
186
+ if (!trap.relevant.test(errText)) {
187
+ continue;
188
+ }
189
+
190
+ if (sources.some((s) => trap.inSource.test(s))) {
191
+ hints.add(trap.hint);
192
+ }
193
+ }
194
+
195
+ return [...hints].map((h) => `- ${h}`).join("\n");
196
+ }
197
+
198
+ /**
199
+ * Pull rule guidance straight from raw command output (tsc text, `eslint
200
+ * --format json`, plain eslint). This is what lets the docs reach the model
201
+ * when IT runs the gate via the `run` tool — otherwise it only sees raw errors
202
+ * and fixes them blind across many rounds.
203
+ */
204
+ export function ruleHelpFromOutput(output: string): string {
205
+ const ids = new Set<string>();
206
+
207
+ for (const m of output.matchAll(/TS\d+/g)) {
208
+ ids.add(m[0]);
209
+ }
210
+
211
+ for (const m of output.matchAll(/"ruleId"\s*:\s*"([^"]+)"/g)) {
212
+ if (m[1] !== undefined) {
213
+ ids.add(m[1]);
214
+ }
215
+ }
216
+
217
+ for (const m of output.matchAll(/@typescript-eslint\/[a-z-]+/g)) {
218
+ ids.add(m[0]);
219
+ }
220
+
221
+ const errors = [...ids].map((rule) => ({ key: rule, rule, message: "" }));
222
+
223
+ return ruleHelp(errors);
224
+ }
225
+
226
+ /** Format the rule docs for whichever rules appear in the current error set. */
227
+ export function ruleHelp(errors: ErrorSet): string {
228
+ const seen = new Set<string>();
229
+ const blocks: string[] = [];
230
+
231
+ for (const e of errors) {
232
+ if (e.rule === undefined || seen.has(e.rule)) {
233
+ continue;
234
+ }
235
+
236
+ // Try curated docs first, then generated, then tsforge pack rules
237
+ let doc = RULE_DOCS[e.rule] ?? GENERATED[e.rule];
238
+
239
+ if (doc === undefined && e.rule.startsWith("tsforge/")) {
240
+ // For tsforge pack rules, look up in generated docs
241
+ doc = GENERATED[e.rule];
242
+ }
243
+
244
+ if (doc === undefined) {
245
+ continue;
246
+ }
247
+
248
+ seen.add(e.rule);
249
+ blocks.push(`${e.rule}: ${doc.what}\n ✗ ${doc.bad}\n ✓ ${doc.good}`);
250
+ }
251
+
252
+ return blocks.join("\n");
253
+ }
254
+
255
+ /**
256
+ * Parse a typescript-eslint rule's source `.mdx` into a doc. The format is
257
+ * regular: a frontmatter `description:` and `<TabItem value="❌ Incorrect">` /
258
+ * `"✅ Correct"` sections each followed by a fenced ```ts block. Used offline by
259
+ * the cache builder. Returns null if the expected sections aren't found.
260
+ */
261
+ export function parseRuleMdx(mdx: string): IRuleDoc | null {
262
+ const desc = /^description:\s*['"]([\s\S]+?)['"]\s*$/m.exec(mdx);
263
+ const bad = firstTsBlock(mdx, "❌ Incorrect");
264
+ const good = firstTsBlock(mdx, "✅ Correct");
265
+
266
+ if (bad === null || good === null) {
267
+ return null;
268
+ }
269
+
270
+ return {
271
+ what: desc?.[1] ?? "",
272
+ bad: cap(bad),
273
+ good: cap(good),
274
+ };
275
+ }
276
+
277
+ function firstTsBlock(mdx: string, marker: string): string | null {
278
+ const at = mdx.indexOf(marker);
279
+
280
+ if (at === -1) {
281
+ return null;
282
+ }
283
+
284
+ const block = /```ts\n([\s\S]*?)```/.exec(mdx.slice(at));
285
+
286
+ return block?.[1]?.trimEnd() ?? null;
287
+ }
288
+
289
+ /** Keep examples prompt-lean — first ~8 lines, capped. */
290
+ function cap(code: string): string {
291
+ const lines = code.split("\n").slice(0, 8).join("\n");
292
+
293
+ return lines.length > 360 ? `${lines.slice(0, 360)}…` : lines;
294
+ }
295
+
296
+ /**
297
+ * Targeted fix guidance for a quality-REVIEWER's prose critique — the idiomatic
298
+ * issues the GATE can't flag (the model is already green). Keyed by what the
299
+ * judge actually complains about on Q4 runs: over-annotation, gratuitous
300
+ * undefined guards, locale-less toLocaleString, `+` concatenation, terse names.
301
+ * Turns a vague "make it more idiomatic" into a concrete bad→good, the same way
302
+ * ruleHelp does for lint failures — but on the quality channel, not the gate.
303
+ */
304
+ interface IQualityHint {
305
+ /** Tested against the judge's notes prose. */
306
+ match: RegExp;
307
+ advice: string;
308
+ }
309
+
310
+ const QUALITY_HINTS: readonly IQualityHint[] = [
311
+ {
312
+ match: /verbose|explicit|redundant|annotation/i,
313
+ advice:
314
+ "Drop redundant type annotations — let TS infer obvious locals/returns (`const n = total;`, not `const n: number = total;`). Annotate parameters and unclear inference only.",
315
+ },
316
+ {
317
+ match: /unnecessary|undefined check|null check/i,
318
+ advice:
319
+ "Remove `=== undefined`/null guards the compiler doesn't require (a value already narrowed, or a non-indexed access). Guard ONLY where `noUncheckedIndexedAccess` actually flags it.",
320
+ },
321
+ {
322
+ match: /locale|toLocaleString/i,
323
+ advice:
324
+ 'Pass an explicit locale: `n.toLocaleString("en-US")` — bare `toLocaleString()` is environment-dependent.',
325
+ },
326
+ {
327
+ match: /concatenat|string \+| \+ |\bconcat\b/i,
328
+ advice:
329
+ "Prefer template literals over `+` string concatenation: `` `${dollars}.${cents}` ``.",
330
+ },
331
+ {
332
+ match: /terse|short name|parameter name|\bnaming\b/i,
333
+ advice: "Name parameters descriptively (`acc`, `ratio` — not `a`, `r`).",
334
+ },
335
+ ];
336
+
337
+ /** Concrete fix guidance for a quality reviewer's critique, or "" if none match. */
338
+ export function qualityHints(notes: string): string {
339
+ return QUALITY_HINTS.filter((h) => h.match.test(notes))
340
+ .map((h) => `- ${h.advice}`)
341
+ .join("\n");
342
+ }
@@ -0,0 +1,19 @@
1
+ export * from "./loop.types";
2
+ export * from "./loop.constants";
3
+ export { runTask } from "./run";
4
+ export { runSpec } from "./run-spec";
5
+ export { qualityRepair } from "./quality";
6
+ export {
7
+ toolsFor,
8
+ buildTsService,
9
+ runToolCalls,
10
+ settleGate,
11
+ type ILoopCtx,
12
+ type ILoopState,
13
+ } from "./turn";
14
+ export {
15
+ Session,
16
+ PLAN_APPROVED_NOTE,
17
+ type ISessionConfig,
18
+ type ISendResult,
19
+ } from "./session";
@@ -0,0 +1,68 @@
1
+ /** Terminal status of a single task run — compare against these, not bare strings. */
2
+ export const RUN_STATUS = {
3
+ done: "done",
4
+ stuck: "stuck",
5
+ redNotConfirmed: "red-not-confirmed",
6
+ } as const;
7
+
8
+ /** Why a run gave up (only set when status is `stuck`). */
9
+ export const STUCK_REASON = {
10
+ stalled: "stalled",
11
+ cap: "cap",
12
+ } as const;
13
+
14
+ /** Whole-spec outcome — compare against these, never the bare string. */
15
+ export const SPEC_STATUS = {
16
+ done: "done",
17
+ blocked: "blocked",
18
+ } as const;
19
+
20
+ /**
21
+ * Loop tuning — kept with the loop domain (not a global bucket). Each value's
22
+ * rationale lives here so a tuning pass sees the whole budget at a glance.
23
+ */
24
+ export const LOOP_LIMITS = {
25
+ /** Max chars of a tool's output fed back to the model (keeps context bounded). */
26
+ maxToolOutputChars: 4000,
27
+ /**
28
+ * Reject an edit replacement spanning more than this many lines — a push
29
+ * toward surgical changes over lazy whole-file rewrites. 50 admits real
30
+ * functions, still rejects ~80-line rewrites; the gate re-validates anyway.
31
+ */
32
+ maxEditLines: 50,
33
+ /**
34
+ * Give up after the gate shows the EXACT same error set this many edits in a
35
+ * row (genuine spinning). Generous; the turn cap is the real backstop.
36
+ */
37
+ gateStuckRepeats: 10,
38
+ /**
39
+ * Above this many chars of combined file content, the seed prompt sends a
40
+ * navigable project MAP instead of full dumps. Below it, full dumps.
41
+ */
42
+ mapThresholdChars: 12000,
43
+ /** Hard backstop on model turns per task. */
44
+ maxTurns: 40,
45
+ /** Turn budget for a from-scratch WEB build (heavy gate, many files): used by
46
+ * headless web builds AND applied when an interactive session scaffolds via
47
+ * `scaffold_web` — measured: a todo app was still WRITING components when it
48
+ * hit the default 40 cap, before the gate ever ran. */
49
+ webMaxTurns: 180,
50
+ /**
51
+ * How many times a build turn may dump file contents as a chat message (instead
52
+ * of calling `create`) before the session gives up. Each time we nudge it to use
53
+ * tools; past this it's clearly not going to act, so stop with a clear message
54
+ * rather than loop forever narrating code.
55
+ */
56
+ maxBuildNudges: 2,
57
+ /**
58
+ * Default reasoning-token cap for SCRATCH (create-from-spec) tasks, where the
59
+ * model over-thinks unbounded (~92s turn-1, occasional 198s spirals) without
60
+ * converging faster. Measured knee on money: 2048 ≈ 73s/4 turns vs 206s/5.7
61
+ * uncapped, Q-neutral; 4096 rambles back up to 133s. NOT applied to existing-
62
+ * code runs — there the cap HURT navigation (react-board hit 12 turns @2048),
63
+ * since understanding a codebase genuinely needs reasoning. Override per-run
64
+ * with TSFORGE_THINKING_BUDGET / opts.thinkingTokenBudget. Harder algorithmic
65
+ * scratch targets may want a higher override.
66
+ */
67
+ scratchThinkingBudget: 2048,
68
+ } as const;