@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.
- package/bin/tsforge.js +2 -0
- package/package.json +35 -0
- package/src/agent/agent.constants.ts +382 -0
- package/src/agent/agent.types.ts +34 -0
- package/src/agent/index.ts +4 -0
- package/src/agent/model-agent.ts +297 -0
- package/src/agent/tool-repair.ts +194 -0
- package/src/agent/tools.ts +190 -0
- package/src/browser/checks.ts +96 -0
- package/src/browser/index.ts +8 -0
- package/src/browser/oracle.ts +303 -0
- package/src/classify.ts +48 -0
- package/src/cli.ts +1333 -0
- package/src/config/config.constants.ts +9 -0
- package/src/config/flags.ts +32 -0
- package/src/config/index.ts +8 -0
- package/src/config/tsforge-config.ts +301 -0
- package/src/constitution/baseline.ts +257 -0
- package/src/detect-gate.ts +498 -0
- package/src/eval/eval.types.ts +36 -0
- package/src/eval/index.ts +3 -0
- package/src/eval/judge.ts +62 -0
- package/src/eval/score.ts +39 -0
- package/src/files/create.ts +22 -0
- package/src/files/edit.ts +193 -0
- package/src/files/files.constants.ts +11 -0
- package/src/files/files.types.ts +81 -0
- package/src/files/hashline-format.ts +110 -0
- package/src/files/hashline.ts +689 -0
- package/src/files/index.ts +19 -0
- package/src/index.ts +8 -0
- package/src/inference/index.ts +6 -0
- package/src/inference/inference.constants.ts +34 -0
- package/src/inference/inference.types.ts +123 -0
- package/src/inference/openai-compatible.ts +113 -0
- package/src/inference/stream-guard.ts +161 -0
- package/src/inference/stream.ts +370 -0
- package/src/inference/transport.ts +78 -0
- package/src/inference/wire.ts +0 -0
- package/src/lib/fs/fs.ts +126 -0
- package/src/lib/fs/fs.types.ts +5 -0
- package/src/lib/fs/index.ts +3 -0
- package/src/lib/fs/process.ts +146 -0
- package/src/lib/guards/guards.ts +9 -0
- package/src/lib/guards/index.ts +1 -0
- package/src/lib/json/index.ts +1 -0
- package/src/lib/json/json.ts +12 -0
- package/src/lib/scope/index.ts +2 -0
- package/src/lib/scope/scope.constants.ts +3 -0
- package/src/lib/scope/scope.ts +40 -0
- package/src/loop/astgrep-fix.ts +228 -0
- package/src/loop/feedback/feedback.ts +138 -0
- package/src/loop/feedback/index.ts +8 -0
- package/src/loop/feedback/meta-rule-docs.ts +41 -0
- package/src/loop/feedback/meta-rule-feedback.ts +61 -0
- package/src/loop/feedback/rule-docs.generated.json +112 -0
- package/src/loop/feedback/rule-docs.ts +342 -0
- package/src/loop/index.ts +19 -0
- package/src/loop/loop.constants.ts +68 -0
- package/src/loop/loop.types.ts +99 -0
- package/src/loop/prompt/index.ts +2 -0
- package/src/loop/prompt/project-map.ts +69 -0
- package/src/loop/prompt/prompt.ts +107 -0
- package/src/loop/quality.ts +174 -0
- package/src/loop/rule-docs.generated.json +367 -0
- package/src/loop/run-spec.ts +88 -0
- package/src/loop/run.ts +400 -0
- package/src/loop/session.ts +1410 -0
- package/src/loop/tools/add-dependency.ts +71 -0
- package/src/loop/tools/condense.ts +498 -0
- package/src/loop/tools/edit-hashline.ts +80 -0
- package/src/loop/tools/execute-tool.ts +80 -0
- package/src/loop/tools/file-ops.ts +323 -0
- package/src/loop/tools/index.ts +2 -0
- package/src/loop/tools/lsp-ops.ts +222 -0
- package/src/loop/tools/scaffold-routes.ts +68 -0
- package/src/loop/tools/scaffold-ui.ts +62 -0
- package/src/loop/tools/scaffold-web.ts +35 -0
- package/src/loop/tools/tool-context.ts +126 -0
- package/src/loop/ttsr-defaults.ts +53 -0
- package/src/loop/ttsr.ts +322 -0
- package/src/loop/turn.ts +856 -0
- package/src/lsp/index.ts +2 -0
- package/src/lsp/lsp.types.ts +56 -0
- package/src/lsp/service.ts +500 -0
- package/src/meta-rules/context.ts +195 -0
- package/src/meta-rules/index.ts +9 -0
- package/src/meta-rules/meta-rules.types.ts +47 -0
- package/src/meta-rules/parsers/package-json-parser.ts +51 -0
- package/src/meta-rules/registry.ts +37 -0
- package/src/meta-rules/rules/ci/workflow-actions-pinned.ts +59 -0
- package/src/meta-rules/rules/ci/workflow-runner-pinned.ts +57 -0
- package/src/meta-rules/rules/ci/workflow-timeout-required.ts +114 -0
- package/src/meta-rules/rules/config/tsconfig-paths-exist.ts +117 -0
- package/src/meta-rules/rules/config/tsconfig-strict.ts +91 -0
- package/src/meta-rules/rules/source-text/no-eslint-disable-comments.ts +34 -0
- package/src/meta-rules/rules/source-text/no-ts-suppressions.ts +38 -0
- package/src/meta-rules/rules/supply-chain/no-overlapping-libs.ts +57 -0
- package/src/meta-rules/rules/supply-chain/package-exact-deps.ts +55 -0
- package/src/meta-rules/rules/testing/test-sibling-required.ts +110 -0
- package/src/meta-rules/runner.ts +64 -0
- package/src/models-config.ts +196 -0
- package/src/render/ansi.ts +289 -0
- package/src/render/banner.ts +113 -0
- package/src/render/box.ts +134 -0
- package/src/render/index.ts +7 -0
- package/src/render/markdown.ts +123 -0
- package/src/render/render.types.ts +21 -0
- package/src/render/stream-markdown.ts +128 -0
- package/src/render/style.ts +26 -0
- package/src/rule-packs/bullmq/index.ts +39 -0
- package/src/rule-packs/bullmq/rules/index.ts +7 -0
- package/src/rule-packs/bullmq/rules/job-name-must-be-constant.ts +141 -0
- package/src/rule-packs/bullmq/rules/job-options-must-set-attempts.ts +174 -0
- package/src/rule-packs/bullmq/rules/no-blocking-concurrency-zero.ts +103 -0
- package/src/rule-packs/bullmq/rules/queue-options-must-set-removeoncomplete.ts +130 -0
- package/src/rule-packs/bullmq/rules/queue-options-must-set-removeonfail.ts +130 -0
- package/src/rule-packs/bullmq/rules/worker-must-implement-close.ts +182 -0
- package/src/rule-packs/bullmq/rules/worker-must-listen-failed.ts +140 -0
- package/src/rule-packs/bullmq/utils.ts +334 -0
- package/src/rule-packs/code-flow/index.ts +25 -0
- package/src/rule-packs/code-flow/rules/index.ts +3 -0
- package/src/rule-packs/code-flow/rules/no-bare-date-now.ts +138 -0
- package/src/rule-packs/code-flow/rules/no-template-trim-empty-ternary.ts +87 -0
- package/src/rule-packs/code-flow/rules/prefer-early-return.ts +80 -0
- package/src/rule-packs/code-flow/utils/prefer-early-return.ts +132 -0
- package/src/rule-packs/comment-hygiene/index.ts +25 -0
- package/src/rule-packs/comment-hygiene/rules/index.ts +3 -0
- package/src/rule-packs/comment-hygiene/rules/no-historical-comments.ts +102 -0
- package/src/rule-packs/comment-hygiene/rules/no-narration-comments.ts +83 -0
- package/src/rule-packs/comment-hygiene/rules/no-pr-reference-comments.ts +90 -0
- package/src/rule-packs/create-rule.ts +9 -0
- package/src/rule-packs/drizzle/index.ts +41 -0
- package/src/rule-packs/drizzle/rules/account-scoped-tables-require-where.ts +371 -0
- package/src/rule-packs/drizzle/rules/index.ts +8 -0
- package/src/rule-packs/drizzle/rules/no-nested-db-transaction.ts +127 -0
- package/src/rule-packs/drizzle/rules/no-raw-sql-outside-allowlist.ts +100 -0
- package/src/rule-packs/drizzle/rules/relations-must-cover-fks.ts +209 -0
- package/src/rule-packs/drizzle/rules/schema-files-must-not-import-driver.ts +127 -0
- package/src/rule-packs/drizzle/rules/schema-files-must-only-export-schema.ts +149 -0
- package/src/rule-packs/drizzle/rules/tables-must-have-timestamps.ts +312 -0
- package/src/rule-packs/drizzle/rules/timestamp-must-specify-mode.ts +166 -0
- package/src/rule-packs/drizzle/utils.ts +115 -0
- package/src/rule-packs/elysia/index.ts +43 -0
- package/src/rule-packs/elysia/rules/consistent-status-via-set.ts +69 -0
- package/src/rule-packs/elysia/rules/no-decorate-state-collision.ts +276 -0
- package/src/rule-packs/elysia/rules/no-separate-model-interfaces.ts +144 -0
- package/src/rule-packs/elysia/rules/prefer-destructured-context.ts +155 -0
- package/src/rule-packs/elysia/rules/prefer-direct-return.ts +176 -0
- package/src/rule-packs/elysia/rules/prefer-static-services.ts +159 -0
- package/src/rule-packs/elysia/rules/prefer-throw-status.ts +151 -0
- package/src/rule-packs/elysia/rules/require-hooks-before-routes.ts +209 -0
- package/src/rule-packs/elysia/rules/require-plugin-name.ts +107 -0
- package/src/rule-packs/elysia/utils/elysiaChain.ts +306 -0
- package/src/rule-packs/env-access/index.ts +23 -0
- package/src/rule-packs/env-access/rules/index.ts +2 -0
- package/src/rule-packs/env-access/rules/no-direct-process-env.ts +133 -0
- package/src/rule-packs/env-access/rules/no-process-exit.ts +95 -0
- package/src/rule-packs/i18n-keys/index.ts +19 -0
- package/src/rule-packs/i18n-keys/rules/static-translation-key-exists.ts +173 -0
- package/src/rule-packs/index.ts +139 -0
- package/src/rule-packs/jwt-cookies/index.ts +25 -0
- package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-be-httponly.ts +150 -0
- package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-be-secure-in-prod.ts +149 -0
- package/src/rule-packs/jwt-cookies/rules/bcrypt-rounds-min.ts +195 -0
- package/src/rule-packs/jwt-cookies/utils.ts +188 -0
- package/src/rule-packs/oauth-security/index.ts +25 -0
- package/src/rule-packs/oauth-security/rules/pkce-required-for-oidc.ts +296 -0
- package/src/rule-packs/oauth-security/rules/state-must-be-redis-backed.ts +193 -0
- package/src/rule-packs/oauth-security/rules/state-ttl-bounded.ts +219 -0
- package/src/rule-packs/oauth-security/utils.ts +127 -0
- package/src/rule-packs/react-component-architecture/index.ts +35 -0
- package/src/rule-packs/react-component-architecture/rules/component-folder-structure.ts +123 -0
- package/src/rule-packs/react-component-architecture/rules/forwardref-display-name.ts +93 -0
- package/src/rule-packs/react-component-architecture/rules/index-must-reexport-default.ts +123 -0
- package/src/rule-packs/react-component-architecture/rules/max-hooks-per-file.ts +122 -0
- package/src/rule-packs/react-component-architecture/rules/no-cross-feature-imports.ts +170 -0
- package/src/rule-packs/react-component-architecture/rules/no-inline-jsx-functions.ts +66 -0
- package/src/rule-packs/react-component-architecture/utils.ts +47 -0
- package/src/rule-packs/rule-packs.types.ts +18 -0
- package/src/rule-packs/structured-logging/index.ts +26 -0
- package/src/rule-packs/structured-logging/rules/mask-pii-fields.ts +221 -0
- package/src/rule-packs/structured-logging/rules/no-error-stringify.ts +217 -0
- package/src/rule-packs/structured-logging/rules/require-event-field.ts +136 -0
- package/src/rule-packs/structured-logging/utils/logger.ts +104 -0
- package/src/rule-packs/tanstack-query/index.ts +20 -0
- package/src/rule-packs/tanstack-query/rules/prefix-query-key-must-use-set-queries-data.ts +321 -0
- package/src/rule-packs/test-conventions/index.ts +23 -0
- package/src/rule-packs/test-conventions/rules/index.ts +2 -0
- package/src/rule-packs/test-conventions/rules/no-focused-tests.ts +170 -0
- package/src/rule-packs/test-conventions/rules/test-file-mirrors-source.ts +127 -0
- package/src/rule-packs/utils.ts +142 -0
- package/src/session-store.ts +359 -0
- package/src/spec/generate-tests.ts +213 -0
- package/src/spec/index.ts +5 -0
- package/src/spec/parse.ts +152 -0
- package/src/spec/review-tests.ts +162 -0
- package/src/spec/spec.constants.ts +13 -0
- package/src/spec/spec.types.ts +79 -0
- package/src/stack-detection/detect.ts +246 -0
- package/src/stack-detection/index.ts +3 -0
- package/src/stack-detection/packs.ts +174 -0
- package/src/stack-detection/stack-detection.types.ts +47 -0
- package/src/validate/accept.ts +49 -0
- package/src/validate/errors.ts +35 -0
- package/src/validate/index.ts +12 -0
- package/src/validate/parse.ts +148 -0
- package/src/validate/run-tests.ts +59 -0
- package/src/validate/validate.ts +40 -0
- package/src/validate/validate.types.ts +52 -0
- package/src/web-components.ts +638 -0
- package/src/web-coverage.ts +89 -0
- package/src/web-routes.ts +151 -0
- package/src/web-templates.ts +1011 -0
- package/strict.eslint.config.mjs +84 -0
- package/strict.web.eslint.config.mjs +185 -0
package/src/loop/run.ts
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import type { ITask } from "../spec";
|
|
2
|
+
import type { IChatMessage, IModelResponse, IProvider } from "../inference";
|
|
3
|
+
import { validate, type ErrorParser } from "../validate";
|
|
4
|
+
import { parseEslintJson } from "../validate";
|
|
5
|
+
import { readFiles } from "../lib/fs";
|
|
6
|
+
import { RUN_STATUS, STUCK_REASON, LOOP_LIMITS } from "./loop.constants";
|
|
7
|
+
import type { IRunResult, IRunOptions, Reporter } from "./loop.types";
|
|
8
|
+
import { flags } from "../config";
|
|
9
|
+
import { SYSTEM, seedPrompt } from "./prompt";
|
|
10
|
+
import { detectStack } from "../stack-detection";
|
|
11
|
+
import { TtsrManager } from "./ttsr";
|
|
12
|
+
import { DEFAULT_TTSR_RULES } from "./ttsr-defaults";
|
|
13
|
+
import {
|
|
14
|
+
type ILoopCtx,
|
|
15
|
+
type ILoopState,
|
|
16
|
+
toolsFor,
|
|
17
|
+
buildTsService,
|
|
18
|
+
runToolCalls,
|
|
19
|
+
settleGate,
|
|
20
|
+
emitTiming,
|
|
21
|
+
NO_TOOL_CALL_NUDGE,
|
|
22
|
+
} from "./turn";
|
|
23
|
+
|
|
24
|
+
/** Report any salvaged malformed tool calls, then stop the task if the stream
|
|
25
|
+
* degenerated into a repetition loop (returns a terminal stuck result; null to
|
|
26
|
+
* keep going) — mirrors the interactive Session's degeneration handling. */
|
|
27
|
+
function handleDegeneration(
|
|
28
|
+
res: IModelResponse,
|
|
29
|
+
ctx: ILoopCtx,
|
|
30
|
+
state: ILoopState,
|
|
31
|
+
at: { turn: number; turnStart: number; taskStart: number }
|
|
32
|
+
): IRunResult | null {
|
|
33
|
+
const { report } = ctx;
|
|
34
|
+
const { id } = ctx.task;
|
|
35
|
+
|
|
36
|
+
if (res.salvaged !== undefined && res.salvaged > 0) {
|
|
37
|
+
report({
|
|
38
|
+
kind: "tool",
|
|
39
|
+
task: id,
|
|
40
|
+
message: `recovered ${res.salvaged} malformed tool call(s) (server tool-call parser mismatch)`,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (res.degenerated !== true) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
report({
|
|
49
|
+
kind: "stuck",
|
|
50
|
+
task: id,
|
|
51
|
+
cycles: at.turn,
|
|
52
|
+
message:
|
|
53
|
+
"model fell into a repetition loop - stopped. Try a smaller task or steer it with a narrower instruction.",
|
|
54
|
+
});
|
|
55
|
+
emitTiming(report, id, at.turn, at.turnStart, at.taskStart);
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
task: id,
|
|
59
|
+
redConfirmed: true,
|
|
60
|
+
status: RUN_STATUS.stuck,
|
|
61
|
+
cycles: at.turn,
|
|
62
|
+
reason: STUCK_REASON.stalled,
|
|
63
|
+
edits: state.edits,
|
|
64
|
+
regressions: state.regressions,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Build and configure a TTSR manager if enabled. Returns null if disabled. */
|
|
69
|
+
function initTtsrManager(): TtsrManager | null {
|
|
70
|
+
if (!flags.ttsr()) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const manager = new TtsrManager();
|
|
75
|
+
|
|
76
|
+
for (const rule of DEFAULT_TTSR_RULES) {
|
|
77
|
+
manager.addRule(rule);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return manager;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Handle a TTSR interrupt: report, inject corrective message, and optionally disable. */
|
|
84
|
+
function handleTtsrInterrupt(
|
|
85
|
+
ttsrFired: { ruleName: string; guidance: string },
|
|
86
|
+
state: ILoopState,
|
|
87
|
+
messages: IChatMessage[],
|
|
88
|
+
report: Reporter,
|
|
89
|
+
taskId: string,
|
|
90
|
+
turn: number,
|
|
91
|
+
turnStart: number,
|
|
92
|
+
taskStart: number,
|
|
93
|
+
ttsrManager: TtsrManager | null
|
|
94
|
+
): void {
|
|
95
|
+
state.ttsrInterrupts += 1;
|
|
96
|
+
|
|
97
|
+
report({
|
|
98
|
+
kind: "ttsr",
|
|
99
|
+
task: taskId,
|
|
100
|
+
message: `⚠ TTSR interrupted: ${ttsrFired.ruleName}`,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Hard cap: after 3 interrupts, disable TTSR to prevent loops
|
|
104
|
+
if (state.ttsrInterrupts >= 3) {
|
|
105
|
+
report({
|
|
106
|
+
kind: "tool",
|
|
107
|
+
task: taskId,
|
|
108
|
+
message: `TTSR disabled after ${state.ttsrInterrupts} interrupts (hit cap)`,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
ttsrManager?.disable();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Append corrective message and retry without counting as a normal cycle
|
|
115
|
+
messages.push({
|
|
116
|
+
role: "user",
|
|
117
|
+
content: `⚠ generation interrupted: ${ttsrFired.guidance} Rewrite the affected part without that pattern.`,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
emitTiming(report, taskId, turn, turnStart, taskStart);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Assemble per-call completion options, leaving optional knobs unset when absent. */
|
|
124
|
+
function completionOptionsFor(args: {
|
|
125
|
+
tools: unknown[];
|
|
126
|
+
temperature: number;
|
|
127
|
+
enableThinking: boolean | undefined;
|
|
128
|
+
thinkingTokenBudget: number | undefined;
|
|
129
|
+
ttsrManager: TtsrManager | null;
|
|
130
|
+
report: Reporter;
|
|
131
|
+
taskId: string;
|
|
132
|
+
}): Parameters<IProvider["complete"]>[1] {
|
|
133
|
+
return {
|
|
134
|
+
tools: args.tools,
|
|
135
|
+
temperature: args.temperature,
|
|
136
|
+
toolChoice: "auto",
|
|
137
|
+
...(args.enableThinking === undefined
|
|
138
|
+
? {}
|
|
139
|
+
: { enableThinking: args.enableThinking }),
|
|
140
|
+
...(args.thinkingTokenBudget === undefined
|
|
141
|
+
? {}
|
|
142
|
+
: { thinkingTokenBudget: args.thinkingTokenBudget }),
|
|
143
|
+
...(args.ttsrManager === null ? {} : { ttsrManager: args.ttsrManager }),
|
|
144
|
+
onToken: (text) => {
|
|
145
|
+
args.report({ kind: "token", task: args.taskId, message: text });
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** A/B control for the gate-feedback-fidelity win: TSFORGE_LEGACY_FEEDBACK=1
|
|
151
|
+
* forces the OLD mis-selected parser (eslint-json on chained tsc&&eslint). */
|
|
152
|
+
function effectiveParserFor(
|
|
153
|
+
parse: ErrorParser | undefined
|
|
154
|
+
): ErrorParser | undefined {
|
|
155
|
+
return flags.legacyFeedback() ? parseEslintJson : parse;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Detect the stack and fold in tsforge.config.json pack/rule overrides. */
|
|
159
|
+
async function resolveStackForRun(cwd: string): Promise<{
|
|
160
|
+
stackProfile: Awaited<ReturnType<typeof detectStack>>;
|
|
161
|
+
ruleOverrides: Readonly<Record<string, "error" | "warn" | "off">>;
|
|
162
|
+
}> {
|
|
163
|
+
const detectedProfile = await detectStack(cwd);
|
|
164
|
+
const { loadTsforgeConfig, resolveActivePacks, normalizeRuleOverrides } =
|
|
165
|
+
await import("../config/tsforge-config");
|
|
166
|
+
const cfg = await loadTsforgeConfig(cwd);
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
stackProfile: {
|
|
170
|
+
...detectedProfile,
|
|
171
|
+
packs: resolveActivePacks(detectedProfile.packs, cfg),
|
|
172
|
+
},
|
|
173
|
+
ruleOverrides: normalizeRuleOverrides(cfg),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The implement loop as a persistent, tool-using conversation. The model drives
|
|
179
|
+
* — it can `read`, `run` (tests/tsc/eslint), `edit`, `create` — and the whole
|
|
180
|
+
* conversation is retained as memory. When it stops calling tools (believes it's
|
|
181
|
+
* done), the harness runs the deterministic gate, which is the ONLY authority on
|
|
182
|
+
* "done": green ⇒ finished; red ⇒ the errors go back into the conversation and it
|
|
183
|
+
* continues. It can't fake completion.
|
|
184
|
+
*
|
|
185
|
+
* This is the RED-first, drive-to-green wrapper the EVAL harness uses; the
|
|
186
|
+
* interactive CLI composes the same `turn.ts` primitives via `Session`.
|
|
187
|
+
*/
|
|
188
|
+
export async function runTask(
|
|
189
|
+
task: ITask,
|
|
190
|
+
cwd: string,
|
|
191
|
+
provider: IProvider,
|
|
192
|
+
opts: IRunOptions = {}
|
|
193
|
+
): Promise<IRunResult> {
|
|
194
|
+
const { parse, enableThinking, thinkingTokenBudget } = opts;
|
|
195
|
+
const effectiveParse = effectiveParserFor(parse);
|
|
196
|
+
const temperature = opts.temperature ?? 0;
|
|
197
|
+
const maxTurns = opts.maxTurns ?? LOOP_LIMITS.maxTurns;
|
|
198
|
+
const report: Reporter = opts.onEvent ?? (() => undefined);
|
|
199
|
+
|
|
200
|
+
report({
|
|
201
|
+
kind: "start",
|
|
202
|
+
task: task.id,
|
|
203
|
+
message: `task ${task.id}: checking current state`,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// RED: the goalpost must fail before we build.
|
|
207
|
+
const red = await validate(task, cwd, effectiveParse);
|
|
208
|
+
|
|
209
|
+
if (red.passed) {
|
|
210
|
+
report({
|
|
211
|
+
kind: "done",
|
|
212
|
+
task: task.id,
|
|
213
|
+
cycles: 0,
|
|
214
|
+
message: `task ${task.id}: already green`,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
task: task.id,
|
|
219
|
+
redConfirmed: false,
|
|
220
|
+
status: RUN_STATUS.redNotConfirmed,
|
|
221
|
+
cycles: 0,
|
|
222
|
+
edits: 0,
|
|
223
|
+
regressions: 0,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
report({
|
|
228
|
+
kind: "red",
|
|
229
|
+
task: task.id,
|
|
230
|
+
errors: red.errors.length,
|
|
231
|
+
message: `task ${task.id}: RED (${red.errors.length} error(s))`,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Detect stack once per run, early; tsforge.config.json may adjust it
|
|
235
|
+
const { stackProfile, ruleOverrides } = await resolveStackForRun(cwd);
|
|
236
|
+
|
|
237
|
+
report({
|
|
238
|
+
kind: "tool",
|
|
239
|
+
task: task.id,
|
|
240
|
+
message: `detected stack: ${stackProfile.name} (${stackProfile.reason})`,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const editable = await readFiles(cwd, task.files);
|
|
244
|
+
const context = await readFiles(cwd, task.context ?? []);
|
|
245
|
+
const messages: IChatMessage[] = [
|
|
246
|
+
{ role: "system", content: SYSTEM },
|
|
247
|
+
{
|
|
248
|
+
role: "user",
|
|
249
|
+
content: seedPrompt(task, editable, context, stackProfile),
|
|
250
|
+
},
|
|
251
|
+
];
|
|
252
|
+
|
|
253
|
+
// Existing code to navigate? (editable files already have content). Only then
|
|
254
|
+
// do the LSP nav tools earn their decision-surface cost — see toolsFor().
|
|
255
|
+
const hasExistingCode = editable.some((f) => f.content.trim().length > 0);
|
|
256
|
+
const tools = toolsFor(hasExistingCode);
|
|
257
|
+
|
|
258
|
+
// Mode-aware reasoning cap: scratch tasks over-think unbounded, so default
|
|
259
|
+
// them to the measured knee; existing-code runs stay uncapped (the cap hurts
|
|
260
|
+
// navigation). An explicit opts.thinkingTokenBudget always wins.
|
|
261
|
+
const effectiveThinkingBudget =
|
|
262
|
+
thinkingTokenBudget ??
|
|
263
|
+
(hasExistingCode ? undefined : LOOP_LIMITS.scratchThinkingBudget);
|
|
264
|
+
|
|
265
|
+
const ttsrManager = initTtsrManager();
|
|
266
|
+
|
|
267
|
+
const ctx: ILoopCtx = {
|
|
268
|
+
task,
|
|
269
|
+
cwd,
|
|
270
|
+
tsService: await buildTsService(cwd),
|
|
271
|
+
parse: effectiveParse,
|
|
272
|
+
report,
|
|
273
|
+
messages,
|
|
274
|
+
stackProfile,
|
|
275
|
+
ruleOverrides:
|
|
276
|
+
Object.keys(ruleOverrides).length > 0 ? ruleOverrides : undefined,
|
|
277
|
+
};
|
|
278
|
+
const state: ILoopState = {
|
|
279
|
+
prevGateErrors: red.errors,
|
|
280
|
+
gateNoProgress: 0,
|
|
281
|
+
lastGateCount: -1,
|
|
282
|
+
edits: 0,
|
|
283
|
+
regressions: 0,
|
|
284
|
+
ttsrInterrupts: 0,
|
|
285
|
+
};
|
|
286
|
+
const taskStart = performance.now();
|
|
287
|
+
|
|
288
|
+
for (let turn = 1; turn <= maxTurns; turn += 1) {
|
|
289
|
+
const turnStart = performance.now();
|
|
290
|
+
|
|
291
|
+
report({
|
|
292
|
+
kind: "cycle",
|
|
293
|
+
task: task.id,
|
|
294
|
+
cycle: turn,
|
|
295
|
+
message: `task ${task.id} · turn ${turn}: asking model`,
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
ttsrManager?.resetBuffer();
|
|
299
|
+
|
|
300
|
+
const res = await provider.complete(
|
|
301
|
+
messages,
|
|
302
|
+
completionOptionsFor({
|
|
303
|
+
tools,
|
|
304
|
+
temperature,
|
|
305
|
+
enableThinking,
|
|
306
|
+
thinkingTokenBudget: effectiveThinkingBudget,
|
|
307
|
+
ttsrManager,
|
|
308
|
+
report,
|
|
309
|
+
taskId: task.id,
|
|
310
|
+
})
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
messages.push({
|
|
314
|
+
role: "assistant",
|
|
315
|
+
content: res.content,
|
|
316
|
+
toolCalls: res.toolCalls,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Every model call advances cooldown accounting — including interrupted
|
|
320
|
+
// ones, otherwise repeatGap rules mis-count after a TTSR retry.
|
|
321
|
+
ttsrManager?.incrementTurnCount();
|
|
322
|
+
|
|
323
|
+
// TTSR interrupts are checked BEFORE degeneration so corrective guidance
|
|
324
|
+
// lands at the earliest point. If the TTSR retry itself degenerates, the
|
|
325
|
+
// next iteration's degeneration check catches it.
|
|
326
|
+
if (res.ttsrFired !== undefined) {
|
|
327
|
+
handleTtsrInterrupt(
|
|
328
|
+
res.ttsrFired,
|
|
329
|
+
state,
|
|
330
|
+
messages,
|
|
331
|
+
report,
|
|
332
|
+
task.id,
|
|
333
|
+
turn,
|
|
334
|
+
turnStart,
|
|
335
|
+
taskStart,
|
|
336
|
+
ttsrManager
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
// Continue to next turn without settling the gate
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const looped = handleDegeneration(res, ctx, state, {
|
|
344
|
+
turn,
|
|
345
|
+
turnStart,
|
|
346
|
+
taskStart,
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
if (looped !== null) {
|
|
350
|
+
return looped;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const touchedEditable =
|
|
354
|
+
res.toolCalls.length === 0
|
|
355
|
+
? false
|
|
356
|
+
: await runToolCalls(res.toolCalls, ctx, state);
|
|
357
|
+
|
|
358
|
+
// Settle the gate whenever the model stopped OR changed an editable file.
|
|
359
|
+
// (A read-only turn neither finishes nor mutates — just loop again.)
|
|
360
|
+
if (res.toolCalls.length === 0 || touchedEditable) {
|
|
361
|
+
const settled = await settleGate(ctx, state, turn);
|
|
362
|
+
|
|
363
|
+
emitTiming(report, task.id, turn, turnStart, taskStart);
|
|
364
|
+
|
|
365
|
+
if (settled !== null) {
|
|
366
|
+
return {
|
|
367
|
+
...settled,
|
|
368
|
+
edits: state.edits,
|
|
369
|
+
regressions: state.regressions,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Stopped with no tool call while still red → nudge it to act, not narrate.
|
|
374
|
+
if (res.toolCalls.length === 0) {
|
|
375
|
+
messages.push({ role: "user", content: NO_TOOL_CALL_NUDGE });
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
emitTiming(report, task.id, turn, turnStart, taskStart);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
report({
|
|
385
|
+
kind: "stuck",
|
|
386
|
+
task: task.id,
|
|
387
|
+
cycles: maxTurns,
|
|
388
|
+
message: `task ${task.id}: stuck (hit ${maxTurns}-turn cap)`,
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
task: task.id,
|
|
393
|
+
redConfirmed: true,
|
|
394
|
+
status: RUN_STATUS.stuck,
|
|
395
|
+
cycles: maxTurns,
|
|
396
|
+
reason: STUCK_REASON.cap,
|
|
397
|
+
edits: state.edits,
|
|
398
|
+
regressions: state.regressions,
|
|
399
|
+
};
|
|
400
|
+
}
|