@nowcrew/daemon 0.6.68 → 0.6.69
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/dist/agent-ability/materializer.js +30 -1
- package/dist/agent-ability/resolver.js +27 -16
- package/dist/agent-ability/runtime.js +3 -0
- package/dist/agent-ability/types.js +6 -11
- package/dist/automatic-install-target.js +15 -10
- package/dist/background-job.js +50 -0
- package/dist/computer-cli.js +10 -1
- package/dist/computer-service.js +41 -1
- package/dist/config.js +44 -0
- package/dist/console-formatter.js +76 -1
- package/dist/console.js +23 -4
- package/dist/daemon-migration-controller.js +16 -18
- package/dist/execution-protocol.js +0 -1
- package/dist/execution-runner.js +60 -11
- package/dist/i18n.js +2 -0
- package/dist/local-execution-prompt.js +32 -13
- package/dist/local-executor.js +107 -55
- package/dist/local-memory/index-budget.js +74 -0
- package/dist/local-memory/pending.js +111 -0
- package/dist/local-memory/runtime.js +87 -0
- package/dist/local-memory/types.js +1 -0
- package/dist/local-memory/write.js +448 -0
- package/dist/machine-info.js +3 -4
- package/dist/main.js +83 -0
- package/dist/managed-service-lifecycle.js +47 -2
- package/dist/managed-service-startup.js +2 -0
- package/dist/profile-layout.js +10 -4
- package/dist/project-dependencies.js +139 -124
- package/dist/project-dependency-lock.js +87 -0
- package/dist/project-dependency-manifest.js +2 -12
- package/dist/project-registry-wiring.js +1 -4
- package/dist/project-skills/controller.js +2 -0
- package/dist/project-skills/runtime-launch.js +10 -7
- package/dist/prompt.js +19 -1
- package/dist/provider-env.js +3 -21
- package/dist/repository-identity.js +45 -0
- package/dist/runner.js +4 -2
- package/dist/runtimes/codex-app-server-runner.js +55 -23
- package/dist/runtimes/codex-command-safety.js +75 -0
- package/dist/runtimes/codex-contract.js +20 -5
- package/dist/runtimes/codex-deepseek-config.js +7 -17
- package/dist/runtimes/codex-turn-retry.js +69 -0
- package/dist/runtimes/codex.js +4 -3
- package/dist/serve.js +68 -3
- package/dist/storage-config.js +175 -0
- package/dist/storage-control.js +193 -0
- package/dist/storage-copy.js +628 -0
- package/dist/storage-credential-guard.js +21 -0
- package/dist/storage-doctor.js +228 -0
- package/dist/storage-identity.js +29 -0
- package/dist/storage-layout.js +143 -0
- package/dist/storage-manifest.js +855 -0
- package/dist/storage-migration-controller.js +420 -0
- package/dist/storage-migration-manifest.js +105 -0
- package/dist/storage-migration-policy.js +100 -0
- package/dist/storage-runtime-controller.js +339 -0
- package/dist/storage-runtime-readiness.js +184 -0
- package/dist/storage-runtime.js +39 -0
- package/dist/storage-safe-fs.js +302 -0
- package/dist/storage-serve-adapter.js +65 -0
- package/dist/storage-status-delivery.js +209 -0
- package/dist/storage-status-replay.js +125 -0
- package/dist/storage-target-confirmation.js +129 -0
- package/dist/storage-worker-command.js +37 -0
- package/dist/storage-worker-executor.js +430 -0
- package/dist/storage-worker-handoff.js +151 -0
- package/dist/storage-worker-operation.js +284 -0
- package/dist/storage-worker-status.js +259 -0
- package/dist/supervised-runtime.js +3 -1
- package/dist/workspace.js +34 -14
- package/package.json +1 -1
- package/dist/self-training-workspace.js +0 -15
|
@@ -14,6 +14,7 @@ export async function assertManagedServiceStartup(input) {
|
|
|
14
14
|
nodePath: input.nodePath,
|
|
15
15
|
entryPath: input.entryPath,
|
|
16
16
|
profileHome: input.daemonHome,
|
|
17
|
+
...(input.environment === undefined ? {} : { environment: input.environment }),
|
|
17
18
|
});
|
|
18
19
|
const readRegistry = input.readManagedServices
|
|
19
20
|
?? (() => readManagedServiceRegistry({ userHome: input.userHome }));
|
|
@@ -42,6 +43,7 @@ export async function assertManagedServiceStartup(input) {
|
|
|
42
43
|
nodePath: input.nodePath,
|
|
43
44
|
entryPath: input.entryPath,
|
|
44
45
|
profileHome: input.daemonHome,
|
|
46
|
+
...(input.environment === undefined ? {} : { environment: input.environment }),
|
|
45
47
|
legacyServiceId: true,
|
|
46
48
|
});
|
|
47
49
|
const legacyDescriptor = await readDescriptor(legacySpec);
|
package/dist/profile-layout.js
CHANGED
|
@@ -2,6 +2,10 @@ import { posix, win32 } from "node:path";
|
|
|
2
2
|
export const PROFILE_LAYOUT_VERSION = 1;
|
|
3
3
|
const PROFILE_NAME = /^[a-z0-9][a-z0-9_-]{0,47}$/u;
|
|
4
4
|
const LOCAL_INSTANCE_NAME = /^[a-z0-9][a-z0-9_-]{0,39}$/u;
|
|
5
|
+
/** Accept the legacy `in` tag at the boundary, but never persist it in a new layout. */
|
|
6
|
+
export function normalizeProfileEnvironment(value) {
|
|
7
|
+
return value === "in" ? "production" : value;
|
|
8
|
+
}
|
|
5
9
|
export function validateLocalInstanceName(value) {
|
|
6
10
|
const normalized = value.trim().toLowerCase();
|
|
7
11
|
if (!LOCAL_INSTANCE_NAME.test(normalized) || normalized.includes("..")) {
|
|
@@ -10,19 +14,21 @@ export function validateLocalInstanceName(value) {
|
|
|
10
14
|
return normalized;
|
|
11
15
|
}
|
|
12
16
|
export function profileForLocalInstance(environment, localInstanceName) {
|
|
13
|
-
const
|
|
17
|
+
const canonicalEnvironment = normalizeProfileEnvironment(environment);
|
|
18
|
+
const profile = `nw-${canonicalEnvironment}-${validateLocalInstanceName(localInstanceName)}`;
|
|
14
19
|
if (!PROFILE_NAME.test(profile))
|
|
15
20
|
throw new Error("derived daemon profile exceeds 48 characters");
|
|
16
21
|
return profile;
|
|
17
22
|
}
|
|
18
23
|
export function profileLayout(input) {
|
|
24
|
+
const environment = normalizeProfileEnvironment(input.environment);
|
|
19
25
|
const localInstanceName = validateLocalInstanceName(input.localInstanceName);
|
|
20
|
-
const profile = profileForLocalInstance(
|
|
26
|
+
const profile = profileForLocalInstance(environment, localInstanceName);
|
|
21
27
|
const paths = input.platform === "win32" ? win32 : posix;
|
|
22
|
-
const base = `${
|
|
28
|
+
const base = `${environment}-${localInstanceName}`;
|
|
23
29
|
return Object.freeze({
|
|
24
30
|
layoutVersion: PROFILE_LAYOUT_VERSION,
|
|
25
|
-
environment
|
|
31
|
+
environment,
|
|
26
32
|
localInstanceName,
|
|
27
33
|
profile,
|
|
28
34
|
daemonHome: paths.join(input.userHome, ".crew", `daemon-${base}`),
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import { lstat, mkdir, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { lstat, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, join, resolve, sep } from "node:path";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
import { parse } from "yaml";
|
|
6
6
|
import { validateAbilityGitHost } from "./agent-ability/resolver.js";
|
|
7
7
|
import { dslog } from "./slog.js";
|
|
8
|
+
import { ProjectDependencyLockTimeoutError, withProjectDependencyLock, } from "./project-dependency-lock.js";
|
|
9
|
+
import { normalizeRepositoryIdentity, worktreeDirectoryName } from "./repository-identity.js";
|
|
8
10
|
import { PROJECT_BRANCH, PROJECT_BRANCH_FORBIDDEN, PROJECT_MANIFEST_SOURCE_PATH, ProjectDependencyManifestSchema, } from "./project-dependency-manifest.js";
|
|
9
11
|
const runFile = promisify(execFile);
|
|
10
12
|
const PROJECT_MANIFEST_RELATIVE_PATH = PROJECT_MANIFEST_SOURCE_PATH;
|
|
@@ -14,12 +16,12 @@ export { ProjectDependencyManifestSchema } from "./project-dependency-manifest.j
|
|
|
14
16
|
export class ProjectDependencyError extends Error {
|
|
15
17
|
code;
|
|
16
18
|
detail;
|
|
17
|
-
|
|
18
|
-
constructor(code, detail
|
|
19
|
+
repositoryKey;
|
|
20
|
+
constructor(code, detail, repositoryKey) {
|
|
19
21
|
super(code);
|
|
20
22
|
this.code = code;
|
|
21
23
|
this.detail = detail;
|
|
22
|
-
this.
|
|
24
|
+
this.repositoryKey = repositoryKey;
|
|
23
25
|
this.name = "ProjectDependencyError";
|
|
24
26
|
}
|
|
25
27
|
}
|
|
@@ -46,16 +48,23 @@ const defaultGit = {
|
|
|
46
48
|
}
|
|
47
49
|
},
|
|
48
50
|
};
|
|
49
|
-
const safeBranchDirectory = (branch) => branch
|
|
50
|
-
.replaceAll("/", "__")
|
|
51
|
-
.replace(/[^A-Za-z0-9._-]/gu, "_")
|
|
52
|
-
.replace(/^[-.]+/u, "_")
|
|
53
|
-
.slice(0, 128) || "default";
|
|
54
51
|
const projectManifestPath = (trainingRoot) => resolve(trainingRoot, PROJECT_MANIFEST_RELATIVE_PATH);
|
|
55
52
|
const isWithin = (root, target) => {
|
|
56
53
|
const resolvedRoot = resolve(root);
|
|
57
54
|
const resolvedTarget = resolve(target);
|
|
58
|
-
return resolvedTarget === resolvedRoot || resolvedTarget.startsWith(
|
|
55
|
+
return resolvedTarget === resolvedRoot || resolvedTarget.startsWith(resolvedRoot + sep);
|
|
56
|
+
};
|
|
57
|
+
const existingAncestorIsSymlink = async (root, target) => {
|
|
58
|
+
const resolvedRoot = resolve(root);
|
|
59
|
+
let current = resolve(target);
|
|
60
|
+
while (current !== resolvedRoot && isWithin(resolvedRoot, current)) {
|
|
61
|
+
const info = await lstat(current).catch(() => null);
|
|
62
|
+
if (info?.isSymbolicLink())
|
|
63
|
+
return true;
|
|
64
|
+
current = dirname(current);
|
|
65
|
+
}
|
|
66
|
+
const rootInfo = await lstat(resolvedRoot).catch(() => null);
|
|
67
|
+
return rootInfo?.isSymbolicLink() === true;
|
|
59
68
|
};
|
|
60
69
|
async function readProjectManifest(trainingRoot) {
|
|
61
70
|
const path = projectManifestPath(trainingRoot);
|
|
@@ -78,127 +87,127 @@ async function readProjectManifest(trainingRoot) {
|
|
|
78
87
|
throw new ProjectDependencyError("project_dependency_manifest_invalid", "project dependency manifest schema is invalid");
|
|
79
88
|
}
|
|
80
89
|
}
|
|
81
|
-
async function resolveDefaultBranch(git, repo,
|
|
90
|
+
async function resolveDefaultBranch(git, repo, repositoryKey) {
|
|
82
91
|
let output;
|
|
83
92
|
try {
|
|
84
93
|
output = await git.run(["ls-remote", "--symref", repo, "HEAD"]);
|
|
85
94
|
}
|
|
86
|
-
catch
|
|
87
|
-
throw new ProjectDependencyError("project_dependency_default_branch_unavailable",
|
|
95
|
+
catch {
|
|
96
|
+
throw new ProjectDependencyError("project_dependency_default_branch_unavailable", "cannot resolve remote default branch", repositoryKey);
|
|
88
97
|
}
|
|
89
98
|
const branch = output.split(/\r?\n/u)
|
|
90
99
|
.map((line) => line.match(/^ref:\s+refs\/heads\/([^\s]+)\s+HEAD$/u)?.[1])
|
|
91
100
|
.find((value) => value !== undefined);
|
|
92
101
|
if (branch === undefined || !PROJECT_BRANCH.test(branch) || PROJECT_BRANCH_FORBIDDEN.test(branch)) {
|
|
93
|
-
throw new ProjectDependencyError("project_dependency_default_branch_unavailable",
|
|
102
|
+
throw new ProjectDependencyError("project_dependency_default_branch_unavailable", "remote default branch is unavailable", repositoryKey);
|
|
94
103
|
}
|
|
95
104
|
return branch;
|
|
96
105
|
}
|
|
97
|
-
async function
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
106
|
+
async function ensureWorktreeExclude(mainRoot) {
|
|
107
|
+
const excludePath = join(mainRoot, ".git", "info", "exclude");
|
|
108
|
+
const current = await readFile(excludePath, "utf8").catch(() => "");
|
|
109
|
+
if (current.split(/\r?\n/u).includes(".worktrees/"))
|
|
110
|
+
return;
|
|
111
|
+
const suffix = current.length === 0 || current.endsWith("\n") ? "" : "\n";
|
|
112
|
+
await writeFile(excludePath, current + suffix + ".worktrees\n", "utf8");
|
|
113
|
+
}
|
|
114
|
+
async function ensureMainClone(git, repo, repositoryKey, reposRoot) {
|
|
115
|
+
const mainRoot = resolve(reposRoot, repositoryKey);
|
|
116
|
+
if (!isWithin(reposRoot, mainRoot) || await existingAncestorIsSymlink(reposRoot, mainRoot)) {
|
|
117
|
+
throw new ProjectDependencyError("project_dependency_checkout_conflict", "repository path is unsafe", repositoryKey);
|
|
118
|
+
}
|
|
119
|
+
const info = await lstat(mainRoot).catch(() => null);
|
|
120
|
+
if (info?.isSymbolicLink() || (info !== null && !info.isDirectory())) {
|
|
121
|
+
throw new ProjectDependencyError("project_dependency_checkout_conflict", "main clone is not a directory", repositoryKey);
|
|
101
122
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
await mkdir(dirname(target), { recursive: true });
|
|
123
|
+
if (info === null) {
|
|
124
|
+
await mkdir(dirname(mainRoot), { recursive: true });
|
|
105
125
|
try {
|
|
106
|
-
await git.run(["clone", "--
|
|
126
|
+
await git.run(["clone", "--no-checkout", repo, mainRoot]);
|
|
107
127
|
}
|
|
108
|
-
catch
|
|
109
|
-
await rm(
|
|
110
|
-
throw new ProjectDependencyError("project_dependency_checkout_failed",
|
|
128
|
+
catch {
|
|
129
|
+
await rm(mainRoot, { recursive: true, force: true }).catch(() => { });
|
|
130
|
+
throw new ProjectDependencyError("project_dependency_checkout_failed", "main clone failed", repositoryKey);
|
|
111
131
|
}
|
|
112
132
|
}
|
|
113
133
|
else {
|
|
114
|
-
let origin;
|
|
115
134
|
try {
|
|
116
|
-
origin = await git.run(["remote", "get-url", "origin"],
|
|
117
|
-
if (origin !==
|
|
118
|
-
throw new ProjectDependencyError("project_dependency_checkout_conflict", "
|
|
135
|
+
const origin = await git.run(["remote", "get-url", "origin"], mainRoot);
|
|
136
|
+
if (normalizeRepositoryIdentity(origin).repositoryKey !== repositoryKey) {
|
|
137
|
+
throw new ProjectDependencyError("project_dependency_checkout_conflict", "clone origin does not match repo", repositoryKey);
|
|
119
138
|
}
|
|
120
|
-
|
|
139
|
+
await ensureWorktreeExclude(mainRoot);
|
|
140
|
+
const dirty = await git.run(["status", "--porcelain"], mainRoot);
|
|
121
141
|
if (dirty.length > 0) {
|
|
122
|
-
throw new ProjectDependencyError("project_dependency_checkout_dirty", "
|
|
142
|
+
throw new ProjectDependencyError("project_dependency_checkout_dirty", "main clone has local changes", repositoryKey);
|
|
123
143
|
}
|
|
124
|
-
await git.run(["fetch", "--no-tags", "origin", branch], target);
|
|
125
|
-
await git.run(["checkout", "--force", "-B", branch, `origin/${branch}`], target);
|
|
126
144
|
}
|
|
127
145
|
catch (error) {
|
|
128
146
|
if (error instanceof ProjectDependencyError)
|
|
129
147
|
throw error;
|
|
130
|
-
throw new ProjectDependencyError("project_dependency_checkout_conflict",
|
|
148
|
+
throw new ProjectDependencyError("project_dependency_checkout_conflict", "cannot inspect main clone", repositoryKey);
|
|
131
149
|
}
|
|
132
150
|
}
|
|
151
|
+
await ensureWorktreeExclude(mainRoot);
|
|
152
|
+
return mainRoot;
|
|
153
|
+
}
|
|
154
|
+
async function resolveCommit(git, project, branch, repositoryKey, mainRoot) {
|
|
133
155
|
try {
|
|
134
156
|
if (project.commit !== undefined) {
|
|
135
|
-
await git.run(["
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
157
|
+
await git.run(["fetch", "--no-tags", "origin", project.commit], mainRoot);
|
|
158
|
+
const resolved = await git.run(["rev-parse", "FETCH_HEAD"], mainRoot);
|
|
159
|
+
if (resolved !== project.commit) {
|
|
160
|
+
throw new ProjectDependencyError("project_dependency_commit_mismatch", "requested commit was not resolved", repositoryKey);
|
|
161
|
+
}
|
|
162
|
+
return resolved;
|
|
140
163
|
}
|
|
141
|
-
|
|
164
|
+
await git.run(["fetch", "--no-tags", "origin", branch], mainRoot);
|
|
165
|
+
return await git.run(["rev-parse", "origin/" + branch], mainRoot);
|
|
142
166
|
}
|
|
143
|
-
catch {
|
|
144
|
-
if (
|
|
145
|
-
throw
|
|
146
|
-
|
|
147
|
-
throw new ProjectDependencyError("project_dependency_checkout_failed", "checkout commit cannot be resolved", project.id);
|
|
167
|
+
catch (error) {
|
|
168
|
+
if (error instanceof ProjectDependencyError)
|
|
169
|
+
throw error;
|
|
170
|
+
throw new ProjectDependencyError("project_dependency_checkout_failed", "cannot resolve repository commit", repositoryKey);
|
|
148
171
|
}
|
|
149
172
|
}
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const info = await lstat(current).catch(() => null);
|
|
155
|
-
if (info?.isSymbolicLink())
|
|
156
|
-
return true;
|
|
157
|
-
current = dirname(current);
|
|
173
|
+
async function ensureWorktree(git, mainRoot, repositoryKey, branch, resolvedCommit) {
|
|
174
|
+
const worktreeRoot = resolve(mainRoot, ".worktrees", worktreeDirectoryName(branch, resolvedCommit));
|
|
175
|
+
if (!isWithin(mainRoot, worktreeRoot) || await existingAncestorIsSymlink(mainRoot, worktreeRoot)) {
|
|
176
|
+
throw new ProjectDependencyError("project_dependency_checkout_conflict", "worktree path is unsafe", repositoryKey);
|
|
158
177
|
}
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
async function ensureRegistered(registry, project, root) {
|
|
163
|
-
let registrations;
|
|
164
|
-
try {
|
|
165
|
-
registrations = await registry.list();
|
|
178
|
+
const info = await lstat(worktreeRoot).catch(() => null);
|
|
179
|
+
if (info?.isSymbolicLink() || (info !== null && !info.isDirectory())) {
|
|
180
|
+
throw new ProjectDependencyError("project_dependency_checkout_conflict", "worktree target is not a directory", repositoryKey);
|
|
166
181
|
}
|
|
167
|
-
|
|
168
|
-
throw new ProjectDependencyError("project_dependency_registration_conflict", `cannot read project registry for ${project.id}`, project.id);
|
|
169
|
-
}
|
|
170
|
-
const existing = registrations.find((candidate) => candidate.projectId === project.id);
|
|
171
|
-
if (existing === undefined) {
|
|
182
|
+
if (info !== null) {
|
|
172
183
|
try {
|
|
173
|
-
await
|
|
184
|
+
const head = await git.run(["rev-parse", "HEAD"], worktreeRoot);
|
|
185
|
+
const dirty = await git.run(["status", "--porcelain"], worktreeRoot);
|
|
186
|
+
if (dirty.length > 0) {
|
|
187
|
+
throw new ProjectDependencyError("project_dependency_checkout_dirty", "worktree has local changes", repositoryKey);
|
|
188
|
+
}
|
|
189
|
+
if (head !== resolvedCommit) {
|
|
190
|
+
throw new ProjectDependencyError("project_dependency_checkout_conflict", "worktree commit does not match", repositoryKey);
|
|
191
|
+
}
|
|
192
|
+
return worktreeRoot;
|
|
174
193
|
}
|
|
175
194
|
catch (error) {
|
|
176
|
-
|
|
195
|
+
if (error instanceof ProjectDependencyError)
|
|
196
|
+
throw error;
|
|
197
|
+
throw new ProjectDependencyError("project_dependency_checkout_conflict", "cannot inspect worktree", repositoryKey);
|
|
177
198
|
}
|
|
178
|
-
return;
|
|
179
199
|
}
|
|
180
|
-
|
|
181
|
-
return;
|
|
182
|
-
// A manifest branch change intentionally moves the logical project ID to the
|
|
183
|
-
// new checkout. Keep the old registration if the replacement cannot finish.
|
|
200
|
+
await mkdir(dirname(worktreeRoot), { recursive: true });
|
|
184
201
|
try {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
throw new Error("project registration disappeared before replacement");
|
|
188
|
-
}
|
|
189
|
-
try {
|
|
190
|
-
await registry.add(project.id, root);
|
|
191
|
-
}
|
|
192
|
-
catch (error) {
|
|
193
|
-
await registry.add(project.id, existing.root).catch(() => { });
|
|
194
|
-
throw error;
|
|
195
|
-
}
|
|
202
|
+
await git.run(["worktree", "add", "--detach", worktreeRoot, resolvedCommit], mainRoot);
|
|
203
|
+
return worktreeRoot;
|
|
196
204
|
}
|
|
197
205
|
catch {
|
|
198
|
-
|
|
206
|
+
await rm(worktreeRoot, { recursive: true, force: true }).catch(() => { });
|
|
207
|
+
throw new ProjectDependencyError("project_dependency_checkout_failed", "worktree creation failed", repositoryKey);
|
|
199
208
|
}
|
|
200
209
|
}
|
|
201
|
-
export function createProjectDependencyProvisioner(options) {
|
|
210
|
+
export function createProjectDependencyProvisioner(options = {}) {
|
|
202
211
|
const git = options.git ?? defaultGit;
|
|
203
212
|
return {
|
|
204
213
|
async prepare(input) {
|
|
@@ -206,60 +215,66 @@ export function createProjectDependencyProvisioner(options) {
|
|
|
206
215
|
dslog("project_dependencies.manifest", "读取项目依赖清单", {
|
|
207
216
|
training_root: input.trainingRoot,
|
|
208
217
|
manifest_present: manifest !== null,
|
|
209
|
-
|
|
210
|
-
declared_project_ids: JSON.stringify(manifest?.projects.map((project) => project.id) ?? []),
|
|
218
|
+
declared_project_count: manifest?.projects.length ?? 0,
|
|
211
219
|
});
|
|
212
|
-
if (
|
|
220
|
+
if (manifest === null)
|
|
213
221
|
return Object.freeze([]);
|
|
214
|
-
if (manifest === null) {
|
|
215
|
-
throw new ProjectDependencyError("project_dependency_manifest_missing", "project dependency manifest is missing from the active training package");
|
|
216
|
-
}
|
|
217
222
|
if (!isWithin(input.trainingRoot, projectManifestPath(input.trainingRoot))) {
|
|
218
223
|
throw new ProjectDependencyError("project_dependency_manifest_invalid", "project manifest path escapes training root");
|
|
219
224
|
}
|
|
220
|
-
const
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
for (const projectId of requested) {
|
|
224
|
-
const project = byId.get(projectId);
|
|
225
|
-
if (project === undefined) {
|
|
226
|
-
throw new ProjectDependencyError("project_dependency_not_declared", `project ${projectId} is not declared by the active training package`, projectId);
|
|
227
|
-
}
|
|
225
|
+
const grouped = new Map();
|
|
226
|
+
for (const project of manifest.projects) {
|
|
227
|
+
let identity;
|
|
228
228
|
try {
|
|
229
229
|
validateAbilityGitHost(project.repo);
|
|
230
|
+
identity = normalizeRepositoryIdentity(project.repo);
|
|
230
231
|
}
|
|
231
232
|
catch {
|
|
232
|
-
throw new ProjectDependencyError("project_dependency_host_not_allowed", "repository host is not allowed"
|
|
233
|
-
}
|
|
234
|
-
const branchSource = project.branch === undefined ? "remote-default" : "manifest";
|
|
235
|
-
const branch = project.branch ?? await resolveDefaultBranch(git, project.repo, project.id);
|
|
236
|
-
const root = resolve(input.projectsRoot, project.id, safeBranchDirectory(branch));
|
|
237
|
-
if (!isWithin(input.projectsRoot, root)) {
|
|
238
|
-
throw new ProjectDependencyError("project_dependency_checkout_conflict", "checkout path escapes Projects root", project.id);
|
|
233
|
+
throw new ProjectDependencyError("project_dependency_host_not_allowed", "repository host is not allowed");
|
|
239
234
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
235
|
+
const existing = grouped.get(identity.repositoryKey);
|
|
236
|
+
if (existing === undefined)
|
|
237
|
+
grouped.set(identity.repositoryKey, { identity, projects: [project] });
|
|
238
|
+
else
|
|
239
|
+
existing.projects.push(project);
|
|
240
|
+
}
|
|
241
|
+
const results = [];
|
|
242
|
+
const lockRoot = input.lockRoot ?? join(dirname(resolve(input.reposRoot)), ".crew", "project-dependencies");
|
|
243
|
+
for (const { identity, projects } of grouped.values()) {
|
|
244
244
|
try {
|
|
245
|
-
await
|
|
245
|
+
await withProjectDependencyLock(lockRoot, identity.repositoryKey, async () => {
|
|
246
|
+
const mainRoot = await ensureMainClone(git, projects[0].repo, identity.repositoryKey, input.reposRoot);
|
|
247
|
+
const seenVersions = new Set();
|
|
248
|
+
for (const project of projects) {
|
|
249
|
+
const branchSource = project.branch === undefined ? "remote-default" : "manifest";
|
|
250
|
+
const branch = project.branch ?? await resolveDefaultBranch(git, project.repo, identity.repositoryKey);
|
|
251
|
+
const resolvedCommit = await resolveCommit(git, project, branch, identity.repositoryKey, mainRoot);
|
|
252
|
+
const versionKey = branch + "\0" + resolvedCommit;
|
|
253
|
+
if (seenVersions.has(versionKey))
|
|
254
|
+
continue;
|
|
255
|
+
seenVersions.add(versionKey);
|
|
256
|
+
const localRoot = await ensureWorktree(git, mainRoot, identity.repositoryKey, branch, resolvedCommit);
|
|
257
|
+
results.push(Object.freeze({
|
|
258
|
+
repositoryKey: identity.repositoryKey,
|
|
259
|
+
displayName: identity.displayName,
|
|
260
|
+
repo: project.repo,
|
|
261
|
+
branch,
|
|
262
|
+
branchSource,
|
|
263
|
+
...(project.commit === undefined ? {} : { requestedCommit: project.commit }),
|
|
264
|
+
resolvedCommit,
|
|
265
|
+
checkoutMode: "worktree",
|
|
266
|
+
localRoot,
|
|
267
|
+
summary: project.summary,
|
|
268
|
+
}));
|
|
269
|
+
}
|
|
270
|
+
});
|
|
246
271
|
}
|
|
247
272
|
catch (error) {
|
|
248
|
-
if (
|
|
249
|
-
|
|
273
|
+
if (error instanceof ProjectDependencyLockTimeoutError) {
|
|
274
|
+
throw new ProjectDependencyError("project_dependency_checkout_conflict", "project preparation lock timed out", identity.repositoryKey);
|
|
275
|
+
}
|
|
250
276
|
throw error;
|
|
251
277
|
}
|
|
252
|
-
results.push(Object.freeze({
|
|
253
|
-
projectId: project.id,
|
|
254
|
-
repo: project.repo,
|
|
255
|
-
branch,
|
|
256
|
-
branchSource,
|
|
257
|
-
...(project.commit === undefined ? {} : { requestedCommit: project.commit }),
|
|
258
|
-
resolvedCommit: checkout.resolvedCommit,
|
|
259
|
-
checkoutMode: "shared",
|
|
260
|
-
localRoot: root,
|
|
261
|
-
summary: project.summary,
|
|
262
|
-
}));
|
|
263
278
|
}
|
|
264
279
|
return Object.freeze(results);
|
|
265
280
|
},
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
const LOCK_RETRY_MS = 100;
|
|
4
|
+
const LOCK_WAIT_TIMEOUT_MS = 120_000;
|
|
5
|
+
const LOCK_STALE_MS = 15 * 60_000;
|
|
6
|
+
const localTails = new Map();
|
|
7
|
+
export class ProjectDependencyLockTimeoutError extends Error {
|
|
8
|
+
lockPath;
|
|
9
|
+
constructor(lockPath) {
|
|
10
|
+
super("project dependency preparation lock timed out");
|
|
11
|
+
this.lockPath = lockPath;
|
|
12
|
+
this.name = "ProjectDependencyLockTimeoutError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const delay = (milliseconds) => new Promise((resolveDelay) => setTimeout(resolveDelay, milliseconds));
|
|
16
|
+
const processIsAlive = (pid) => {
|
|
17
|
+
try {
|
|
18
|
+
process.kill(pid, 0);
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
return error.code !== "ESRCH";
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
async function staleLock(lockPath) {
|
|
26
|
+
const owner = await readFile(join(lockPath, "owner"), "utf8").catch(() => "");
|
|
27
|
+
const pid = Number.parseInt(owner.split(" ", 1)[0] ?? "", 10);
|
|
28
|
+
if (Number.isInteger(pid) && pid > 0)
|
|
29
|
+
return !processIsAlive(pid);
|
|
30
|
+
const lockInfo = await stat(lockPath).catch(() => null);
|
|
31
|
+
return lockInfo !== null && Date.now() - lockInfo.mtimeMs > LOCK_STALE_MS;
|
|
32
|
+
}
|
|
33
|
+
async function acquire(lockPath) {
|
|
34
|
+
const startedAt = Date.now();
|
|
35
|
+
while (true) {
|
|
36
|
+
try {
|
|
37
|
+
await mkdir(lockPath);
|
|
38
|
+
try {
|
|
39
|
+
await writeFile(join(lockPath, "owner"), `${process.pid} ${Date.now()}\n`, "utf8");
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
await rm(lockPath, { recursive: true, force: true }).catch(() => { });
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
return async () => {
|
|
46
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (error.code !== "EEXIST")
|
|
51
|
+
throw error;
|
|
52
|
+
if (await staleLock(lockPath)) {
|
|
53
|
+
await rm(lockPath, { recursive: true, force: true }).catch(() => { });
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (Date.now() - startedAt >= LOCK_WAIT_TIMEOUT_MS) {
|
|
57
|
+
throw new ProjectDependencyLockTimeoutError(lockPath);
|
|
58
|
+
}
|
|
59
|
+
await delay(LOCK_RETRY_MS);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export async function withProjectDependencyLock(lockRoot, repositoryKey, operation) {
|
|
64
|
+
const root = resolve(lockRoot);
|
|
65
|
+
const lockPath = join(root, `${repositoryKey}.lock`);
|
|
66
|
+
await mkdir(root, { recursive: true });
|
|
67
|
+
const previous = localTails.get(lockPath) ?? Promise.resolve();
|
|
68
|
+
let releaseLocal;
|
|
69
|
+
const current = new Promise((resolveCurrent) => { releaseLocal = resolveCurrent; });
|
|
70
|
+
localTails.set(lockPath, current);
|
|
71
|
+
await previous;
|
|
72
|
+
let releaseFile;
|
|
73
|
+
try {
|
|
74
|
+
releaseFile = await acquire(lockPath);
|
|
75
|
+
return await operation();
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
try {
|
|
79
|
+
await releaseFile?.();
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
releaseLocal();
|
|
83
|
+
if (localTails.get(lockPath) === current)
|
|
84
|
+
localTails.delete(lockPath);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -1,21 +1,16 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { AbilityRepositoryUrlSchema } from "./agent-ability/types.js";
|
|
3
|
-
|
|
4
|
-
export const PROJECT_MANIFEST_SOURCE_PATH = "workspace/project-dependencies.yaml";
|
|
3
|
+
export const PROJECT_MANIFEST_SOURCE_PATH = "git/project-dependencies.yaml";
|
|
5
4
|
export const PROJECT_BRANCH = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,255}$/u;
|
|
6
5
|
export const PROJECT_BRANCH_FORBIDDEN = /(?:^|\/)\.\.?(?:\/|$)|[\s\p{Cc}\\]/u;
|
|
7
6
|
export const PROJECT_SUMMARY_MAX_LENGTH = 1_000;
|
|
8
7
|
export const PROJECT_COMMIT = /^[a-f0-9]{40}$/u;
|
|
9
8
|
const ProjectDependencySchema = z.object({
|
|
10
|
-
id: z.string().min(1).max(64),
|
|
11
9
|
repo: AbilityRepositoryUrlSchema,
|
|
12
10
|
branch: z.string().min(1).max(256).optional(),
|
|
13
11
|
commit: z.string().optional(),
|
|
14
12
|
summary: z.string().min(1).max(PROJECT_SUMMARY_MAX_LENGTH),
|
|
15
13
|
}).strict().superRefine((project, ctx) => {
|
|
16
|
-
if (!isProjectId(project.id)) {
|
|
17
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["id"], message: "invalid project id" });
|
|
18
|
-
}
|
|
19
14
|
if (project.branch !== undefined
|
|
20
15
|
&& (!PROJECT_BRANCH.test(project.branch) || PROJECT_BRANCH_FORBIDDEN.test(project.branch))) {
|
|
21
16
|
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["branch"], message: "invalid project branch" });
|
|
@@ -27,9 +22,4 @@ const ProjectDependencySchema = z.object({
|
|
|
27
22
|
export const ProjectDependencyManifestSchema = z.object({
|
|
28
23
|
version: z.literal(1),
|
|
29
24
|
projects: z.array(ProjectDependencySchema).min(1).max(32),
|
|
30
|
-
}).strict()
|
|
31
|
-
const ids = manifest.projects.map((project) => project.id);
|
|
32
|
-
if (new Set(ids).size !== ids.length) {
|
|
33
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["projects"], message: "duplicate project id" });
|
|
34
|
-
}
|
|
35
|
-
});
|
|
25
|
+
}).strict();
|
|
@@ -14,9 +14,6 @@ export function createProjectRegistryWiring(agentsRoot, input = {}) {
|
|
|
14
14
|
?? writableInjectedRegistry
|
|
15
15
|
?? createProjectRegistry(agentsRoot);
|
|
16
16
|
const projectWorkspaceRegistry = input.projectWorkspaceRegistry ?? projectRegistry;
|
|
17
|
-
if (input.projectDependencies !== undefined && input.projectRegistry === undefined) {
|
|
18
|
-
throw new Error("project_dependency_injection_requires_project_registry");
|
|
19
|
-
}
|
|
20
17
|
if (input.projectWorkspaceRegistry !== undefined && projectWorkspaceRegistry !== projectRegistry) {
|
|
21
18
|
throw new Error("project_registry_injection_requires_writable_registry");
|
|
22
19
|
}
|
|
@@ -24,6 +21,6 @@ export function createProjectRegistryWiring(agentsRoot, input = {}) {
|
|
|
24
21
|
projectRegistry,
|
|
25
22
|
projectWorkspaceRegistry,
|
|
26
23
|
projectDependencyProvisioner: input.projectDependencies
|
|
27
|
-
?? createProjectDependencyProvisioner(
|
|
24
|
+
?? createProjectDependencyProvisioner(),
|
|
28
25
|
});
|
|
29
26
|
}
|
|
@@ -183,6 +183,8 @@ export function createProjectSkillsController(deps) {
|
|
|
183
183
|
const parsed = ProjectCommandSchema.safeParse(input);
|
|
184
184
|
if (!parsed.success)
|
|
185
185
|
return { ok: false, error: "invalid_project_command" };
|
|
186
|
+
if (deps.isAdmissionClosed?.() === true)
|
|
187
|
+
return { ok: false, error: "storage_migration_quiescing" };
|
|
186
188
|
try {
|
|
187
189
|
assertCurrentEpoch(epochToken);
|
|
188
190
|
}
|
|
@@ -57,6 +57,7 @@ export function projectSkillRuntimeLaunchFacts(runtime, root) {
|
|
|
57
57
|
}
|
|
58
58
|
/** Builds Runtime-native arrays while keeping v1 shared roots separate from v2 exact roots. */
|
|
59
59
|
export async function projectSkillRuntimeDirectories(input) {
|
|
60
|
+
const trainingProjectRoots = input.trainingProjectRoots ?? [];
|
|
60
61
|
const facts = projectSkillRuntimeLaunchFacts(input.runtime, input.runtimeRoot);
|
|
61
62
|
const codexSkillRoots = input.runtime === "codex"
|
|
62
63
|
&& (input.runtimeRoot !== undefined || input.projectContext !== undefined)
|
|
@@ -79,20 +80,22 @@ export async function projectSkillRuntimeDirectories(input) {
|
|
|
79
80
|
claudeAdditionalDirectories: await resolveOrderedUniqueClaudeDirectories([
|
|
80
81
|
...(input.projectContext?.secondary.map((project) => project.root) ?? []),
|
|
81
82
|
facts.claudeAdditionalDirectory,
|
|
83
|
+
...trainingProjectRoots,
|
|
82
84
|
...(input.abilitySkillRoot === undefined ? [] : [input.abilitySkillRoot]),
|
|
83
85
|
], input.projectContext?.primary === undefined ? [] : [input.projectContext.primary.root]),
|
|
84
86
|
});
|
|
85
87
|
}
|
|
86
88
|
const claudeAdditionalDirectories = input.projectContext === undefined
|
|
87
|
-
?
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
89
|
+
? await resolveOrderedUniqueClaudeDirectories([
|
|
90
|
+
...(input.projectSkillsPresent || input.abilitySkillRoot !== undefined
|
|
91
|
+
? [join(input.agentRoot, ".crew", "claude-skills"), input.agentRoot]
|
|
92
|
+
: []),
|
|
93
|
+
...trainingProjectRoots,
|
|
94
|
+
...(input.abilitySkillRoot === undefined ? [] : [input.abilitySkillRoot]),
|
|
95
|
+
])
|
|
94
96
|
: await resolveOrderedUniqueClaudeDirectories([
|
|
95
97
|
...input.projectContext.secondary.map((project) => project.root),
|
|
98
|
+
...trainingProjectRoots,
|
|
96
99
|
...(input.projectSkillsPresent
|
|
97
100
|
? [join(input.agentRoot, ".crew", "claude-skills")]
|
|
98
101
|
: []),
|