@agjs/tsforge 0.2.6 → 0.2.8
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/package.json +1 -1
- package/src/cli.ts +41 -10
- package/src/loop/loop.constants.ts +18 -4
- package/src/loop/loop.types.ts +5 -0
- package/src/loop/memory/consolidate.ts +254 -0
- package/src/loop/memory/index.ts +18 -0
- package/src/loop/memory/memory.types.ts +65 -0
- package/src/loop/memory/mine.ts +76 -0
- package/src/loop/rule-docs.generated.json +136 -1
- package/src/loop/run.ts +76 -82
- package/src/loop/session.ts +156 -14
- package/src/loop/ttsr-init.ts +111 -0
- package/src/loop/turn.ts +76 -1
|
@@ -109,6 +109,21 @@
|
|
|
109
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
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
111
|
},
|
|
112
|
+
"tsforge/id-param-requires-object-authz": {
|
|
113
|
+
"what": "Warn when a handler reads `params.id` and queries the database without an authorization check in the same function.",
|
|
114
|
+
"bad": "// Example that violates the rule",
|
|
115
|
+
"good": "// Corrected version"
|
|
116
|
+
},
|
|
117
|
+
"tsforge/mutating-route-requires-authz": {
|
|
118
|
+
"what": "POST/PUT/PATCH/DELETE route handlers must call an authorization helper before mutating state.",
|
|
119
|
+
"bad": "// Example that violates the rule",
|
|
120
|
+
"good": "// Corrected version"
|
|
121
|
+
},
|
|
122
|
+
"tsforge/server-action-requires-authz": {
|
|
123
|
+
"what": "Files with `\"use server\"` that perform database mutations must call an authorization helper in the same function.",
|
|
124
|
+
"bad": "// Example that violates the rule",
|
|
125
|
+
"good": "// Corrected version"
|
|
126
|
+
},
|
|
112
127
|
"tsforge/job-name-must-be-constant": {
|
|
113
128
|
"what": "Disallow string-literal job names in `<queue>.add(name, ...)` calls — use a constant identifier so all consumers share one source of truth.",
|
|
114
129
|
"bad": "// Example that violates the rule",
|
|
@@ -219,6 +234,16 @@
|
|
|
219
234
|
"bad": "// Example that violates the rule",
|
|
220
235
|
"good": "// Corrected version"
|
|
221
236
|
},
|
|
237
|
+
"tsforge/update-delete-account-scoped-must-filter-scope": {
|
|
238
|
+
"what": "Require Drizzle `.update()` / `.delete()` against account-scoped tables to filter by a scope column in `.where()`.",
|
|
239
|
+
"bad": "// Example that violates the rule",
|
|
240
|
+
"good": "// Corrected version"
|
|
241
|
+
},
|
|
242
|
+
"tsforge/update-delete-must-have-where": {
|
|
243
|
+
"what": "Require every Drizzle `.update()` and `.delete()` call to include a `.where()` clause — unscoped writes affect every row.",
|
|
244
|
+
"bad": "// Example that violates the rule",
|
|
245
|
+
"good": "// Corrected version"
|
|
246
|
+
},
|
|
222
247
|
"tsforge/consistent-status-via-set": {
|
|
223
248
|
"what": "Inside Elysia route handlers, set HTTP status via `set.status = N`, not by returning a `new Response(body, { status: N })`.",
|
|
224
249
|
"bad": "// Example that violates the rule",
|
|
@@ -319,11 +344,26 @@
|
|
|
319
344
|
"bad": "// Example that violates the rule",
|
|
320
345
|
"good": "// Corrected version"
|
|
321
346
|
},
|
|
347
|
+
"tsforge/auth-cookie-must-set-maxage-or-expires": {
|
|
348
|
+
"what": "Auth-cookie writes should set `maxAge` or `expires` so session cookies do not live forever by default.",
|
|
349
|
+
"bad": "// Example that violates the rule",
|
|
350
|
+
"good": "// Corrected version"
|
|
351
|
+
},
|
|
352
|
+
"tsforge/auth-cookie-must-set-samesite": {
|
|
353
|
+
"what": "Auth-cookie writes must set `sameSite` (`strict` or `lax`) — missing SameSite allows cross-site cookie delivery.",
|
|
354
|
+
"bad": "// Example that violates the rule",
|
|
355
|
+
"good": "// Corrected version"
|
|
356
|
+
},
|
|
322
357
|
"tsforge/bcrypt-rounds-min": {
|
|
323
358
|
"what": "Disallow `bcrypt.hash` / `bcrypt.hashSync` calls with a numeric-literal rounds value below the configured minimum (default 10).",
|
|
324
359
|
"bad": "// Example that violates the rule",
|
|
325
360
|
"good": "// Corrected version"
|
|
326
361
|
},
|
|
362
|
+
"tsforge/jwt-must-verify-not-decode": {
|
|
363
|
+
"what": "Disallow `jwt.decode` / `decodeJwt` — decoding without verification accepts forged tokens. Use `jwt.verify` or `jwtVerify` instead.",
|
|
364
|
+
"bad": "// Example that violates the rule",
|
|
365
|
+
"good": "// Corrected version"
|
|
366
|
+
},
|
|
327
367
|
"tsforge/no-import-build-output": {
|
|
328
368
|
"what": "Disallow importing from build/output directories within the project. Source must import source, not compiled artifacts, to avoid stale-code drift and broken module boundaries.",
|
|
329
369
|
"bad": "// Example that violates the rule",
|
|
@@ -354,6 +394,11 @@
|
|
|
354
394
|
"bad": "// Example that violates the rule",
|
|
355
395
|
"good": "// Corrected version"
|
|
356
396
|
},
|
|
397
|
+
"tsforge/mutation-should-revalidate-cache": {
|
|
398
|
+
"what": "After database mutations in server actions or route handlers, call `revalidatePath` or `revalidateTag` so cached pages reflect the change.",
|
|
399
|
+
"bad": "// Example that violates the rule",
|
|
400
|
+
"good": "// Corrected version"
|
|
401
|
+
},
|
|
357
402
|
"tsforge/no-html-img-element": {
|
|
358
403
|
"what": "Prefer next/image over raw <img> elements for optimized responsive images and Core Web Vitals.",
|
|
359
404
|
"bad": "// Example that violates the rule",
|
|
@@ -374,6 +419,11 @@
|
|
|
374
419
|
"bad": "// Example that violates the rule",
|
|
375
420
|
"good": "// Corrected version"
|
|
376
421
|
},
|
|
422
|
+
"tsforge/no-secret-props-to-client": {
|
|
423
|
+
"what": "Warn when Server Components pass secret-looking props to JSX — values may cross the client boundary.",
|
|
424
|
+
"bad": "// Example that violates the rule",
|
|
425
|
+
"good": "// Corrected version"
|
|
426
|
+
},
|
|
377
427
|
"tsforge/no-sensitive-next-public-env": {
|
|
378
428
|
"what": "Disallow NEXT_PUBLIC_* env vars whose names suggest secrets — public build-time vars are visible in the client bundle.",
|
|
379
429
|
"bad": "// Example that violates the rule",
|
|
@@ -384,6 +434,16 @@
|
|
|
384
434
|
"bad": "// Example that violates the rule",
|
|
385
435
|
"good": "// Corrected version"
|
|
386
436
|
},
|
|
437
|
+
"tsforge/server-action-requires-authz-and-validation": {
|
|
438
|
+
"what": "Server actions (`\"use server\"`) that mutate the database must call authorization helpers and validate input with `.parse()` / `.safeParse()`.",
|
|
439
|
+
"bad": "// Example that violates the rule",
|
|
440
|
+
"good": "// Corrected version"
|
|
441
|
+
},
|
|
442
|
+
"tsforge/server-only-modules-import-server-only": {
|
|
443
|
+
"what": "App-router server modules must import `\"server-only\"` so accidental client bundling fails at build time.",
|
|
444
|
+
"bad": "// Example that violates the rule",
|
|
445
|
+
"good": "// Corrected version"
|
|
446
|
+
},
|
|
387
447
|
"tsforge/pkce-required-for-oidc": {
|
|
388
448
|
"what": "OIDC providers must use PKCE: `buildAuthorizationURL` must call `generateCodeVerifier()` and pass it to `createAuthorizationURL`.",
|
|
389
449
|
"bad": "// Example that violates the rule",
|
|
@@ -399,8 +459,13 @@
|
|
|
399
459
|
"bad": "// Example that violates the rule",
|
|
400
460
|
"good": "// Corrected version"
|
|
401
461
|
},
|
|
462
|
+
"tsforge/component-file-purity": {
|
|
463
|
+
"what": "A component .tsx contains only imports and the component itself — types go to <feature>.types.ts, constants to <feature>.constants.ts, helpers to src/lib",
|
|
464
|
+
"bad": "// Example that violates the rule",
|
|
465
|
+
"good": "// Corrected version"
|
|
466
|
+
},
|
|
402
467
|
"tsforge/component-folder-structure": {
|
|
403
|
-
"what": "
|
|
468
|
+
"what": "A component .tsx must live in src/views/<Feature>/components/ (feature component), src/components/ui/ (shared primitive), or be the view root src/views/<Feature>/index.tsx",
|
|
404
469
|
"bad": "// Example that violates the rule",
|
|
405
470
|
"good": "// Corrected version"
|
|
406
471
|
},
|
|
@@ -469,6 +534,31 @@
|
|
|
469
534
|
"bad": "// Example that violates the rule",
|
|
470
535
|
"good": "// Corrected version"
|
|
471
536
|
},
|
|
537
|
+
"tsforge/no-prototype-polluting-merge": {
|
|
538
|
+
"what": "Disallow merging request body/query/params into objects — enables prototype pollution.",
|
|
539
|
+
"bad": "// Example that violates the rule",
|
|
540
|
+
"good": "// Corrected version"
|
|
541
|
+
},
|
|
542
|
+
"tsforge/no-user-controlled-fetch-url": {
|
|
543
|
+
"what": "Disallow fetch/axios requests to non-literal URLs — dynamic URLs enable SSRF.",
|
|
544
|
+
"bad": "// Example that violates the rule",
|
|
545
|
+
"good": "// Corrected version"
|
|
546
|
+
},
|
|
547
|
+
"tsforge/no-user-controlled-redirect": {
|
|
548
|
+
"what": "Disallow redirects to non-literal URLs — user-controlled redirects enable open redirects.",
|
|
549
|
+
"bad": "// Example that violates the rule",
|
|
550
|
+
"good": "// Corrected version"
|
|
551
|
+
},
|
|
552
|
+
"tsforge/upload-must-set-limits": {
|
|
553
|
+
"what": "Multipart upload handlers should declare `limits` or `maxFileSize` to bound request size.",
|
|
554
|
+
"bad": "// Example that violates the rule",
|
|
555
|
+
"good": "// Corrected version"
|
|
556
|
+
},
|
|
557
|
+
"tsforge/webhook-must-verify-signature-before-parse": {
|
|
558
|
+
"what": "Webhook handlers must verify signatures before calling `.json()` on the request body.",
|
|
559
|
+
"bad": "// Example that violates the rule",
|
|
560
|
+
"good": "// Corrected version"
|
|
561
|
+
},
|
|
472
562
|
"tsforge/catch-must-handle": {
|
|
473
563
|
"what": "Catch blocks must log, rethrow, or propagate errors — not silently return empty defaults on failure.",
|
|
474
564
|
"bad": "// Example that violates the rule",
|
|
@@ -499,6 +589,16 @@
|
|
|
499
589
|
"bad": "// Example that violates the rule",
|
|
500
590
|
"good": "// Corrected version"
|
|
501
591
|
},
|
|
592
|
+
"tsforge/caught-error-log-requires-cause": {
|
|
593
|
+
"what": "When logging a caught error, include a `cause` field in the structured payload so downstream tools preserve the error chain.",
|
|
594
|
+
"bad": "// Example that violates the rule",
|
|
595
|
+
"good": "// Corrected version"
|
|
596
|
+
},
|
|
597
|
+
"tsforge/logger-not-console": {
|
|
598
|
+
"what": "Service modules should use the structured logger instead of `console.*` — console output is unstructured and hard to search.",
|
|
599
|
+
"bad": "// Example that violates the rule",
|
|
600
|
+
"good": "// Corrected version"
|
|
601
|
+
},
|
|
502
602
|
"tsforge/mask-pii-fields": {
|
|
503
603
|
"what": "Disallow unmasked PII (email, phone, password, token, ...) in structured-logger payloads — the #1 way data leaks quietly.",
|
|
504
604
|
"bad": "// Example that violates the rule",
|
|
@@ -519,14 +619,49 @@
|
|
|
519
619
|
"bad": "// Example that violates the rule",
|
|
520
620
|
"good": "// Corrected version"
|
|
521
621
|
},
|
|
622
|
+
"tsforge/fake-timers-must-be-restored": {
|
|
623
|
+
"what": "When a test file calls `useFakeTimers()`, it must also call `useRealTimers()` so later tests are not affected.",
|
|
624
|
+
"bad": "// Example that violates the rule",
|
|
625
|
+
"good": "// Corrected version"
|
|
626
|
+
},
|
|
627
|
+
"tsforge/no-conditional-expect": {
|
|
628
|
+
"what": "Disallow `expect()` inside conditionals — tests must fail when assertions are skipped.",
|
|
629
|
+
"bad": "// Example that violates the rule",
|
|
630
|
+
"good": "// Corrected version"
|
|
631
|
+
},
|
|
522
632
|
"tsforge/no-focused-tests": {
|
|
523
633
|
"what": "Disallow focused tests (`test.only`, `it.only`, `fdescribe`, ...) — the canonical 'I forgot to remove this before committing' leak.",
|
|
524
634
|
"bad": "// Example that violates the rule",
|
|
525
635
|
"good": "// Corrected version"
|
|
526
636
|
},
|
|
637
|
+
"tsforge/no-real-network-in-unit-tests": {
|
|
638
|
+
"what": "Unit tests should not perform real network I/O — mock HTTP clients or move the test to an integration suite.",
|
|
639
|
+
"bad": "// Example that violates the rule",
|
|
640
|
+
"good": "// Corrected version"
|
|
641
|
+
},
|
|
527
642
|
"tsforge/test-file-mirrors-source": {
|
|
528
643
|
"what": "Every test file under `tests/` must mirror a source file under `src/`. Catches orphaned tests left behind after refactors and renames.",
|
|
529
644
|
"bad": "// Example that violates the rule",
|
|
530
645
|
"good": "// Corrected version"
|
|
646
|
+
},
|
|
647
|
+
"tsforge/exported-functions-require-return-type": {
|
|
648
|
+
"what": "Exported functions should declare an explicit return type at module boundaries.",
|
|
649
|
+
"bad": "// Example that violates the rule",
|
|
650
|
+
"good": "// Corrected version"
|
|
651
|
+
},
|
|
652
|
+
"tsforge/fetch-must-check-ok": {
|
|
653
|
+
"what": "HTTP fetch responses must check `.ok` or status before calling `.json()`.",
|
|
654
|
+
"bad": "// Example that violates the rule",
|
|
655
|
+
"good": "// Corrected version"
|
|
656
|
+
},
|
|
657
|
+
"tsforge/json-parse-must-validate": {
|
|
658
|
+
"what": "Disallow bare JSON.parse on untrusted input — validate through a schema library.",
|
|
659
|
+
"bad": "// Example that violates the rule",
|
|
660
|
+
"good": "// Corrected version"
|
|
661
|
+
},
|
|
662
|
+
"tsforge/no-unsafe-boundary-cast": {
|
|
663
|
+
"what": "Disallow type assertions immediately after parsing untrusted boundary input.",
|
|
664
|
+
"bad": "// Example that violates the rule",
|
|
665
|
+
"good": "// Corrected version"
|
|
531
666
|
}
|
|
532
667
|
}
|
package/src/loop/run.ts
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
1
|
import type { ITask } from "../spec";
|
|
3
2
|
import type { IChatMessage, IModelResponse, IProvider } from "../inference";
|
|
4
3
|
import { validate, type ErrorParser } from "../validate";
|
|
5
4
|
import { parseEslintJson } from "../validate";
|
|
6
5
|
import { readFiles } from "../lib/fs";
|
|
7
6
|
import { RUN_STATUS, STUCK_REASON, LOOP_LIMITS } from "./loop.constants";
|
|
8
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
IRunResult,
|
|
9
|
+
IRunOptions,
|
|
10
|
+
Reporter,
|
|
11
|
+
ILoopEvent,
|
|
12
|
+
} from "./loop.types";
|
|
13
|
+
import { mineLessons, consolidate as consolidateMemory } from "./memory";
|
|
9
14
|
import { flags } from "../config";
|
|
10
15
|
import { SYSTEM, seedPrompt } from "./prompt";
|
|
11
16
|
import { detectStack } from "../stack-detection";
|
|
12
|
-
import { TtsrManager
|
|
13
|
-
import {
|
|
17
|
+
import type { TtsrManager } from "./ttsr";
|
|
18
|
+
import {
|
|
19
|
+
initTtsrManager,
|
|
20
|
+
loadProjectTtsrRules,
|
|
21
|
+
applyTtsrInterrupt,
|
|
22
|
+
} from "./ttsr-init";
|
|
14
23
|
import {
|
|
15
24
|
type ILoopCtx,
|
|
16
25
|
type ILoopState,
|
|
@@ -66,57 +75,14 @@ function handleDegeneration(
|
|
|
66
75
|
};
|
|
67
76
|
}
|
|
68
77
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (!(await file.exists())) {
|
|
75
|
-
return [];
|
|
76
|
-
}
|
|
78
|
+
// TTSR init + project/learned-rule loading live in the shared ttsr-init module
|
|
79
|
+
// (the interactive session uses the same loaders). Re-exported here so existing
|
|
80
|
+
// importers (and tests) keep their path.
|
|
81
|
+
export { initTtsrManager, loadProjectTtsrRules };
|
|
77
82
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
/** Build and configure a TTSR manager if enabled. Returns null if disabled.
|
|
82
|
-
* Built-in defaults register first, then optional project rules from
|
|
83
|
-
* `<cwd>/.tsforge/rules.json`; `addRule` ignores duplicate names, so a
|
|
84
|
-
* built-in safety rule always wins over a same-named project rule. */
|
|
85
|
-
export async function initTtsrManager(
|
|
86
|
-
cwd: string,
|
|
87
|
-
report: Reporter,
|
|
88
|
-
taskId: string
|
|
89
|
-
): Promise<TtsrManager | null> {
|
|
90
|
-
if (!flags.ttsr()) {
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const manager = new TtsrManager();
|
|
95
|
-
|
|
96
|
-
for (const rule of DEFAULT_TTSR_RULES) {
|
|
97
|
-
manager.addRule(rule);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
let added = 0;
|
|
101
|
-
|
|
102
|
-
for (const rule of await loadProjectTtsrRules(cwd)) {
|
|
103
|
-
if (manager.addRule(rule)) {
|
|
104
|
-
added += 1;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (added > 0) {
|
|
109
|
-
report({
|
|
110
|
-
kind: "ttsr",
|
|
111
|
-
task: taskId,
|
|
112
|
-
message: `loaded ${added} custom TTSR rule(s) from .tsforge/rules.json`,
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return manager;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** Handle a TTSR interrupt: report, inject corrective message, and optionally disable. */
|
|
83
|
+
/** Handle a TTSR interrupt in the headless loop: apply the shared interrupt
|
|
84
|
+
* (count, report, inject corrective guidance, disable at the cap) then emit
|
|
85
|
+
* timing for the interrupted turn. */
|
|
120
86
|
function handleTtsrInterrupt(
|
|
121
87
|
ttsrFired: { ruleName: string; guidance: string },
|
|
122
88
|
state: ILoopState,
|
|
@@ -128,32 +94,41 @@ function handleTtsrInterrupt(
|
|
|
128
94
|
taskStart: number,
|
|
129
95
|
ttsrManager: TtsrManager | null
|
|
130
96
|
): void {
|
|
131
|
-
state
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
kind: "ttsr",
|
|
135
|
-
task: taskId,
|
|
136
|
-
message: `⚠ TTSR interrupted: ${ttsrFired.ruleName}`,
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
// Hard cap: after 3 interrupts, disable TTSR to prevent loops
|
|
140
|
-
if (state.ttsrInterrupts >= 3) {
|
|
141
|
-
report({
|
|
142
|
-
kind: "tool",
|
|
143
|
-
task: taskId,
|
|
144
|
-
message: `TTSR disabled after ${state.ttsrInterrupts} interrupts (hit cap)`,
|
|
145
|
-
});
|
|
97
|
+
applyTtsrInterrupt(ttsrFired, state, messages, report, taskId, ttsrManager);
|
|
98
|
+
emitTiming(report, taskId, turn, turnStart, taskStart);
|
|
99
|
+
}
|
|
146
100
|
|
|
147
|
-
|
|
101
|
+
/**
|
|
102
|
+
* MEMORY post-run hook: mine this run's events for failure→fix lessons and
|
|
103
|
+
* consolidate them into `.tsforge/`. Gated on the TTSR flag (learned rules are
|
|
104
|
+
* recalled via TTSR, so there's nothing to learn for if it's off). Best-effort:
|
|
105
|
+
* a memory failure never affects the run's result. `runId` is unique per run so
|
|
106
|
+
* the same task re-run counts as a distinct session for the recurrence gate.
|
|
107
|
+
*/
|
|
108
|
+
async function consolidateLessons(
|
|
109
|
+
cwd: string,
|
|
110
|
+
events: readonly ILoopEvent[],
|
|
111
|
+
runId: string,
|
|
112
|
+
report: Reporter
|
|
113
|
+
): Promise<void> {
|
|
114
|
+
if (!flags.ttsr()) {
|
|
115
|
+
return;
|
|
148
116
|
}
|
|
149
117
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
content: `⚠ generation interrupted: ${ttsrFired.guidance} Rewrite the affected part without that pattern.`,
|
|
154
|
-
});
|
|
118
|
+
try {
|
|
119
|
+
const candidates = mineLessons(events);
|
|
120
|
+
const active = await consolidateMemory(cwd, candidates, runId);
|
|
155
121
|
|
|
156
|
-
|
|
122
|
+
if (active > 0) {
|
|
123
|
+
report({
|
|
124
|
+
kind: "ttsr",
|
|
125
|
+
task: runId,
|
|
126
|
+
message: `memory: ${String(active)} learned rule(s) active in .tsforge/learned-rules.json`,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
// Memory is supplementary — never let it break a run.
|
|
131
|
+
}
|
|
157
132
|
}
|
|
158
133
|
|
|
159
134
|
/** Assemble per-call completion options, leaving optional knobs unset when absent. */
|
|
@@ -242,7 +217,25 @@ export async function runTask(
|
|
|
242
217
|
const effectiveParse = effectiveParserFor(parse);
|
|
243
218
|
const temperature = opts.temperature ?? 0;
|
|
244
219
|
const maxTurns = opts.maxTurns ?? LOOP_LIMITS.maxTurns;
|
|
245
|
-
|
|
220
|
+
// Buffer every event so the post-run memory hook can mine the run for
|
|
221
|
+
// failure→fix lessons, while still forwarding live to the real reporter.
|
|
222
|
+
const base: Reporter = opts.onEvent ?? (() => undefined);
|
|
223
|
+
const events: ILoopEvent[] = [];
|
|
224
|
+
|
|
225
|
+
const report: Reporter = (event) => {
|
|
226
|
+
events.push(event);
|
|
227
|
+
base(event);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// Unique per run, so re-running the same task counts as a distinct session for
|
|
231
|
+
// the lesson-recurrence gate.
|
|
232
|
+
const runId = `${task.id}-${Date.now().toString(36)}`;
|
|
233
|
+
|
|
234
|
+
const finish = async (result: IRunResult): Promise<IRunResult> => {
|
|
235
|
+
await consolidateLessons(cwd, events, runId, report);
|
|
236
|
+
|
|
237
|
+
return result;
|
|
238
|
+
};
|
|
246
239
|
|
|
247
240
|
report({
|
|
248
241
|
kind: "start",
|
|
@@ -330,6 +323,7 @@ export async function runTask(
|
|
|
330
323
|
const state: ILoopState = {
|
|
331
324
|
prevGateErrors: red.errors,
|
|
332
325
|
gateNoProgress: 0,
|
|
326
|
+
errorAge: new Map(),
|
|
333
327
|
lastGateCount: -1,
|
|
334
328
|
edits: 0,
|
|
335
329
|
regressions: 0,
|
|
@@ -402,7 +396,7 @@ export async function runTask(
|
|
|
402
396
|
});
|
|
403
397
|
|
|
404
398
|
if (looped !== null) {
|
|
405
|
-
return looped;
|
|
399
|
+
return finish(looped);
|
|
406
400
|
}
|
|
407
401
|
|
|
408
402
|
const touchedEditable =
|
|
@@ -418,11 +412,11 @@ export async function runTask(
|
|
|
418
412
|
emitTiming(report, task.id, turn, turnStart, taskStart);
|
|
419
413
|
|
|
420
414
|
if (settled !== null) {
|
|
421
|
-
return {
|
|
415
|
+
return finish({
|
|
422
416
|
...settled,
|
|
423
417
|
edits: state.edits,
|
|
424
418
|
regressions: state.regressions,
|
|
425
|
-
};
|
|
419
|
+
});
|
|
426
420
|
}
|
|
427
421
|
|
|
428
422
|
// Stopped with no tool call while still red → nudge it to act, not narrate.
|
|
@@ -443,7 +437,7 @@ export async function runTask(
|
|
|
443
437
|
message: `task ${task.id}: stuck (hit ${maxTurns}-turn cap)`,
|
|
444
438
|
});
|
|
445
439
|
|
|
446
|
-
return {
|
|
440
|
+
return finish({
|
|
447
441
|
task: task.id,
|
|
448
442
|
redConfirmed: true,
|
|
449
443
|
status: RUN_STATUS.stuck,
|
|
@@ -451,5 +445,5 @@ export async function runTask(
|
|
|
451
445
|
reason: STUCK_REASON.cap,
|
|
452
446
|
edits: state.edits,
|
|
453
447
|
regressions: state.regressions,
|
|
454
|
-
};
|
|
448
|
+
});
|
|
455
449
|
}
|