pi-ultracode 0.3.2 → 0.5.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/README.md +32 -14
- package/README.zh-CN.md +32 -14
- package/extensions/ultracode.ts +11 -10
- package/package.json +17 -7
- package/src/commands.ts +21 -22
- package/src/depth.ts +38 -0
- package/src/index.ts +9 -1
- package/src/mode.ts +82 -58
- package/src/prompts.ts +65 -32
- package/src/thinking.ts +1 -1
- package/src/workflow/agent-runner.ts +23 -8
- package/src/workflow/agent-types.ts +19 -15
- package/src/workflow/hermetic-git.ts +31 -0
- package/src/workflow/journal.ts +134 -3
- package/src/workflow/parser.ts +254 -38
- package/src/workflow/registry.ts +44 -67
- package/src/workflow/repository-context.ts +76 -0
- package/src/workflow/run-artifacts.ts +60 -14
- package/src/workflow/runtime.ts +380 -180
- package/src/workflow/saved-workflow.ts +37 -0
- package/src/workflow/script-executor.ts +56 -10
- package/src/workflow/script-worker.mjs +65 -19
- package/src/workflow/tool.ts +108 -58
- package/src/workflow/workflow-overlay.ts +4 -4
- package/src/workflow/worktree.ts +1965 -244
- package/types/workflow.d.ts +2 -4
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { hermeticGitEnvironment, stripOneLineEnding } from "./hermetic-git.ts";
|
|
5
|
+
|
|
6
|
+
const MAX_GIT_PATH_BYTES = 1024 * 1024;
|
|
7
|
+
|
|
8
|
+
export interface RepositoryContext {
|
|
9
|
+
identity: string;
|
|
10
|
+
cwd: string;
|
|
11
|
+
repoRoot?: string;
|
|
12
|
+
relativeCwd?: string;
|
|
13
|
+
gitDir?: string;
|
|
14
|
+
gitCommonDir?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Resolve the immutable working-tree target used by resume and delivery. */
|
|
18
|
+
export function resolveRepositoryContext(cwd: string): RepositoryContext {
|
|
19
|
+
const realCwd = fs.realpathSync(cwd);
|
|
20
|
+
if (gitLine(realCwd, ["rev-parse", "--is-inside-work-tree"], true) !== "true") {
|
|
21
|
+
const identity = JSON.stringify({
|
|
22
|
+
kind: "directory",
|
|
23
|
+
cwd: realCwd,
|
|
24
|
+
cwdFile: fileIdentity(realCwd),
|
|
25
|
+
});
|
|
26
|
+
return { identity, cwd: realCwd };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const repoRoot = fs.realpathSync(gitLine(realCwd, ["rev-parse", "--show-toplevel"]));
|
|
30
|
+
const gitDir = fs.realpathSync(gitLine(realCwd, ["rev-parse", "--path-format=absolute", "--git-dir"]));
|
|
31
|
+
const gitCommonDir = fs.realpathSync(
|
|
32
|
+
gitLine(realCwd, ["rev-parse", "--path-format=absolute", "--git-common-dir"]),
|
|
33
|
+
);
|
|
34
|
+
const relativeCwd = path.relative(repoRoot, realCwd);
|
|
35
|
+
if (relativeCwd.startsWith("..") || path.isAbsolute(relativeCwd)) {
|
|
36
|
+
throw new Error("workflow cwd must remain inside its Git working tree");
|
|
37
|
+
}
|
|
38
|
+
const repositoryRootCommit = gitLine(realCwd, ["rev-list", "--max-parents=0", "HEAD"], true);
|
|
39
|
+
const identity = JSON.stringify({
|
|
40
|
+
kind: "git-worktree",
|
|
41
|
+
repoRoot,
|
|
42
|
+
repoRootFile: fileIdentity(repoRoot),
|
|
43
|
+
cwdFile: fileIdentity(realCwd),
|
|
44
|
+
gitDir,
|
|
45
|
+
gitDirFile: fileIdentity(gitDir),
|
|
46
|
+
gitCommonDir,
|
|
47
|
+
gitCommonDirFile: fileIdentity(gitCommonDir),
|
|
48
|
+
repositoryRootCommit,
|
|
49
|
+
relativeCwd,
|
|
50
|
+
});
|
|
51
|
+
return { identity, cwd: realCwd, repoRoot, relativeCwd, gitDir, gitCommonDir };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Read one Git plumbing line without stripping valid path whitespace. */
|
|
55
|
+
function gitLine(cwd: string, args: string[], optional = false): string {
|
|
56
|
+
try {
|
|
57
|
+
const output = execFileSync("git", args, {
|
|
58
|
+
cwd,
|
|
59
|
+
encoding: "utf8",
|
|
60
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
61
|
+
env: hermeticGitEnvironment(),
|
|
62
|
+
maxBuffer: MAX_GIT_PATH_BYTES,
|
|
63
|
+
timeout: 25_000,
|
|
64
|
+
killSignal: "SIGKILL",
|
|
65
|
+
});
|
|
66
|
+
return stripOneLineEnding(output);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (optional) return "";
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function fileIdentity(filePath: string): { dev: number; ino: number; birthtimeMs: number } {
|
|
74
|
+
const stat = fs.statSync(filePath);
|
|
75
|
+
return { dev: stat.dev, ino: stat.ino, birthtimeMs: stat.birthtimeMs };
|
|
76
|
+
}
|
|
@@ -16,21 +16,30 @@ export function ensurePrivateArtifactDirectory(dir: string, trustedRoot?: string
|
|
|
16
16
|
ensureContainedDirectory(trustedRoot, dir);
|
|
17
17
|
return;
|
|
18
18
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
const target = path.resolve(dir);
|
|
20
|
+
const missing: string[] = [];
|
|
21
|
+
let current = target;
|
|
22
|
+
while (true) {
|
|
23
|
+
try {
|
|
24
|
+
assertRealDirectory(current);
|
|
25
|
+
break;
|
|
26
|
+
} catch (error: any) {
|
|
27
|
+
if (error?.code !== "ENOENT") throw error;
|
|
28
|
+
missing.push(current);
|
|
29
|
+
const parent = path.dirname(current);
|
|
30
|
+
if (parent === current) throw error;
|
|
31
|
+
current = parent;
|
|
32
|
+
}
|
|
26
33
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
for (const directory of missing.reverse()) {
|
|
35
|
+
fs.mkdirSync(directory, { mode: 0o700 });
|
|
36
|
+
assertRealDirectory(directory);
|
|
37
|
+
if (process.platform !== "win32") fs.chmodSync(directory, 0o700);
|
|
38
|
+
fsyncArtifactDirectory(directory);
|
|
39
|
+
fsyncArtifactDirectory(path.dirname(directory));
|
|
33
40
|
}
|
|
41
|
+
assertRealDirectory(target);
|
|
42
|
+
if (process.platform !== "win32") fs.chmodSync(target, 0o700);
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
function ensureContainedDirectory(trustedRoot: string, dir: string): void {
|
|
@@ -112,6 +121,10 @@ export function readContainedArtifactFile(
|
|
|
112
121
|
maxBytes = 16 * 1024 * 1024,
|
|
113
122
|
): string {
|
|
114
123
|
const root = path.resolve(trustedRoot);
|
|
124
|
+
const rootStat = fs.lstatSync(root);
|
|
125
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
126
|
+
throw new Error(`${label} trusted root must be a real directory: ${root}`);
|
|
127
|
+
}
|
|
115
128
|
const target = path.resolve(filePath);
|
|
116
129
|
const relative = path.relative(root, target);
|
|
117
130
|
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
@@ -130,7 +143,40 @@ export function readContainedArtifactFile(
|
|
|
130
143
|
return readArtifactFile(target, label, maxBytes);
|
|
131
144
|
}
|
|
132
145
|
|
|
146
|
+
export function readContainedArtifactBytes(
|
|
147
|
+
trustedRoot: string,
|
|
148
|
+
filePath: string,
|
|
149
|
+
label: string,
|
|
150
|
+
maxBytes = 16 * 1024 * 1024,
|
|
151
|
+
): Buffer {
|
|
152
|
+
const root = path.resolve(trustedRoot);
|
|
153
|
+
const rootStat = fs.lstatSync(root);
|
|
154
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
155
|
+
throw new Error(`${label} trusted root must be a real directory: ${root}`);
|
|
156
|
+
}
|
|
157
|
+
const target = path.resolve(filePath);
|
|
158
|
+
const relative = path.relative(root, target);
|
|
159
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
160
|
+
throw new Error(`${label} must stay inside ${root}`);
|
|
161
|
+
}
|
|
162
|
+
let current = root;
|
|
163
|
+
const parts = relative.split(path.sep);
|
|
164
|
+
for (const [index, part] of parts.entries()) {
|
|
165
|
+
current = path.join(current, part);
|
|
166
|
+
const stat = fs.lstatSync(current);
|
|
167
|
+
if (stat.isSymbolicLink()) throw new Error(`${label} path may not contain symlinks: ${current}`);
|
|
168
|
+
if (index < parts.length - 1 && !stat.isDirectory()) {
|
|
169
|
+
throw new Error(`${label} parent must be a directory: ${current}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return readArtifactBytes(target, label, maxBytes);
|
|
173
|
+
}
|
|
174
|
+
|
|
133
175
|
export function readArtifactFile(filePath: string, label: string, maxBytes = 16 * 1024 * 1024): string {
|
|
176
|
+
return readArtifactBytes(filePath, label, maxBytes).toString("utf8");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function readArtifactBytes(filePath: string, label: string, maxBytes = 16 * 1024 * 1024): Buffer {
|
|
134
180
|
assertRegularArtifactFile(filePath, label);
|
|
135
181
|
const fd = fs.openSync(filePath, fs.constants.O_RDONLY | NO_FOLLOW);
|
|
136
182
|
try {
|
|
@@ -147,7 +193,7 @@ export function readArtifactFile(filePath: string, label: string, maxBytes = 16
|
|
|
147
193
|
total += bytesRead;
|
|
148
194
|
}
|
|
149
195
|
if (total > maxBytes) throw new Error(`${label} exceeds ${maxBytes} bytes: ${filePath}`);
|
|
150
|
-
return Buffer.concat(chunks, total)
|
|
196
|
+
return Buffer.concat(chunks, total);
|
|
151
197
|
} finally {
|
|
152
198
|
fs.closeSync(fd);
|
|
153
199
|
}
|