silvermoon 0.0.2 → 0.1.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 +168 -0
- package/README.zh-CN.md +19 -4
- package/docs/core-concepts.md +14 -12
- package/docs/getting-started.md +53 -27
- package/docs/maintaining.md +11 -2
- package/docs/operations.md +77 -30
- package/docs/reference.md +197 -37
- package/docs/repository-tasks.md +25 -13
- package/package.json +2 -2
- package/schema/v1/idea-status.schema.json +3 -3
- package/skills/silvermoon/SKILL.md +142 -187
- package/skills/silvermoon/references/adoption.md +34 -20
- package/src/adoption.js +136 -260
- package/src/cli.js +86 -113
- package/src/create-idea.js +222 -110
- package/src/dialogue.js +303 -0
- package/src/git.js +172 -97
- package/src/idea-layout.js +59 -123
- package/src/index.js +203 -170
- package/src/observation.js +305 -0
- package/src/trace.js +171 -0
- package/src/whatsnext.js +529 -378
package/src/create-idea.js
CHANGED
|
@@ -2,7 +2,12 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import { lstat, mkdir, readFile, rm, rmdir, writeFile } from "node:fs/promises";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
createEnvelope,
|
|
7
|
+
dialogueReadyObservation,
|
|
8
|
+
localize,
|
|
9
|
+
outcome,
|
|
10
|
+
} from "./dialogue.js";
|
|
6
11
|
import {
|
|
7
12
|
DEPLOYMENT_TEMPLATE,
|
|
8
13
|
IDEA_TEMPLATE,
|
|
@@ -12,7 +17,11 @@ import {
|
|
|
12
17
|
import { isValidUlid, serializeIdeaStatus } from "./ideas.js";
|
|
13
18
|
import { canonicalizeLanguageTag } from "./language.js";
|
|
14
19
|
import { IDEAS_ROOT, ideaPaths } from "./layout.js";
|
|
15
|
-
import {
|
|
20
|
+
import { observeSnapshot } from "./observation.js";
|
|
21
|
+
import {
|
|
22
|
+
assessIdeaCreationReadiness,
|
|
23
|
+
projectInstructions,
|
|
24
|
+
} from "./whatsnext.js";
|
|
16
25
|
|
|
17
26
|
const ULID_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
18
27
|
const MAX_ID_ATTEMPTS = 32;
|
|
@@ -45,29 +54,31 @@ async function removeOwnedFile(path, expected, read, remove) {
|
|
|
45
54
|
try {
|
|
46
55
|
current = await read(path);
|
|
47
56
|
} catch (caught) {
|
|
48
|
-
if (caught.code === "ENOENT") return;
|
|
57
|
+
if (caught.code === "ENOENT") return { path, status: "missing" };
|
|
49
58
|
throw caught;
|
|
50
59
|
}
|
|
51
60
|
const expectedBytes = Buffer.from(expected);
|
|
52
|
-
if (
|
|
53
|
-
current.length <= expectedBytes.length &&
|
|
54
|
-
expectedBytes.subarray(0, current.length).equals(current)
|
|
55
|
-
) {
|
|
61
|
+
if (current.equals(expectedBytes)) {
|
|
56
62
|
await remove(path, { force: true });
|
|
63
|
+
return { path, status: "removed" };
|
|
57
64
|
}
|
|
65
|
+
return { path, status: "preserved-modified" };
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
async function removeCreatedDirectories(created, removeDirectory) {
|
|
69
|
+
const results = [];
|
|
61
70
|
for (const path of [...created].reverse()) {
|
|
62
71
|
try {
|
|
63
72
|
await removeDirectory(path);
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
results.push({ path, status: "removed" });
|
|
74
|
+
} catch (caught) {
|
|
75
|
+
results.push({ path, status: "preserved", error: caught.message });
|
|
66
76
|
}
|
|
67
77
|
}
|
|
78
|
+
return results;
|
|
68
79
|
}
|
|
69
80
|
|
|
70
|
-
async function ensureDirectoryPath(root, relativePath,
|
|
81
|
+
async function ensureDirectoryPath(root, relativePath, operations) {
|
|
71
82
|
const created = [];
|
|
72
83
|
let current = root;
|
|
73
84
|
try {
|
|
@@ -75,7 +86,7 @@ async function ensureDirectoryPath(root, relativePath, inspect, makeDirectory, r
|
|
|
75
86
|
current = resolve(current, segment);
|
|
76
87
|
let metadata;
|
|
77
88
|
try {
|
|
78
|
-
metadata = await inspect(current);
|
|
89
|
+
metadata = await operations.inspect(current);
|
|
79
90
|
} catch (caught) {
|
|
80
91
|
if (caught.code !== "ENOENT") throw caught;
|
|
81
92
|
}
|
|
@@ -86,40 +97,67 @@ async function ensureDirectoryPath(root, relativePath, inspect, makeDirectory, r
|
|
|
86
97
|
continue;
|
|
87
98
|
}
|
|
88
99
|
try {
|
|
89
|
-
await makeDirectory(current);
|
|
100
|
+
await operations.makeDirectory(current);
|
|
90
101
|
created.push(current);
|
|
91
102
|
} catch (caught) {
|
|
92
103
|
if (caught.code !== "EEXIST") throw caught;
|
|
93
|
-
metadata = await inspect(current);
|
|
104
|
+
metadata = await operations.inspect(current);
|
|
94
105
|
if (!metadata.isDirectory() || metadata.isSymbolicLink()) throw caught;
|
|
95
106
|
}
|
|
96
107
|
}
|
|
97
108
|
return created;
|
|
98
109
|
} catch (caught) {
|
|
99
|
-
await removeCreatedDirectories(
|
|
110
|
+
caught.cleanup = await removeCreatedDirectories(
|
|
111
|
+
created,
|
|
112
|
+
operations.removeDirectory,
|
|
113
|
+
);
|
|
100
114
|
throw caught;
|
|
101
115
|
}
|
|
102
116
|
}
|
|
103
117
|
|
|
104
|
-
function
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
118
|
+
function cleanupSummary(results) {
|
|
119
|
+
const preserved = results.filter(({ status }) => status.startsWith("preserved"));
|
|
120
|
+
return preserved.length === 0
|
|
121
|
+
? "All paths still owned by this operation were removed."
|
|
122
|
+
: `Preserved ${preserved.length} path(s) because they changed or could not be removed: `
|
|
123
|
+
+ preserved.map(({ path }) => path).join(", ");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function failureEnvelope({
|
|
127
|
+
caught,
|
|
128
|
+
intention,
|
|
129
|
+
observation,
|
|
130
|
+
outcomes,
|
|
131
|
+
cleanup = [],
|
|
132
|
+
language,
|
|
133
|
+
}) {
|
|
134
|
+
outcomes.push(outcome(
|
|
135
|
+
"create-idea-scaffold",
|
|
136
|
+
"failure",
|
|
137
|
+
localize(
|
|
138
|
+
language,
|
|
139
|
+
`Idea creation failed: ${caught.message} ${cleanupSummary(cleanup)}`,
|
|
140
|
+
`创建 idea 失败:${caught.message} ${cleanupSummary(cleanup)}`,
|
|
141
|
+
),
|
|
142
|
+
));
|
|
143
|
+
return createEnvelope(
|
|
144
|
+
intention,
|
|
145
|
+
dialogueReadyObservation(observation, "idea-create-failed"),
|
|
146
|
+
outcomes,
|
|
147
|
+
localize(
|
|
148
|
+
language,
|
|
149
|
+
"Preserve any reported paths, resolve the filesystem error, and retry `silvermoon create-idea`.",
|
|
150
|
+
"保留报告中的路径,解决文件系统错误后,再运行 `silvermoon create-idea`。",
|
|
151
|
+
),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function projectCreationObservation(observation) {
|
|
156
|
+
const { ideas: _ideas, ...project } = observation;
|
|
157
|
+
if (project.observedThrough === "ideas") {
|
|
158
|
+
project.observedThrough = "configuration";
|
|
159
|
+
}
|
|
160
|
+
return project;
|
|
123
161
|
}
|
|
124
162
|
|
|
125
163
|
export async function createIdea({
|
|
@@ -129,42 +167,56 @@ export async function createIdea({
|
|
|
129
167
|
root = process.cwd(),
|
|
130
168
|
userHome,
|
|
131
169
|
} = {}) {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
170
|
+
const requestedRoot = resolve(root);
|
|
171
|
+
const canonicalLanguage = language === undefined
|
|
172
|
+
? undefined
|
|
173
|
+
: canonicalizeLanguageTag(language);
|
|
174
|
+
const intention = {
|
|
175
|
+
command: "create-idea",
|
|
176
|
+
args: { language: canonicalLanguage ?? null },
|
|
177
|
+
};
|
|
178
|
+
const outcomes = [];
|
|
179
|
+
const observed = await observeSnapshot({
|
|
180
|
+
allowMissingIdeas: true,
|
|
181
|
+
ideaLanguage: canonicalLanguage,
|
|
182
|
+
root: requestedRoot,
|
|
183
|
+
userHome,
|
|
184
|
+
version: { type: "worktree" },
|
|
185
|
+
});
|
|
186
|
+
const recheckCommand = canonicalLanguage === undefined
|
|
187
|
+
? "silvermoon create-idea"
|
|
188
|
+
: `silvermoon create-idea --language ${canonicalLanguage}`;
|
|
189
|
+
if (!observed.projectReady) {
|
|
190
|
+
return createEnvelope(
|
|
191
|
+
intention,
|
|
192
|
+
projectCreationObservation(observed.observation),
|
|
193
|
+
outcomes,
|
|
194
|
+
projectInstructions(
|
|
195
|
+
observed,
|
|
196
|
+
observed.observation.root,
|
|
197
|
+
observed.language,
|
|
198
|
+
recheckCommand,
|
|
199
|
+
),
|
|
200
|
+
);
|
|
157
201
|
}
|
|
158
|
-
const
|
|
159
|
-
|
|
202
|
+
const repositoryRoot = observed.observation.root;
|
|
203
|
+
const readiness = await assessIdeaCreationReadiness({
|
|
204
|
+
observed,
|
|
205
|
+
outcomes,
|
|
206
|
+
recheckCommand,
|
|
160
207
|
root: repositoryRoot,
|
|
161
|
-
userHome,
|
|
162
208
|
});
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
209
|
+
if (!readiness.ready) {
|
|
210
|
+
return createEnvelope(
|
|
211
|
+
intention,
|
|
212
|
+
dialogueReadyObservation(
|
|
213
|
+
readiness.observation,
|
|
214
|
+
"repository-preparation-required",
|
|
215
|
+
),
|
|
216
|
+
outcomes,
|
|
217
|
+
readiness.instructions,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
168
220
|
|
|
169
221
|
const inspect = operations.lstat ?? lstat;
|
|
170
222
|
const makeDirectory = operations.mkdir ?? mkdir;
|
|
@@ -172,32 +224,55 @@ export async function createIdea({
|
|
|
172
224
|
const remove = operations.rm ?? rm;
|
|
173
225
|
const removeDirectory = operations.rmdir ?? rmdir;
|
|
174
226
|
const write = operations.writeFile ?? writeFile;
|
|
227
|
+
const fileOperations = {
|
|
228
|
+
inspect,
|
|
229
|
+
makeDirectory,
|
|
230
|
+
removeDirectory,
|
|
231
|
+
};
|
|
175
232
|
let rootDirectories;
|
|
176
233
|
try {
|
|
177
234
|
rootDirectories = await ensureDirectoryPath(
|
|
178
235
|
repositoryRoot,
|
|
179
236
|
IDEAS_ROOT,
|
|
180
|
-
|
|
181
|
-
makeDirectory,
|
|
182
|
-
removeDirectory,
|
|
237
|
+
fileOperations,
|
|
183
238
|
);
|
|
184
239
|
} catch (caught) {
|
|
185
|
-
return
|
|
240
|
+
return failureEnvelope({
|
|
241
|
+
caught,
|
|
242
|
+
intention,
|
|
243
|
+
observation: readiness.observation,
|
|
244
|
+
outcomes,
|
|
245
|
+
cleanup: caught.cleanup,
|
|
246
|
+
language: observed.language,
|
|
247
|
+
});
|
|
186
248
|
}
|
|
187
249
|
|
|
188
|
-
const failRoot = async (caught) => {
|
|
189
|
-
await removeCreatedDirectories(rootDirectories, removeDirectory);
|
|
190
|
-
return failure(repositoryRoot, preflight, caught);
|
|
191
|
-
};
|
|
192
|
-
|
|
193
250
|
for (let attempt = 0; attempt < MAX_ID_ATTEMPTS; attempt += 1) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
251
|
+
let id;
|
|
252
|
+
let paths;
|
|
253
|
+
let folder;
|
|
254
|
+
try {
|
|
255
|
+
id = await generateId();
|
|
256
|
+
if (!isValidUlid(id)) {
|
|
257
|
+
throw new Error(`Generated idea id is not a canonical ULID: ${id}`);
|
|
258
|
+
}
|
|
259
|
+
paths = ideaPaths(id);
|
|
260
|
+
folder = resolve(repositoryRoot, paths.ideaPath);
|
|
261
|
+
if (await pathExists(folder, inspect)) continue;
|
|
262
|
+
} catch (caught) {
|
|
263
|
+
const cleanup = await removeCreatedDirectories(
|
|
264
|
+
rootDirectories,
|
|
265
|
+
removeDirectory,
|
|
266
|
+
);
|
|
267
|
+
return failureEnvelope({
|
|
268
|
+
caught,
|
|
269
|
+
intention,
|
|
270
|
+
observation: readiness.observation,
|
|
271
|
+
outcomes,
|
|
272
|
+
cleanup,
|
|
273
|
+
language: observed.language,
|
|
274
|
+
});
|
|
197
275
|
}
|
|
198
|
-
const paths = ideaPaths(id);
|
|
199
|
-
const folder = resolve(repositoryRoot, paths.ideaPath);
|
|
200
|
-
if (await pathExists(folder, inspect)) continue;
|
|
201
276
|
|
|
202
277
|
const directoryPaths = [
|
|
203
278
|
paths.ideaPath,
|
|
@@ -218,6 +293,7 @@ export async function createIdea({
|
|
|
218
293
|
];
|
|
219
294
|
const createdDirectories = [];
|
|
220
295
|
const cleanupFiles = [];
|
|
296
|
+
let collisionPath = null;
|
|
221
297
|
try {
|
|
222
298
|
for (const relativePath of directoryPaths) {
|
|
223
299
|
const absolutePath = resolve(repositoryRoot, relativePath);
|
|
@@ -231,42 +307,78 @@ export async function createIdea({
|
|
|
231
307
|
try {
|
|
232
308
|
await write(absolutePath, source, { flag: "wx" });
|
|
233
309
|
} catch (caught) {
|
|
234
|
-
if (caught.code === "EEXIST")
|
|
310
|
+
if (caught.code === "EEXIST") {
|
|
311
|
+
cleanupFiles.pop();
|
|
312
|
+
collisionPath = absolutePath;
|
|
313
|
+
}
|
|
235
314
|
throw caught;
|
|
236
315
|
}
|
|
237
316
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
|
|
317
|
+
outcomes.push(outcome(
|
|
318
|
+
"create-idea-scaffold",
|
|
319
|
+
"success",
|
|
320
|
+
localize(
|
|
321
|
+
observed.language,
|
|
322
|
+
`Created idea ${id} at ${paths.ideaPath}.`,
|
|
323
|
+
`已在 ${paths.ideaPath} 创建 idea ${id}。`,
|
|
324
|
+
),
|
|
325
|
+
));
|
|
326
|
+
return createEnvelope(
|
|
327
|
+
intention,
|
|
328
|
+
dialogueReadyObservation(readiness.observation, "idea-created", {
|
|
329
|
+
createdIdea: { id, path: paths.ideaPath, state: "preparing" },
|
|
330
|
+
}),
|
|
331
|
+
outcomes,
|
|
332
|
+
localize(
|
|
333
|
+
observed.language,
|
|
334
|
+
`Use ${observed.language} for natural-language content while describing the requested Ideal World in ${paths.ideaDocumentPath}; keep the stable IDs and placeholders in ${paths.implementationDocumentPath}, ${paths.deploymentDocumentPath}, and ${paths.ledgerPath} synchronized.`,
|
|
335
|
+
`使用 ${observed.language} 在 ${paths.ideaDocumentPath} 中描述请求的理想契约,并保持 ${paths.implementationDocumentPath}、${paths.deploymentDocumentPath} 与 ${paths.ledgerPath} 中的稳定 ID 和占位同步。`,
|
|
336
|
+
),
|
|
337
|
+
);
|
|
259
338
|
} catch (caught) {
|
|
339
|
+
const cleanup = [];
|
|
260
340
|
for (const [path, source] of [...cleanupFiles].reverse()) {
|
|
261
|
-
|
|
341
|
+
try {
|
|
342
|
+
cleanup.push(await removeOwnedFile(path, source, read, remove));
|
|
343
|
+
} catch (cleanupError) {
|
|
344
|
+
cleanup.push({
|
|
345
|
+
path,
|
|
346
|
+
status: "preserved",
|
|
347
|
+
error: cleanupError.message,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
cleanup.push(...await removeCreatedDirectories(
|
|
352
|
+
createdDirectories,
|
|
353
|
+
removeDirectory,
|
|
354
|
+
));
|
|
355
|
+
if (collisionPath) {
|
|
356
|
+
cleanup.push({ path: collisionPath, status: "preserved-existing" });
|
|
262
357
|
}
|
|
263
|
-
await removeCreatedDirectories(
|
|
264
|
-
|
|
265
|
-
|
|
358
|
+
cleanup.push(...await removeCreatedDirectories(
|
|
359
|
+
rootDirectories,
|
|
360
|
+
removeDirectory,
|
|
361
|
+
));
|
|
362
|
+
return failureEnvelope({
|
|
363
|
+
caught,
|
|
364
|
+
intention,
|
|
365
|
+
observation: readiness.observation,
|
|
366
|
+
outcomes,
|
|
367
|
+
cleanup,
|
|
368
|
+
language: observed.language,
|
|
369
|
+
});
|
|
266
370
|
}
|
|
267
371
|
}
|
|
268
372
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
373
|
+
const cleanup = await removeCreatedDirectories(rootDirectories, removeDirectory);
|
|
374
|
+
return failureEnvelope({
|
|
375
|
+
caught: new Error(
|
|
376
|
+
`Could not allocate a unique idea id after ${MAX_ID_ATTEMPTS} attempts.`,
|
|
377
|
+
),
|
|
378
|
+
intention,
|
|
379
|
+
observation: readiness.observation,
|
|
380
|
+
outcomes,
|
|
381
|
+
cleanup,
|
|
382
|
+
language: observed.language,
|
|
383
|
+
});
|
|
272
384
|
}
|