@nowcrew/daemon 0.6.16 → 0.6.18
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/runtime-context.js +7 -1
- package/dist/atomic-private-write.js +54 -1
- package/dist/automatic-install-target.js +40 -11
- package/dist/console.js +9 -0
- package/dist/control-plane-url.js +2 -2
- package/dist/daemon-migration-controller.js +198 -0
- package/dist/daemon-migration-wiring.js +22 -0
- package/dist/daemon-update-eligibility.js +1 -1
- package/dist/directory-projection-identity.js +32 -0
- package/dist/directory-projection.js +922 -0
- package/dist/execution-protocol.js +78 -11
- package/dist/execution-runner.js +50 -2
- package/dist/i18n.js +1 -0
- package/dist/local-execution-prompt.js +57 -0
- package/dist/local-executor.js +99 -40
- package/dist/machine-info.js +45 -9
- package/dist/main.js +0 -0
- package/dist/normalize.js +5 -0
- package/dist/profile-layout.js +41 -0
- package/dist/project-skills/controller.js +74 -14
- package/dist/project-skills/execution-adapter.js +11 -0
- package/dist/project-skills/initialized-reconciler.js +20 -0
- package/dist/project-skills/projection-set-switch.js +419 -0
- package/dist/project-skills/projection-state-domain.js +153 -0
- package/dist/project-skills/projection-state-store.js +841 -0
- package/dist/project-skills/projection-state-transaction.js +318 -0
- package/dist/project-skills/projection-state.js +3 -0
- package/dist/project-skills/reconciler.js +299 -68
- package/dist/project-skills/runtime-warning.js +6 -0
- package/dist/project-skills/scanner.js +30 -1
- package/dist/project-skills/types.js +9 -0
- package/dist/project-workspaces/resolver.js +179 -0
- package/dist/project-workspaces/types.js +1 -0
- package/dist/prompt.js +40 -0
- package/dist/runtimes/claude.js +235 -4
- package/dist/runtimes/codex-app-server-runner.js +100 -25
- package/dist/runtimes/codex-contract.js +123 -0
- package/dist/runtimes/codex.js +2 -0
- package/dist/serve.js +31 -17
- package/dist/session.js +3 -0
- package/dist/supervised-runtime.js +12 -4
- package/dist/workspace.js +14 -5
- package/package.json +10 -9
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import { cp, lstat, mkdir, readdir, readlink,
|
|
1
|
+
import { cp, lstat, mkdir, readdir, readlink, stat, symlink, writeFile } from "node:fs/promises";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { dirname, join,
|
|
3
|
+
import { dirname, join, posix, win32 } from "node:path";
|
|
4
|
+
import { isManagedDirectoryProjectionCopy, projectDirectory, } from "../directory-projection.js";
|
|
4
5
|
import { runtimeProjectionLifetime, } from "./agent-projection-coordinator.js";
|
|
6
|
+
import { commitPreparedProjectSkillManifestUpdate, computeProjectSkillBindingDigest, createAppliedProjectSkillManifest, prepareAppliedProjectSkillManifestRemoval, prepareAppliedProjectSkillManifestUpdate, publishPreparedProjectSkillManifestUpdate, readAppliedProjectSkillManifest, rollbackPreparedProjectSkillManifestUpdate, } from "./projection-state.js";
|
|
7
|
+
import { compareProjectSkillRefs, } from "./types.js";
|
|
8
|
+
import { projectSkillProjectionExpectation, projectSkillResolutionRecords, } from "./scanner.js";
|
|
9
|
+
import { cleanupProjectionStaging, isManagedProjectionRoot, markProjectionStaging, PROJECTION_ROOT_MARKER_NAME, projectionTargets, recoverProjectionSwitch, switchProjectionSet, } from "./projection-set-switch.js";
|
|
5
10
|
export class ProjectProjectionError extends Error {
|
|
6
11
|
code;
|
|
7
12
|
constructor(code) {
|
|
@@ -10,21 +15,72 @@ export class ProjectProjectionError extends Error {
|
|
|
10
15
|
this.name = "ProjectProjectionError";
|
|
11
16
|
}
|
|
12
17
|
}
|
|
18
|
+
export const decideProjectSkillProjection = (expected, applied) => {
|
|
19
|
+
if (applied === null || expected.generation > applied.generation) {
|
|
20
|
+
return Object.freeze({ kind: "ensure-applied" });
|
|
21
|
+
}
|
|
22
|
+
if (expected.generation < applied.generation) {
|
|
23
|
+
return Object.freeze({ kind: "use-advanced", warning: "skill_projection_advanced" });
|
|
24
|
+
}
|
|
25
|
+
if (expected.bindingDigest !== applied.bindingDigest) {
|
|
26
|
+
return Object.freeze({ kind: "reject", code: "skill_projection_snapshot_corrupt" });
|
|
27
|
+
}
|
|
28
|
+
return expected.resolutionDigest === applied.resolutionDigest
|
|
29
|
+
? Object.freeze({ kind: "use-current" })
|
|
30
|
+
: Object.freeze({ kind: "ensure-applied" });
|
|
31
|
+
};
|
|
13
32
|
const exists = async (path) => lstat(path).then(() => true, () => false);
|
|
14
|
-
const
|
|
33
|
+
const normalizeWindowsLinkIdentity = (value) => {
|
|
34
|
+
if (/^\\\\\?\\UNC\\/iu.test(value))
|
|
35
|
+
return `\\\\${value.slice(8)}`;
|
|
36
|
+
if (/^\\\\\?\\[A-Za-z]:\\/u.test(value))
|
|
37
|
+
return value.slice(4);
|
|
38
|
+
return value;
|
|
39
|
+
};
|
|
40
|
+
const isSameOrWithin = (root, candidate, path) => {
|
|
41
|
+
const relative = path.relative(root, candidate);
|
|
42
|
+
return relative === ""
|
|
43
|
+
|| (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
44
|
+
};
|
|
45
|
+
export const canonicalTrainingSkillLinkTarget = (source, link, trainingSkillsRoot, platform) => {
|
|
46
|
+
const path = platform === "win32" ? win32 : posix;
|
|
47
|
+
const normalizedSource = platform === "win32" ? normalizeWindowsLinkIdentity(source) : source;
|
|
48
|
+
const normalizedLink = platform === "win32" ? normalizeWindowsLinkIdentity(link) : link;
|
|
49
|
+
const normalizedTrainingRoot = platform === "win32"
|
|
50
|
+
? normalizeWindowsLinkIdentity(trainingSkillsRoot)
|
|
51
|
+
: trainingSkillsRoot;
|
|
52
|
+
const resolvedLink = path.resolve(path.dirname(normalizedSource), normalizedLink);
|
|
53
|
+
return isSameOrWithin(path.normalize(normalizedTrainingRoot), resolvedLink, path)
|
|
54
|
+
? path.normalize(resolvedLink)
|
|
55
|
+
: null;
|
|
56
|
+
};
|
|
57
|
+
export const isTrainingSkillLink = (source, link, trainingSkillsRoot, platform) => canonicalTrainingSkillLinkTarget(source, link, trainingSkillsRoot, platform) !== null;
|
|
58
|
+
const preserveNonProjectSkills = async (projectionTarget, trainingSkillsRoot, platform) => {
|
|
15
59
|
const names = new Set();
|
|
16
|
-
if (!await exists(target))
|
|
60
|
+
if (!await exists(projectionTarget.target))
|
|
17
61
|
return names;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
62
|
+
const rootInfo = await lstat(projectionTarget.target);
|
|
63
|
+
if (!rootInfo.isDirectory() || rootInfo.isSymbolicLink()) {
|
|
64
|
+
throw new ProjectProjectionError("skill_projection_failed");
|
|
65
|
+
}
|
|
66
|
+
const managedRoot = await isManagedProjectionRoot(projectionTarget.target, projectionTarget);
|
|
67
|
+
for (const entry of await readdir(projectionTarget.target, { withFileTypes: true })) {
|
|
68
|
+
if (entry.name === PROJECTION_ROOT_MARKER_NAME) {
|
|
69
|
+
if (managedRoot)
|
|
70
|
+
continue;
|
|
71
|
+
throw new ProjectProjectionError("skill_projection_failed");
|
|
72
|
+
}
|
|
73
|
+
const source = join(projectionTarget.target, entry.name);
|
|
74
|
+
const destination = join(projectionTarget.staging, entry.name);
|
|
75
|
+
if (platform === "win32" && await isManagedDirectoryProjectionCopy(source, platform))
|
|
76
|
+
continue;
|
|
21
77
|
const info = await lstat(source);
|
|
22
78
|
if (info.isSymbolicLink()) {
|
|
23
79
|
const link = await readlink(source);
|
|
24
|
-
const
|
|
25
|
-
if (
|
|
80
|
+
const canonicalTarget = canonicalTrainingSkillLinkTarget(source, link, trainingSkillsRoot, platform);
|
|
81
|
+
if (canonicalTarget === null)
|
|
26
82
|
continue;
|
|
27
|
-
await symlink(link, destination, "dir");
|
|
83
|
+
await symlink(platform === "win32" ? canonicalTarget : link, destination, platform === "win32" ? "junction" : "dir");
|
|
28
84
|
}
|
|
29
85
|
else {
|
|
30
86
|
await cp(source, destination, { recursive: true, preserveTimestamps: true });
|
|
@@ -33,41 +89,39 @@ const preserveNonProjectSkills = async (target, staging, trainingSkillsRoot) =>
|
|
|
33
89
|
}
|
|
34
90
|
return names;
|
|
35
91
|
};
|
|
36
|
-
async function switchProjectionSet(targets) {
|
|
37
|
-
const moved = [];
|
|
38
|
-
try {
|
|
39
|
-
for (const target of targets) {
|
|
40
|
-
const hadPrevious = await exists(target.target);
|
|
41
|
-
if (hadPrevious)
|
|
42
|
-
await rename(target.target, target.backup);
|
|
43
|
-
const state = { target, hadPrevious, activated: false };
|
|
44
|
-
moved.push(state);
|
|
45
|
-
await rename(target.staging, target.target);
|
|
46
|
-
state.activated = true;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
catch (error) {
|
|
50
|
-
for (const state of [...moved].reverse()) {
|
|
51
|
-
if (state.activated)
|
|
52
|
-
await rm(state.target.target, { recursive: true, force: true }).catch(() => { });
|
|
53
|
-
if (state.hadPrevious)
|
|
54
|
-
await rename(state.target.backup, state.target.target).catch(() => { });
|
|
55
|
-
}
|
|
56
|
-
throw error;
|
|
57
|
-
}
|
|
58
|
-
await Promise.all(targets.map((target) => rm(target.backup, { recursive: true, force: true })));
|
|
59
|
-
}
|
|
60
92
|
export function createProjectSkillsReconciler(deps) {
|
|
61
93
|
const platform = deps.platform ?? process.platform;
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
94
|
+
const directoryProjector = deps.projectDirectory ?? projectDirectory;
|
|
95
|
+
const readOptions = { platform };
|
|
96
|
+
const readApplied = deps.readAppliedManifest ?? readAppliedProjectSkillManifest;
|
|
97
|
+
const prepareRemoval = deps.prepareAppliedManifestRemoval ?? prepareAppliedProjectSkillManifestRemoval;
|
|
98
|
+
const prepareUpdate = deps.prepareAppliedManifestUpdate ?? prepareAppliedProjectSkillManifestUpdate;
|
|
99
|
+
const publishUpdate = deps.publishAppliedManifestUpdate
|
|
100
|
+
?? ((update) => publishPreparedProjectSkillManifestUpdate(update, readOptions));
|
|
101
|
+
const commitUpdate = deps.commitAppliedManifestUpdate
|
|
102
|
+
?? ((update) => commitPreparedProjectSkillManifestUpdate(update, readOptions));
|
|
103
|
+
const rollbackUpdate = deps.rollbackAppliedManifestUpdate
|
|
104
|
+
?? ((update) => rollbackPreparedProjectSkillManifestUpdate(update, readOptions));
|
|
105
|
+
const manifestOperations = Object.freeze({
|
|
106
|
+
publish: publishUpdate,
|
|
107
|
+
commit: commitUpdate,
|
|
108
|
+
rollback: rollbackUpdate,
|
|
109
|
+
});
|
|
110
|
+
let lastProjectionDiagnostics = Object.freeze([]);
|
|
111
|
+
const reconcileUnlocked = async (handle, bindings, projects = deps.scannedProjects(), recover = true, manifestPublication) => {
|
|
112
|
+
const agentRoot = join(deps.agentsRoot, handle);
|
|
113
|
+
if (recover) {
|
|
114
|
+
try {
|
|
115
|
+
await recoverProjectionSwitch(agentRoot, deps.syncDirectory);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
throw new ProjectProjectionError("skill_projection_failed");
|
|
119
|
+
}
|
|
65
120
|
}
|
|
66
|
-
const projects = deps.scannedProjects();
|
|
67
121
|
const uniqueBindings = [...new Map(bindings.map((binding) => [
|
|
68
122
|
`${binding.projectId}\0${binding.skillName}`,
|
|
69
123
|
binding,
|
|
70
|
-
])).values()].sort(
|
|
124
|
+
])).values()].sort(compareProjectSkillRefs);
|
|
71
125
|
const names = new Map();
|
|
72
126
|
for (const binding of uniqueBindings) {
|
|
73
127
|
const owner = names.get(binding.skillName);
|
|
@@ -78,68 +132,245 @@ export function createProjectSkillsReconciler(deps) {
|
|
|
78
132
|
}
|
|
79
133
|
const linked = [];
|
|
80
134
|
const resolutions = [];
|
|
135
|
+
const resolutionRecords = [];
|
|
81
136
|
for (const binding of uniqueBindings) {
|
|
82
137
|
const project = projects.find((candidate) => candidate.inventory.projectId === binding.projectId
|
|
83
138
|
&& candidate.inventory.status === "available");
|
|
84
139
|
const skill = project?.resolved.find((candidate) => candidate.name === binding.skillName);
|
|
85
140
|
const available = skill !== undefined && (await stat(skill.sourcePath).catch(() => null))?.isDirectory() === true;
|
|
86
141
|
resolutions.push(Object.freeze({ ...binding, status: available ? "linked" : "unavailable" }));
|
|
87
|
-
if (available && skill !== undefined)
|
|
142
|
+
if (available && skill !== undefined) {
|
|
88
143
|
linked.push({ binding, skill });
|
|
144
|
+
resolutionRecords.push(Object.freeze({
|
|
145
|
+
...binding,
|
|
146
|
+
sourcePath: skill.sourcePath,
|
|
147
|
+
mode: "resolved",
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
resolutionRecords.push(Object.freeze({ ...binding, sourcePath: null, mode: "missing" }));
|
|
152
|
+
}
|
|
89
153
|
}
|
|
90
|
-
const agentRoot = join(deps.agentsRoot, handle);
|
|
91
154
|
const id = randomUUID();
|
|
92
|
-
const targets =
|
|
93
|
-
|
|
94
|
-
runtime: "codex",
|
|
95
|
-
target: join(agentRoot, ".agents", "skills"),
|
|
96
|
-
staging: join(agentRoot, ".agents", `.skills-next-${id}`),
|
|
97
|
-
backup: join(agentRoot, ".agents", `.skills-previous-${id}`),
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
runtime: "claude",
|
|
101
|
-
target: join(agentRoot, ".crew", "claude-skills", ".claude", "skills"),
|
|
102
|
-
staging: join(agentRoot, ".crew", "claude-skills", ".claude", `.skills-next-${id}`),
|
|
103
|
-
backup: join(agentRoot, ".crew", "claude-skills", ".claude", `.skills-previous-${id}`),
|
|
104
|
-
},
|
|
105
|
-
];
|
|
155
|
+
const targets = projectionTargets(agentRoot, id);
|
|
156
|
+
const nextProjectionDiagnostics = [];
|
|
106
157
|
try {
|
|
107
158
|
for (const target of targets) {
|
|
108
159
|
await mkdir(dirname(target.target), { recursive: true });
|
|
109
160
|
await mkdir(target.staging, { mode: 0o700 });
|
|
110
|
-
|
|
161
|
+
await markProjectionStaging(target, id);
|
|
162
|
+
const preservedNames = await preserveNonProjectSkills(target, join(agentRoot, "training", "skills"), platform);
|
|
111
163
|
for (const item of linked) {
|
|
112
164
|
if (preservedNames.has(item.binding.skillName)) {
|
|
113
165
|
throw new ProjectProjectionError("skill_name_conflict");
|
|
114
166
|
}
|
|
115
|
-
|
|
167
|
+
const projectionTarget = join(target.staging, item.binding.skillName);
|
|
168
|
+
const projection = await directoryProjector(item.skill.sourcePath, projectionTarget, platform, { finalTarget: join(target.target, item.binding.skillName) });
|
|
169
|
+
nextProjectionDiagnostics.push(Object.freeze({
|
|
170
|
+
runtime: target.runtime,
|
|
171
|
+
projectId: item.binding.projectId,
|
|
172
|
+
skillName: item.binding.skillName,
|
|
173
|
+
mode: projection.mode,
|
|
174
|
+
...(projection.copyProtection === undefined
|
|
175
|
+
? {}
|
|
176
|
+
: { copyProtection: projection.copyProtection }),
|
|
177
|
+
}));
|
|
116
178
|
}
|
|
117
179
|
}
|
|
180
|
+
await writeFile(join(agentRoot, ".nowwork-root"), "", { encoding: "utf8", mode: 0o600 });
|
|
118
181
|
for (const target of targets)
|
|
119
182
|
await deps.beforeSwitch?.(target.runtime);
|
|
120
|
-
await
|
|
121
|
-
await
|
|
122
|
-
|
|
183
|
+
const publication = await manifestPublication?.(Object.freeze(resolutionRecords));
|
|
184
|
+
await switchProjectionSet(agentRoot, targets, id, deps.switchFault, deps.syncDirectory, publication);
|
|
185
|
+
lastProjectionDiagnostics = Object.freeze(nextProjectionDiagnostics);
|
|
186
|
+
return Object.freeze({
|
|
187
|
+
resolutions: Object.freeze(resolutions),
|
|
188
|
+
resolutionRecords: Object.freeze(resolutionRecords),
|
|
189
|
+
});
|
|
123
190
|
}
|
|
124
191
|
catch (error) {
|
|
125
|
-
|
|
192
|
+
try {
|
|
193
|
+
await Promise.all(targets.map((target) => cleanupProjectionStaging(target, id, deps.syncDirectory)));
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
throw new ProjectProjectionError("skill_projection_failed");
|
|
197
|
+
}
|
|
126
198
|
if (error instanceof ProjectProjectionError)
|
|
127
199
|
throw error;
|
|
128
200
|
throw new ProjectProjectionError("skill_projection_failed");
|
|
129
201
|
}
|
|
130
202
|
};
|
|
203
|
+
const validateExpectation = (expected) => {
|
|
204
|
+
const canonical = [...expected.bindings].sort(compareProjectSkillRefs);
|
|
205
|
+
const isCanonical = canonical.length === expected.bindings.length
|
|
206
|
+
&& canonical.every((binding, index) => binding.projectId === expected.bindings[index]?.projectId
|
|
207
|
+
&& binding.skillName === expected.bindings[index]?.skillName)
|
|
208
|
+
&& canonical.every((binding, index) => index === 0
|
|
209
|
+
|| binding.projectId !== canonical[index - 1]?.projectId
|
|
210
|
+
|| binding.skillName !== canonical[index - 1]?.skillName);
|
|
211
|
+
const digestShape = /^sha256:[a-f0-9]{64}$/u;
|
|
212
|
+
let bindingDigestMatches = false;
|
|
213
|
+
try {
|
|
214
|
+
bindingDigestMatches = computeProjectSkillBindingDigest(expected.bindings) === expected.bindingDigest;
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
bindingDigestMatches = false;
|
|
218
|
+
}
|
|
219
|
+
if (!isCanonical
|
|
220
|
+
|| !Number.isInteger(expected.generation)
|
|
221
|
+
|| expected.generation < 0
|
|
222
|
+
|| expected.generation > 2_147_483_647
|
|
223
|
+
|| !digestShape.test(expected.resolutionDigest)
|
|
224
|
+
|| !bindingDigestMatches) {
|
|
225
|
+
throw new ProjectProjectionError("skill_projection_snapshot_corrupt");
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
const rejectDecision = (decision) => {
|
|
229
|
+
if (decision.kind === "reject")
|
|
230
|
+
throw new ProjectProjectionError(decision.code);
|
|
231
|
+
};
|
|
232
|
+
const ensureAppliedLocked = async (handle, expected, projectsSnapshot) => {
|
|
233
|
+
const agentRoot = join(deps.agentsRoot, handle);
|
|
234
|
+
try {
|
|
235
|
+
await recoverProjectionSwitch(agentRoot, deps.syncDirectory, manifestOperations);
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
throw new ProjectProjectionError("skill_projection_failed");
|
|
239
|
+
}
|
|
240
|
+
const current = await readApplied(agentRoot, readOptions);
|
|
241
|
+
const lockedDecision = decideProjectSkillProjection(expected, current);
|
|
242
|
+
rejectDecision(lockedDecision);
|
|
243
|
+
if (lockedDecision.kind !== "ensure-applied")
|
|
244
|
+
return lockedDecision;
|
|
245
|
+
const projects = projectsSnapshot ?? deps.scannedProjects();
|
|
246
|
+
const reconciled = await reconcileUnlocked(handle, expected.bindings, projects, false, async (records) => {
|
|
247
|
+
const applied = createAppliedProjectSkillManifest({
|
|
248
|
+
bindings: expected.bindings,
|
|
249
|
+
generation: expected.generation,
|
|
250
|
+
resolutions: records,
|
|
251
|
+
platform,
|
|
252
|
+
});
|
|
253
|
+
const lockedExpectation = Object.freeze({
|
|
254
|
+
bindings: expected.bindings,
|
|
255
|
+
generation: expected.generation,
|
|
256
|
+
bindingDigest: expected.bindingDigest,
|
|
257
|
+
resolutionDigest: applied.resolutionDigest,
|
|
258
|
+
});
|
|
259
|
+
const update = await prepareUpdate(agentRoot, applied, readOptions);
|
|
260
|
+
return Object.freeze({
|
|
261
|
+
update,
|
|
262
|
+
operations: manifestOperations,
|
|
263
|
+
verify: async () => {
|
|
264
|
+
const rechecked = await readApplied(agentRoot, readOptions);
|
|
265
|
+
const finalDecision = decideProjectSkillProjection(lockedExpectation, rechecked);
|
|
266
|
+
rejectDecision(finalDecision);
|
|
267
|
+
if (finalDecision.kind !== "use-current") {
|
|
268
|
+
throw new ProjectProjectionError("skill_projection_failed");
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
...(deps.afterManifestPublishedBeforeCommit === undefined
|
|
272
|
+
? {}
|
|
273
|
+
: { afterPublishedBeforeCommit: deps.afterManifestPublishedBeforeCommit }),
|
|
274
|
+
...(deps.unlinkSwitchJournal === undefined
|
|
275
|
+
? {}
|
|
276
|
+
: { unlinkJournal: deps.unlinkSwitchJournal }),
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
const warnings = reconciled.resolutions
|
|
280
|
+
.filter((resolution) => resolution.status === "unavailable")
|
|
281
|
+
.map((resolution) => Object.freeze({
|
|
282
|
+
projectId: resolution.projectId,
|
|
283
|
+
skillName: resolution.skillName,
|
|
284
|
+
code: "project_skill_unavailable",
|
|
285
|
+
}));
|
|
286
|
+
return warnings.length === 0
|
|
287
|
+
? Object.freeze({ kind: "ensure-applied" })
|
|
288
|
+
: Object.freeze({ kind: "ensure-applied", warnings: Object.freeze(warnings) });
|
|
289
|
+
};
|
|
290
|
+
const ensureApplied = async (handle, expected) => {
|
|
291
|
+
validateExpectation(expected);
|
|
292
|
+
return deps.coordinator.runExclusive(deps.agentsRoot, handle, () => ensureAppliedLocked(handle, expected));
|
|
293
|
+
};
|
|
294
|
+
const ensureSnapshot = async (handle, snapshot) => deps.coordinator.runExclusive(deps.agentsRoot, handle, async () => {
|
|
295
|
+
const projects = deps.scannedProjects();
|
|
296
|
+
let expected;
|
|
297
|
+
try {
|
|
298
|
+
expected = projectSkillProjectionExpectation(snapshot, projects, platform);
|
|
299
|
+
validateExpectation(expected);
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
if (error instanceof ProjectProjectionError)
|
|
303
|
+
throw error;
|
|
304
|
+
throw new ProjectProjectionError("skill_projection_snapshot_corrupt");
|
|
305
|
+
}
|
|
306
|
+
const decision = await ensureAppliedLocked(handle, expected, projects);
|
|
307
|
+
if (decision.kind !== "use-current")
|
|
308
|
+
return decision;
|
|
309
|
+
const warnings = projectSkillResolutionRecords(snapshot.bindings, projects, platform)
|
|
310
|
+
.filter((record) => record.mode === "missing")
|
|
311
|
+
.map((record) => Object.freeze({
|
|
312
|
+
projectId: record.projectId,
|
|
313
|
+
skillName: record.skillName,
|
|
314
|
+
code: "project_skill_unavailable",
|
|
315
|
+
}));
|
|
316
|
+
return warnings.length === 0
|
|
317
|
+
? decision
|
|
318
|
+
: Object.freeze({ kind: "use-current", warnings: Object.freeze(warnings) });
|
|
319
|
+
});
|
|
320
|
+
const reconcileLegacyUnlocked = async (handle, bindings) => {
|
|
321
|
+
const agentRoot = join(deps.agentsRoot, handle);
|
|
322
|
+
try {
|
|
323
|
+
await recoverProjectionSwitch(agentRoot, deps.syncDirectory, manifestOperations);
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
throw new ProjectProjectionError("skill_projection_failed");
|
|
327
|
+
}
|
|
328
|
+
return reconcileUnlocked(handle, bindings, deps.scannedProjects(), false, async () => {
|
|
329
|
+
const update = await prepareRemoval(agentRoot, readOptions);
|
|
330
|
+
return Object.freeze({
|
|
331
|
+
update,
|
|
332
|
+
operations: manifestOperations,
|
|
333
|
+
verify: async () => {
|
|
334
|
+
if (await readApplied(agentRoot, readOptions) !== null) {
|
|
335
|
+
throw new ProjectProjectionError("skill_projection_failed");
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
...(deps.afterManifestPublishedBeforeCommit === undefined
|
|
339
|
+
? {}
|
|
340
|
+
: { afterPublishedBeforeCommit: deps.afterManifestPublishedBeforeCommit }),
|
|
341
|
+
...(deps.unlinkSwitchJournal === undefined
|
|
342
|
+
? {}
|
|
343
|
+
: { unlinkJournal: deps.unlinkSwitchJournal }),
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
};
|
|
131
347
|
return {
|
|
132
348
|
reconcile(handle, bindings) {
|
|
133
|
-
return deps.coordinator.runExclusive(deps.agentsRoot, handle, () =>
|
|
349
|
+
return deps.coordinator.runExclusive(deps.agentsRoot, handle, async () => (await reconcileLegacyUnlocked(handle, bindings)).resolutions);
|
|
134
350
|
},
|
|
135
|
-
|
|
351
|
+
ensureSnapshot,
|
|
352
|
+
ensureApplied,
|
|
353
|
+
projectionDiagnostics: () => lastProjectionDiagnostics,
|
|
354
|
+
async prepareAndLaunch(agentsRoot, handle, projection, launch, onWarning) {
|
|
136
355
|
if (agentsRoot !== deps.agentsRoot) {
|
|
137
|
-
|
|
356
|
+
throw new ProjectProjectionError("skill_projection_failed");
|
|
357
|
+
}
|
|
358
|
+
if (Array.isArray(projection)) {
|
|
359
|
+
return deps.coordinator.runExclusiveUntil(deps.agentsRoot, handle, async () => {
|
|
360
|
+
await reconcileLegacyUnlocked(handle, projection);
|
|
361
|
+
return launch();
|
|
362
|
+
}, runtimeProjectionLifetime);
|
|
363
|
+
}
|
|
364
|
+
const decision = "resolutionDigest" in projection
|
|
365
|
+
? await ensureApplied(handle, projection)
|
|
366
|
+
: await ensureSnapshot(handle, projection);
|
|
367
|
+
if (decision.kind === "use-advanced")
|
|
368
|
+
onWarning?.({ code: decision.warning });
|
|
369
|
+
if ("warnings" in decision) {
|
|
370
|
+
for (const warning of decision.warnings ?? [])
|
|
371
|
+
onWarning?.(warning);
|
|
138
372
|
}
|
|
139
|
-
return
|
|
140
|
-
await reconcileUnlocked(handle, bindings);
|
|
141
|
-
return launch();
|
|
142
|
-
}, runtimeProjectionLifetime);
|
|
373
|
+
return launch();
|
|
143
374
|
},
|
|
144
375
|
};
|
|
145
376
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Stable, path-free console text; only logical IDs from the server snapshot are included. */
|
|
2
|
+
export function formatProjectSkillRuntimeWarning(warning) {
|
|
3
|
+
return warning.code === "skill_projection_advanced"
|
|
4
|
+
? "[runtime-warning] skill_projection_advanced"
|
|
5
|
+
: `[runtime-warning] project_skill_unavailable project=${warning.projectId} skill=${warning.skillName}`;
|
|
6
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { isProjectSkillName, MAX_MACHINE_PROJECT_SKILLS, MAX_PROJECT_SKILL_DESCRIPTION_LENGTH, MAX_PROJECT_SKILL_FILE_BYTES, MAX_PROJECT_SKILLS_PER_PROJECT, } from "./types.js";
|
|
3
|
+
import { compareProjectSkillRefs, isProjectSkillName, MAX_MACHINE_PROJECT_SKILLS, MAX_PROJECT_SKILL_DESCRIPTION_LENGTH, MAX_PROJECT_SKILL_FILE_BYTES, MAX_PROJECT_SKILLS_PER_PROJECT, } from "./types.js";
|
|
4
4
|
import { parseSkillFrontmatter } from "../skill-frontmatter.js";
|
|
5
|
+
import { computeProjectSkillBindingDigest, computeProjectSkillResolutionDigest, normalizeProjectSkillResolutionRecords, } from "./projection-state.js";
|
|
5
6
|
const unavailable = (projectId, scannedAt, errorCode) => Object.freeze({
|
|
6
7
|
inventory: Object.freeze({
|
|
7
8
|
projectId,
|
|
@@ -130,3 +131,31 @@ export async function scanProjects(projects, now = () => new Date(), retainedPro
|
|
|
130
131
|
const sorted = [...projects].sort((left, right) => left.projectId.localeCompare(right.projectId));
|
|
131
132
|
return boundScannedProjects(await Promise.all(sorted.map((project) => scanProject(project, now))), retainedProjectIds);
|
|
132
133
|
}
|
|
134
|
+
/** V2-only source identity mapper; existing v1 inventory and resolved scan shapes remain unchanged. */
|
|
135
|
+
export function projectSkillResolutionRecords(bindings, projects, platform = process.platform) {
|
|
136
|
+
const records = [...bindings].sort(compareProjectSkillRefs).map((binding) => {
|
|
137
|
+
const project = projects.find((candidate) => candidate.inventory.projectId === binding.projectId
|
|
138
|
+
&& candidate.inventory.status === "available");
|
|
139
|
+
const skill = project?.resolved.find((candidate) => candidate.name === binding.skillName);
|
|
140
|
+
return skill === undefined
|
|
141
|
+
? Object.freeze({ ...binding, sourcePath: null, mode: "missing" })
|
|
142
|
+
: Object.freeze({ ...binding, sourcePath: skill.sourcePath, mode: "resolved" });
|
|
143
|
+
});
|
|
144
|
+
return normalizeProjectSkillResolutionRecords(records, platform);
|
|
145
|
+
}
|
|
146
|
+
/** Daemon-local expectation derived from one immutable scanner snapshot; no path crosses the wire. */
|
|
147
|
+
export function projectSkillProjectionExpectation(snapshot, projects, platform = process.platform) {
|
|
148
|
+
const bindings = Object.freeze(snapshot.bindings
|
|
149
|
+
.map((binding) => Object.freeze({
|
|
150
|
+
projectId: binding.projectId,
|
|
151
|
+
skillName: binding.skillName,
|
|
152
|
+
}))
|
|
153
|
+
.sort(compareProjectSkillRefs));
|
|
154
|
+
const resolutions = projectSkillResolutionRecords(bindings, projects, platform);
|
|
155
|
+
return Object.freeze({
|
|
156
|
+
bindings,
|
|
157
|
+
generation: snapshot.generation,
|
|
158
|
+
bindingDigest: computeProjectSkillBindingDigest(bindings),
|
|
159
|
+
resolutionDigest: computeProjectSkillResolutionDigest(resolutions, platform),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
@@ -11,3 +11,12 @@ const PROJECT_ID = /^[a-z0-9][a-z0-9._-]*$/u;
|
|
|
11
11
|
const SKILL_NAME = /^[a-z0-9][a-z0-9._:-]*$/u;
|
|
12
12
|
export const isProjectId = (value) => value.length > 0 && value.length <= MAX_PROJECT_ID_LENGTH && PROJECT_ID.test(value);
|
|
13
13
|
export const isProjectSkillName = (value) => value.length > 0 && value.length <= MAX_PROJECT_SKILL_NAME_LENGTH && SKILL_NAME.test(value);
|
|
14
|
+
const compareBinary = (left, right) => {
|
|
15
|
+
if (left < right)
|
|
16
|
+
return -1;
|
|
17
|
+
if (left > right)
|
|
18
|
+
return 1;
|
|
19
|
+
return 0;
|
|
20
|
+
};
|
|
21
|
+
export const compareProjectSkillRefs = (left, right) => compareBinary(left.projectId, right.projectId)
|
|
22
|
+
|| compareBinary(left.skillName, right.skillName);
|