@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,99 @@
1
+ import type { ErrorParser } from "../validate";
2
+ import {
3
+ type RUN_STATUS,
4
+ type STUCK_REASON,
5
+ type SPEC_STATUS,
6
+ } from "./loop.constants";
7
+
8
+ /** A progress event emitted as the loop runs, for live observability. */
9
+ export interface ILoopEvent {
10
+ kind:
11
+ | "start"
12
+ | "red"
13
+ | "cycle"
14
+ | "token"
15
+ | "message"
16
+ | "fix"
17
+ | "edit"
18
+ | "create"
19
+ | "validated"
20
+ | "done"
21
+ | "stuck"
22
+ | "run"
23
+ | "tool"
24
+ | "repair"
25
+ | "timing"
26
+ | "usage"
27
+ | "ttsr";
28
+ task: string;
29
+ message: string;
30
+ cycle?: number;
31
+ cycles?: number;
32
+ /** For `timing` events: how long the turn took, in milliseconds. */
33
+ ms?: number;
34
+ errors?: number;
35
+ passed?: boolean;
36
+ file?: string;
37
+ /** For `create` events: the new file's content (rendered as a code block). */
38
+ content?: string;
39
+ /** For `edit` events: the replaced / replacement snippets (rendered as a diff). */
40
+ oldString?: string;
41
+ newString?: string;
42
+ /** For `run` events: the shell command and its result. */
43
+ command?: string;
44
+ exitCode?: number;
45
+ output?: string;
46
+ /** For `token` events: which stream it came from. The renderer collapses
47
+ * `reasoning` to a compact "thinking…" indicator (the full text still goes to
48
+ * the log); `tool` markers and gate output (no channel) print normally. */
49
+ channel?: "reasoning" | "content" | "tool";
50
+ /** For `usage` events: real per-call token accounting (for the --log metrics). */
51
+ promptTokens?: number;
52
+ completionTokens?: number;
53
+ totalTokens?: number;
54
+ /** For `usage` (and salvage-warning `tool`) events: whether THIS model call
55
+ * ran with thinking enabled — lets the analyzer correlate malformed-tool-call
56
+ * rate with the thinking mode (see analyze-malformed). */
57
+ thinking?: boolean;
58
+ /** For the `start` event: run metadata, so a log is self-describing for the
59
+ * analyzer (which model / how big a context window the metrics are against). */
60
+ model?: string;
61
+ contextWindow?: number;
62
+ }
63
+
64
+ export type Reporter = (event: ILoopEvent) => void;
65
+
66
+ export type RunStatus = (typeof RUN_STATUS)[keyof typeof RUN_STATUS];
67
+ export type StuckReason = (typeof STUCK_REASON)[keyof typeof STUCK_REASON];
68
+ export type SpecStatus = (typeof SPEC_STATUS)[keyof typeof SPEC_STATUS];
69
+
70
+ export interface IRunResult {
71
+ task: string;
72
+ /** The gate failed before we started (a real goalpost). */
73
+ redConfirmed: boolean;
74
+ status: RunStatus;
75
+ /** Model turns used. */
76
+ cycles: number;
77
+ reason?: StuckReason;
78
+ /** Edits/creates applied to editable files (measure edit churn). */
79
+ edits?: number;
80
+ /** Times an edit RAISED the gate error count (regressions). */
81
+ regressions?: number;
82
+ }
83
+
84
+ export interface IRunOptions {
85
+ parse?: ErrorParser;
86
+ onEvent?: Reporter;
87
+ temperature?: number;
88
+ /** Per-request thinking toggle passed to the provider. */
89
+ enableThinking?: boolean;
90
+ /** Cap reasoning tokens per model call (vLLM `thinking_token_budget`). */
91
+ thinkingTokenBudget?: number;
92
+ /** Hard backstop on model turns (default LOOP_LIMITS.maxTurns). */
93
+ maxTurns?: number;
94
+ }
95
+
96
+ export interface ISpecResult {
97
+ status: SpecStatus;
98
+ results: IRunResult[];
99
+ }
@@ -0,0 +1,2 @@
1
+ export { SYSTEM, CHAT_SYSTEM, COMPACT_SYSTEM, seedPrompt } from "./prompt";
2
+ export { renderFileSection, exportedSymbols } from "./project-map";
@@ -0,0 +1,69 @@
1
+ import { LOOP_LIMITS } from "../loop.constants";
2
+ import type { IFileView } from "../../lib/fs";
3
+
4
+ /** Exported symbol names in a file (lightweight regex — for the project map). */
5
+ export function exportedSymbols(content: string): string[] {
6
+ const names = new Set<string>();
7
+ const decl =
8
+ /export\s+(?:default\s+)?(?:async\s+)?(?:function|const|let|class|interface|type|enum)\s+([A-Za-z_$][\w$]*)/g;
9
+
10
+ for (const m of content.matchAll(decl)) {
11
+ if (m[1] !== undefined) {
12
+ names.add(m[1]);
13
+ }
14
+ }
15
+
16
+ for (const m of content.matchAll(/export\s*\{([^}]*)\}/g)) {
17
+ const inner = m[1];
18
+
19
+ if (inner === undefined) {
20
+ continue;
21
+ }
22
+
23
+ for (const part of inner.split(",")) {
24
+ const name = part
25
+ .trim()
26
+ .split(/\s+as\s+/)[0]
27
+ ?.trim();
28
+
29
+ if (name !== undefined && name.length > 0) {
30
+ names.add(name);
31
+ }
32
+ }
33
+ }
34
+
35
+ return [...names];
36
+ }
37
+
38
+ /** A compact map: `path (N lines) — exports: A, B`, one per file. */
39
+ function projectMap(views: readonly IFileView[]): string {
40
+ return views
41
+ .map((v) => {
42
+ const lines = v.content.split("\n").length;
43
+ const ex = exportedSymbols(v.content);
44
+
45
+ return ` ${v.path} (${String(lines)} lines)${ex.length > 0 ? ` — exports: ${ex.join(", ")}` : ""}`;
46
+ })
47
+ .join("\n");
48
+ }
49
+
50
+ /**
51
+ * Render a set of files for the prompt: full contents when small, a navigable
52
+ * MAP when the combined size exceeds LOOP_LIMITS.mapThresholdChars (the model then
53
+ * uses read/search/symbol_search to inspect specifics). Exported for testing.
54
+ */
55
+ export function renderFileSection(views: readonly IFileView[]): {
56
+ text: string;
57
+ mapped: boolean;
58
+ } {
59
+ const total = views.reduce((n, v) => n + v.content.length, 0);
60
+
61
+ if (total > LOOP_LIMITS.mapThresholdChars) {
62
+ return { text: projectMap(views), mapped: true };
63
+ }
64
+
65
+ return {
66
+ text: views.map((v) => `File ${v.path}:\n${v.content}`).join("\n\n"),
67
+ mapped: false,
68
+ };
69
+ }
@@ -0,0 +1,107 @@
1
+ import type { ITask } from "../../spec";
2
+ import type { IFileView } from "../../lib/fs";
3
+ import { PACK_REGISTRY } from "../../stack-detection";
4
+ import type { IStackProfile } from "../../stack-detection";
5
+ import { renderFileSection } from "./project-map";
6
+
7
+ /** The implement-agent system prompt: who it is, the tools, and the strict-TS
8
+ * house rules the gate enforces. */
9
+ export const SYSTEM = [
10
+ "You are an expert TypeScript engineer working inside tsforge, a harness specialized for STRICT TypeScript. Implement the task by editing code until the gate passes.",
11
+ "Tools: `read` (inspect a file), `edit` (replace an exact, unique snippet), `create` (a new file), `run` (execute any shell command and see its output).",
12
+ "Lead with action: write the implementation FIRST (one `create`/`edit`) — do NOT deliberate at length before writing any code.",
13
+ "After every edit the harness AUTOMATICALLY runs the gate and gives you the result (the errors + fix guidance for the failing rules). You do NOT need to run the acceptance command yourself — read that result and fix exactly what it reports, then edit again. Keep going until it reports green; the harness ends the task at that point.",
14
+ "Test hypotheses by RUNNING them, never by reasoning them out. Unsure about an edge case, rounding, or ordering (`Math.floor(100/3)`, largest-remainder ties)? `run` a quick `bun -e '…console.log(…)'`, or write a throwaway `scratch/check.ts` importing your impl and `run` it. `scratch/` is yours — the gate ignores it.",
15
+ "The gate is `tsc` strict + eslint with every rule an error, so write TypeScript that satisfies it: interfaces are `I`-prefixed; `===`; no `var`; never the non-null `!` — guard index access (`const x = arr[i]; if (x === undefined) {...}`); no `any` and no `as` — type every parameter (e.g. `.reduce((acc: number, r: number) => …, 0)`); explicit boolean conditions. When the gate flags errors in read-only files (tests/types), they come from your editable file being missing or wrong-shaped and vanish once it's correct — don't edit them.",
16
+ ].join("\n");
17
+
18
+ /**
19
+ * The INTERACTIVE assistant prompt (the CLI's `Session`). Unlike `SYSTEM` — which
20
+ * drives a single task to a gate and is told to "keep going until green" — this
21
+ * frames an open-ended conversation: investigate with tools, then ANSWER or ACT
22
+ * and STOP. Without this framing the model treats every message as implement-to-
23
+ * green and scans the repo forever when asked a question (there's no gate to hit).
24
+ */
25
+ export const CHAT_SYSTEM = [
26
+ "You are tsforge, an expert TypeScript coding assistant. You are launched inside a repository, but NOT every request is about that repository. The user talks to you; you help by answering, and by inspecting/changing code with your tools.",
27
+ "Tools: `read` (inspect a file), `run` (execute any shell command — `ls`, `rg`, tests, `tsc`), `edit` (replace an exact, unique snippet), `create` (a new file).",
28
+ "File paths are RELATIVE to the workspace root: use `tsconfig.json` or `src/app.ts` — never an absolute path, and never repeat the workspace folder in the path.",
29
+ "MATCH EFFORT TO THE REQUEST. A self-contained ask — 'write a `double` function', 'explain `satisfies`' — has nothing to do with the surrounding repo: just answer it directly (reply with the code; only `create` a file if asked). Do NOT read or scan the repository for these. Investigate the codebase ONLY when the request is actually about THIS project (a bug here, a change here, 'what would you change?').",
30
+ "ASK BEFORE GUESSING when the request is genuinely ambiguous — unclear scope, unclear which file to touch, or unclear whether it even relates to this repo (e.g. 'add a retry' with no target). Ask ONE short clarifying question and stop; the user will answer and you continue. But don't over-ask: when a sensible default is obvious, take it and state the assumption in one line.",
31
+ "Be decisive, not exhaustive. When you do investigate, a few targeted reads beat reading everything — as soon as you can answer or act, STOP calling tools and reply.",
32
+ "For a QUESTION about the repo, investigate briefly then give a concise, concrete answer (cite specific files/symbols; offer your top few recommendations, not a survey). For a CHANGE, make it with `edit`/`create`, verify by `run`ning the tests or `tsc`, then briefly state what you did.",
33
+ "When you write code, use strict TypeScript: `I`-prefixed interfaces; `===`; no `var`; never the non-null `!` (guard index access: `const x = arr[i]; if (x === undefined) {…}`); no `any`/`as` (type parameters); explicit boolean conditions.",
34
+ ].join("\n");
35
+
36
+ /** Prompt for `/compact`: condense a long conversation, keeping what matters for
37
+ * continuing the work — not a chatty recap. */
38
+ export const COMPACT_SYSTEM = [
39
+ "You are compacting a coding session to save context. Summarize the conversation below into a concise brief that lets the assistant CONTINUE seamlessly.",
40
+ "Preserve: the user's goals/requests, decisions made, files created or changed (with their purpose), key facts learned about the codebase, and any OPEN threads or next steps.",
41
+ "Drop: small talk, redundant tool output, and anything already superseded. Use terse bullet points. Do not invent anything not in the transcript.",
42
+ ].join("\n");
43
+
44
+ /** Build stack-aware guidance from an IStackProfile. Includes the stack name
45
+ * and guidance strings from active packs (skipping empty/always-on packs). */
46
+ export function buildStackGuidance(profile: IStackProfile): string {
47
+ const lines: string[] = [];
48
+
49
+ lines.push(`## Project stack & conventions`);
50
+ lines.push(`Stack: **${profile.name}** (${profile.reason})`);
51
+
52
+ for (const packId of profile.packs) {
53
+ // Find the descriptor by ID from the registry
54
+ const descriptor = Object.values(PACK_REGISTRY).find(
55
+ (d) => d.id === packId
56
+ );
57
+
58
+ // Add guidance if present and non-empty
59
+ if (descriptor?.guidance !== undefined) {
60
+ lines.push(`- ${descriptor.guidance}`);
61
+ }
62
+ }
63
+
64
+ return lines.length > 1 ? lines.join("\n") : "";
65
+ }
66
+
67
+ /** Build the first user message: the task contract + editable/context files
68
+ * (full dumps when small, a navigable MAP when large — see renderFileSection). */
69
+ export function seedPrompt(
70
+ task: ITask,
71
+ editable: IFileView[],
72
+ context: IFileView[],
73
+ stack?: IStackProfile
74
+ ): string {
75
+ const intent =
76
+ task.intent !== undefined && task.intent.length > 0
77
+ ? `Spec contract — implement EXACTLY this:\n${task.intent}`
78
+ : "";
79
+
80
+ const ed = renderFileSection(editable);
81
+ const editableText =
82
+ editable.length === 0
83
+ ? "(none of the editable files exist yet — create them)"
84
+ : ed.mapped
85
+ ? `The editable files are large — here is a MAP (path · lines · exports). INSPECT specifics with read/search/symbol_search/find_references before editing; don't guess:\n${ed.text}`
86
+ : ed.text;
87
+
88
+ const ctx = context.length > 0 ? renderFileSection(context) : null;
89
+ const contextText =
90
+ ctx === null
91
+ ? ""
92
+ : `Read-only context (do NOT edit)${ctx.mapped ? " — MAP; read specifics on demand" : ""}:\n${ctx.text}`;
93
+
94
+ const stackText = stack !== undefined ? buildStackGuidance(stack) : "";
95
+
96
+ return [
97
+ `Task ${task.id}.`,
98
+ intent,
99
+ stackText,
100
+ `Acceptance command (run this to verify — it must exit 0): ${task.accept}`,
101
+ `Editable files: ${task.files.join(", ")}`,
102
+ `Current editable contents:\n${editableText}`,
103
+ contextText,
104
+ ]
105
+ .filter((s) => s.length > 0)
106
+ .join("\n\n");
107
+ }
@@ -0,0 +1,174 @@
1
+ import { join } from "node:path";
2
+ import type { ITask } from "../spec";
3
+ import type { IAgent } from "../agent";
4
+ import type { IProvider } from "../inference";
5
+ import { validate, type ErrorParser } from "../validate";
6
+ import { runAccept } from "../validate";
7
+ import { judge } from "../eval";
8
+ import { qualityHints } from "./feedback";
9
+ import type { Reporter } from "./loop.types";
10
+
11
+ export interface IQualityResult {
12
+ quality: number;
13
+ notes: string;
14
+ attempts: number;
15
+ }
16
+
17
+ export interface IQualityMeta {
18
+ goal: string;
19
+ criteria: string;
20
+ }
21
+
22
+ export interface IQualityOptions {
23
+ /** Stop when quality reaches this (default 5). */
24
+ target?: number;
25
+ /** Max improvement attempts (default 2). */
26
+ maxAttempts?: number;
27
+ parse?: ErrorParser;
28
+ onEvent?: Reporter;
29
+ }
30
+
31
+ /**
32
+ * After a task is green, drive its *quality* up: judge it, and while it's below
33
+ * target, feed the reviewer's critique back as an improvement instruction,
34
+ * re-validate (must stay green), and re-judge — keeping only changes that both
35
+ * keep the gate green and raise the score. Never ends below the green baseline.
36
+ */
37
+ export async function qualityRepair(
38
+ task: ITask,
39
+ cwd: string,
40
+ agent: IAgent,
41
+ judgeProvider: IProvider,
42
+ meta: IQualityMeta,
43
+ opts: IQualityOptions = {}
44
+ ): Promise<IQualityResult> {
45
+ const target = opts.target ?? 5;
46
+ const maxAttempts = opts.maxAttempts ?? 2;
47
+ const report: Reporter = opts.onEvent ?? (() => undefined);
48
+
49
+ let best = await score(task, cwd, judgeProvider, meta);
50
+
51
+ report({
52
+ kind: "fix",
53
+ task: task.id,
54
+ message: `quality ${best.quality}/5 — ${best.notes}`,
55
+ });
56
+
57
+ let attempts = 0;
58
+
59
+ while (best.quality < target && attempts < maxAttempts) {
60
+ attempts += 1;
61
+
62
+ const snapshot = await snapshotFiles(task, cwd);
63
+
64
+ // Turn the reviewer's prose into concrete bad→good guidance where we have a
65
+ // card for the issue it named (the quality channel — these are idiomatic
66
+ // problems the gate can't flag).
67
+ const hints = qualityHints(best.notes);
68
+ const guidance =
69
+ hints.length > 0
70
+ ? `\n\nConcrete fixes for the idioms it named:\n${hints}`
71
+ : "";
72
+
73
+ await agent.implement({
74
+ cwd,
75
+ task,
76
+ errors: [
77
+ {
78
+ key: "quality",
79
+ message: `The code is green but a senior reviewer scored it ${best.quality}/5: "${best.notes}". Improve the code to address that critique.${guidance} Do NOT break the tests or the gate.`,
80
+ },
81
+ ],
82
+ cycle: attempts,
83
+ report,
84
+ });
85
+
86
+ if (task.fix !== undefined && task.fix.length > 0) {
87
+ await runAccept({ ...task, accept: task.fix }, cwd);
88
+ }
89
+
90
+ const gate = await validate(task, cwd, opts.parse);
91
+
92
+ if (!gate.passed) {
93
+ await restoreFiles(snapshot, cwd);
94
+ report({
95
+ kind: "fix",
96
+ task: task.id,
97
+ message: `quality attempt ${attempts}: broke the gate — reverted`,
98
+ });
99
+ continue;
100
+ }
101
+
102
+ const next = await score(task, cwd, judgeProvider, meta);
103
+
104
+ if (next.quality > best.quality) {
105
+ best = next;
106
+ report({
107
+ kind: "fix",
108
+ task: task.id,
109
+ message: `quality ↑ ${best.quality}/5`,
110
+ });
111
+ } else {
112
+ await restoreFiles(snapshot, cwd);
113
+ report({
114
+ kind: "fix",
115
+ task: task.id,
116
+ message: `quality attempt ${attempts}: no gain (${next.quality}/5) — kept previous`,
117
+ });
118
+ }
119
+ }
120
+
121
+ return { quality: best.quality, notes: best.notes, attempts };
122
+ }
123
+
124
+ interface IScore {
125
+ quality: number;
126
+ notes: string;
127
+ }
128
+
129
+ async function score(
130
+ task: ITask,
131
+ cwd: string,
132
+ judgeProvider: IProvider,
133
+ meta: IQualityMeta
134
+ ): Promise<IScore> {
135
+ let code = "";
136
+
137
+ for (const file of task.files) {
138
+ code += `// ${file}\n${await Bun.file(join(cwd, file)).text()}\n\n`;
139
+ }
140
+
141
+ const result = await judge(judgeProvider, {
142
+ goal: meta.goal,
143
+ criteria: meta.criteria,
144
+ code,
145
+ });
146
+
147
+ return { quality: result.overall, notes: result.notes };
148
+ }
149
+
150
+ async function snapshotFiles(
151
+ task: ITask,
152
+ cwd: string
153
+ ): Promise<Map<string, string>> {
154
+ const snapshot = new Map<string, string>();
155
+
156
+ for (const file of task.files) {
157
+ const handle = Bun.file(join(cwd, file));
158
+
159
+ if (await handle.exists()) {
160
+ snapshot.set(file, await handle.text());
161
+ }
162
+ }
163
+
164
+ return snapshot;
165
+ }
166
+
167
+ async function restoreFiles(
168
+ snapshot: Map<string, string>,
169
+ cwd: string
170
+ ): Promise<void> {
171
+ for (const [file, content] of snapshot) {
172
+ await Bun.write(join(cwd, file), content);
173
+ }
174
+ }