@pugi/cli 0.1.0-beta.23 → 0.1.0-beta.24
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/core/auto-update/channels.js +122 -0
- package/dist/core/auto-update/checker.js +241 -0
- package/dist/core/auto-update/state.js +235 -0
- package/dist/core/engine/compaction-hook.js +154 -0
- package/dist/core/init/scaffold.js +195 -0
- package/dist/core/repl/codebase-survey.js +308 -0
- package/dist/core/repl/init-interview.js +457 -0
- package/dist/core/repl/onboarding-state.js +297 -0
- package/dist/core/repl/session.js +49 -0
- package/dist/core/repl/slash-commands.js +15 -0
- package/dist/runtime/cli.js +80 -0
- package/dist/runtime/commands/update.js +289 -0
- package/dist/runtime/version.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/init` interview orchestrator - 4 phases.
|
|
3
|
+
*
|
|
4
|
+
* Inspired by Claude Code's /init slash command (the upstream prompts
|
|
5
|
+
* the operator across four phases: ask scope → survey codebase →
|
|
6
|
+
* gap-fill questions → synthesise CLAUDE.md + skills + hooks).
|
|
7
|
+
* Independent implementation: this module is a pure state machine
|
|
8
|
+
* that emits the next operator-facing step; the REPL session module
|
|
9
|
+
* owns the side effects (mounting modals, surfacing the survey
|
|
10
|
+
* digest, writing PUGI.md and skill scaffolds to disk).
|
|
11
|
+
*
|
|
12
|
+
* # Why a state machine
|
|
13
|
+
*
|
|
14
|
+
* The four phases need three blocking AskUserQuestion modals (one in
|
|
15
|
+
* Phase 1 for scope, two in Phase 3 for gap-fill) plus one
|
|
16
|
+
* synthesise-and-write step in Phase 4. The session module's modal
|
|
17
|
+
* surface is asynchronous and pull-shaped: it sets `pendingAsk` and
|
|
18
|
+
* waits for `resolveAsk` to fire. Building the interview as a state
|
|
19
|
+
* machine that surfaces "what to ask next" / "what to write next"
|
|
20
|
+
* means the session can drive the modal loop without this module
|
|
21
|
+
* needing to know about Ink, the SDK transport, or the file writer.
|
|
22
|
+
*
|
|
23
|
+
* The machine is deterministic: every transition is a pure function
|
|
24
|
+
* of the current step + the operator's most recent answer + the
|
|
25
|
+
* Phase 2 survey result. This makes it trivially testable - the spec
|
|
26
|
+
* drives the machine to completion without any Ink, fs, or network.
|
|
27
|
+
*
|
|
28
|
+
* # Phase summary
|
|
29
|
+
*
|
|
30
|
+
* Phase 1: scope - "Which contexts? PUGI.md / personal / both?" +
|
|
31
|
+
* "Skills and hooks? skills only / hooks only / neither?".
|
|
32
|
+
* Phase 2: survey - this module's `runSurvey()` calls into the
|
|
33
|
+
* codebase-survey helper (a pure fs scan; see that file for
|
|
34
|
+
* why we do not spawn a subagent).
|
|
35
|
+
* Phase 3: gap-fill - one question for the project codebase practices
|
|
36
|
+
* (only if the operator chose project or both) + one
|
|
37
|
+
* personal question (only if they chose personal or both).
|
|
38
|
+
* Phase 4: synthesise - emit a write plan (a list of {path, body}
|
|
39
|
+
* pairs) the session module writes to disk + a list of
|
|
40
|
+
* optional skill scaffolds + an optional hooks.json stub.
|
|
41
|
+
*/
|
|
42
|
+
import { signatureForAsk } from './ask.js';
|
|
43
|
+
import { surveyCodebase } from './codebase-survey.js';
|
|
44
|
+
/* ------------------------------------------------------------------ */
|
|
45
|
+
/* Public API */
|
|
46
|
+
/* ------------------------------------------------------------------ */
|
|
47
|
+
/**
|
|
48
|
+
* Start a fresh interview. The caller drives the loop by alternating
|
|
49
|
+
* `nextStep(state)` and `advance(state, answer)` until `state.phase`
|
|
50
|
+
* becomes `'complete'`.
|
|
51
|
+
*/
|
|
52
|
+
export function startInterview() {
|
|
53
|
+
return { phase: 'phase1_scope' };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Compute the next step the session should drive. Pure: returns one
|
|
57
|
+
* of the seven `NextStep` variants based on the current phase.
|
|
58
|
+
*/
|
|
59
|
+
export function nextStep(state) {
|
|
60
|
+
switch (state.phase) {
|
|
61
|
+
case 'phase1_scope':
|
|
62
|
+
return { kind: 'ask_scope' };
|
|
63
|
+
case 'phase1_extras':
|
|
64
|
+
return { kind: 'ask_extras' };
|
|
65
|
+
case 'phase2_survey':
|
|
66
|
+
return { kind: 'run_survey' };
|
|
67
|
+
case 'phase3_project_gap':
|
|
68
|
+
return { kind: 'ask_project_gap', survey: state.survey };
|
|
69
|
+
case 'phase3_personal_gap':
|
|
70
|
+
return { kind: 'ask_personal_gap', survey: state.survey };
|
|
71
|
+
case 'phase4_synthesise':
|
|
72
|
+
return {
|
|
73
|
+
kind: 'synthesise',
|
|
74
|
+
scope: state.scope,
|
|
75
|
+
extras: state.extras,
|
|
76
|
+
survey: state.survey,
|
|
77
|
+
gaps: state.gaps,
|
|
78
|
+
};
|
|
79
|
+
case 'complete':
|
|
80
|
+
return { kind: 'done', plan: state.plan };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Advance the machine one step. Returns the next state. Throws on a
|
|
85
|
+
* mismatched answer shape (the session module is the only caller and
|
|
86
|
+
* it always passes the matching variant; a mismatch is a bug, not an
|
|
87
|
+
* operator-input edge case).
|
|
88
|
+
*/
|
|
89
|
+
export function advance(state, answer) {
|
|
90
|
+
switch (state.phase) {
|
|
91
|
+
case 'phase1_scope': {
|
|
92
|
+
if (answer.kind === 'scope') {
|
|
93
|
+
return { phase: 'phase1_extras', scope: answer.value };
|
|
94
|
+
}
|
|
95
|
+
throw new InterviewMisuseError('phase1_scope expects a scope answer');
|
|
96
|
+
}
|
|
97
|
+
case 'phase1_extras': {
|
|
98
|
+
if (answer.kind === 'extras') {
|
|
99
|
+
return { phase: 'phase2_survey', scope: state.scope, extras: answer.value };
|
|
100
|
+
}
|
|
101
|
+
throw new InterviewMisuseError('phase1_extras expects an extras answer');
|
|
102
|
+
}
|
|
103
|
+
case 'phase2_survey': {
|
|
104
|
+
if (answer.kind === 'survey_result') {
|
|
105
|
+
// Branch into Phase 3 based on the scope choice. If the
|
|
106
|
+
// operator picked `personal` only, skip the project-gap
|
|
107
|
+
// question entirely (the upstream pattern does the same:
|
|
108
|
+
// "ask only the questions the scope warrants").
|
|
109
|
+
if (state.scope === 'personal') {
|
|
110
|
+
return {
|
|
111
|
+
phase: 'phase3_personal_gap',
|
|
112
|
+
scope: state.scope,
|
|
113
|
+
extras: state.extras,
|
|
114
|
+
survey: answer.value,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
phase: 'phase3_project_gap',
|
|
119
|
+
scope: state.scope,
|
|
120
|
+
extras: state.extras,
|
|
121
|
+
survey: answer.value,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
throw new InterviewMisuseError('phase2_survey expects a survey_result answer');
|
|
125
|
+
}
|
|
126
|
+
case 'phase3_project_gap': {
|
|
127
|
+
if (answer.kind === 'project_gap' || answer.kind === 'skip') {
|
|
128
|
+
const projectNote = answer.kind === 'project_gap' ? answer.value.trim() : '';
|
|
129
|
+
// If the scope skipped personal, jump straight to Phase 4.
|
|
130
|
+
if (state.scope === 'project') {
|
|
131
|
+
return {
|
|
132
|
+
phase: 'phase4_synthesise',
|
|
133
|
+
scope: state.scope,
|
|
134
|
+
extras: state.extras,
|
|
135
|
+
survey: state.survey,
|
|
136
|
+
gaps: { projectNote: projectNote || undefined },
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
phase: 'phase3_personal_gap',
|
|
141
|
+
scope: state.scope,
|
|
142
|
+
extras: state.extras,
|
|
143
|
+
survey: state.survey,
|
|
144
|
+
projectNote: projectNote || undefined,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
throw new InterviewMisuseError('phase3_project_gap expects a project_gap or skip answer');
|
|
148
|
+
}
|
|
149
|
+
case 'phase3_personal_gap': {
|
|
150
|
+
if (answer.kind === 'personal_gap' || answer.kind === 'skip') {
|
|
151
|
+
const personalNote = answer.kind === 'personal_gap' ? answer.value.trim() : '';
|
|
152
|
+
const gaps = {};
|
|
153
|
+
if (state.projectNote !== undefined && state.projectNote.length > 0) {
|
|
154
|
+
gaps.projectNote = state.projectNote;
|
|
155
|
+
}
|
|
156
|
+
if (personalNote.length > 0) {
|
|
157
|
+
gaps.personalNote = personalNote;
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
phase: 'phase4_synthesise',
|
|
161
|
+
scope: state.scope,
|
|
162
|
+
extras: state.extras,
|
|
163
|
+
survey: state.survey,
|
|
164
|
+
gaps,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
throw new InterviewMisuseError('phase3_personal_gap expects a personal_gap or skip answer');
|
|
168
|
+
}
|
|
169
|
+
case 'phase4_synthesise': {
|
|
170
|
+
if (answer.kind === 'synthesise_complete') {
|
|
171
|
+
return { phase: 'complete', plan: answer.value };
|
|
172
|
+
}
|
|
173
|
+
throw new InterviewMisuseError('phase4_synthesise expects a synthesise_complete answer');
|
|
174
|
+
}
|
|
175
|
+
case 'complete':
|
|
176
|
+
throw new InterviewMisuseError('interview already complete');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Phase 2 worker. Wraps `surveyCodebase` so the session module can
|
|
181
|
+
* stay symmetric with the modal-driven phases - every phase has a
|
|
182
|
+
* single helper that turns "current state" into "next answer".
|
|
183
|
+
*/
|
|
184
|
+
export function runSurvey(workspaceRoot) {
|
|
185
|
+
return surveyCodebase(workspaceRoot);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Phase 4 worker. Pure: turns the accumulated answers into a write
|
|
189
|
+
* plan the session module commits. The session is responsible for
|
|
190
|
+
* actually writing files; this function decides WHAT to write but
|
|
191
|
+
* never touches the disk.
|
|
192
|
+
*/
|
|
193
|
+
export function synthesise(scope, extras, survey, gaps, paths) {
|
|
194
|
+
const entries = [];
|
|
195
|
+
const summaryLines = [];
|
|
196
|
+
if (scope === 'project' || scope === 'both') {
|
|
197
|
+
const body = renderPugiMd(survey, gaps);
|
|
198
|
+
entries.push({
|
|
199
|
+
kind: 'pugi_md',
|
|
200
|
+
path: survey.hasExistingPugiMd ? paths.pugiMdProposalPath : paths.pugiMdPath,
|
|
201
|
+
body,
|
|
202
|
+
});
|
|
203
|
+
summaryLines.push(survey.hasExistingPugiMd
|
|
204
|
+
? `Proposed PUGI.md updates at ${paths.pugiMdProposalPath} (existing PUGI.md left untouched)`
|
|
205
|
+
: `Wrote PUGI.md at ${paths.pugiMdPath}`);
|
|
206
|
+
}
|
|
207
|
+
if (scope === 'personal' || scope === 'both') {
|
|
208
|
+
const body = renderPugiLocalMd(gaps);
|
|
209
|
+
entries.push({
|
|
210
|
+
kind: 'pugi_local_md',
|
|
211
|
+
path: paths.pugiLocalMdPath,
|
|
212
|
+
body,
|
|
213
|
+
});
|
|
214
|
+
summaryLines.push(`Wrote PUGI.local.md at ${paths.pugiLocalMdPath}`);
|
|
215
|
+
}
|
|
216
|
+
if (extras === 'skills_and_hooks' || extras === 'skills_only') {
|
|
217
|
+
// Phase 4 default skill proposal: a `verify` skill when the survey
|
|
218
|
+
// found a test command. The upstream pattern is "propose skills
|
|
219
|
+
// grounded in what we found"; this is the Pugi minimum that
|
|
220
|
+
// demonstrates the slot without committing to an opinionated
|
|
221
|
+
// catalogue. The skill body cites the Phase 2 test command so the
|
|
222
|
+
// operator can edit it once and have a runnable workflow.
|
|
223
|
+
if (survey.testCommand) {
|
|
224
|
+
const skillBody = renderVerifySkill(survey);
|
|
225
|
+
entries.push({
|
|
226
|
+
kind: 'skill',
|
|
227
|
+
name: 'verify',
|
|
228
|
+
path: paths.skillsDir + '/verify/SKILL.md',
|
|
229
|
+
body: skillBody,
|
|
230
|
+
});
|
|
231
|
+
summaryLines.push(`Scaffolded /verify skill (runs ${survey.packageManager} ${survey.testCommand})`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (extras === 'skills_and_hooks' || extras === 'hooks_only') {
|
|
235
|
+
if (survey.formatCommand) {
|
|
236
|
+
const hooksBody = renderHooksJson(survey);
|
|
237
|
+
entries.push({
|
|
238
|
+
kind: 'hooks_json',
|
|
239
|
+
path: paths.hooksJsonPath,
|
|
240
|
+
body: hooksBody,
|
|
241
|
+
});
|
|
242
|
+
summaryLines.push(`Stubbed hooks.json with a PostToolUse format hook (${survey.packageManager} ${survey.formatCommand})`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
scope,
|
|
247
|
+
extras,
|
|
248
|
+
survey,
|
|
249
|
+
entries,
|
|
250
|
+
summaryLines,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/* ------------------------------------------------------------------ */
|
|
254
|
+
/* Question synthesis helpers - the session mounts these as <pugi-ask> */
|
|
255
|
+
/* ------------------------------------------------------------------ */
|
|
256
|
+
/**
|
|
257
|
+
* Build the Phase 1 scope ask. The session module passes the returned
|
|
258
|
+
* `AskTag` to the same modal layer that handles persona-emitted asks
|
|
259
|
+
* so the operator UX is identical across surfaces.
|
|
260
|
+
*
|
|
261
|
+
* Returns a structurally valid `AskTag`. Callers may rely on
|
|
262
|
+
* `signatureForAsk` matching the parser-side hash.
|
|
263
|
+
*/
|
|
264
|
+
export function scopeAskTag() {
|
|
265
|
+
const question = 'Which PUGI context files should /init set up?';
|
|
266
|
+
const options = [
|
|
267
|
+
{ value: 'project', label: 'Project PUGI.md', desc: 'Team-shared, checked in' },
|
|
268
|
+
{ value: 'personal', label: 'Personal PUGI.local.md', desc: 'Gitignored, private' },
|
|
269
|
+
{ value: 'both', label: 'Both project + personal', desc: 'Most flexible' },
|
|
270
|
+
];
|
|
271
|
+
return {
|
|
272
|
+
question,
|
|
273
|
+
options,
|
|
274
|
+
signature: signatureForAsk(question, options),
|
|
275
|
+
start: 0,
|
|
276
|
+
end: 0,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Phase 1 extras ask. Same shape as scope; the session reuses the
|
|
281
|
+
* `<pugi-ask>` modal.
|
|
282
|
+
*/
|
|
283
|
+
export function extrasAskTag() {
|
|
284
|
+
const question = 'Also set up skills and hooks?';
|
|
285
|
+
const options = [
|
|
286
|
+
{ value: 'skills_and_hooks', label: 'Skills + hooks', desc: 'Both surfaces' },
|
|
287
|
+
{ value: 'skills_only', label: 'Skills only', desc: 'On-demand workflows' },
|
|
288
|
+
{ value: 'hooks_only', label: 'Hooks only', desc: 'Deterministic events' },
|
|
289
|
+
{ value: 'neither', label: 'Neither', desc: 'Just PUGI.md' },
|
|
290
|
+
];
|
|
291
|
+
return {
|
|
292
|
+
question,
|
|
293
|
+
options,
|
|
294
|
+
signature: signatureForAsk(question, options),
|
|
295
|
+
start: 0,
|
|
296
|
+
end: 0,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Phase 3 project gap ask. The question text adapts to whether the
|
|
301
|
+
* survey found a manifest - when it did the operator is asked about
|
|
302
|
+
* gotchas; when it did not we ask for the build command outright.
|
|
303
|
+
*
|
|
304
|
+
* Two-option `<pugi-ask>` (answer / skip) - the freeform text path is
|
|
305
|
+
* captured via the modal's "Other" sentinel which the session module
|
|
306
|
+
* translates into a `project_gap` answer.
|
|
307
|
+
*/
|
|
308
|
+
export function projectGapAskTag(survey) {
|
|
309
|
+
const question = survey.manifest === 'unknown'
|
|
310
|
+
? 'What build/test/lint commands does this repo use?'
|
|
311
|
+
: 'Any non-obvious gotchas Pugi should know?';
|
|
312
|
+
const options = [
|
|
313
|
+
{ value: 'answer', label: 'I will type a note', desc: 'Use the Other field below' },
|
|
314
|
+
{ value: 'skip', label: 'Skip', desc: 'Leave PUGI.md minimal' },
|
|
315
|
+
];
|
|
316
|
+
return {
|
|
317
|
+
question,
|
|
318
|
+
options,
|
|
319
|
+
signature: signatureForAsk(question, options),
|
|
320
|
+
start: 0,
|
|
321
|
+
end: 0,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Phase 3 personal gap ask. Always fixed copy - the upstream pattern
|
|
326
|
+
* is "ask about THEM, not the codebase" so we keep the question scoped
|
|
327
|
+
* to operator preferences.
|
|
328
|
+
*/
|
|
329
|
+
export function personalGapAskTag() {
|
|
330
|
+
const question = 'Anything Pugi should know about you (role, preferences)?';
|
|
331
|
+
const options = [
|
|
332
|
+
{ value: 'answer', label: 'I will type a note', desc: 'Use the Other field below' },
|
|
333
|
+
{ value: 'skip', label: 'Skip', desc: 'Leave PUGI.local.md minimal' },
|
|
334
|
+
];
|
|
335
|
+
return {
|
|
336
|
+
question,
|
|
337
|
+
options,
|
|
338
|
+
signature: signatureForAsk(question, options),
|
|
339
|
+
start: 0,
|
|
340
|
+
end: 0,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
/* ------------------------------------------------------------------ */
|
|
344
|
+
/* Body renderers */
|
|
345
|
+
/* ------------------------------------------------------------------ */
|
|
346
|
+
function renderPugiMd(survey, gaps) {
|
|
347
|
+
const lines = [
|
|
348
|
+
'# PUGI.md',
|
|
349
|
+
'',
|
|
350
|
+
'This file provides guidance to Pugi when working with code in this repository.',
|
|
351
|
+
'',
|
|
352
|
+
'## Stack',
|
|
353
|
+
'',
|
|
354
|
+
];
|
|
355
|
+
if (survey.manifest !== 'unknown') {
|
|
356
|
+
lines.push(`- Manifest: ${survey.manifest}`);
|
|
357
|
+
}
|
|
358
|
+
if (survey.packageManager !== 'unknown') {
|
|
359
|
+
lines.push(`- Package manager: ${survey.packageManager}`);
|
|
360
|
+
}
|
|
361
|
+
if (survey.languages.length > 0) {
|
|
362
|
+
lines.push(`- Languages: ${survey.languages.join(', ')}`);
|
|
363
|
+
}
|
|
364
|
+
lines.push('');
|
|
365
|
+
lines.push('## Commands');
|
|
366
|
+
lines.push('');
|
|
367
|
+
if (survey.buildCommand) {
|
|
368
|
+
lines.push(`- Build: \`${survey.packageManager} run ${survey.buildCommand}\``);
|
|
369
|
+
}
|
|
370
|
+
if (survey.testCommand) {
|
|
371
|
+
lines.push(`- Test: \`${survey.packageManager} run ${survey.testCommand}\``);
|
|
372
|
+
}
|
|
373
|
+
if (survey.lintCommand) {
|
|
374
|
+
lines.push(`- Lint: \`${survey.packageManager} run ${survey.lintCommand}\``);
|
|
375
|
+
}
|
|
376
|
+
if (survey.formatCommand) {
|
|
377
|
+
lines.push(`- Format: \`${survey.packageManager} run ${survey.formatCommand}\``);
|
|
378
|
+
}
|
|
379
|
+
lines.push('');
|
|
380
|
+
if (gaps.projectNote && gaps.projectNote.length > 0) {
|
|
381
|
+
lines.push('## Project Notes');
|
|
382
|
+
lines.push('');
|
|
383
|
+
lines.push(gaps.projectNote);
|
|
384
|
+
lines.push('');
|
|
385
|
+
}
|
|
386
|
+
lines.push('## Conventions');
|
|
387
|
+
lines.push('');
|
|
388
|
+
lines.push('- Generated code, comments, commits, PR text default to English.');
|
|
389
|
+
lines.push('- Do not add AI Co-Authored-By trailers.');
|
|
390
|
+
lines.push('- Do not store secrets or credentials in this file.');
|
|
391
|
+
lines.push('');
|
|
392
|
+
return lines.join('\n');
|
|
393
|
+
}
|
|
394
|
+
function renderPugiLocalMd(gaps) {
|
|
395
|
+
const lines = [
|
|
396
|
+
'# PUGI.local.md',
|
|
397
|
+
'',
|
|
398
|
+
'Personal preferences for this project. Gitignored.',
|
|
399
|
+
'',
|
|
400
|
+
];
|
|
401
|
+
if (gaps.personalNote && gaps.personalNote.length > 0) {
|
|
402
|
+
lines.push('## About You');
|
|
403
|
+
lines.push('');
|
|
404
|
+
lines.push(gaps.personalNote);
|
|
405
|
+
lines.push('');
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
lines.push('Edit this file to add your role, communication preferences,');
|
|
409
|
+
lines.push('and any sandbox URLs Pugi should know about.');
|
|
410
|
+
lines.push('');
|
|
411
|
+
}
|
|
412
|
+
return lines.join('\n');
|
|
413
|
+
}
|
|
414
|
+
function renderVerifySkill(survey) {
|
|
415
|
+
const cmd = `${survey.packageManager === 'unknown' ? 'npm' : survey.packageManager} run ${survey.testCommand ?? 'test'}`;
|
|
416
|
+
return [
|
|
417
|
+
'---',
|
|
418
|
+
'name: verify',
|
|
419
|
+
'description: Run the project test suite end-to-end and report PASS/FAIL.',
|
|
420
|
+
'---',
|
|
421
|
+
'',
|
|
422
|
+
`Run \`${cmd}\` and report the result. On failure, surface the first failing test name`,
|
|
423
|
+
'and the file:line so the operator can navigate directly.',
|
|
424
|
+
'',
|
|
425
|
+
].join('\n');
|
|
426
|
+
}
|
|
427
|
+
function renderHooksJson(survey) {
|
|
428
|
+
const cmd = `${survey.packageManager === 'unknown' ? 'npm' : survey.packageManager} run ${survey.formatCommand ?? 'format'}`;
|
|
429
|
+
const payload = {
|
|
430
|
+
schema: 1,
|
|
431
|
+
hooks: [
|
|
432
|
+
{
|
|
433
|
+
event: 'PostToolUse',
|
|
434
|
+
matcher: 'Write|Edit',
|
|
435
|
+
command: cmd,
|
|
436
|
+
description: 'Format edited files after every write.',
|
|
437
|
+
},
|
|
438
|
+
],
|
|
439
|
+
};
|
|
440
|
+
return `${JSON.stringify(payload, null, 2)}\n`;
|
|
441
|
+
}
|
|
442
|
+
/* ------------------------------------------------------------------ */
|
|
443
|
+
/* Errors */
|
|
444
|
+
/* ------------------------------------------------------------------ */
|
|
445
|
+
/**
|
|
446
|
+
* Thrown when the session module hands the orchestrator an answer
|
|
447
|
+
* shape that does not match the current phase. This is a programming
|
|
448
|
+
* bug, not an operator-input edge case - bubbled up so the session
|
|
449
|
+
* crashes loudly during development rather than silently drifting.
|
|
450
|
+
*/
|
|
451
|
+
export class InterviewMisuseError extends Error {
|
|
452
|
+
constructor(message) {
|
|
453
|
+
super(`init-interview: ${message}`);
|
|
454
|
+
this.name = 'InterviewMisuseError';
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
//# sourceMappingURL=init-interview.js.map
|