@astrosheep/keiyaku 0.1.76 → 0.1.78
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/README.md +88 -96
- package/build/.tsbuildinfo +1 -1
- package/build/config/apply-argument-descriptions.js +1 -1
- package/build/config/base-rules.js +14 -7
- package/build/config/dotenv.js +17 -11
- package/build/config/keiyaku-home.js +9 -0
- package/build/config/settings.js +41 -24
- package/build/config/term-presets/resolver.js +0 -3
- package/build/errno.js +3 -0
- package/build/flow-error.js +2 -0
- package/build/generated/version.js +1 -1
- package/build/git/diff/constants.js +1 -0
- package/build/git/diff/filter.js +3 -18
- package/build/git/diff/parsers.js +149 -61
- package/build/git/diff/preview.js +16 -2
- package/build/git/diff/read.js +32 -20
- package/build/git/snapshot.js +5 -24
- package/build/git/worktree.js +5 -4
- package/build/mcp/responses.js +3 -2
- package/build/mcp/server.js +61 -69
- package/build/protocol/draft-artifacts.js +2 -1
- package/build/protocol/file-guards.js +2 -1
- package/build/protocol/markdown/lex.js +52 -14
- package/build/protocol/markdown/normalization.js +3 -2
- package/build/protocol/markdown/parser.js +2 -2
- package/build/protocol/response-history.js +44 -5
- package/build/protocol/status-previews.js +20 -8
- package/build/protocol/summon-draft.js +3 -2
- package/build/protocol/summon-input.js +1 -0
- package/build/protocol/trace.js +1 -1
- package/build/tools/amend/index.js +11 -21
- package/build/tools/ask/index.js +11 -18
- package/build/tools/ask/persist.js +60 -37
- package/build/tools/ask/run.js +17 -7
- package/build/tools/create-handler.js +31 -0
- package/build/tools/drive/index.js +11 -24
- package/build/tools/drive/run.js +9 -5
- package/build/tools/petition/claim-gates.js +38 -9
- package/build/tools/petition/claim.js +20 -2
- package/build/tools/petition/forfeit.js +4 -1
- package/build/tools/petition/index.js +43 -58
- package/build/tools/petition/run.js +12 -0
- package/build/tools/round/head-guard.js +10 -0
- package/build/tools/round/incremental-diff.js +6 -2
- package/build/tools/round/report.js +24 -2
- package/build/tools/round/run.js +6 -0
- package/build/tools/round/worktree.js +6 -2
- package/build/tools/status/index.js +11 -24
- package/build/tools/status/read.js +6 -4
- package/build/tools/summon/index.js +17 -27
- package/build/tools/summon/run.js +21 -18
- package/package.json +6 -6
- package/build/git/diff/stat.js +0 -9
package/build/config/settings.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import { isErrnoException } from "../errno.js";
|
|
4
5
|
import { FlowError } from "../flow-error.js";
|
|
5
6
|
import { AGENT_ROLES, CODEX_MODEL_REASONING_EFFORT_CONFIG_KEY, ROUND_TIERS, SETTINGS_FILE, } from "../keiyaku.js";
|
|
7
|
+
import { getKeiyakuHome } from "./keiyaku-home.js";
|
|
6
8
|
const reasoningEffortSchema = z.enum(["low", "medium", "high"]);
|
|
7
9
|
const tomlValueSchema = z.lazy(() => z.union([z.string(), z.number(), z.boolean(), z.array(tomlValueSchema), z.record(z.string(), tomlValueSchema)]));
|
|
8
10
|
function optionalTrimmedString() {
|
|
@@ -73,8 +75,6 @@ const opencodeAgentProfileSchema = z
|
|
|
73
75
|
command: commandSchema,
|
|
74
76
|
})
|
|
75
77
|
.strict();
|
|
76
|
-
export const roundTierSchema = z.enum(ROUND_TIERS);
|
|
77
|
-
export const agentRoleSchema = z.enum(AGENT_ROLES);
|
|
78
78
|
export const agentProfileSchema = z.discriminatedUnion("provider", [
|
|
79
79
|
codexAgentProfileSchema,
|
|
80
80
|
codexSdkAgentProfileSchema,
|
|
@@ -132,41 +132,61 @@ export class SettingsParseError extends Error {
|
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
function parseSettings(rawSettings) {
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
parsed = JSON.parse(rawSettings);
|
|
138
|
-
}
|
|
139
|
-
catch (error) {
|
|
140
|
-
throw new SettingsParseError("INVALID_JSON", "Invalid settings JSON.", { cause: error });
|
|
141
|
-
}
|
|
142
|
-
const result = settingsSchema.safeParse(parsed);
|
|
135
|
+
const result = settingsSchema.safeParse(rawSettings);
|
|
143
136
|
if (!result.success) {
|
|
144
137
|
const issues = result.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`);
|
|
145
138
|
throw new SettingsParseError("INVALID_SCHEMA", `Invalid settings schema. ${issues.join("; ")}`, { issues });
|
|
146
139
|
}
|
|
147
140
|
return result.data;
|
|
148
141
|
}
|
|
149
|
-
|
|
150
|
-
|
|
142
|
+
function isPlainObject(value) {
|
|
143
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
144
|
+
}
|
|
145
|
+
function deepMerge(base, override) {
|
|
146
|
+
if (!isPlainObject(base) || !isPlainObject(override)) {
|
|
147
|
+
return override;
|
|
148
|
+
}
|
|
149
|
+
const merged = { ...base };
|
|
150
|
+
for (const [key, value] of Object.entries(override)) {
|
|
151
|
+
merged[key] = key in merged ? deepMerge(merged[key], value) : value;
|
|
152
|
+
}
|
|
153
|
+
return merged;
|
|
154
|
+
}
|
|
155
|
+
async function readOptionalSettings(filePath) {
|
|
151
156
|
try {
|
|
152
|
-
|
|
157
|
+
const raw = await fs.readFile(filePath, "utf-8");
|
|
158
|
+
return { found: true, value: JSON.parse(raw) };
|
|
153
159
|
}
|
|
154
160
|
catch (error) {
|
|
155
|
-
if (error
|
|
156
|
-
return
|
|
161
|
+
if (isErrnoException(error) && error.code === "ENOENT") {
|
|
162
|
+
return { found: false, value: undefined };
|
|
163
|
+
}
|
|
164
|
+
if (error instanceof SyntaxError) {
|
|
165
|
+
throw new FlowError("INVALID_SETTINGS", `Invalid settings in ${filePath} (must be valid JSON).`, {
|
|
166
|
+
cause: error,
|
|
167
|
+
});
|
|
157
168
|
}
|
|
158
169
|
throw error;
|
|
159
170
|
}
|
|
171
|
+
}
|
|
172
|
+
export async function loadKeiyakuSettings(cwd) {
|
|
173
|
+
const localSettingsPath = path.join(cwd, SETTINGS_FILE);
|
|
174
|
+
const globalSettingsPath = path.join(getKeiyakuHome(), path.basename(SETTINGS_FILE));
|
|
175
|
+
const globalSettings = await readOptionalSettings(globalSettingsPath);
|
|
176
|
+
const localSettings = await readOptionalSettings(localSettingsPath);
|
|
177
|
+
if (!globalSettings.found && !localSettings.found) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
const resolvedSettings = globalSettings.found && localSettings.found
|
|
181
|
+
? deepMerge(globalSettings.value, localSettings.value)
|
|
182
|
+
: globalSettings.found
|
|
183
|
+
? globalSettings.value
|
|
184
|
+
: localSettings.value;
|
|
160
185
|
try {
|
|
161
|
-
return parseSettings(
|
|
186
|
+
return parseSettings(resolvedSettings);
|
|
162
187
|
}
|
|
163
188
|
catch (error) {
|
|
164
189
|
if (error instanceof SettingsParseError) {
|
|
165
|
-
if (error.code === "INVALID_JSON") {
|
|
166
|
-
throw new FlowError("INVALID_SETTINGS", `Invalid settings in ${SETTINGS_FILE} (must be valid JSON).`, {
|
|
167
|
-
cause: error,
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
190
|
throw new FlowError("INVALID_SETTINGS", `Invalid settings in ${SETTINGS_FILE} (invalid schema).`, {
|
|
171
191
|
cause: error,
|
|
172
192
|
hints: error.issues,
|
|
@@ -175,9 +195,6 @@ export async function loadKeiyakuSettings(cwd) {
|
|
|
175
195
|
throw error;
|
|
176
196
|
}
|
|
177
197
|
}
|
|
178
|
-
export function getDefaultAgentProfile(role) {
|
|
179
|
-
return DEFAULT_AGENT_PROFILES[role];
|
|
180
|
-
}
|
|
181
198
|
export function resolveAgentProfile(settings, name) {
|
|
182
199
|
const configuredProfile = settings?.agent?.[name];
|
|
183
200
|
if (configuredProfile) {
|
|
@@ -10,9 +10,6 @@ export function resolveTermPreset() {
|
|
|
10
10
|
}
|
|
11
11
|
throw new Error(`Unsupported ${ENV_KEYS.TERM_PRESET} '${raw}'. Expected '${fallbackPreset}'.`);
|
|
12
12
|
}
|
|
13
|
-
export function listTermPresets() {
|
|
14
|
-
return [DEFAULT_PRESET];
|
|
15
|
-
}
|
|
16
13
|
function extractPresetAvailableNames(preset) {
|
|
17
14
|
return (preset.availableNames?.map((value) => value.trim()).filter((value) => value.length > 0) ?? []);
|
|
18
15
|
}
|
package/build/errno.js
ADDED
package/build/flow-error.js
CHANGED
|
@@ -88,6 +88,8 @@ function hintForFlowCode(code, context) {
|
|
|
88
88
|
return "Invalid close intent. Valid values are `CLAIM` and `FORFEIT`.";
|
|
89
89
|
case "INVALID_SETTINGS":
|
|
90
90
|
return `Fix invalid configuration in \`${SETTINGS_FILE}\` and retry.`;
|
|
91
|
+
case "CONCURRENT_MODIFICATION":
|
|
92
|
+
return `Concurrent repository changes were detected during \`${context.driveToolName}\`. Retry on the latest branch state.`;
|
|
91
93
|
case "INTERNAL_STATE":
|
|
92
94
|
return "An internal state error occurred. Please verify your repository status.";
|
|
93
95
|
case "REVIEW_REJECTED":
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by scripts/generate-version.mjs
|
|
2
|
-
export const VERSION = "0.1.
|
|
2
|
+
export const VERSION = "0.1.78";
|
|
@@ -4,3 +4,4 @@ export const MAX_DIFF_LINES_PER_FILE = 80;
|
|
|
4
4
|
export const INCREMENTAL_DIFF_RANGE = "HEAD~1...HEAD";
|
|
5
5
|
export const TARGETED_DIFF_WARNING = "Warning: no valid diff coordinates found in KEIYAKU_TRACE.md; showing stat-only diff.";
|
|
6
6
|
export const NOT_SHOWN_IN_DETAIL_PREFIX = "--- not shown in detail: ";
|
|
7
|
+
export const DIFF_TRUNCATED_MARKER = "...[truncated]";
|
package/build/git/diff/filter.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NOT_SHOWN_IN_DETAIL_PREFIX } from "./constants.js";
|
|
2
|
-
import { isDeletedDiffSection, parseDiffPathFromHeader, parseUnifiedHunks, splitDiffByFile,
|
|
2
|
+
import { isDeletedDiffSection, parseDiffPathFromHeader, parseUnifiedHunks, splitDiffByFile, truncateDiffSectionByLineCount, } from "./parsers.js";
|
|
3
3
|
function rangesOverlap(aStart, aEnd, bStart, bEnd) {
|
|
4
4
|
return aStart <= bEnd && bStart <= aEnd;
|
|
5
5
|
}
|
|
@@ -12,21 +12,6 @@ function buildCoordinateIndex(coordinates) {
|
|
|
12
12
|
}
|
|
13
13
|
return index;
|
|
14
14
|
}
|
|
15
|
-
export function capTargetedDiffText(diffText, maxChars) {
|
|
16
|
-
if (diffText.length <= maxChars)
|
|
17
|
-
return diffText;
|
|
18
|
-
return truncateTextWithFooter(diffText, maxChars, diffText.length - maxChars);
|
|
19
|
-
}
|
|
20
|
-
function capDiffSectionByLineCount(section, maxLinesPerFile) {
|
|
21
|
-
if (maxLinesPerFile < 1)
|
|
22
|
-
return "";
|
|
23
|
-
const lines = section.split("\n");
|
|
24
|
-
if (lines.length <= maxLinesPerFile)
|
|
25
|
-
return section;
|
|
26
|
-
const truncated = lines.slice(0, maxLinesPerFile);
|
|
27
|
-
truncated.push(`... [truncated ${lines.length - maxLinesPerFile} lines for this file]`);
|
|
28
|
-
return truncated.join("\n");
|
|
29
|
-
}
|
|
30
15
|
export function filterDiffByCoordinates(rawPatch, coordinates, maxLinesPerFile = Number.POSITIVE_INFINITY) {
|
|
31
16
|
const sections = splitDiffByFile(rawPatch);
|
|
32
17
|
if (sections.length === 0)
|
|
@@ -66,11 +51,11 @@ export function filterDiffByCoordinates(rawPatch, coordinates, maxLinesPerFile =
|
|
|
66
51
|
...prelude,
|
|
67
52
|
...matchingHunks.flatMap((hunk) => [hunk.header, ...hunk.lines]),
|
|
68
53
|
].join("\n");
|
|
69
|
-
selectedSectionsByPath.set(coordinate.path,
|
|
54
|
+
selectedSectionsByPath.set(coordinate.path, truncateDiffSectionByLineCount(rendered, maxLinesPerFile));
|
|
70
55
|
}
|
|
71
56
|
const selectedSections = [
|
|
72
57
|
...selectedSectionsByPath.values(),
|
|
73
|
-
...deletedSections.map((section) =>
|
|
58
|
+
...deletedSections.map((section) => truncateDiffSectionByLineCount(section, maxLinesPerFile)),
|
|
74
59
|
];
|
|
75
60
|
if (selectedSections.length === 0)
|
|
76
61
|
return "";
|
|
@@ -1,58 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const binary = addRaw === "-" || delRaw === "-";
|
|
16
|
-
rows.push({
|
|
17
|
-
path: filePath,
|
|
18
|
-
additions: binary ? 0 : Number.parseInt(addRaw, 10) || 0,
|
|
19
|
-
deletions: binary ? 0 : Number.parseInt(delRaw, 10) || 0,
|
|
20
|
-
binary,
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
return rows;
|
|
24
|
-
}
|
|
25
|
-
export function parseNameStatus(content) {
|
|
26
|
-
const map = new Map();
|
|
27
|
-
for (const line of content.split(/\r?\n/)) {
|
|
28
|
-
if (!line.trim())
|
|
29
|
-
continue;
|
|
30
|
-
const parts = line.split("\t");
|
|
31
|
-
const statusRaw = (parts[0] ?? "").trim();
|
|
32
|
-
if (!statusRaw)
|
|
33
|
-
continue;
|
|
34
|
-
if ((statusRaw.startsWith("R") || statusRaw.startsWith("C")) && parts.length >= 3) {
|
|
35
|
-
const scoreRaw = statusRaw.slice(1);
|
|
36
|
-
const score = Number.parseInt(scoreRaw, 10);
|
|
37
|
-
const oldPath = (parts[1] ?? "").trim();
|
|
38
|
-
const path = (parts[2] ?? "").trim();
|
|
39
|
-
if (!path)
|
|
40
|
-
continue;
|
|
41
|
-
const status = statusRaw[0] === "R" ? "R" : "C";
|
|
42
|
-
map.set(path, { status, score: Number.isFinite(score) ? score : 0, oldPath, path });
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
const path = (parts[1] ?? "").trim();
|
|
46
|
-
if (!path)
|
|
47
|
-
continue;
|
|
48
|
-
// Git can emit single-letter statuses, possibly in combinations; we keep just the first.
|
|
49
|
-
const status = statusRaw[0] ?? "";
|
|
50
|
-
if (!isSimpleNameStatus(status))
|
|
51
|
-
continue;
|
|
52
|
-
map.set(path, { status, path });
|
|
53
|
-
}
|
|
54
|
-
return map;
|
|
55
|
-
}
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { DIFF_TRUNCATED_MARKER } from "./constants.js";
|
|
3
|
+
const DIFF_GIT_HEADER_PREFIX = "diff --git ";
|
|
4
|
+
const GIT_QUOTED_PATH_ESCAPES = {
|
|
5
|
+
'"': 34,
|
|
6
|
+
"\\": 92,
|
|
7
|
+
a: 7,
|
|
8
|
+
b: 8,
|
|
9
|
+
f: 12,
|
|
10
|
+
n: 10,
|
|
11
|
+
r: 13,
|
|
12
|
+
t: 9,
|
|
13
|
+
v: 11,
|
|
14
|
+
};
|
|
56
15
|
export function splitDiffByFile(content) {
|
|
57
16
|
const sections = [];
|
|
58
17
|
let current = [];
|
|
@@ -135,10 +94,83 @@ export function isMissingBaseRevisionError(err) {
|
|
|
135
94
|
text.includes("unknown revision or path not in the working tree") ||
|
|
136
95
|
text.includes("ambiguous argument"));
|
|
137
96
|
}
|
|
97
|
+
function unescapeQuotedDiffPath(value) {
|
|
98
|
+
const bytes = [];
|
|
99
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
100
|
+
const char = value[index];
|
|
101
|
+
if (char !== "\\") {
|
|
102
|
+
bytes.push(...Buffer.from(char ?? "", "utf8"));
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const escaped = value[index + 1];
|
|
106
|
+
if (escaped === undefined) {
|
|
107
|
+
bytes.push(92);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (escaped >= "0" && escaped <= "7") {
|
|
111
|
+
let octal = escaped;
|
|
112
|
+
let cursor = index + 2;
|
|
113
|
+
while (cursor < value.length && octal.length < 3) {
|
|
114
|
+
const digit = value[cursor];
|
|
115
|
+
if (digit === undefined || digit < "0" || digit > "7")
|
|
116
|
+
break;
|
|
117
|
+
octal += digit;
|
|
118
|
+
cursor += 1;
|
|
119
|
+
}
|
|
120
|
+
bytes.push(Number.parseInt(octal, 8));
|
|
121
|
+
index = cursor - 1;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const decoded = GIT_QUOTED_PATH_ESCAPES[escaped];
|
|
125
|
+
if (decoded !== undefined) {
|
|
126
|
+
bytes.push(decoded);
|
|
127
|
+
index += 1;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
bytes.push(...Buffer.from(escaped, "utf8"));
|
|
131
|
+
index += 1;
|
|
132
|
+
}
|
|
133
|
+
return Buffer.from(bytes).toString("utf8");
|
|
134
|
+
}
|
|
135
|
+
function readDiffHeaderToken(line, startIndex) {
|
|
136
|
+
let index = startIndex;
|
|
137
|
+
while (index < line.length && line[index] === " ") {
|
|
138
|
+
index += 1;
|
|
139
|
+
}
|
|
140
|
+
if (index >= line.length)
|
|
141
|
+
return null;
|
|
142
|
+
if (line[index] !== '"') {
|
|
143
|
+
let endIndex = index;
|
|
144
|
+
while (endIndex < line.length && line[endIndex] !== " ") {
|
|
145
|
+
endIndex += 1;
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
value: line.slice(index, endIndex),
|
|
149
|
+
nextIndex: endIndex,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
let endIndex = index + 1;
|
|
153
|
+
let escaped = false;
|
|
154
|
+
while (endIndex < line.length) {
|
|
155
|
+
const char = line[endIndex];
|
|
156
|
+
if (!escaped && char === '"') {
|
|
157
|
+
return {
|
|
158
|
+
value: unescapeQuotedDiffPath(line.slice(index + 1, endIndex)),
|
|
159
|
+
nextIndex: endIndex + 1,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
escaped = !escaped && char === "\\";
|
|
163
|
+
endIndex += 1;
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
138
167
|
export function parseDiffPathFromHeader(diffGitLine) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
168
|
+
if (!diffGitLine.startsWith(DIFF_GIT_HEADER_PREFIX))
|
|
169
|
+
return null;
|
|
170
|
+
const aPart = readDiffHeaderToken(diffGitLine, DIFF_GIT_HEADER_PREFIX.length);
|
|
171
|
+
if (!aPart)
|
|
172
|
+
return null;
|
|
173
|
+
const bPart = readDiffHeaderToken(diffGitLine, aPart.nextIndex)?.value ?? "";
|
|
142
174
|
if (!bPart.startsWith("b/"))
|
|
143
175
|
return null;
|
|
144
176
|
return bPart.slice(2);
|
|
@@ -158,20 +190,76 @@ export function truncateTextWithFooter(text, maxChars, omittedChars) {
|
|
|
158
190
|
const cut = text.slice(0, maxChars - footer.length);
|
|
159
191
|
return `${cut}${footer}`;
|
|
160
192
|
}
|
|
161
|
-
|
|
193
|
+
function isChangedDiffLine(line) {
|
|
194
|
+
return (line.startsWith("+") && !line.startsWith("+++")) || (line.startsWith("-") && !line.startsWith("---"));
|
|
195
|
+
}
|
|
196
|
+
function trimUnsafeDiffBoundary(kept, remaining) {
|
|
197
|
+
if (kept.length === 0 || remaining.length === 0)
|
|
198
|
+
return kept;
|
|
199
|
+
const safe = [...kept];
|
|
200
|
+
while (safe.length > 0 && isChangedDiffLine(safe[safe.length - 1] ?? "") && isChangedDiffLine(remaining[0] ?? "")) {
|
|
201
|
+
safe.pop();
|
|
202
|
+
}
|
|
203
|
+
if ((safe[safe.length - 1] ?? "").startsWith("@@ ")) {
|
|
204
|
+
safe.pop();
|
|
205
|
+
}
|
|
206
|
+
return safe;
|
|
207
|
+
}
|
|
208
|
+
function renderTruncatedDiff(lines) {
|
|
209
|
+
if (lines.length === 0)
|
|
210
|
+
return DIFF_TRUNCATED_MARKER;
|
|
211
|
+
return `${lines.join("\n")}\n${DIFF_TRUNCATED_MARKER}`;
|
|
212
|
+
}
|
|
213
|
+
export function truncateDiffSectionByLineCount(section, maxLines) {
|
|
214
|
+
if (maxLines < 1)
|
|
215
|
+
return "";
|
|
216
|
+
const lines = section.split("\n");
|
|
217
|
+
if (lines.length <= maxLines)
|
|
218
|
+
return section;
|
|
219
|
+
const { prelude, hunks } = parseUnifiedHunks(lines);
|
|
220
|
+
if (hunks.length === 0 || prelude.length >= maxLines) {
|
|
221
|
+
return renderTruncatedDiff(trimUnsafeDiffBoundary(lines.slice(0, maxLines), lines.slice(maxLines)));
|
|
222
|
+
}
|
|
223
|
+
const kept = [...prelude];
|
|
224
|
+
let lineCount = prelude.length;
|
|
225
|
+
for (let index = 0; index < hunks.length; index += 1) {
|
|
226
|
+
const hunk = hunks[index];
|
|
227
|
+
const hunkLines = [hunk.header, ...hunk.lines];
|
|
228
|
+
if (lineCount + hunkLines.length <= maxLines) {
|
|
229
|
+
kept.push(...hunkLines);
|
|
230
|
+
lineCount += hunkLines.length;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
kept.push(...hunkLines);
|
|
234
|
+
lineCount += hunkLines.length;
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
if (lineCount >= lines.length)
|
|
238
|
+
return section;
|
|
239
|
+
return renderTruncatedDiff(kept);
|
|
240
|
+
}
|
|
241
|
+
export function truncateDiffBlockToCharBudget(block, maxChars) {
|
|
162
242
|
if (maxChars <= 0)
|
|
163
243
|
return "";
|
|
164
244
|
if (block.length <= maxChars)
|
|
165
245
|
return block;
|
|
246
|
+
if (DIFF_TRUNCATED_MARKER.length >= maxChars) {
|
|
247
|
+
return DIFF_TRUNCATED_MARKER.slice(0, maxChars);
|
|
248
|
+
}
|
|
166
249
|
const lines = block.split("\n");
|
|
167
250
|
const kept = [];
|
|
168
251
|
let used = 0;
|
|
169
252
|
for (const line of lines) {
|
|
170
253
|
const delta = (kept.length > 0 ? 1 : 0) + line.length;
|
|
171
|
-
|
|
254
|
+
const reservedMarker = 1 + DIFF_TRUNCATED_MARKER.length;
|
|
255
|
+
if (used + delta + reservedMarker > maxChars)
|
|
172
256
|
break;
|
|
173
257
|
kept.push(line);
|
|
174
258
|
used += delta;
|
|
175
259
|
}
|
|
176
|
-
|
|
260
|
+
const safe = trimUnsafeDiffBoundary(kept, lines.slice(kept.length));
|
|
261
|
+
if (safe.length === 0) {
|
|
262
|
+
return DIFF_TRUNCATED_MARKER.slice(0, maxChars);
|
|
263
|
+
}
|
|
264
|
+
return renderTruncatedDiff(safe);
|
|
177
265
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createGit, wrapGitError } from "../core.js";
|
|
2
2
|
import { DIFF_EXCLUDES } from "./constants.js";
|
|
3
|
-
import { parseNumStat } from "./parsers.js";
|
|
4
3
|
export async function getDiffStats(cwd, baseBranch) {
|
|
5
4
|
const git = createGit(cwd);
|
|
6
5
|
let rawNumStat;
|
|
@@ -10,7 +9,22 @@ export async function getDiffStats(cwd, baseBranch) {
|
|
|
10
9
|
catch (err) {
|
|
11
10
|
throw wrapGitError(`diff --numstat ${baseBranch}...HEAD`, err, cwd);
|
|
12
11
|
}
|
|
13
|
-
const rows =
|
|
12
|
+
const rows = rawNumStat
|
|
13
|
+
.split(/\r?\n/)
|
|
14
|
+
.flatMap((line) => {
|
|
15
|
+
if (!line.trim())
|
|
16
|
+
return [];
|
|
17
|
+
const [addRaw, delRaw, ...pathParts] = line.split("\t");
|
|
18
|
+
const filePath = pathParts.join("\t").trim();
|
|
19
|
+
if (!filePath)
|
|
20
|
+
return [];
|
|
21
|
+
const binary = addRaw === "-" || delRaw === "-";
|
|
22
|
+
return [{
|
|
23
|
+
path: filePath,
|
|
24
|
+
additions: binary ? 0 : Number.parseInt(addRaw, 10) || 0,
|
|
25
|
+
deletions: binary ? 0 : Number.parseInt(delRaw, 10) || 0,
|
|
26
|
+
}];
|
|
27
|
+
});
|
|
14
28
|
return {
|
|
15
29
|
filesChanged: rows.length,
|
|
16
30
|
insertions: rows.reduce((sum, row) => sum + row.additions, 0),
|
package/build/git/diff/read.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { createGit, wrapGitError } from "../core.js";
|
|
2
2
|
import { DIFF_EXCLUDES } from "./constants.js";
|
|
3
|
-
import { isMissingBaseRevisionError, splitDiffByFile, totalJoinedLength,
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import { isMissingBaseRevisionError, splitDiffByFile, totalJoinedLength, truncateDiffBlockToCharBudget, truncateDiffSectionByLineCount, truncateTextWithFooter, } from "./parsers.js";
|
|
4
|
+
function normalizeDiffTarget(target) {
|
|
5
|
+
return typeof target === "string" ? [target] : [...target];
|
|
6
|
+
}
|
|
7
|
+
function formatDiffTarget(target) {
|
|
8
|
+
return typeof target === "string" ? target : target.join(" ");
|
|
9
|
+
}
|
|
10
|
+
function buildPatchCommand(target, options) {
|
|
6
11
|
const args = ["diff", "--no-color", "--no-ext-diff", "--unified=3"];
|
|
7
12
|
if (options.algorithm === "histogram") {
|
|
8
13
|
args.push("--diff-algorithm=histogram");
|
|
9
14
|
}
|
|
10
|
-
args.push(
|
|
15
|
+
args.push(...normalizeDiffTarget(target), "--", ".", ...DIFF_EXCLUDES);
|
|
11
16
|
return args;
|
|
12
17
|
}
|
|
13
18
|
function renderPatchDiff(output, options) {
|
|
@@ -26,11 +31,9 @@ function renderPatchDiff(output, options) {
|
|
|
26
31
|
let omittedFiles = 0;
|
|
27
32
|
for (let i = 0; i < sections.length; i += 1) {
|
|
28
33
|
const section = sections[i];
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
const fileBlock = content.join("\n");
|
|
34
|
+
const fileBlock = maxLinesPerFile === undefined
|
|
35
|
+
? section.join("\n")
|
|
36
|
+
: truncateDiffSectionByLineCount(section.join("\n"), maxLinesPerFile);
|
|
34
37
|
if (maxChars === undefined) {
|
|
35
38
|
renderedSections.push(fileBlock);
|
|
36
39
|
continue;
|
|
@@ -44,7 +47,7 @@ function renderPatchDiff(output, options) {
|
|
|
44
47
|
if (renderedSections.length === 0) {
|
|
45
48
|
const notice = `... [omitted ${omittedFiles} file(s) to stay under ${maxChars} chars] ...`;
|
|
46
49
|
const partialBudget = Math.max(0, maxChars - totalJoinedLength(["", notice]));
|
|
47
|
-
const partial =
|
|
50
|
+
const partial = truncateDiffBlockToCharBudget(fileBlock, partialBudget);
|
|
48
51
|
if (partial.trim()) {
|
|
49
52
|
renderedSections.push(partial);
|
|
50
53
|
}
|
|
@@ -62,20 +65,29 @@ function renderPatchDiff(output, options) {
|
|
|
62
65
|
}
|
|
63
66
|
return renderedSections.join("\n\n").trim() || "No diff.";
|
|
64
67
|
}
|
|
65
|
-
export async function readDiff(cwd,
|
|
68
|
+
export async function readDiff(cwd, target, format, options = {}) {
|
|
66
69
|
const git = createGit(cwd);
|
|
70
|
+
const formattedTarget = formatDiffTarget(target);
|
|
67
71
|
try {
|
|
68
72
|
if (format === "stat") {
|
|
69
73
|
const statVariant = options.statVariant ?? "summary";
|
|
70
74
|
const args = statVariant === "numstat"
|
|
71
|
-
? ["diff", "--numstat",
|
|
72
|
-
: ["diff", "--stat=120,80", "--compact-summary",
|
|
75
|
+
? ["diff", "--numstat", ...normalizeDiffTarget(target), "--", ".", ...DIFF_EXCLUDES]
|
|
76
|
+
: ["diff", "--stat=120,80", "--compact-summary", ...normalizeDiffTarget(target), "--", ".", ...DIFF_EXCLUDES];
|
|
73
77
|
const output = await git.raw(args);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
if (statVariant !== "summary") {
|
|
79
|
+
return output.trim() || "No diff.";
|
|
80
|
+
}
|
|
81
|
+
const trimmed = output.trim();
|
|
82
|
+
if (!trimmed)
|
|
83
|
+
return "No diff.";
|
|
84
|
+
const maxChars = options.maxChars ?? Number.POSITIVE_INFINITY;
|
|
85
|
+
if (trimmed.length <= maxChars) {
|
|
86
|
+
return trimmed;
|
|
87
|
+
}
|
|
88
|
+
return truncateTextWithFooter(trimmed, maxChars, trimmed.length - maxChars);
|
|
77
89
|
}
|
|
78
|
-
const output = await git.raw(buildPatchCommand(
|
|
90
|
+
const output = await git.raw(buildPatchCommand(target, options));
|
|
79
91
|
return renderPatchDiff(output, options);
|
|
80
92
|
}
|
|
81
93
|
catch (error) {
|
|
@@ -84,9 +96,9 @@ export async function readDiff(cwd, range, format, options = {}) {
|
|
|
84
96
|
}
|
|
85
97
|
const commandLabel = format === "stat"
|
|
86
98
|
? options.statVariant === "numstat"
|
|
87
|
-
? `diff --numstat ${
|
|
88
|
-
: `diff --stat=120,80 --compact-summary ${
|
|
89
|
-
: `diff --no-color --no-ext-diff --unified=3 ${
|
|
99
|
+
? `diff --numstat ${formattedTarget}`
|
|
100
|
+
: `diff --stat=120,80 --compact-summary ${formattedTarget}`
|
|
101
|
+
: `diff --no-color --no-ext-diff --unified=3 ${formattedTarget}`;
|
|
90
102
|
throw wrapGitError(commandLabel, error, cwd);
|
|
91
103
|
}
|
|
92
104
|
}
|
package/build/git/snapshot.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "fs/promises";
|
|
2
2
|
import * as os from "os";
|
|
3
3
|
import * as path from "path";
|
|
4
|
-
import {
|
|
4
|
+
import { NOT_GIT_REPOSITORY_PATTERNS, createGit, errorContainsAnyPattern, wrapGitError } from "./core.js";
|
|
5
5
|
async function withTempIndex(cwd, run) {
|
|
6
6
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "keiyaku-git-index-"));
|
|
7
7
|
const tempIndexPath = path.join(tempDir, "index");
|
|
@@ -18,26 +18,8 @@ export async function createWorkspaceSnapshot(cwd) {
|
|
|
18
18
|
try {
|
|
19
19
|
return await withTempIndex(cwd, async (git) => {
|
|
20
20
|
await git.raw(["add", "-A"]);
|
|
21
|
-
const tree =
|
|
22
|
-
|
|
23
|
-
return undefined;
|
|
24
|
-
let head;
|
|
25
|
-
try {
|
|
26
|
-
head = (await git.revparse(["--verify", "HEAD"])).trim() || undefined;
|
|
27
|
-
}
|
|
28
|
-
catch (error) {
|
|
29
|
-
if (!errorContainsAnyPattern(error, MISSING_HEAD_PATTERNS)) {
|
|
30
|
-
throw wrapGitError("rev-parse --verify HEAD", error, cwd);
|
|
31
|
-
}
|
|
32
|
-
head = undefined;
|
|
33
|
-
}
|
|
34
|
-
const args = ["commit-tree", tree];
|
|
35
|
-
if (head) {
|
|
36
|
-
args.push("-p", head);
|
|
37
|
-
}
|
|
38
|
-
args.push("-m", "keiyaku ask snapshot");
|
|
39
|
-
const sha = await git.raw(args);
|
|
40
|
-
return sha.trim() || undefined;
|
|
21
|
+
const tree = await git.raw(["write-tree"]);
|
|
22
|
+
return tree.trim() || undefined;
|
|
41
23
|
});
|
|
42
24
|
}
|
|
43
25
|
catch (error) {
|
|
@@ -50,11 +32,10 @@ export async function createWorkspaceSnapshot(cwd) {
|
|
|
50
32
|
export async function getChangedFilesBetweenSnapshots(cwd, beforeSha, afterSha) {
|
|
51
33
|
const git = createGit(cwd);
|
|
52
34
|
try {
|
|
53
|
-
const output = await git.raw(["diff", "--name-status",
|
|
35
|
+
const output = await git.raw(["diff", "--name-status", beforeSha, afterSha]);
|
|
54
36
|
return output.trim();
|
|
55
37
|
}
|
|
56
38
|
catch (err) {
|
|
57
|
-
throw wrapGitError(`diff --name-status ${beforeSha}
|
|
39
|
+
throw wrapGitError(`diff --name-status ${beforeSha} ${afterSha}`, err, cwd);
|
|
58
40
|
}
|
|
59
41
|
}
|
|
60
|
-
export { withTempIndex };
|
package/build/git/worktree.js
CHANGED
|
@@ -6,6 +6,7 @@ export const DIRTY_FILE_CATEGORY = {
|
|
|
6
6
|
renamed: "renamed",
|
|
7
7
|
conflicted: "conflicted",
|
|
8
8
|
copied: "copied",
|
|
9
|
+
untracked: "untracked",
|
|
9
10
|
};
|
|
10
11
|
function normalizeGitPath(filePath) {
|
|
11
12
|
return filePath.trim().replace(/^\.\/+/, "");
|
|
@@ -22,6 +23,9 @@ function toDirtyStatusPathSets(status) {
|
|
|
22
23
|
};
|
|
23
24
|
}
|
|
24
25
|
function deriveDirtyFileCategory(file, statusPathSets) {
|
|
26
|
+
if (statusPathSets.untracked.has(file.path)) {
|
|
27
|
+
return DIRTY_FILE_CATEGORY.untracked;
|
|
28
|
+
}
|
|
25
29
|
if (statusPathSets.conflicted.has(file.path)) {
|
|
26
30
|
return DIRTY_FILE_CATEGORY.conflicted;
|
|
27
31
|
}
|
|
@@ -56,10 +60,7 @@ export async function getDirtyFiles(cwd) {
|
|
|
56
60
|
throw wrapGitError("status --porcelain", err, cwd);
|
|
57
61
|
}
|
|
58
62
|
const statusPathSets = toDirtyStatusPathSets(status);
|
|
59
|
-
return status.files
|
|
60
|
-
.filter((file) => !(file.index === "?" && file.working_dir === "?"))
|
|
61
|
-
.filter((file) => !statusPathSets.untracked.has(file.path))
|
|
62
|
-
.map((file) => ({
|
|
63
|
+
return status.files.map((file) => ({
|
|
63
64
|
path: file.path,
|
|
64
65
|
index: file.index,
|
|
65
66
|
working_dir: file.working_dir,
|
package/build/mcp/responses.js
CHANGED
|
@@ -142,7 +142,7 @@ function formatReview(review) {
|
|
|
142
142
|
`Review ${review.review}: ${review.result}`,
|
|
143
143
|
...formatMaybe("See", review.historyPath, FORMAT_LIST_MAX_ITEM_CHARS),
|
|
144
144
|
];
|
|
145
|
-
if (review.
|
|
145
|
+
if (review.findings?.trim()) {
|
|
146
146
|
lines.push("", truncateForDisplay(review.findings, DISPLAY_TEXT_MAX_CHARS));
|
|
147
147
|
}
|
|
148
148
|
return lines;
|
|
@@ -380,6 +380,7 @@ export function buildPetitionClaimResponse(result, input) {
|
|
|
380
380
|
? `Scores: placement=${input.placement} exactness=${input.exactness} containment=${input.containment} idiomatic=${input.idiomatic} [total: ${input.placement + input.exactness + input.containment + input.idiomatic}/${CLOSE_TOTAL_SCORE_MAX}]`
|
|
381
381
|
: "Claim Gates: bypassed by Architect";
|
|
382
382
|
const diffSection = result.diff ? buildSection(DIFF_OVERVIEW_SECTION_TITLE, result.diff) : null;
|
|
383
|
+
const reviewSection = result.review ? buildSection(REVIEW_SECTION_TITLE, formatReview(result.review)) : null;
|
|
383
384
|
const infoLines = [
|
|
384
385
|
...formatMaybe("Result", result.result, INFO_COMMIT_MAX_CHARS),
|
|
385
386
|
...formatMaybe("Title", input.title, INFO_BRANCH_MAX_CHARS),
|
|
@@ -392,7 +393,7 @@ export function buildPetitionClaimResponse(result, input) {
|
|
|
392
393
|
scoreLine,
|
|
393
394
|
...formatMaybe("Oath", input.oath, INFO_PLEA_MAX_CHARS),
|
|
394
395
|
];
|
|
395
|
-
const text = assembleResponse(`${RESPONSE_MARKERS.claim} Keiyaku Fulfilled (Claim)`, `Merged '${result.deletedBranch}' into '${result.mergedInto}'${result.commit ? ` [${result.commit}]` : ''}. Deleted feature branch.`, [diffSection].filter((section) => section !== null), infoLines);
|
|
396
|
+
const text = assembleResponse(`${RESPONSE_MARKERS.claim} Keiyaku Fulfilled (Claim)`, `Merged '${result.deletedBranch}' into '${result.mergedInto}'${result.commit ? ` [${result.commit}]` : ''}. Deleted feature branch.`, [diffSection, reviewSection].filter((section) => section !== null), infoLines);
|
|
396
397
|
return {
|
|
397
398
|
content: [{ type: "text", text }],
|
|
398
399
|
structuredContent: buildSuccessStructuredContent(resultData),
|