pi-long-task 0.1.0 → 0.1.1
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 +1 -1
- package/package.json +1 -1
- package/src/git.ts +97 -3
package/README.md
CHANGED
package/package.json
CHANGED
package/src/git.ts
CHANGED
|
@@ -3,10 +3,16 @@ import { realpathSync } from "node:fs";
|
|
|
3
3
|
import { promisify } from "node:util";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import type { SessionOutcome } from "./worker_session.ts";
|
|
7
7
|
|
|
8
8
|
const execFileAsync = promisify(execFile);
|
|
9
9
|
|
|
10
|
+
const GENERATED_TODO_COMMIT_PREFIX_RE = /^(?:Complete|Progress)\s+TODO\s+\d+(?:\s+[—-]\s*)?/i;
|
|
11
|
+
const TODO_LABEL_PREFIX_RE = /^TODO\s+\d+(?:\s+[—-]\s*)?/i;
|
|
12
|
+
const CONVENTIONAL_SUBJECT_RE = /^([a-z][a-z0-9-]*)(\([^)]*\))?(!)?:\s+(.+)$/;
|
|
13
|
+
const DEFAULT_COMMIT_SUBJECT = "Update project files";
|
|
14
|
+
const RECENT_COMMIT_SUBJECT_LIMIT = 20;
|
|
15
|
+
|
|
10
16
|
export interface GitRunResult {
|
|
11
17
|
stdout: string;
|
|
12
18
|
stderr: string;
|
|
@@ -104,8 +110,7 @@ export async function commitAfterSession(options: CommitAfterSessionOptions): Pr
|
|
|
104
110
|
return { error: "not inside a git repository" };
|
|
105
111
|
}
|
|
106
112
|
|
|
107
|
-
const
|
|
108
|
-
const commitMessage = `${messagePrefix} ${taskLabel(options.outcome.task)}`;
|
|
113
|
+
const commitMessage = await commitMessageForOutcome(root, options.outcome);
|
|
109
114
|
|
|
110
115
|
try {
|
|
111
116
|
const add = await runGit(root, ["add", "-A"]);
|
|
@@ -154,6 +159,95 @@ export async function commitAfterSession(options: CommitAfterSessionOptions): Pr
|
|
|
154
159
|
}
|
|
155
160
|
}
|
|
156
161
|
|
|
162
|
+
async function commitMessageForOutcome(
|
|
163
|
+
root: string,
|
|
164
|
+
outcome: Pick<SessionOutcome, "task" | "reportedStatus" | "done" | "error" | "timedOut" | "aborted">,
|
|
165
|
+
): Promise<string> {
|
|
166
|
+
const subject = normalizedTaskSubject(outcome.task.title);
|
|
167
|
+
const recentSubjects = await recentCommitSubjects(root);
|
|
168
|
+
return formatSubjectLikeRecentCommits(subject, recentSubjects);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function recentCommitSubjects(root: string): Promise<string[]> {
|
|
172
|
+
const result = await runGit(root, ["log", `-${RECENT_COMMIT_SUBJECT_LIMIT}`, "--format=%s"]);
|
|
173
|
+
if (result.code !== 0) {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return result.stdout
|
|
178
|
+
.split(/\r?\n/g)
|
|
179
|
+
.map((subject) => subject.trim())
|
|
180
|
+
.filter(Boolean)
|
|
181
|
+
.filter((subject) => !GENERATED_TODO_COMMIT_PREFIX_RE.test(subject))
|
|
182
|
+
.filter((subject) => !/^Merge\b/.test(subject) && !/^Revert\b/.test(subject));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function formatSubjectLikeRecentCommits(subject: string, recentSubjects: readonly string[]): string {
|
|
186
|
+
const sample = recentSubjects[0];
|
|
187
|
+
if (!sample) {
|
|
188
|
+
return ensureSafeCommitSubject(subject);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const conventional = CONVENTIONAL_SUBJECT_RE.exec(sample);
|
|
192
|
+
if (conventional) {
|
|
193
|
+
const prefix = `${conventional[1]}${conventional[2] ?? ""}${conventional[3] ?? ""}: `;
|
|
194
|
+
return ensureSafeCommitSubject(`${prefix}${formatSubjectBody(subject, conventional[4])}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return ensureSafeCommitSubject(formatSubjectBody(subject, sample));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function formatSubjectBody(subject: string, sampleBody: string): string {
|
|
201
|
+
let formatted = normalizedTaskSubject(subject);
|
|
202
|
+
const sampleFirstLetter = sampleBody.match(/[A-Za-z]/)?.[0];
|
|
203
|
+
if (sampleFirstLetter && sampleFirstLetter === sampleFirstLetter.toLowerCase()) {
|
|
204
|
+
formatted = lowercaseFirstLetter(formatted);
|
|
205
|
+
} else if (sampleFirstLetter && sampleFirstLetter === sampleFirstLetter.toUpperCase()) {
|
|
206
|
+
formatted = uppercaseFirstLetter(formatted);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
formatted = formatted.replace(/[.!?]+$/g, "");
|
|
210
|
+
if (/\.$/.test(sampleBody.trim())) {
|
|
211
|
+
formatted = `${formatted}.`;
|
|
212
|
+
}
|
|
213
|
+
return formatted;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function normalizedTaskSubject(title: string): string {
|
|
217
|
+
const normalized = stripGeneratedTodoPrefix(title)
|
|
218
|
+
.replace(/\s+/g, " ")
|
|
219
|
+
.replace(/[.!?]+$/g, "")
|
|
220
|
+
.trim();
|
|
221
|
+
return normalized || DEFAULT_COMMIT_SUBJECT;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function ensureSafeCommitSubject(subject: string): string {
|
|
225
|
+
const normalized = stripGeneratedTodoPrefix(subject).replace(/\s+/g, " ").trim();
|
|
226
|
+
return normalized || DEFAULT_COMMIT_SUBJECT;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function stripGeneratedTodoPrefix(subject: string): string {
|
|
230
|
+
let cleaned = subject.trim();
|
|
231
|
+
let previous = "";
|
|
232
|
+
while (cleaned && cleaned !== previous) {
|
|
233
|
+
previous = cleaned;
|
|
234
|
+
cleaned = cleaned
|
|
235
|
+
.replace(GENERATED_TODO_COMMIT_PREFIX_RE, "")
|
|
236
|
+
.replace(TODO_LABEL_PREFIX_RE, "")
|
|
237
|
+
.replace(/^[:\s—-]+/g, "")
|
|
238
|
+
.trim();
|
|
239
|
+
}
|
|
240
|
+
return cleaned;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function lowercaseFirstLetter(value: string): string {
|
|
244
|
+
return value.replace(/[A-Za-z]/, (letter) => letter.toLowerCase());
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function uppercaseFirstLetter(value: string): string {
|
|
248
|
+
return value.replace(/[A-Za-z]/, (letter) => letter.toUpperCase());
|
|
249
|
+
}
|
|
250
|
+
|
|
157
251
|
async function stagedArtifactPaths(root: string, runDir?: string): Promise<Set<string>> {
|
|
158
252
|
const artifacts = new Set<string>();
|
|
159
253
|
const runDirRel = runDir ? relToRoot(runDir, root) : "";
|