@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
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { runCommand } from "../../agent";
|
|
3
|
+
import { reject, str, type IToolContext } from "./tool-context";
|
|
4
|
+
|
|
5
|
+
/** A valid npm package spec: optional @scope/, name, optional @version-range.
|
|
6
|
+
* Anything else (flags, paths, shell metacharacters) is rejected — the names
|
|
7
|
+
* are the ONLY untrusted text that reaches the `bun add` command line. The
|
|
8
|
+
* first character class deliberately excludes `-` so a leading flag like
|
|
9
|
+
* `-g`/`--registry` can never validate as a name. */
|
|
10
|
+
const PACKAGE_SPEC_RE =
|
|
11
|
+
/^(@[a-z0-9~][a-z0-9-._~]*\/)?[a-z0-9~][a-z0-9-._~]*(@[a-zA-Z0-9.^~<>=+-]+)?$/;
|
|
12
|
+
|
|
13
|
+
/** Parse + validate the space-separated package list; null if any spec is bad. */
|
|
14
|
+
export function parsePackageSpecs(packages: string): string[] | null {
|
|
15
|
+
const specs = packages.trim().split(/\s+/).filter(Boolean);
|
|
16
|
+
|
|
17
|
+
if (specs.length === 0 || !specs.every((s) => PACKAGE_SPEC_RE.test(s))) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return specs;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* `add_dependency` — install npm packages with bun. The measured gap: builds
|
|
26
|
+
* dead-ended (or hand-rolled a library badly) whenever a feature needed a dep
|
|
27
|
+
* the scaffold didn't ship. Validation is strict so the model can't smuggle
|
|
28
|
+
* flags or shell syntax; the install runs in the workspace root with the
|
|
29
|
+
* turn's abort signal, like any `run` command.
|
|
30
|
+
*/
|
|
31
|
+
export async function doAddDependency(
|
|
32
|
+
args: Record<string, unknown>,
|
|
33
|
+
ctx: IToolContext
|
|
34
|
+
): Promise<string> {
|
|
35
|
+
const specs = parsePackageSpecs(str(args, "packages"));
|
|
36
|
+
|
|
37
|
+
if (specs === null) {
|
|
38
|
+
return reject(
|
|
39
|
+
ctx,
|
|
40
|
+
"add_dependency",
|
|
41
|
+
"add_dependency: `packages` must be plain npm package names " +
|
|
42
|
+
"(optionally @versioned), space-separated — no flags or paths."
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!(await Bun.file(join(ctx.cwd, "package.json")).exists())) {
|
|
47
|
+
return reject(
|
|
48
|
+
ctx,
|
|
49
|
+
"add_dependency",
|
|
50
|
+
"add_dependency: no package.json in the workspace — this isn't an npm " +
|
|
51
|
+
"project (scaffold or init one first)."
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const dev = args.dev === true;
|
|
56
|
+
const command = `bun add ${dev ? "-d " : ""}${specs.join(" ")}`;
|
|
57
|
+
|
|
58
|
+
ctx.report({ kind: "tool", task: ctx.task, message: `↳ ${command}` });
|
|
59
|
+
|
|
60
|
+
const res = await runCommand(
|
|
61
|
+
ctx.cwd,
|
|
62
|
+
command,
|
|
63
|
+
ctx.signal === undefined ? {} : { signal: ctx.signal }
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
if (res.exitCode !== 0) {
|
|
67
|
+
return `add_dependency failed (exit ${String(res.exitCode)}):\n${res.stdout}${res.stderr}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return `installed ${specs.join(", ")}${dev ? " (dev)" : ""} — import and use them now.`;
|
|
71
|
+
}
|
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { isRecord } from "../../lib/guards";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* OUTPUT CONDITIONING — the single pipeline that maximizes signal-per-token for
|
|
6
|
+
* whatever the model runs through the `run` (arbitrary shell) tool. At a slow local
|
|
7
|
+
* model, every byte of a command's output is re-read each subsequent turn, so noisy
|
|
8
|
+
* binaries (eslint's per-file JSON, vite's chunk table, a `find` listing) waste real
|
|
9
|
+
* time and context. ONE chokepoint conditions it, governed by one invariant:
|
|
10
|
+
*
|
|
11
|
+
* DROP CEREMONY, PRESERVE SIGNAL, GO VERBOSE ON FAILURE.
|
|
12
|
+
*
|
|
13
|
+
* - ceremony (safe to cut): progress/spinners, success banners, repeated path
|
|
14
|
+
* prefixes, all-clear blobs, repeated identical lines, asset/chunk tables.
|
|
15
|
+
* - signal (never cut): errors, warnings, the actual answer, counts, the specific
|
|
16
|
+
* paths/lines the model must act on.
|
|
17
|
+
* - exit-code: on a non-zero exit, condense MINIMALLY — a failure is exactly when
|
|
18
|
+
* the detail matters.
|
|
19
|
+
*
|
|
20
|
+
* The architecture is layered, most-leverage first:
|
|
21
|
+
* L1 generic SHAPE condensers (tool-agnostic): one covers a whole class of
|
|
22
|
+
* binaries — path-list (find/ls/tree), repeated-line collapse, big-JSON.
|
|
23
|
+
* L2 per-tool condensers (precise, high-frequency): eslint, vite.
|
|
24
|
+
* L3 universal truncation backstop: a SIGNAL-PRESERVING head+tail cap (never a
|
|
25
|
+
* blind tail-slice that could cut off the summary).
|
|
26
|
+
* Each condenser returns null when the output isn't its shape, so anything
|
|
27
|
+
* unrecognized passes through to the backstop untouched.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** Context for condensing one command's output. */
|
|
31
|
+
export interface ICondenseCtx {
|
|
32
|
+
command: string;
|
|
33
|
+
output: string;
|
|
34
|
+
exitCode: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The conditioned text + which condenser fired (for instrumentation). */
|
|
38
|
+
export interface ICondenseResult {
|
|
39
|
+
text: string;
|
|
40
|
+
/** Condenser name, `"<name>+truncate"`, `"truncate"`, or null if untouched. */
|
|
41
|
+
via: string | null;
|
|
42
|
+
originalChars: number;
|
|
43
|
+
finalChars: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface IOutputCondenser {
|
|
47
|
+
name: string;
|
|
48
|
+
condense(ctx: ICondenseCtx): string | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── L2: eslint --format json ────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
/** Max DISTINCT (file × rule) groups to show when condensing ESLint JSON. */
|
|
54
|
+
const MAX_ESLINT_GROUPS = 25;
|
|
55
|
+
/** Max line numbers listed per group before eliding (the same rule on many lines). */
|
|
56
|
+
const MAX_GROUP_LINES = 15;
|
|
57
|
+
|
|
58
|
+
/** One (file × rule) group: the SAME rule firing on N lines collapses to one line. */
|
|
59
|
+
interface IEslintGroup {
|
|
60
|
+
file: string;
|
|
61
|
+
rule: string;
|
|
62
|
+
message: string;
|
|
63
|
+
severe: boolean;
|
|
64
|
+
lines: number[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface IEslintTally {
|
|
68
|
+
errors: number;
|
|
69
|
+
warnings: number;
|
|
70
|
+
/** Keyed by `${file}|${rule}` so a repeated rule aggregates instead of repeating. */
|
|
71
|
+
groups: Map<string, IEslintGroup>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Tally one ESLint message into `acc`, grouping by file+rule. */
|
|
75
|
+
function tallyEslintMessage(
|
|
76
|
+
filePath: string,
|
|
77
|
+
message: unknown,
|
|
78
|
+
acc: IEslintTally
|
|
79
|
+
): void {
|
|
80
|
+
if (!isRecord(message)) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const severe = message.severity === 2;
|
|
85
|
+
|
|
86
|
+
acc.errors += severe ? 1 : 0;
|
|
87
|
+
acc.warnings += severe ? 0 : 1;
|
|
88
|
+
|
|
89
|
+
const file = basename(filePath);
|
|
90
|
+
const rule = typeof message.ruleId === "string" ? message.ruleId : "?";
|
|
91
|
+
const text = typeof message.message === "string" ? message.message : "";
|
|
92
|
+
const line = typeof message.line === "number" ? message.line : 0;
|
|
93
|
+
const key = `${file}|${rule}`;
|
|
94
|
+
const existing = acc.groups.get(key);
|
|
95
|
+
|
|
96
|
+
if (existing === undefined) {
|
|
97
|
+
acc.groups.set(key, { file, rule, message: text, severe, lines: [line] });
|
|
98
|
+
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
existing.lines.push(line);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Render one group: a single occurrence stays `file:line msg (rule)`; many
|
|
106
|
+
* occurrences of the same rule collapse to `file msg (rule) — L1,L2,… (×N)`. */
|
|
107
|
+
function renderEslintGroup(g: IEslintGroup): string {
|
|
108
|
+
const tag = g.severe ? "" : " [warn]";
|
|
109
|
+
|
|
110
|
+
if (g.lines.length === 1) {
|
|
111
|
+
return ` ${g.file}:${String(g.lines[0])} ${g.message} (${g.rule})${tag}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const shown = g.lines.slice(0, MAX_GROUP_LINES).map(String).join(",");
|
|
115
|
+
const elided = g.lines.length > MAX_GROUP_LINES ? ",…" : "";
|
|
116
|
+
|
|
117
|
+
return ` ${g.file} ${g.message} (${g.rule})${tag} — L${shown}${elided} (×${String(g.lines.length)})`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* ESLint `--format json` dumps a full object PER FILE — for a 30-file app that's
|
|
122
|
+
* ~30 verbose blobs of "errorCount: 0" noise. Collapse to a summary: "0 problems ✓",
|
|
123
|
+
* or the problems GROUPED by file+rule (the same rule on 9 lines is one line, not
|
|
124
|
+
* nine). Returns null if the output isn't ESLint JSON. The GATE parses raw JSON
|
|
125
|
+
* elsewhere; this only affects what the MODEL reads from `run`.
|
|
126
|
+
*/
|
|
127
|
+
function condenseEslintJson(output: string): string | null {
|
|
128
|
+
if (!output.includes('"filePath"') || !output.includes('"messages"')) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Locate the JSON array even if eslint printed a deprecation warning or the
|
|
133
|
+
// command was echoed before it. Slice from the first "[" to the last "]".
|
|
134
|
+
const start = output.indexOf("[");
|
|
135
|
+
const end = output.lastIndexOf("]");
|
|
136
|
+
|
|
137
|
+
if (start < 0 || end <= start) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let parsed: unknown;
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
parsed = JSON.parse(output.slice(start, end + 1));
|
|
145
|
+
} catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!Array.isArray(parsed)) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const acc: IEslintTally = { errors: 0, warnings: 0, groups: new Map() };
|
|
154
|
+
|
|
155
|
+
for (const entry of parsed) {
|
|
156
|
+
if (!isRecord(entry) || !Array.isArray(entry.messages)) {
|
|
157
|
+
return null; // not the ESLint shape — don't risk mangling it
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const filePath = typeof entry.filePath === "string" ? entry.filePath : "?";
|
|
161
|
+
|
|
162
|
+
for (const message of entry.messages) {
|
|
163
|
+
tallyEslintMessage(filePath, message, acc);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (acc.errors === 0 && acc.warnings === 0) {
|
|
168
|
+
return `eslint: ${String(parsed.length)} files checked, 0 problems ✓`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const groups = [...acc.groups.values()];
|
|
172
|
+
const shownGroups = groups.slice(0, MAX_ESLINT_GROUPS);
|
|
173
|
+
const body = shownGroups.map(renderEslintGroup).join("\n");
|
|
174
|
+
const more =
|
|
175
|
+
groups.length > shownGroups.length
|
|
176
|
+
? `\n …and ${String(groups.length - shownGroups.length)} more rule/file group(s)`
|
|
177
|
+
: "";
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
`eslint: ${String(acc.errors)} error(s), ${String(acc.warnings)} warning(s) in ` +
|
|
181
|
+
`${String(parsed.length)} files:\n${body}${more}`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ─── L2: vite build ──────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* `vite build` prints a 20+ row chunk table (every `dist/assets/*.js` with raw +
|
|
189
|
+
* gzip sizes) — noise the model never acts on. On a SUCCESSFUL build collapse to one
|
|
190
|
+
* line. Returns null when it isn't a vite-build success (errors pass through — the
|
|
191
|
+
* model must see those).
|
|
192
|
+
*/
|
|
193
|
+
function condenseViteBuild(output: string): string | null {
|
|
194
|
+
const built = /built in ([\d.]+\s*m?s)/.exec(output);
|
|
195
|
+
|
|
196
|
+
// Only condense a clean success: "✓ N modules transformed" + "built in …".
|
|
197
|
+
if (built === null || !output.includes("modules transformed")) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (/error|Could not resolve|failed|✗/i.test(output)) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const modules = /(\d+)\s+modules transformed/.exec(output);
|
|
206
|
+
const chunks = (output.match(/dist\/assets\//g) ?? []).length;
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
`vite build ✓ — ${modules?.[1] ?? "?"} modules, ${String(chunks)} chunks, ` +
|
|
210
|
+
`built in ${built[1] ?? "?"} (chunk table elided)`
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ─── L1: path-list (find / ls -R / tree / git ls-files) ──────────────────────
|
|
215
|
+
|
|
216
|
+
/** Min path-like lines before factoring a shared directory prefix. */
|
|
217
|
+
const MIN_PATH_LIST_LINES = 6;
|
|
218
|
+
/** Max relative entries listed after factoring, before eliding. */
|
|
219
|
+
const MAX_PATH_LIST = 60;
|
|
220
|
+
|
|
221
|
+
/** The longest common directory prefix (trimmed back to the last `/`) of `lines`. */
|
|
222
|
+
function commonDirPrefix(lines: readonly string[]): string {
|
|
223
|
+
let prefix = lines[0] ?? "";
|
|
224
|
+
|
|
225
|
+
for (const line of lines) {
|
|
226
|
+
let i = 0;
|
|
227
|
+
|
|
228
|
+
while (i < prefix.length && i < line.length && prefix[i] === line[i]) {
|
|
229
|
+
i += 1;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
prefix = prefix.slice(0, i);
|
|
233
|
+
|
|
234
|
+
if (prefix.length === 0) {
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const lastSlash = prefix.lastIndexOf("/");
|
|
240
|
+
|
|
241
|
+
return lastSlash >= 0 ? prefix.slice(0, lastSlash + 1) : "";
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* A bare file listing (`find /abs/dir`, `ls -R`, `tree`) repeats the SAME long
|
|
246
|
+
* directory prefix on every line. When most lines share a common directory prefix,
|
|
247
|
+
* show it ONCE and list the rest relative. Returns null unless the output is clearly
|
|
248
|
+
* such a listing (≥MIN lines, ≥90% contain a `/`, ≥8-char shared prefix) so arbitrary
|
|
249
|
+
* output passes through untouched.
|
|
250
|
+
*/
|
|
251
|
+
function condensePathList(output: string): string | null {
|
|
252
|
+
const lines = output
|
|
253
|
+
.split("\n")
|
|
254
|
+
.map((l) => l.trimEnd())
|
|
255
|
+
.filter((l) => l.length > 0);
|
|
256
|
+
|
|
257
|
+
if (lines.length < MIN_PATH_LIST_LINES) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const pathy = lines.filter((l) => l.includes("/"));
|
|
262
|
+
|
|
263
|
+
if (pathy.length < lines.length * 0.9) {
|
|
264
|
+
return null; // mostly non-path lines — not a listing, leave it alone
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const prefix = commonDirPrefix(lines);
|
|
268
|
+
|
|
269
|
+
if (prefix.length < 8) {
|
|
270
|
+
return null; // no meaningful shared prefix to factor out
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const rels = lines.map((l) =>
|
|
274
|
+
l.startsWith(prefix) ? l.slice(prefix.length) : l
|
|
275
|
+
);
|
|
276
|
+
const shown = rels
|
|
277
|
+
.slice(0, MAX_PATH_LIST)
|
|
278
|
+
.map((r) => ` ${r}`)
|
|
279
|
+
.join("\n");
|
|
280
|
+
const more =
|
|
281
|
+
rels.length > MAX_PATH_LIST
|
|
282
|
+
? `\n …and ${String(rels.length - MAX_PATH_LIST)} more`
|
|
283
|
+
: "";
|
|
284
|
+
|
|
285
|
+
return `${prefix} (${String(lines.length)} entries):\n${shown}${more}`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ─── L1: repeated-line collapse (dep installs, repeated warnings) ─────────────
|
|
289
|
+
|
|
290
|
+
/** Min run of identical consecutive lines before collapsing to `line (×N)`. */
|
|
291
|
+
const MIN_REPEAT_RUN = 3;
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Collapse runs of ≥3 identical consecutive lines into one `line (×N)`. Covers the
|
|
295
|
+
* long tail of tools that spam the same line (dependency-resolution logs, a warning
|
|
296
|
+
* emitted per file). Lossless for signal — the line is still shown, just once.
|
|
297
|
+
* Returns null when nothing repeats enough to matter.
|
|
298
|
+
*/
|
|
299
|
+
function condenseRepeatedLines(output: string): string | null {
|
|
300
|
+
const lines = output.split("\n");
|
|
301
|
+
const out: string[] = [];
|
|
302
|
+
let changed = false;
|
|
303
|
+
let i = 0;
|
|
304
|
+
|
|
305
|
+
while (i < lines.length) {
|
|
306
|
+
let j = i + 1;
|
|
307
|
+
|
|
308
|
+
while (j < lines.length && lines[j] === lines[i]) {
|
|
309
|
+
j += 1;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const run = j - i;
|
|
313
|
+
|
|
314
|
+
if (run >= MIN_REPEAT_RUN) {
|
|
315
|
+
out.push(`${lines[i] ?? ""} (×${String(run)})`);
|
|
316
|
+
changed = true;
|
|
317
|
+
} else {
|
|
318
|
+
for (let k = i; k < j; k += 1) {
|
|
319
|
+
out.push(lines[k] ?? "");
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
i = j;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return changed ? out.join("\n") : null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ─── L1: big-JSON data dump ───────────────────────────────────────────────────
|
|
330
|
+
|
|
331
|
+
/** Only summarize a JSON ARRAY this large — a clear data dump, not a config file. */
|
|
332
|
+
const MIN_JSON_ARRAY_ITEMS = 50;
|
|
333
|
+
const MIN_JSON_CHARS = 4000;
|
|
334
|
+
const JSON_SAMPLE_CHARS = 400;
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* A huge JSON ARRAY (≥50 items, ≥4k chars) is almost always a data listing where the
|
|
338
|
+
* shape + first item is enough; summarize it with an escape hatch to re-fetch. VERY
|
|
339
|
+
* conservative on purpose — config-file objects, small arrays, and any FAILURE pass
|
|
340
|
+
* through (JSON content is often the signal, not ceremony). eslint JSON is caught by
|
|
341
|
+
* its own condenser first, so this only sees other tools' `--json` dumps.
|
|
342
|
+
*/
|
|
343
|
+
function condenseBigJsonArray(ctx: ICondenseCtx): string | null {
|
|
344
|
+
if (ctx.exitCode !== 0 || ctx.output.length < MIN_JSON_CHARS) {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const start = ctx.output.indexOf("[");
|
|
349
|
+
const end = ctx.output.lastIndexOf("]");
|
|
350
|
+
|
|
351
|
+
if (start < 0 || end <= start) {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
let parsed: unknown;
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
parsed = JSON.parse(ctx.output.slice(start, end + 1));
|
|
359
|
+
} catch {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (!Array.isArray(parsed) || parsed.length < MIN_JSON_ARRAY_ITEMS) {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const first: unknown = parsed[0];
|
|
368
|
+
const keys = isRecord(first) ? Object.keys(first).join(", ") : "(non-object)";
|
|
369
|
+
const sample = JSON.stringify(first).slice(0, JSON_SAMPLE_CHARS);
|
|
370
|
+
|
|
371
|
+
return (
|
|
372
|
+
`JSON array of ${String(parsed.length)} items (elided). keys: ${keys}\n` +
|
|
373
|
+
`first: ${sample}…\n(re-run piping to a file + read specific entries if you need the full data)`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ─── pre-normalize: progress/spinner noise ────────────────────────────────────
|
|
378
|
+
|
|
379
|
+
/** A line that's only spinner glyphs / a progress bar (after trimming) — pure noise. */
|
|
380
|
+
const SPINNER_ONLY = /^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏━─]+$/;
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Universal pre-step applied to every command's output: collapse carriage-return
|
|
384
|
+
* progress redraws (`\rProgress 50%\rProgress 100%` → keep the final state) and drop
|
|
385
|
+
* spinner-only lines. No tool is harmed — the final state of each line is preserved —
|
|
386
|
+
* and animated progress (npm/bun installs, downloads) stops bloating the context.
|
|
387
|
+
*/
|
|
388
|
+
function stripProgress(output: string): string {
|
|
389
|
+
return output
|
|
390
|
+
.split("\n")
|
|
391
|
+
.map((line) => {
|
|
392
|
+
const cr = line.lastIndexOf("\r");
|
|
393
|
+
|
|
394
|
+
return cr >= 0 ? line.slice(cr + 1) : line;
|
|
395
|
+
})
|
|
396
|
+
.filter(
|
|
397
|
+
(line) => !SPINNER_ONLY.test(line.trim()) || line.trim().length === 0
|
|
398
|
+
)
|
|
399
|
+
.join("\n");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ─── L3: signal-preserving truncation backstop ────────────────────────────────
|
|
403
|
+
|
|
404
|
+
const HEAD_LINES = 40;
|
|
405
|
+
const TAIL_LINES = 20;
|
|
406
|
+
const SIGNAL_CAP = 40;
|
|
407
|
+
/** Lines worth keeping from the elided middle, even on an otherwise-quiet run. */
|
|
408
|
+
const SIGNAL_RE =
|
|
409
|
+
/\b(error|errors|warning|warn|fail|failed|failure|cannot|undefined|exception|unexpected|not found)\b|✗|✖/i;
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* The universal floor: when conditioned output still exceeds the cap, keep the HEAD
|
|
413
|
+
* and TAIL (where the command and its summary live) plus any error/warning lines from
|
|
414
|
+
* the middle — never a blind tail-slice that could cut off the very summary the model
|
|
415
|
+
* needs. This is what catches the infinite long tail of binaries we have no specific
|
|
416
|
+
* condenser for.
|
|
417
|
+
*/
|
|
418
|
+
function truncate(output: string, maxChars: number): string {
|
|
419
|
+
if (output.length <= maxChars) {
|
|
420
|
+
return output;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const lines = output.split("\n");
|
|
424
|
+
|
|
425
|
+
if (lines.length <= HEAD_LINES + TAIL_LINES) {
|
|
426
|
+
// Few but very long lines — slice by char, head-weighted, keep the tail.
|
|
427
|
+
const headChars = Math.floor(maxChars * 0.7);
|
|
428
|
+
const tailChars = Math.max(0, maxChars - headChars - 40);
|
|
429
|
+
|
|
430
|
+
return `${output.slice(0, headChars)}\n… (${String(output.length - maxChars)} chars elided) …\n${output.slice(-tailChars)}`;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const head = lines.slice(0, HEAD_LINES);
|
|
434
|
+
const tail = lines.slice(lines.length - TAIL_LINES);
|
|
435
|
+
const middle = lines.slice(HEAD_LINES, lines.length - TAIL_LINES);
|
|
436
|
+
const signal = middle.filter((l) => SIGNAL_RE.test(l)).slice(0, SIGNAL_CAP);
|
|
437
|
+
const kept =
|
|
438
|
+
signal.length > 0
|
|
439
|
+
? `, ${String(signal.length)} error/warn line(s) kept`
|
|
440
|
+
: "";
|
|
441
|
+
const parts = [
|
|
442
|
+
...head,
|
|
443
|
+
`… ${String(middle.length - signal.length)} lines elided${kept} …`,
|
|
444
|
+
...signal,
|
|
445
|
+
...tail,
|
|
446
|
+
];
|
|
447
|
+
const text = parts.join("\n");
|
|
448
|
+
|
|
449
|
+
return text.length > maxChars
|
|
450
|
+
? `${text.slice(0, maxChars)}\n… (truncated)`
|
|
451
|
+
: text;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ─── the pipeline ──────────────────────────────────────────────────────────────
|
|
455
|
+
|
|
456
|
+
// Order: most-specific first (per-tool), then generic shapes. First non-null wins.
|
|
457
|
+
const CONDENSERS: readonly IOutputCondenser[] = [
|
|
458
|
+
{ name: "eslint", condense: (c) => condenseEslintJson(c.output) },
|
|
459
|
+
{ name: "vite", condense: (c) => condenseViteBuild(c.output) },
|
|
460
|
+
{ name: "path-list", condense: (c) => condensePathList(c.output) },
|
|
461
|
+
{ name: "repeated-lines", condense: (c) => condenseRepeatedLines(c.output) },
|
|
462
|
+
{ name: "json", condense: condenseBigJsonArray },
|
|
463
|
+
];
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Condition one command's output for the model: strip progress noise, run the
|
|
467
|
+
* registry (first matching condenser wins), then apply the signal-preserving
|
|
468
|
+
* truncation backstop. Returns the text plus which condenser fired and the
|
|
469
|
+
* before/after sizes (so the loop can SHOW the saving and we can spot over-condensing).
|
|
470
|
+
*/
|
|
471
|
+
export function condenseToolOutput(
|
|
472
|
+
ctx: ICondenseCtx,
|
|
473
|
+
maxChars: number
|
|
474
|
+
): ICondenseResult {
|
|
475
|
+
const originalChars = ctx.output.length;
|
|
476
|
+
const normalized = stripProgress(ctx.output);
|
|
477
|
+
const nctx: ICondenseCtx = { ...ctx, output: normalized };
|
|
478
|
+
|
|
479
|
+
let text = normalized;
|
|
480
|
+
let via: string | null = normalized === ctx.output ? null : "progress";
|
|
481
|
+
|
|
482
|
+
for (const condenser of CONDENSERS) {
|
|
483
|
+
const result = condenser.condense(nctx);
|
|
484
|
+
|
|
485
|
+
if (result !== null) {
|
|
486
|
+
text = result;
|
|
487
|
+
via = condenser.name;
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (text.length > maxChars) {
|
|
493
|
+
text = truncate(text, maxChars);
|
|
494
|
+
via = via === null ? "truncate" : `${via}+truncate`;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return { text, via, originalChars, finalChars: text.length };
|
|
498
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyHashlineEdit,
|
|
3
|
+
parseHashlineEdit,
|
|
4
|
+
SessionSnapshotStore,
|
|
5
|
+
} from "../../files/hashline";
|
|
6
|
+
import { parseOrRepair, reject, type IToolContext } from "./tool-context";
|
|
7
|
+
import { toHashlineEdit } from "../../agent";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Hashline edit handler: content-hash-anchored line edits with stale-anchor recovery.
|
|
11
|
+
* Parses the input, applies ops bottom-up, and handles stale tags via snapshot-based
|
|
12
|
+
* 3-way merge. The snapshot store lives on the session context.
|
|
13
|
+
*/
|
|
14
|
+
export async function doHashlineEdit(
|
|
15
|
+
args: Record<string, unknown>,
|
|
16
|
+
ctx: IToolContext & { snapshotStore?: SessionSnapshotStore }
|
|
17
|
+
): Promise<string> {
|
|
18
|
+
const { value: edit, feedback } = parseOrRepair(
|
|
19
|
+
args,
|
|
20
|
+
toHashlineEdit,
|
|
21
|
+
ctx,
|
|
22
|
+
"edit_lines"
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
if (edit === null) {
|
|
26
|
+
if (feedback !== undefined && feedback.length > 0) {
|
|
27
|
+
return feedback;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return "edit_lines: malformed args (need `file` and `input`)";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
ctx.report({
|
|
34
|
+
kind: "tool",
|
|
35
|
+
task: ctx.task,
|
|
36
|
+
message: `edit_lines ${edit.file}`,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Parse the hashline input
|
|
40
|
+
const parsed = parseHashlineEdit(edit.input);
|
|
41
|
+
|
|
42
|
+
if (parsed.errors.length > 0) {
|
|
43
|
+
return reject(
|
|
44
|
+
ctx,
|
|
45
|
+
"edit_lines:parse-error",
|
|
46
|
+
`edit_lines ${edit.file} REJECTED: ${parsed.errors[0] ?? "unparseable edit"}`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Ensure the store exists on the context
|
|
51
|
+
ctx.snapshotStore ??= new SessionSnapshotStore();
|
|
52
|
+
|
|
53
|
+
const result = await applyHashlineEdit(
|
|
54
|
+
ctx.snapshotStore,
|
|
55
|
+
ctx.cwd,
|
|
56
|
+
edit.file,
|
|
57
|
+
edit.hash,
|
|
58
|
+
parsed.ops
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
if (result.ok) {
|
|
62
|
+
ctx.report({
|
|
63
|
+
kind: "edit",
|
|
64
|
+
task: ctx.task,
|
|
65
|
+
file: edit.file,
|
|
66
|
+
message: `edit_lines ${edit.file} (new hash #${result.newHash ?? "?"})`,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return `edited ${edit.file} (new hash #${result.newHash ?? "?"})`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const feedbackText =
|
|
73
|
+
result.suggestions?.[0] ?? result.reason ?? "unknown error";
|
|
74
|
+
|
|
75
|
+
return reject(
|
|
76
|
+
ctx,
|
|
77
|
+
`edit_lines:${result.reason ?? "error"}`,
|
|
78
|
+
`edit_lines ${edit.file} REJECTED: ${feedbackText}`
|
|
79
|
+
);
|
|
80
|
+
}
|