silvermoon 0.0.1 → 0.0.3
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 +149 -236
- package/README.zh-CN.md +114 -199
- package/assets/silvermoon.svg +176 -175
- package/bin/silvermoon.js +0 -0
- package/docs/assets/silvermoon-avatar.svg +395 -0
- package/docs/core-concepts.md +69 -0
- package/docs/getting-started.md +115 -0
- package/docs/maintaining.md +56 -0
- package/docs/npm-package-releases.md +139 -0
- package/docs/operations.md +110 -0
- package/docs/reference.md +138 -0
- package/docs/repository-tasks.md +61 -0
- package/package.json +69 -65
- package/schema/v1/config.schema.json +27 -24
- package/schema/v1/definitions.schema.json +30 -25
- package/schema/v1/idea-status.schema.json +44 -41
- package/schema/v1/user-config.schema.json +19 -0
- package/skills/silvermoon/SKILL.md +216 -209
- package/skills/silvermoon/references/adoption.md +122 -122
- package/src/adoption.js +313 -313
- package/src/cli.js +200 -196
- package/src/config.js +193 -176
- package/src/create-idea.js +272 -235
- package/src/git.js +284 -284
- package/src/idea-layout.js +439 -439
- package/src/idea-templates.js +109 -109
- package/src/ideas.js +140 -131
- package/src/index.js +260 -260
- package/src/language.js +29 -0
- package/src/layout.js +21 -21
- package/src/repository.js +87 -87
- package/src/user-config.js +135 -0
- package/src/whatsnext.js +453 -378
- package/src/yaml.js +66 -66
package/src/whatsnext.js
CHANGED
|
@@ -1,379 +1,454 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
2
|
-
|
|
3
|
-
import { inspectAdoption } from "./adoption.js";
|
|
4
|
-
import { loadConfig } from "./config.js";
|
|
5
|
-
import {
|
|
6
|
-
fetchPrimary,
|
|
7
|
-
parseWorktreeChanges,
|
|
8
|
-
runGit,
|
|
9
|
-
withTemporaryWorktree,
|
|
10
|
-
} from "./git.js";
|
|
11
|
-
import { inspectIdeaLayout } from "./idea-layout.js";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
{
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
revision: idea.
|
|
108
|
-
decisionField: "
|
|
109
|
-
cascade: "
|
|
110
|
-
},
|
|
111
|
-
},
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
if (idea.state === "
|
|
115
|
-
const world = idea.worlds.
|
|
116
|
-
return action(
|
|
117
|
-
"
|
|
118
|
-
`
|
|
119
|
-
{
|
|
120
|
-
ideaPath: idea.relativePath,
|
|
121
|
-
statusPath: idea.statusPath,
|
|
122
|
-
ledgerPath: idea.ledgerPath,
|
|
123
|
-
world: {
|
|
124
|
-
...world,
|
|
125
|
-
auxiliaryRoot: world.path,
|
|
126
|
-
nestedWorldPath: idea.worlds.
|
|
127
|
-
revision: idea.
|
|
128
|
-
decisionField: "
|
|
129
|
-
cascade: "
|
|
130
|
-
},
|
|
131
|
-
},
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
"
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
let
|
|
264
|
-
try {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
|
|
3
|
+
import { inspectAdoption } from "./adoption.js";
|
|
4
|
+
import { loadConfig } from "./config.js";
|
|
5
|
+
import {
|
|
6
|
+
fetchPrimary,
|
|
7
|
+
parseWorktreeChanges,
|
|
8
|
+
runGit,
|
|
9
|
+
withTemporaryWorktree,
|
|
10
|
+
} from "./git.js";
|
|
11
|
+
import { inspectIdeaLayout } from "./idea-layout.js";
|
|
12
|
+
import { resolveLanguage } from "./language.js";
|
|
13
|
+
import { loadUserConfig } from "./user-config.js";
|
|
14
|
+
|
|
15
|
+
const ACTIVE_STATES = new Set(["preparing", "implementing", "deploying"]);
|
|
16
|
+
|
|
17
|
+
function diagnostic(code, message, remediation, path) {
|
|
18
|
+
return { code, level: "error", ...(path ? { path } : {}), message, remediation };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function failed(root, diagnostics, request, observedPrimaryCommit = null) {
|
|
22
|
+
return {
|
|
23
|
+
command: "whats-next",
|
|
24
|
+
ok: false,
|
|
25
|
+
root,
|
|
26
|
+
diagnostics,
|
|
27
|
+
result: request
|
|
28
|
+
? { observedPrimaryCommit, request, selectedIdea: null, action: null }
|
|
29
|
+
: null,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function ideaName(idea) {
|
|
34
|
+
return idea.alias ?? idea.id;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function summary(idea) {
|
|
38
|
+
const value = {
|
|
39
|
+
id: idea.id,
|
|
40
|
+
idealRevision: idea.idealRevision,
|
|
41
|
+
implementationRevision: idea.implementationRevision,
|
|
42
|
+
deploymentRevision: idea.deploymentRevision,
|
|
43
|
+
ledgerPath: idea.ledgerPath,
|
|
44
|
+
state: idea.state,
|
|
45
|
+
};
|
|
46
|
+
if (idea.alias !== undefined) value.alias = idea.alias;
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function action(code, message, details = {}) {
|
|
51
|
+
return { code, message, details };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function actionWithLanguage(nextAction, language) {
|
|
55
|
+
return {
|
|
56
|
+
...nextAction,
|
|
57
|
+
message: `${nextAction.message} Use ${language.tag} for natural-language content and user-facing explanations.`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function onboardingGaps(onboarding) {
|
|
62
|
+
if (onboarding.findings.length === 0) return null;
|
|
63
|
+
return {
|
|
64
|
+
status: onboarding.status,
|
|
65
|
+
gaps: onboarding.findings,
|
|
66
|
+
recommendedAction: onboarding.recommendedAction,
|
|
67
|
+
recheck: onboarding.recheck,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function success(root, observedPrimaryCommit, request, selectedIdea, nextAction, language) {
|
|
72
|
+
return {
|
|
73
|
+
command: "whats-next",
|
|
74
|
+
ok: true,
|
|
75
|
+
root,
|
|
76
|
+
diagnostics: [],
|
|
77
|
+
result: {
|
|
78
|
+
observedPrimaryCommit,
|
|
79
|
+
request,
|
|
80
|
+
language,
|
|
81
|
+
selectedIdea: selectedIdea ? summary(selectedIdea) : null,
|
|
82
|
+
action: actionWithLanguage(nextAction, language),
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function stateAction(idea) {
|
|
88
|
+
if (idea.state === "abandoned") {
|
|
89
|
+
return action(
|
|
90
|
+
"review-abandoned",
|
|
91
|
+
`Review abandoned idea ${ideaName(idea)} and decide whether to keep it abandoned or revise it.`,
|
|
92
|
+
{ statusPath: idea.statusPath, ledgerPath: idea.ledgerPath },
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (idea.state === "preparing") {
|
|
96
|
+
const world = idea.worlds.idealRevision;
|
|
97
|
+
return action(
|
|
98
|
+
"prepare-idea",
|
|
99
|
+
`Clarify the Ideal World (道心) for idea ${ideaName(idea)} in Idea.md and its supporting files, then record approval for ${idea.idealRevision}.`,
|
|
100
|
+
{
|
|
101
|
+
ideaPath: idea.relativePath,
|
|
102
|
+
statusPath: idea.statusPath,
|
|
103
|
+
ledgerPath: idea.ledgerPath,
|
|
104
|
+
world: {
|
|
105
|
+
...world,
|
|
106
|
+
auxiliaryRoot: world.path,
|
|
107
|
+
revision: idea.idealRevision,
|
|
108
|
+
decisionField: "approvedRevision",
|
|
109
|
+
cascade: "Changes invalidate approval, implementation acceptance, and deployment acceptance.",
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
if (idea.state === "implementing") {
|
|
115
|
+
const world = idea.worlds.implementationRevision;
|
|
116
|
+
return action(
|
|
117
|
+
"implement-idea",
|
|
118
|
+
`Shape the Inner World (内景) for idea ${ideaName(idea)} in Implementation.md and its supporting files, then accept implementation ${idea.implementationRevision}.`,
|
|
119
|
+
{
|
|
120
|
+
ideaPath: idea.relativePath,
|
|
121
|
+
statusPath: idea.statusPath,
|
|
122
|
+
ledgerPath: idea.ledgerPath,
|
|
123
|
+
world: {
|
|
124
|
+
...world,
|
|
125
|
+
auxiliaryRoot: world.path,
|
|
126
|
+
nestedWorldPath: idea.worlds.idealRevision.path,
|
|
127
|
+
revision: idea.implementationRevision,
|
|
128
|
+
decisionField: "implementationAcceptedRevision",
|
|
129
|
+
cascade: "Inner World changes invalidate implementation and deployment acceptance; Ideal World changes return to preparing.",
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
if (idea.state === "deploying") {
|
|
135
|
+
const world = idea.worlds.deploymentRevision;
|
|
136
|
+
return action(
|
|
137
|
+
"deploy-idea",
|
|
138
|
+
`Verify the Outer World (现世) for idea ${ideaName(idea)} from Deployment.md and its supporting files, then accept deployment ${idea.deploymentRevision}.`,
|
|
139
|
+
{
|
|
140
|
+
ideaPath: idea.relativePath,
|
|
141
|
+
statusPath: idea.statusPath,
|
|
142
|
+
ledgerPath: idea.ledgerPath,
|
|
143
|
+
world: {
|
|
144
|
+
...world,
|
|
145
|
+
auxiliaryRoot: world.path,
|
|
146
|
+
nestedWorldPath: idea.worlds.implementationRevision.path,
|
|
147
|
+
revision: idea.deploymentRevision,
|
|
148
|
+
decisionField: "deploymentAcceptedRevision",
|
|
149
|
+
cascade: "Outer World changes invalidate deployment acceptance only; nested world changes cascade inward.",
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return action(
|
|
155
|
+
"review-completed",
|
|
156
|
+
`Review completed idea ${ideaName(idea)} and decide whether to revise it or create a new idea.`,
|
|
157
|
+
{
|
|
158
|
+
ideaPath: idea.relativePath,
|
|
159
|
+
statusPath: idea.statusPath,
|
|
160
|
+
ledgerPath: idea.ledgerPath,
|
|
161
|
+
revisions: idea.revisions,
|
|
162
|
+
},
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function selectIdea(ideas, selector) {
|
|
167
|
+
if (!selector) return null;
|
|
168
|
+
return ideas.find(({ id, alias }) => id === selector || alias === selector) ?? null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function worktreeAction(root, config, observedPrimaryCommit) {
|
|
172
|
+
const symbolic = runGit(root, ["symbolic-ref", "--quiet", "HEAD"]);
|
|
173
|
+
const expectedBranch = `refs/heads/${config.primaryBranch}`;
|
|
174
|
+
if (!symbolic.ok || symbolic.stdout !== expectedBranch) {
|
|
175
|
+
return action(
|
|
176
|
+
"switch-to-primary",
|
|
177
|
+
`Preserve current work, then switch to configured primary branch ${config.primaryBranch}.`,
|
|
178
|
+
{ actual: symbolic.ok ? symbolic.stdout : null, expected: expectedBranch },
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const status = runGit(root, ["status", "--porcelain=v2", "-z", "--untracked-files=all"]);
|
|
183
|
+
if (!status.ok) {
|
|
184
|
+
return action(
|
|
185
|
+
"inspect-worktree-changes",
|
|
186
|
+
"Worktree changes could not be inspected; repair Git state before continuing.",
|
|
187
|
+
{ error: status.stderr },
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
let changes;
|
|
191
|
+
try {
|
|
192
|
+
changes = parseWorktreeChanges(status.stdout);
|
|
193
|
+
} catch (caught) {
|
|
194
|
+
return action(
|
|
195
|
+
"inspect-worktree-changes",
|
|
196
|
+
"Worktree changes could not be parsed; inspect Git state before continuing.",
|
|
197
|
+
{ error: caught.message },
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
if (changes.conflicted.length > 0) {
|
|
201
|
+
return action(
|
|
202
|
+
"resolve-conflicts",
|
|
203
|
+
"Resolve worktree conflicts without discarding either side, then run whats-next again.",
|
|
204
|
+
changes,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
if (changes.staged.length > 0 || changes.unstaged.length > 0 || changes.untracked.length > 0) {
|
|
208
|
+
return action(
|
|
209
|
+
"inspect-worktree-changes",
|
|
210
|
+
"Inspect the exact worktree diff; preserve unknown work and commit, isolate, or explicitly handle each path.",
|
|
211
|
+
changes,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const local = runGit(root, ["rev-parse", "HEAD"]);
|
|
216
|
+
if (!local.ok) {
|
|
217
|
+
return action(
|
|
218
|
+
"integrate-primary",
|
|
219
|
+
"Local HEAD is unavailable; repair the primary checkout before continuing.",
|
|
220
|
+
{ error: local.stderr },
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
if (local.stdout === observedPrimaryCommit) return null;
|
|
224
|
+
if (runGit(root, ["merge-base", "--is-ancestor", local.stdout, observedPrimaryCommit]).ok) {
|
|
225
|
+
return action(
|
|
226
|
+
"fast-forward-primary",
|
|
227
|
+
`Fast-forward local ${config.primaryBranch} to observed primary ${observedPrimaryCommit}.`,
|
|
228
|
+
{ from: local.stdout, to: observedPrimaryCommit },
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
if (runGit(root, ["merge-base", "--is-ancestor", observedPrimaryCommit, local.stdout]).ok) {
|
|
232
|
+
return action(
|
|
233
|
+
"publish-primary",
|
|
234
|
+
`Validate and publish local primary ${local.stdout} with expected remote tip ${observedPrimaryCommit}.`,
|
|
235
|
+
{
|
|
236
|
+
repository: config.primaryRepository,
|
|
237
|
+
branch: config.primaryBranch,
|
|
238
|
+
commit: local.stdout,
|
|
239
|
+
expectedRemoteTip: observedPrimaryCommit,
|
|
240
|
+
validation: { target: "commit", revision: local.stdout },
|
|
241
|
+
},
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
return action(
|
|
245
|
+
"integrate-primary",
|
|
246
|
+
"Preserve both local and remote primary histories and integrate them without force-pushing.",
|
|
247
|
+
{ local: local.stdout, remote: observedPrimaryCommit },
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function lifecycleWhatsNext({
|
|
252
|
+
create = false,
|
|
253
|
+
idea: selector,
|
|
254
|
+
root = process.cwd(),
|
|
255
|
+
userHome,
|
|
256
|
+
} = {}) {
|
|
257
|
+
const repositoryRoot = resolve(root);
|
|
258
|
+
const request = create
|
|
259
|
+
? { kind: "create-idea" }
|
|
260
|
+
: selector === undefined
|
|
261
|
+
? { kind: "navigate" }
|
|
262
|
+
: { kind: "select-idea", selector };
|
|
263
|
+
let local;
|
|
264
|
+
try {
|
|
265
|
+
local = await withTemporaryWorktree(repositoryRoot, "HEAD", (worktree) =>
|
|
266
|
+
loadConfig({ root: worktree }),
|
|
267
|
+
);
|
|
268
|
+
} catch (caught) {
|
|
269
|
+
return failed(repositoryRoot, [diagnostic(
|
|
270
|
+
"head.observation-failed",
|
|
271
|
+
caught.message,
|
|
272
|
+
"Restore a valid committed HEAD snapshot and retry.",
|
|
273
|
+
)], request);
|
|
274
|
+
}
|
|
275
|
+
if (!local.config) return failed(repositoryRoot, local.diagnostics, request);
|
|
276
|
+
|
|
277
|
+
let observedPrimaryCommit;
|
|
278
|
+
try {
|
|
279
|
+
observedPrimaryCommit = fetchPrimary(repositoryRoot, local.config);
|
|
280
|
+
} catch (caught) {
|
|
281
|
+
return failed(repositoryRoot, [diagnostic(
|
|
282
|
+
"primary.refresh-failed",
|
|
283
|
+
caught.message,
|
|
284
|
+
"Check network, authorization, repository URL, and primary branch, then retry.",
|
|
285
|
+
)], request);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let observed;
|
|
289
|
+
try {
|
|
290
|
+
observed = await withTemporaryWorktree(
|
|
291
|
+
repositoryRoot,
|
|
292
|
+
observedPrimaryCommit,
|
|
293
|
+
async (worktree, tree) => {
|
|
294
|
+
const loaded = await loadConfig({ root: worktree });
|
|
295
|
+
if (!loaded.config) return { config: null, diagnostics: loaded.diagnostics, ideas: [] };
|
|
296
|
+
const layout = await inspectIdeaLayout({
|
|
297
|
+
config: loaded.config,
|
|
298
|
+
gitRoot: repositoryRoot,
|
|
299
|
+
historyCommit: observedPrimaryCommit,
|
|
300
|
+
root: worktree,
|
|
301
|
+
snapshotTree: tree,
|
|
302
|
+
});
|
|
303
|
+
return { config: loaded.config, ...layout };
|
|
304
|
+
},
|
|
305
|
+
);
|
|
306
|
+
} catch (caught) {
|
|
307
|
+
return failed(repositoryRoot, [diagnostic(
|
|
308
|
+
"primary.observation-failed",
|
|
309
|
+
caught.message,
|
|
310
|
+
"Repair the fetched primary snapshot and retry.",
|
|
311
|
+
)], request, observedPrimaryCommit);
|
|
312
|
+
}
|
|
313
|
+
const observationDiagnostics = create
|
|
314
|
+
? observed.diagnostics.filter(({ code }) => code !== "layout.ideas.missing")
|
|
315
|
+
: observed.diagnostics;
|
|
316
|
+
if (observationDiagnostics.length > 0) {
|
|
317
|
+
return failed(repositoryRoot, observationDiagnostics, request, observedPrimaryCommit);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const loadedUserConfig = await loadUserConfig({ home: userHome });
|
|
321
|
+
if (loadedUserConfig.diagnostics.length > 0) {
|
|
322
|
+
return failed(
|
|
323
|
+
repositoryRoot,
|
|
324
|
+
loadedUserConfig.diagnostics,
|
|
325
|
+
request,
|
|
326
|
+
observedPrimaryCommit,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
const requestedIdea = selectIdea(observed.ideas, selector);
|
|
330
|
+
const repositoryLanguage = {
|
|
331
|
+
project: observed.config.preferredLanguage,
|
|
332
|
+
global: loadedUserConfig.config?.preferredLanguage,
|
|
333
|
+
};
|
|
334
|
+
const languageFor = (idea) => resolveLanguage({
|
|
335
|
+
idea: idea?.status.language,
|
|
336
|
+
...repositoryLanguage,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const hygiene = worktreeAction(repositoryRoot, local.config, observedPrimaryCommit);
|
|
340
|
+
if (hygiene) {
|
|
341
|
+
return success(
|
|
342
|
+
repositoryRoot,
|
|
343
|
+
observedPrimaryCommit,
|
|
344
|
+
request,
|
|
345
|
+
null,
|
|
346
|
+
hygiene,
|
|
347
|
+
languageFor(requestedIdea),
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (create) {
|
|
352
|
+
return success(repositoryRoot, observedPrimaryCommit, request, null, action(
|
|
353
|
+
"create-idea",
|
|
354
|
+
"Create one new three-world idea scaffold in .silvermoon/ideas/.",
|
|
355
|
+
), languageFor());
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (!selector) {
|
|
359
|
+
const active = observed.ideas.filter(({ state }) => ACTIVE_STATES.has(state));
|
|
360
|
+
if (active.length > 1) {
|
|
361
|
+
return success(repositoryRoot, observedPrimaryCommit, request, null, action(
|
|
362
|
+
"select-active-idea",
|
|
363
|
+
"Select one active idea by ULID or unique alias.",
|
|
364
|
+
{ ideas: active.map(summary) },
|
|
365
|
+
), languageFor());
|
|
366
|
+
}
|
|
367
|
+
if (active.length === 1) {
|
|
368
|
+
return success(repositoryRoot, observedPrimaryCommit, request, active[0], action(
|
|
369
|
+
"continue-active-idea",
|
|
370
|
+
`Continue active idea ${ideaName(active[0])}.`,
|
|
371
|
+
{ idea: summary(active[0]) },
|
|
372
|
+
), languageFor(active[0]));
|
|
373
|
+
}
|
|
374
|
+
return success(repositoryRoot, observedPrimaryCommit, request, null, action(
|
|
375
|
+
"create-idea",
|
|
376
|
+
"Discuss the next goal and create one self-contained three-world idea.",
|
|
377
|
+
), languageFor());
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const selected = requestedIdea;
|
|
381
|
+
if (!selected) {
|
|
382
|
+
return failed(repositoryRoot, [diagnostic(
|
|
383
|
+
"idea.not-found",
|
|
384
|
+
`Idea ${selector} does not exist in observed primary ${observedPrimaryCommit}.`,
|
|
385
|
+
"Choose a ULID or exact alias reported by silvermoon whats-next.",
|
|
386
|
+
selector,
|
|
387
|
+
)], request, observedPrimaryCommit);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return success(
|
|
391
|
+
repositoryRoot,
|
|
392
|
+
observedPrimaryCommit,
|
|
393
|
+
request,
|
|
394
|
+
selected,
|
|
395
|
+
stateAction(selected),
|
|
396
|
+
languageFor(selected),
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export async function whatsNext({
|
|
401
|
+
create = false,
|
|
402
|
+
enforceOnboarding = false,
|
|
403
|
+
idea: selector,
|
|
404
|
+
root = process.cwd(),
|
|
405
|
+
userHome,
|
|
406
|
+
} = {}) {
|
|
407
|
+
const repositoryRoot = resolve(root);
|
|
408
|
+
const onboarding = await inspectAdoption({ root: repositoryRoot, selector });
|
|
409
|
+
const gapReport = onboardingGaps(onboarding);
|
|
410
|
+
const request = create
|
|
411
|
+
? { kind: "create-idea" }
|
|
412
|
+
: selector === undefined
|
|
413
|
+
? { kind: "navigate" }
|
|
414
|
+
: { kind: "select-idea", selector };
|
|
415
|
+
if (enforceOnboarding && onboarding.status !== "ready") {
|
|
416
|
+
const loadedUserConfig = await loadUserConfig({ home: userHome });
|
|
417
|
+
if (loadedUserConfig.diagnostics.length > 0) {
|
|
418
|
+
return failed(repositoryRoot, loadedUserConfig.diagnostics, request);
|
|
419
|
+
}
|
|
420
|
+
const language = resolveLanguage({
|
|
421
|
+
global: loadedUserConfig.config?.preferredLanguage,
|
|
422
|
+
});
|
|
423
|
+
return {
|
|
424
|
+
command: "whats-next",
|
|
425
|
+
ok: true,
|
|
426
|
+
root: repositoryRoot,
|
|
427
|
+
diagnostics: [],
|
|
428
|
+
result: {
|
|
429
|
+
observedPrimaryCommit: null,
|
|
430
|
+
request,
|
|
431
|
+
language,
|
|
432
|
+
selectedIdea: null,
|
|
433
|
+
onboarding: gapReport,
|
|
434
|
+
action: actionWithLanguage(action(
|
|
435
|
+
"adopt-silvermoon",
|
|
436
|
+
"Complete the blocking Silvermoon onboarding gaps before idea lifecycle work.",
|
|
437
|
+
{
|
|
438
|
+
gaps: gapReport.gaps,
|
|
439
|
+
recommendedAction: gapReport.recommendedAction,
|
|
440
|
+
recheck: gapReport.recheck,
|
|
441
|
+
},
|
|
442
|
+
), language),
|
|
443
|
+
},
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
const report = await lifecycleWhatsNext({
|
|
447
|
+
create,
|
|
448
|
+
idea: selector,
|
|
449
|
+
root: repositoryRoot,
|
|
450
|
+
userHome,
|
|
451
|
+
});
|
|
452
|
+
if (report.result && gapReport) report.result.onboarding = gapReport;
|
|
453
|
+
return report;
|
|
379
454
|
}
|