Skip to content

Commit 8dce012

Browse files
Merge pull request #875 from qwen-code-dev-bot/issue/874-task-plan-truncation
fix(cli): surrogate-safe truncation for task-plan objective + verify commands (Issue #874)
2 parents 9311e36 + a4c3e00 commit 8dce012

3 files changed

Lines changed: 31 additions & 3 deletions

File tree

src/repo-context.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { execFileSync } from "node:child_process";
1717
import fs from "node:fs";
1818
import path from "node:path";
1919
import { redactSecrets } from "./permission-impact.js";
20+
import { safeCutEnd } from "./text-cut.js";
2021

2122
export const REPO_CONTEXT_SCHEMA = "oh-my-cli.repo-context";
2223
export const REPO_CONTEXT_VERSION = 1;
@@ -120,7 +121,10 @@ function redactName(text: string): string {
120121
}
121122

122123
function redactCommand(text: string): string {
123-
return redactSecrets(text).text.slice(0, MAX_COMMAND_LEN);
124+
const t = redactSecrets(text).text;
125+
// Issue #874: safeCutEnd drops an astral char straddling the bound whole
126+
// rather than orphaning a surrogate.
127+
return t.slice(0, safeCutEnd(t, MAX_COMMAND_LEN));
124128
}
125129

126130
// A locale-independent comparator (UTF-16 code-unit order) so ordering is

src/task-plan.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import { collectRepoContext } from "./repo-context.js";
1616
import type { RepoContextSnapshot, CanonicalCommand } from "./repo-context.js";
1717
import { redactSecrets } from "./permission-impact.js";
18+
import { safeCutEnd } from "./text-cut.js";
1819

1920
export const PLAN_SCHEMA = "oh-my-cli.plan";
2021
export const PLAN_VERSION = 1;
@@ -60,7 +61,10 @@ export interface TaskPlanOptions {
6061
}
6162

6263
function redactObjective(task: string): string {
63-
return redactSecrets(task ?? "").text.trim().slice(0, MAX_OBJECTIVE);
64+
const trimmed = redactSecrets(task ?? "").text.trim();
65+
// Issue #874: safeCutEnd drops an astral char straddling the bound whole
66+
// rather than orphaning a surrogate.
67+
return trimmed.slice(0, safeCutEnd(trimmed, MAX_OBJECTIVE));
6468
}
6569

6670
// Derive the ordered verify commands from a repository-context snapshot, in
@@ -70,7 +74,12 @@ export function deriveVerifyCommands(snapshot: RepoContextSnapshot): string[] {
7074
const commands: string[] = [];
7175
for (const key of CANONICAL_ORDER) {
7276
const ref = snapshot.commands[key];
73-
if (ref) commands.push(redactSecrets(ref.command).text.slice(0, MAX_COMMAND_LEN));
77+
if (ref) {
78+
const text = redactSecrets(ref.command).text;
79+
// Issue #874: safeCutEnd drops an astral char straddling the bound whole
80+
// rather than orphaning a surrogate.
81+
commands.push(text.slice(0, safeCutEnd(text, MAX_COMMAND_LEN)));
82+
}
7483
if (commands.length >= MAX_COMMANDS) break;
7584
}
7685
return commands;

tests/unit/task-plan.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,21 @@ describe("redaction and bounding", () => {
141141
expect(plan.objective.length).toBe(500);
142142
});
143143

144+
it("does not orphan a surrogate when bounding the objective (Issue #874)", () => {
145+
const dir = tmp();
146+
const plan = planTask({ task: "a".repeat(499) + "🚀 tail", workspace: dir });
147+
const LONE = /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/;
148+
expect(plan.objective).not.toMatch(LONE);
149+
});
150+
151+
it("does not orphan a surrogate when bounding a derived verify command (Issue #874)", () => {
152+
const dir = tmp();
153+
write(dir, "package.json", JSON.stringify({ scripts: { test: "a".repeat(119) + "🚀 tail" } }));
154+
const plan = planTask({ task: "fix", workspace: dir });
155+
const LONE = /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/;
156+
expect(plan.verifyCommands[0]).not.toMatch(LONE);
157+
});
158+
144159
it("never leaks the workspace path", () => {
145160
const dir = tmp();
146161
write(dir, "src/index.ts", "");

0 commit comments

Comments
 (0)