project-tiny-context-harness 0.8.13 → 0.8.16
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 +26 -16
- package/assets/README.md +25 -13
- package/assets/README.zh-CN.md +28 -14
- package/assets/agents/AGENTS_CORE.md +2 -2
- package/assets/agents/long-task-implementation.toml +1 -1
- package/assets/skills/long-task-workflow/SKILL.md +9 -7
- package/assets/skills/long-task-workflow/agents/openai.yaml +1 -1
- package/assets/skills/long-task-workflow/references/authority-lifecycle.md +1 -1
- package/dist/commands/long-task-revision.js +4 -4
- package/dist/lib/long-task-authority-revision-brief.js +3 -1
- package/dist/lib/long-task-authority-revision-summary.js +6 -2
- package/dist/lib/long-task-authority-revision-types.d.ts +2 -0
- package/dist/lib/long-task-design-resource-method-binding.js +8 -2
- package/dist/lib/long-task-git.d.ts +26 -0
- package/dist/lib/long-task-git.js +130 -0
- package/dist/lib/long-task-paths.js +1 -1
- package/dist/lib/long-task-process-runtime-closure.js +74 -42
- package/dist/lib/long-task-verifier-authority.js +2 -0
- package/dist/lib/long-task-workspace-manifest.d.ts +8 -0
- package/dist/lib/long-task-workspace-manifest.js +173 -0
- package/dist/lib/long-task-workspace-snapshot.d.ts +8 -0
- package/dist/lib/long-task-workspace-snapshot.js +176 -0
- package/dist/lib/long-task-workspace.d.ts +4 -25
- package/dist/lib/long-task-workspace.js +3 -432
- package/dist/long-task-hook.js +12 -3
- package/package.json +1 -1
|
@@ -1,433 +1,4 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { copyFile, mkdir, mkdtemp, readFile, readdir, rm, stat, symlink, } from "node:fs/promises";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
1
|
export { resolveInsideRepository } from "./repository-path-safety.js";
|
|
6
|
-
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
const gitEntry = await stat(path.join(resolved, ".git")).catch(() => null);
|
|
10
|
-
if (gitEntry?.isDirectory() || gitEntry?.isFile())
|
|
11
|
-
return resolved;
|
|
12
|
-
return path.resolve(await gitOutput(resolved, ["rev-parse", "--show-toplevel"]));
|
|
13
|
-
}
|
|
14
|
-
export async function gitCommonDir(root) {
|
|
15
|
-
return path.resolve(await gitOutput(root, [
|
|
16
|
-
"rev-parse",
|
|
17
|
-
"--path-format=absolute",
|
|
18
|
-
"--git-common-dir",
|
|
19
|
-
]));
|
|
20
|
-
}
|
|
21
|
-
export async function captureWorkspaceFingerprint(rootInput, excludedPrefixes = []) {
|
|
22
|
-
const root = path.resolve(rootInput);
|
|
23
|
-
const indexTree = await gitOutput(root, ["write-tree"]);
|
|
24
|
-
const [head, headTree, staged, unstaged, statusBytes, untracked] = await Promise.all([
|
|
25
|
-
gitOutput(root, ["rev-parse", "HEAD"]),
|
|
26
|
-
gitOutput(root, ["rev-parse", "HEAD^{tree}"]),
|
|
27
|
-
gitBuffer(root, scopedDiffArgs(["diff", "--cached", "--binary", "--no-ext-diff"], excludedPrefixes)),
|
|
28
|
-
gitBuffer(root, scopedDiffArgs(["diff", "--binary", "--no-ext-diff"], excludedPrefixes)),
|
|
29
|
-
Promise.all([
|
|
30
|
-
gitBuffer(root, scopedDiffArgs(["diff", "--cached", "--raw", "-z", "-M"], excludedPrefixes)),
|
|
31
|
-
gitBuffer(root, scopedDiffArgs(["diff", "--raw", "-z", "-M"], excludedPrefixes)),
|
|
32
|
-
]).then((rows) => Buffer.concat(rows)),
|
|
33
|
-
untrackedIdentity(root, excludedPrefixes),
|
|
34
|
-
]);
|
|
35
|
-
const unsigned = {
|
|
36
|
-
head,
|
|
37
|
-
head_tree: headTree,
|
|
38
|
-
index_tree: indexTree,
|
|
39
|
-
staged_diff_sha256: sha256Hex(staged),
|
|
40
|
-
unstaged_diff_sha256: sha256Hex(unstaged),
|
|
41
|
-
untracked_sha256: untracked,
|
|
42
|
-
status_sha256: sha256Hex(statusBytes),
|
|
43
|
-
};
|
|
44
|
-
return {
|
|
45
|
-
...unsigned,
|
|
46
|
-
identity: sha256Hex(canonicalValueJson(unsigned)),
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
export async function captureWorkspaceManifest(rootInput, workdirInput, _copyRoot, additionalExcludedWorkdirs = []) {
|
|
50
|
-
const root = path.resolve(rootInput);
|
|
51
|
-
const workdir = path.resolve(workdirInput);
|
|
52
|
-
const workdirRelative = repoRelative(root, workdir);
|
|
53
|
-
if (!workdirRelative)
|
|
54
|
-
throw new Error("long_task_workdir_must_not_be_repository_root");
|
|
55
|
-
const excluded = excludedPrefixes(root, [
|
|
56
|
-
workdir,
|
|
57
|
-
...additionalExcludedWorkdirs,
|
|
58
|
-
]);
|
|
59
|
-
const fingerprint = await captureWorkspaceFingerprint(root, excluded);
|
|
60
|
-
const [indexBytes, modifiedBytes, untrackedBytes] = await Promise.all([
|
|
61
|
-
gitBuffer(root, ["ls-files", "--stage", "-z"]),
|
|
62
|
-
gitBuffer(root, ["diff", "--name-only", "-z"]),
|
|
63
|
-
gitBuffer(root, ["ls-files", "--others", "--exclude-standard", "-z"]),
|
|
64
|
-
]);
|
|
65
|
-
const files = new Map();
|
|
66
|
-
for (const record of splitZero(indexBytes)) {
|
|
67
|
-
const tab = record.indexOf("\t");
|
|
68
|
-
if (tab < 0)
|
|
69
|
-
continue;
|
|
70
|
-
const [modeText, objectId, stage] = record.slice(0, tab).split(" ");
|
|
71
|
-
const relative = record.slice(tab + 1).replace(/\\/gu, "/");
|
|
72
|
-
if (stage !== "0" || excludedPath(relative, excluded))
|
|
73
|
-
continue;
|
|
74
|
-
files.set(relative, {
|
|
75
|
-
path: relative,
|
|
76
|
-
mode: Number.parseInt(modeText, 8),
|
|
77
|
-
size: 0,
|
|
78
|
-
sha256: `git:${objectId}`,
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
const overlays = new Set([
|
|
82
|
-
...splitZero(modifiedBytes),
|
|
83
|
-
...splitZero(untrackedBytes),
|
|
84
|
-
]);
|
|
85
|
-
const overlayNames = [...overlays]
|
|
86
|
-
.map((raw) => raw.replace(/\\/gu, "/"))
|
|
87
|
-
.filter((relative) => !excludedPath(relative, excluded));
|
|
88
|
-
const overlayInfo = new Map(await Promise.all(overlayNames.map(async (relative) => [
|
|
89
|
-
relative,
|
|
90
|
-
await stat(path.join(root, ...relative.split("/")), {
|
|
91
|
-
bigint: true,
|
|
92
|
-
}).catch(() => null),
|
|
93
|
-
])));
|
|
94
|
-
const overlayHashes = await gitObjectIds(root, overlayNames.filter((relative) => overlayInfo.get(relative)?.isFile()));
|
|
95
|
-
for (const relative of overlayNames) {
|
|
96
|
-
const absolute = path.join(root, ...relative.split("/"));
|
|
97
|
-
const info = overlayInfo.get(relative);
|
|
98
|
-
if (!info?.isFile()) {
|
|
99
|
-
files.delete(relative);
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
const bytes = await readFile(absolute);
|
|
103
|
-
files.set(relative, {
|
|
104
|
-
path: relative,
|
|
105
|
-
mode: gitFileMode(Number(info.mode)),
|
|
106
|
-
size: bytes.length,
|
|
107
|
-
sha256: `git:${overlayHashes.get(relative)}`,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
return {
|
|
111
|
-
repository_root: root,
|
|
112
|
-
git_head: fingerprint.head,
|
|
113
|
-
files: [...files.values()].sort((a, b) => a.path.localeCompare(b.path)),
|
|
114
|
-
fingerprint,
|
|
115
|
-
snapshot_sha256: fingerprint.identity,
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
export async function createWorkspaceSnapshot(rootInput, workdirInput, label, additionalExcludedWorkdirs = []) {
|
|
119
|
-
const started = performance.now();
|
|
120
|
-
const root = path.resolve(rootInput);
|
|
121
|
-
const workdir = path.resolve(workdirInput);
|
|
122
|
-
const fingerprintExcluded = excludedPrefixes(root, [
|
|
123
|
-
workdir,
|
|
124
|
-
...additionalExcludedWorkdirs,
|
|
125
|
-
]);
|
|
126
|
-
const snapshotExcluded = snapshotExcludedPrefixes(root, [
|
|
127
|
-
workdir,
|
|
128
|
-
...additionalExcludedWorkdirs,
|
|
129
|
-
]);
|
|
130
|
-
const manifest = await captureWorkspaceManifest(root, workdir, undefined, additionalExcludedWorkdirs);
|
|
131
|
-
const before = manifest.fingerprint;
|
|
132
|
-
const temporary = await mkdtemp(path.join(os.tmpdir(), `ty-context-${safe(label)}-`));
|
|
133
|
-
try {
|
|
134
|
-
await gitVoid(root, [
|
|
135
|
-
"checkout-index",
|
|
136
|
-
"--all",
|
|
137
|
-
"--force",
|
|
138
|
-
`--prefix=${temporary.replace(/\\/gu, "/")}/`,
|
|
139
|
-
]);
|
|
140
|
-
await overlayTrackedEolDifferences(root, temporary);
|
|
141
|
-
const [modified, untracked] = await Promise.all([
|
|
142
|
-
gitBuffer(root, ["diff", "--name-only", "-z"]),
|
|
143
|
-
gitBuffer(root, ["ls-files", "--others", "--exclude-standard", "-z"]),
|
|
144
|
-
]);
|
|
145
|
-
for (const raw of new Set([
|
|
146
|
-
...splitZero(modified),
|
|
147
|
-
...splitZero(untracked),
|
|
148
|
-
])) {
|
|
149
|
-
const relative = raw.replace(/\\/gu, "/");
|
|
150
|
-
if (excludedPath(relative, snapshotExcluded))
|
|
151
|
-
continue;
|
|
152
|
-
const source = path.join(root, ...relative.split("/"));
|
|
153
|
-
const target = path.join(temporary, ...relative.split("/"));
|
|
154
|
-
const info = await stat(source).catch(() => null);
|
|
155
|
-
if (!info?.isFile()) {
|
|
156
|
-
await rm(target, { recursive: true, force: true });
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
await mkdir(path.dirname(target), { recursive: true });
|
|
160
|
-
await copyFile(source, target);
|
|
161
|
-
}
|
|
162
|
-
await removeExcludedSnapshotPaths(temporary, snapshotExcluded);
|
|
163
|
-
await linkDependencyTrees(root, temporary, [
|
|
164
|
-
workdir,
|
|
165
|
-
...additionalExcludedWorkdirs,
|
|
166
|
-
]);
|
|
167
|
-
const after = await captureWorkspaceFingerprint(root, fingerprintExcluded);
|
|
168
|
-
if (after.identity !== before.identity)
|
|
169
|
-
throw new Error("workspace_changed_during_snapshot");
|
|
170
|
-
return {
|
|
171
|
-
root: temporary,
|
|
172
|
-
manifest,
|
|
173
|
-
preparation_ms: performance.now() - started,
|
|
174
|
-
dispose: () => rm(temporary, { recursive: true, force: true }),
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
catch (error) {
|
|
178
|
-
await rm(temporary, { recursive: true, force: true });
|
|
179
|
-
throw error;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
export function changedWorkspacePaths(baseline, current) {
|
|
183
|
-
const before = new Map(baseline.files.map((file) => [file.path, `${file.mode}:${file.sha256}`]));
|
|
184
|
-
const after = new Map(current.files.map((file) => [file.path, `${file.mode}:${file.sha256}`]));
|
|
185
|
-
return [...new Set([...before.keys(), ...after.keys()])]
|
|
186
|
-
.filter((file) => before.get(file) !== after.get(file))
|
|
187
|
-
.sort();
|
|
188
|
-
}
|
|
189
|
-
export async function changedWorkspacePathsFromHead(rootInput, workdirInput, additionalExcludedWorkdirs = []) {
|
|
190
|
-
const root = path.resolve(rootInput);
|
|
191
|
-
const workdir = path.resolve(workdirInput);
|
|
192
|
-
const excluded = excludedPrefixes(root, [
|
|
193
|
-
workdir,
|
|
194
|
-
...additionalExcludedWorkdirs,
|
|
195
|
-
]);
|
|
196
|
-
const [trackedBytes, untrackedBytes] = await Promise.all([
|
|
197
|
-
gitBuffer(root, scopedDiffArgs(["diff", "--name-only", "--no-renames", "--no-ext-diff", "-z", "HEAD"], excluded)),
|
|
198
|
-
gitBuffer(root, ["ls-files", "--others", "--exclude-standard", "-z"]),
|
|
199
|
-
]);
|
|
200
|
-
return [
|
|
201
|
-
...new Set([...splitZero(trackedBytes), ...splitZero(untrackedBytes)]
|
|
202
|
-
.map((raw) => raw.replace(/\\/gu, "/"))
|
|
203
|
-
.filter((relative) => !excludedPath(relative, excluded))),
|
|
204
|
-
].sort();
|
|
205
|
-
}
|
|
206
|
-
export async function currentGitState(root) {
|
|
207
|
-
const [head, tree, raw] = await Promise.all([
|
|
208
|
-
gitOutput(root, ["rev-parse", "HEAD"]),
|
|
209
|
-
gitOutput(root, ["rev-parse", "HEAD^{tree}"]),
|
|
210
|
-
gitOutput(root, ["status", "--short", "--untracked-files=all"]),
|
|
211
|
-
]);
|
|
212
|
-
return {
|
|
213
|
-
head,
|
|
214
|
-
tree,
|
|
215
|
-
dirty: raw ? raw.split(/\r?\n/u).filter(Boolean) : [],
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
export async function currentGitTree(root) {
|
|
219
|
-
return gitOutput(root, ["rev-parse", "HEAD^{tree}"]);
|
|
220
|
-
}
|
|
221
|
-
export async function gitPath(root, pathSpec) {
|
|
222
|
-
return path.resolve(await gitOutput(root, [
|
|
223
|
-
"rev-parse",
|
|
224
|
-
"--path-format=absolute",
|
|
225
|
-
"--git-path",
|
|
226
|
-
pathSpec,
|
|
227
|
-
]));
|
|
228
|
-
}
|
|
229
|
-
export async function gitConfigGet(root, name) {
|
|
230
|
-
try {
|
|
231
|
-
return await gitOutput(root, ["config", "--local", "--get", name]);
|
|
232
|
-
}
|
|
233
|
-
catch (error) {
|
|
234
|
-
if (message(error).includes("git_exit:1"))
|
|
235
|
-
return null;
|
|
236
|
-
throw error;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
export async function gitConfigSet(root, name, value) {
|
|
240
|
-
await gitVoid(root, ["config", "--local", name, value]);
|
|
241
|
-
}
|
|
242
|
-
export async function gitConfigUnset(root, name) {
|
|
243
|
-
try {
|
|
244
|
-
await gitVoid(root, ["config", "--local", "--unset-all", name]);
|
|
245
|
-
}
|
|
246
|
-
catch (error) {
|
|
247
|
-
if (!message(error).includes("git_exit:5"))
|
|
248
|
-
throw error;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
export function repoRelative(rootInput, fileInput) {
|
|
252
|
-
const value = path
|
|
253
|
-
.relative(path.resolve(rootInput), path.resolve(fileInput))
|
|
254
|
-
.replace(/\\/gu, "/");
|
|
255
|
-
if (value.startsWith("../") || path.isAbsolute(value))
|
|
256
|
-
throw new Error(`path_outside_repository:${fileInput}`);
|
|
257
|
-
return value;
|
|
258
|
-
}
|
|
259
|
-
async function untrackedIdentity(root, excluded) {
|
|
260
|
-
const names = splitZero(await gitBuffer(root, ["ls-files", "--others", "--exclude-standard", "-z"]))
|
|
261
|
-
.map((name) => name.replace(/\\/gu, "/"))
|
|
262
|
-
.filter((name) => !excludedPath(name, excluded))
|
|
263
|
-
.sort();
|
|
264
|
-
const hashes = await gitObjectIds(root, names);
|
|
265
|
-
const rows = names.map((name) => [
|
|
266
|
-
name,
|
|
267
|
-
hashes.get(name) ?? "missing",
|
|
268
|
-
]);
|
|
269
|
-
return sha256Hex(canonicalValueJson(rows));
|
|
270
|
-
}
|
|
271
|
-
function excludedPrefixes(root, workdirs) {
|
|
272
|
-
return [
|
|
273
|
-
...snapshotExcludedPrefixes(root, workdirs),
|
|
274
|
-
"project_context",
|
|
275
|
-
].filter(Boolean);
|
|
276
|
-
}
|
|
277
|
-
function snapshotExcludedPrefixes(root, workdirs) {
|
|
278
|
-
return [
|
|
279
|
-
...workdirs.map((workdir) => repoRelative(root, path.resolve(workdir))),
|
|
280
|
-
"tmp/ty-context/long-task-runs",
|
|
281
|
-
].filter(Boolean);
|
|
282
|
-
}
|
|
283
|
-
function excludedPath(relative, excluded) {
|
|
284
|
-
const normalized = relative.replace(/\\/gu, "/");
|
|
285
|
-
return (normalized.split("/").includes("node_modules") ||
|
|
286
|
-
excluded.some((prefix) => normalized === prefix || normalized.startsWith(`${prefix}/`)));
|
|
287
|
-
}
|
|
288
|
-
async function removeExcludedSnapshotPaths(snapshotRoot, excluded) {
|
|
289
|
-
for (const relative of excluded)
|
|
290
|
-
await rm(path.join(snapshotRoot, ...relative.split("/")), {
|
|
291
|
-
recursive: true,
|
|
292
|
-
force: true,
|
|
293
|
-
});
|
|
294
|
-
async function visit(directory) {
|
|
295
|
-
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
296
|
-
const target = path.join(directory, entry.name);
|
|
297
|
-
if (entry.isDirectory() && entry.name === "node_modules") {
|
|
298
|
-
await rm(target, { recursive: true, force: true });
|
|
299
|
-
}
|
|
300
|
-
else if (entry.isDirectory())
|
|
301
|
-
await visit(target);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
await visit(snapshotRoot);
|
|
305
|
-
}
|
|
306
|
-
async function overlayTrackedEolDifferences(sourceRoot, snapshotRoot) {
|
|
307
|
-
const [raw, autocrlf] = await Promise.all([
|
|
308
|
-
gitBuffer(sourceRoot, ["ls-files", "--eol", "-z"]),
|
|
309
|
-
gitEffectiveConfigGet(sourceRoot, "core.autocrlf"),
|
|
310
|
-
]);
|
|
311
|
-
for (const record of splitZero(raw)) {
|
|
312
|
-
const tab = record.indexOf("\t");
|
|
313
|
-
if (tab < 0)
|
|
314
|
-
continue;
|
|
315
|
-
const metadata = record.slice(0, tab);
|
|
316
|
-
const relative = record.slice(tab + 1).replace(/\\/gu, "/");
|
|
317
|
-
const match = metadata.match(/^i\/(\S+)\s+w\/(\S+)\s+attr\/(.*)$/u);
|
|
318
|
-
if (!match)
|
|
319
|
-
continue;
|
|
320
|
-
const [, indexEol, worktreeEol, attributesRaw] = match;
|
|
321
|
-
const expected = checkoutEol(indexEol, attributesRaw.trim(), autocrlf);
|
|
322
|
-
if (worktreeEol === expected)
|
|
323
|
-
continue;
|
|
324
|
-
const source = path.join(sourceRoot, ...relative.split("/"));
|
|
325
|
-
const target = path.join(snapshotRoot, ...relative.split("/"));
|
|
326
|
-
const info = await stat(source).catch(() => null);
|
|
327
|
-
if (!info?.isFile())
|
|
328
|
-
continue;
|
|
329
|
-
await mkdir(path.dirname(target), { recursive: true });
|
|
330
|
-
await copyFile(source, target);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
async function gitEffectiveConfigGet(root, name) {
|
|
334
|
-
try {
|
|
335
|
-
return await gitOutput(root, ["config", "--get", name]);
|
|
336
|
-
}
|
|
337
|
-
catch (error) {
|
|
338
|
-
if (message(error).includes("git_exit:1"))
|
|
339
|
-
return null;
|
|
340
|
-
throw error;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
function checkoutEol(indexEol, attributes, autocrlf) {
|
|
344
|
-
const explicit = attributes.match(/(?:^|\s)eol=(lf|crlf)(?:\s|$)/u)?.[1];
|
|
345
|
-
if (explicit)
|
|
346
|
-
return explicit;
|
|
347
|
-
if (/(?:^|\s)-text(?:\s|$)/u.test(attributes))
|
|
348
|
-
return indexEol;
|
|
349
|
-
if (indexEol === "-text" || indexEol === "none")
|
|
350
|
-
return indexEol;
|
|
351
|
-
return autocrlf?.toLowerCase() === "true" ? "crlf" : indexEol;
|
|
352
|
-
}
|
|
353
|
-
async function linkDependencyTrees(sourceRoot, snapshotRoot, workdirs) {
|
|
354
|
-
const protectedWorkdirs = workdirs.map((workdir) => repoRelative(sourceRoot, workdir));
|
|
355
|
-
async function visit(directory, relative = "") {
|
|
356
|
-
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
357
|
-
const next = relative ? `${relative}/${entry.name}` : entry.name;
|
|
358
|
-
if (protectedWorkdirs.some((protectedWorkdir) => next === protectedWorkdir ||
|
|
359
|
-
next.startsWith(`${protectedWorkdir}/`)) ||
|
|
360
|
-
entry.name === ".git")
|
|
361
|
-
continue;
|
|
362
|
-
const source = path.join(directory, entry.name);
|
|
363
|
-
if (entry.isDirectory() && entry.name === "node_modules") {
|
|
364
|
-
const target = path.join(snapshotRoot, ...next.split("/"));
|
|
365
|
-
await mkdir(path.dirname(target), { recursive: true });
|
|
366
|
-
await rm(target, { recursive: true, force: true });
|
|
367
|
-
await symlink(source, target, process.platform === "win32" ? "junction" : "dir");
|
|
368
|
-
}
|
|
369
|
-
else if (entry.isDirectory())
|
|
370
|
-
await visit(source, next);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
await visit(sourceRoot);
|
|
374
|
-
}
|
|
375
|
-
function splitZero(value) {
|
|
376
|
-
return value.toString("utf8").split("\0").filter(Boolean);
|
|
377
|
-
}
|
|
378
|
-
function scopedDiffArgs(base, excluded) {
|
|
379
|
-
return [
|
|
380
|
-
...base,
|
|
381
|
-
"--",
|
|
382
|
-
".",
|
|
383
|
-
...excluded.map((prefix) => `:(exclude)${prefix}/**`),
|
|
384
|
-
":(exclude)**/node_modules/**",
|
|
385
|
-
];
|
|
386
|
-
}
|
|
387
|
-
async function gitObjectIds(root, names) {
|
|
388
|
-
if (!names.length)
|
|
389
|
-
return new Map();
|
|
390
|
-
const output = await gitBufferInput(root, ["hash-object", "--stdin-paths"], Buffer.from(`${names.join("\n")}\n`, "utf8"));
|
|
391
|
-
const ids = output.toString("utf8").trim().split(/\r?\n/u);
|
|
392
|
-
return new Map(names.map((name, index) => [name, ids[index]]));
|
|
393
|
-
}
|
|
394
|
-
function gitFileMode(mode) {
|
|
395
|
-
return mode & 0o111 ? 0o100755 : 0o100644;
|
|
396
|
-
}
|
|
397
|
-
async function gitOutput(root, argv) {
|
|
398
|
-
return (await gitBuffer(root, argv)).toString("utf8").trim();
|
|
399
|
-
}
|
|
400
|
-
async function gitVoid(root, argv) {
|
|
401
|
-
await gitBuffer(root, argv);
|
|
402
|
-
}
|
|
403
|
-
async function gitBuffer(root, argv) {
|
|
404
|
-
return gitBufferInput(root, argv);
|
|
405
|
-
}
|
|
406
|
-
async function gitBufferInput(root, argv, input) {
|
|
407
|
-
return new Promise((resolve, reject) => {
|
|
408
|
-
const child = spawn("git", argv, {
|
|
409
|
-
cwd: root,
|
|
410
|
-
shell: false,
|
|
411
|
-
windowsHide: true,
|
|
412
|
-
});
|
|
413
|
-
const stdout = [];
|
|
414
|
-
const stderr = [];
|
|
415
|
-
child.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
416
|
-
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
417
|
-
child.on("error", reject);
|
|
418
|
-
if (input)
|
|
419
|
-
child.stdin.end(input);
|
|
420
|
-
child.on("close", (code) => {
|
|
421
|
-
if (code === 0)
|
|
422
|
-
resolve(Buffer.concat(stdout));
|
|
423
|
-
else
|
|
424
|
-
reject(new Error(`git_exit:${code}:${argv.join(" ")}:${Buffer.concat(stderr).toString("utf8").trim()}`));
|
|
425
|
-
});
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
function safe(value) {
|
|
429
|
-
return value.replace(/[^A-Za-z0-9._-]/gu, "-").slice(0, 80);
|
|
430
|
-
}
|
|
431
|
-
function message(error) {
|
|
432
|
-
return error instanceof Error ? error.message : String(error);
|
|
433
|
-
}
|
|
2
|
+
export { currentGitState, currentGitTree, gitCommonDir, gitConfigGet, gitConfigSet, gitConfigUnset, gitPath, repoRelative, repositoryRoot, } from "./long-task-git.js";
|
|
3
|
+
export { captureWorkspaceFingerprint, captureWorkspaceManifest, changedWorkspacePaths, changedWorkspacePathsFromHead, } from "./long-task-workspace-manifest.js";
|
|
4
|
+
export { createWorkspaceSnapshot } from "./long-task-workspace-snapshot.js";
|
package/dist/long-task-hook.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { readConfig } from "./lib/config.js";
|
|
3
|
+
import { harnessRoot } from "./lib/harness-root.js";
|
|
4
|
+
import { longTaskCodexAgentProfileBootstrapPaths } from "./lib/long-task-codex-agent-profile.js";
|
|
5
|
+
import { isProfileEnabled } from "./lib/profiles.js";
|
|
2
6
|
import { readActiveLongTaskBinding } from "./lib/long-task-state.js";
|
|
3
7
|
import { stopCheckDeliveryTask } from "./lib/long-task-status-v2.js";
|
|
4
8
|
import { LONG_TASK_IMPLEMENTATION_AGENT, selectedAgentType, } from "./lib/long-task-worker-selection.js";
|
|
@@ -12,6 +16,7 @@ const LONG_TASK_IMPLEMENTATION_BOUNDARY = [
|
|
|
12
16
|
].join("\n");
|
|
13
17
|
const AGENT_SPAWN_TOOLS = new Set(["spawn_agent", "Agent"]);
|
|
14
18
|
const EXACT_WORKER_REQUIRED = "Active Tiny Context Long-Task permits delegation only to the exact custom agent long_task_implementation. The current host request does not explicitly select it. Do not substitute a generic worker; complete this packet in the parent Goal.";
|
|
19
|
+
const EXACT_WORKER_UNAVAILABLE = "Active Tiny Context Long-Task cannot use the exact custom agent long_task_implementation because its current package-managed profile is unavailable, invalid, outdated or conflicting. Do not spawn a substitute agent; complete this packet in the parent Goal.";
|
|
15
20
|
const input = await readStdin();
|
|
16
21
|
const isAgentSpawnPreToolUse = input.hook_event_name === "PreToolUse" &&
|
|
17
22
|
AGENT_SPAWN_TOOLS.has(input.tool_name ?? "");
|
|
@@ -23,9 +28,13 @@ try {
|
|
|
23
28
|
if (!active)
|
|
24
29
|
output({});
|
|
25
30
|
if (isAgentSpawnPreToolUse) {
|
|
26
|
-
if (selectedAgentType(input.tool_input)
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
if (selectedAgentType(input.tool_input) !== LONG_TASK_IMPLEMENTATION_AGENT)
|
|
32
|
+
denyAgentSpawn(EXACT_WORKER_REQUIRED);
|
|
33
|
+
const rootConfig = await readConfig(root);
|
|
34
|
+
const profilePaths = await longTaskCodexAgentProfileBootstrapPaths(root, await harnessRoot(root), isProfileEnabled(rootConfig, "long-task"));
|
|
35
|
+
if (profilePaths.length === 0)
|
|
36
|
+
denyAgentSpawn(EXACT_WORKER_UNAVAILABLE);
|
|
37
|
+
output({});
|
|
29
38
|
}
|
|
30
39
|
if (input.hook_event_name === "SubagentStart")
|
|
31
40
|
output(input.agent_type === LONG_TASK_IMPLEMENTATION_AGENT
|