linersh 0.5.7 → 1.0.1
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 +84 -159
- package/bin/liner.js +89 -44
- package/cli-update-docs/README.md +6 -6
- package/cli-update-docs/SKILL.md +72 -37
- package/cli-update-docs/quality-check-tests.md +256 -8
- package/cli-update-docs/source-finding-tactics.md +140 -3
- package/cli-update-docs/source-quality-hierarchy.md +83 -0
- package/cli-update-docs/synthesis-guidance.md +19 -5
- package/dist/agents/artifact-validation.js +440 -0
- package/dist/agents/artifact-validation.js.map +1 -0
- package/dist/agents/candidate-longlist.js +100 -0
- package/dist/agents/candidate-longlist.js.map +1 -0
- package/dist/agents/detect.js +3 -2
- package/dist/agents/detect.js.map +1 -1
- package/dist/agents/evaluation-assembly.js +270 -0
- package/dist/agents/evaluation-assembly.js.map +1 -0
- package/dist/agents/headless-runner.js +227 -0
- package/dist/agents/headless-runner.js.map +1 -0
- package/dist/agents/jtbd-clarify.js +42 -41
- package/dist/agents/jtbd-clarify.js.map +1 -1
- package/dist/agents/models.js +6 -7
- package/dist/agents/models.js.map +1 -1
- package/dist/agents/prompts.js +220 -20
- package/dist/agents/prompts.js.map +1 -1
- package/dist/agents/runner.js +462 -21
- package/dist/agents/runner.js.map +1 -1
- package/dist/phases.js +13 -13
- package/dist/phases.js.map +1 -1
- package/dist/source-inbox.js +297 -0
- package/dist/source-inbox.js.map +1 -0
- package/dist/version.js +3 -9
- package/dist/version.js.map +1 -1
- package/dist/yaml-io.js +42 -11
- package/dist/yaml-io.js.map +1 -1
- package/package.json +26 -23
- package/cli-update-docs/ABOUT.md +0 -69
- package/cli-update-docs/AI-HANDOFF.md +0 -241
- package/cli-update-docs/CURATION.md +0 -261
- package/cli-update-docs/JOB-STORY-MIGRATION.md +0 -99
- package/cli-update-docs/JTBD-MASTER-PROMPT-ABOUT.md +0 -133
- package/cli-update-docs/JTBD-MASTER-PROMPT.md +0 -147
- package/cli-update-docs/LINER-MASTER.md +0 -546
- package/cli-update-docs/MIXTAPE-FORMAT.md +0 -398
- package/cli-update-docs/PLATFORM.md +0 -210
- package/cli-update-docs/TUI-BACKLOG.md +0 -155
- package/dist/App.js +0 -220
- package/dist/App.js.map +0 -1
- package/dist/colors.js +0 -132
- package/dist/colors.js.map +0 -1
- package/dist/components/Header.js +0 -15
- package/dist/components/Header.js.map +0 -1
- package/dist/components/KeyHints.js +0 -17
- package/dist/components/KeyHints.js.map +0 -1
- package/dist/components/LabeledBox.js +0 -53
- package/dist/components/LabeledBox.js.map +0 -1
- package/dist/components/NextSteps.js +0 -18
- package/dist/components/NextSteps.js.map +0 -1
- package/dist/index.js +0 -17
- package/dist/index.js.map +0 -1
- package/dist/screens/AgentSetup.js +0 -224
- package/dist/screens/AgentSetup.js.map +0 -1
- package/dist/screens/BootSplash.js +0 -163
- package/dist/screens/BootSplash.js.map +0 -1
- package/dist/screens/CompileView.js +0 -567
- package/dist/screens/CompileView.js.map +0 -1
- package/dist/screens/FramingConfirmReview.js +0 -197
- package/dist/screens/FramingConfirmReview.js.map +0 -1
- package/dist/screens/Gate1Review.js +0 -147
- package/dist/screens/Gate1Review.js.map +0 -1
- package/dist/screens/Gate2Review.js +0 -162
- package/dist/screens/Gate2Review.js.map +0 -1
- package/dist/screens/ImportPrompt.js +0 -173
- package/dist/screens/ImportPrompt.js.map +0 -1
- package/dist/screens/JsSetup.js +0 -77
- package/dist/screens/JsSetup.js.map +0 -1
- package/dist/screens/LocalFilePicker.js +0 -139
- package/dist/screens/LocalFilePicker.js.map +0 -1
- package/dist/screens/NewMixtapeWizard.js +0 -662
- package/dist/screens/NewMixtapeWizard.js.map +0 -1
- package/dist/screens/PhaseRunner.js +0 -1203
- package/dist/screens/PhaseRunner.js.map +0 -1
- package/dist/screens/ProcessManifest.js +0 -160
- package/dist/screens/ProcessManifest.js.map +0 -1
- package/dist/screens/ProjectHub.js +0 -557
- package/dist/screens/ProjectHub.js.map +0 -1
- package/dist/screens/SourceModal.js +0 -269
- package/dist/screens/SourceModal.js.map +0 -1
- package/dist/screens/TapeBrowser.js +0 -312
- package/dist/screens/TapeBrowser.js.map +0 -1
- package/dist/screens/TapeDraftReview.js +0 -444
- package/dist/screens/TapeDraftReview.js.map +0 -1
- package/dist/screens/TapeEditor.js +0 -310
- package/dist/screens/TapeEditor.js.map +0 -1
- package/dist/screens/cassette-art.js +0 -72
- package/dist/screens/cassette-art.js.map +0 -1
- package/dist/store.js +0 -10
- package/dist/store.js.map +0 -1
|
@@ -1,662 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useMemo, useState } from "react";
|
|
2
|
-
import { Box, Text, useInput } from "ink";
|
|
3
|
-
import Spinner from "ink-spinner";
|
|
4
|
-
import TextInput from "ink-text-input";
|
|
5
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
-
import { Header } from "../components/Header.js";
|
|
8
|
-
import { KeyHints } from "../components/KeyHints.js";
|
|
9
|
-
import { useApp } from "../store.js";
|
|
10
|
-
import { runInit } from "../ipc.js";
|
|
11
|
-
import { readTape, slugify, writeTape, projectFolder } from "../yaml-io.js";
|
|
12
|
-
import { writeProgress } from "../progress.js";
|
|
13
|
-
import { detectAgents, resolveSkillPath } from "../agents/detect.js";
|
|
14
|
-
import { elicitClarifyingQuestions } from "../agents/jtbd-clarify.js";
|
|
15
|
-
import { MUTED, CURRENT, ERROR, HEADING, HIGHLIGHT, STRUCTURE, SUCCESS, WARNING } from "../colors.js";
|
|
16
|
-
// 6 input steps: slug, jtbd, jtbd-clarify, title, description, curator.
|
|
17
|
-
const TOTAL_STEPS = 6;
|
|
18
|
-
const STEP_ORDER = [
|
|
19
|
-
"slug",
|
|
20
|
-
"jtbd",
|
|
21
|
-
"jtbd-clarify",
|
|
22
|
-
"title",
|
|
23
|
-
"description",
|
|
24
|
-
"curator",
|
|
25
|
-
];
|
|
26
|
-
export function NewMixtapeWizard() {
|
|
27
|
-
const app = useApp();
|
|
28
|
-
const [step, setStep] = useState("slug");
|
|
29
|
-
// Draft fields start empty — we don't pre-fill suggestions into draft any
|
|
30
|
-
// more, because that left the previous-step text sitting in the new input.
|
|
31
|
-
// Suggestions are surfaced via `suggestionFor()` as placeholders; on empty
|
|
32
|
-
// submit we accept the suggestion as the value.
|
|
33
|
-
const [draft, setDraft] = useState({
|
|
34
|
-
mode: "quick",
|
|
35
|
-
slug: "",
|
|
36
|
-
jtbd: "",
|
|
37
|
-
clarifications: [],
|
|
38
|
-
title: "",
|
|
39
|
-
description: "",
|
|
40
|
-
curator: "",
|
|
41
|
-
});
|
|
42
|
-
const [buffer, setBuffer] = useState("");
|
|
43
|
-
const [error, setError] = useState(null);
|
|
44
|
-
// When the user jumps into a field from the review screen (via 1-6), we
|
|
45
|
-
// record that here so the next goForward returns straight to review instead
|
|
46
|
-
// of traversing the rest of the wizard. Reset after the bounce-back.
|
|
47
|
-
const [returningToReview, setReturningToReview] = useState(false);
|
|
48
|
-
// JTBD-clarify sub-state. Idle until the user enters the step; loading while
|
|
49
|
-
// the agent generates questions; asking while the user answers them.
|
|
50
|
-
const [clarifyState, setClarifyState] = useState({ phase: "idle" });
|
|
51
|
-
const stepIndex = useMemo(() => {
|
|
52
|
-
if (step === "review")
|
|
53
|
-
return TOTAL_STEPS;
|
|
54
|
-
if (step === "creating")
|
|
55
|
-
return TOTAL_STEPS + 1;
|
|
56
|
-
return STEP_ORDER.indexOf(step) + 1;
|
|
57
|
-
}, [step]);
|
|
58
|
-
// When the user enters the JTBD-clarify step for the first time, fire off
|
|
59
|
-
// the agent call to generate questions. On success: store the questions in
|
|
60
|
-
// clarifyState and the user starts answering. On any failure: the helper
|
|
61
|
-
// returns the hardcoded baseline, so the user still gets a clarify pass.
|
|
62
|
-
useEffect(() => {
|
|
63
|
-
if (step !== "jtbd-clarify")
|
|
64
|
-
return;
|
|
65
|
-
if (clarifyState.phase !== "idle")
|
|
66
|
-
return;
|
|
67
|
-
let cancelled = false;
|
|
68
|
-
setClarifyState({ phase: "loading" });
|
|
69
|
-
const agents = detectAgents();
|
|
70
|
-
const skillPath = resolveSkillPath();
|
|
71
|
-
void elicitClarifyingQuestions({
|
|
72
|
-
jtbd: draft.jtbd,
|
|
73
|
-
agent: agents[0] ?? null,
|
|
74
|
-
skillPath,
|
|
75
|
-
cwd: app.baseDir,
|
|
76
|
-
}).then((questions) => {
|
|
77
|
-
if (cancelled)
|
|
78
|
-
return;
|
|
79
|
-
setClarifyState({
|
|
80
|
-
phase: "asking",
|
|
81
|
-
questions,
|
|
82
|
-
answers: new Array(questions.length).fill(""),
|
|
83
|
-
index: 0,
|
|
84
|
-
});
|
|
85
|
-
setBuffer("");
|
|
86
|
-
});
|
|
87
|
-
return () => {
|
|
88
|
-
cancelled = true;
|
|
89
|
-
};
|
|
90
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
91
|
-
}, [step]);
|
|
92
|
-
function startEditing(target) {
|
|
93
|
-
setBuffer(draft[target]);
|
|
94
|
-
setError(null);
|
|
95
|
-
setReturningToReview(true);
|
|
96
|
-
setStep(target);
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Resolve a submitted value, falling back to the per-step suggestion when
|
|
100
|
-
* the user submitted an empty buffer. Lets Enter "accept the default" while
|
|
101
|
-
* a non-empty type-then-Enter overrides it cleanly.
|
|
102
|
-
*/
|
|
103
|
-
function resolveSubmit(rawValue, currentStep) {
|
|
104
|
-
const trimmed = rawValue.trim();
|
|
105
|
-
if (trimmed)
|
|
106
|
-
return trimmed;
|
|
107
|
-
return suggestionFor(currentStep, draft).trim();
|
|
108
|
-
}
|
|
109
|
-
// Commit one answer in the clarify step and advance to the next question
|
|
110
|
-
// (or to the title step when finished). Empty answers are kept — the user
|
|
111
|
-
// can skip a question they don't have an answer for; the clarification is
|
|
112
|
-
// still recorded so the agent can see "user skipped this one".
|
|
113
|
-
function commitClarifyAnswer(value) {
|
|
114
|
-
if (clarifyState.phase !== "asking")
|
|
115
|
-
return;
|
|
116
|
-
const trimmed = value.trim();
|
|
117
|
-
const nextAnswers = clarifyState.answers.slice();
|
|
118
|
-
nextAnswers[clarifyState.index] = trimmed;
|
|
119
|
-
const nextIndex = clarifyState.index + 1;
|
|
120
|
-
if (nextIndex >= clarifyState.questions.length) {
|
|
121
|
-
// Last question — fold answers into the draft as JtbdClarification[]
|
|
122
|
-
// and advance to the title step.
|
|
123
|
-
const clarifications = clarifyState.questions
|
|
124
|
-
.map((q, i) => ({ question: q, answer: nextAnswers[i] ?? "" }))
|
|
125
|
-
// Drop pairs the user skipped entirely — keeping them just adds
|
|
126
|
-
// noise in tape.yaml and in the Phase 1 prompt.
|
|
127
|
-
.filter((c) => c.answer.trim().length > 0);
|
|
128
|
-
setDraft({ ...draft, clarifications });
|
|
129
|
-
setClarifyState({ phase: "idle" });
|
|
130
|
-
setBuffer("");
|
|
131
|
-
goForward();
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
setClarifyState({ ...clarifyState, answers: nextAnswers, index: nextIndex });
|
|
135
|
-
setBuffer("");
|
|
136
|
-
}
|
|
137
|
-
function commitField(value) {
|
|
138
|
-
setError(null);
|
|
139
|
-
if (step === "slug") {
|
|
140
|
-
const resolved = resolveSubmit(value, "slug") || "untitled";
|
|
141
|
-
const cleaned = slugify(resolved);
|
|
142
|
-
const folder = join(app.baseDir, cleaned);
|
|
143
|
-
if (existsSync(folder)) {
|
|
144
|
-
setError(`${cleaned} already exists — pick a different slug.`);
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
setDraft({ ...draft, slug: cleaned });
|
|
148
|
-
goForward();
|
|
149
|
-
}
|
|
150
|
-
else if (step === "jtbd") {
|
|
151
|
-
const trimmed = resolveSubmit(value, "jtbd");
|
|
152
|
-
if (trimmed.length < 12) {
|
|
153
|
-
setError("Push for a real sentence — what are you trying to do with the AI in this domain?");
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
setDraft({ ...draft, jtbd: trimmed });
|
|
157
|
-
goForward();
|
|
158
|
-
}
|
|
159
|
-
else if (step === "title") {
|
|
160
|
-
const resolved = resolveSubmit(value, "title");
|
|
161
|
-
if (!resolved) {
|
|
162
|
-
setError("Title is required.");
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
setDraft({ ...draft, title: resolved });
|
|
166
|
-
goForward();
|
|
167
|
-
}
|
|
168
|
-
else if (step === "description") {
|
|
169
|
-
const resolved = resolveSubmit(value, "description");
|
|
170
|
-
if (!resolved) {
|
|
171
|
-
setError("One-line description helps the AI consuming this mixtape.");
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
setDraft({ ...draft, description: resolved });
|
|
175
|
-
goForward();
|
|
176
|
-
}
|
|
177
|
-
else if (step === "curator") {
|
|
178
|
-
const resolved = resolveSubmit(value, "curator");
|
|
179
|
-
if (!resolved) {
|
|
180
|
-
setError("Curator name is required.");
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
setDraft({ ...draft, curator: resolved });
|
|
184
|
-
setBuffer("");
|
|
185
|
-
// Curator is the last input step; commit always lands on review,
|
|
186
|
-
// whether we got here from review-edit or the normal forward path.
|
|
187
|
-
setReturningToReview(false);
|
|
188
|
-
setStep("review");
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
function goForward() {
|
|
192
|
-
// Bounce back to review if the user entered this step via [1-6] on the
|
|
193
|
-
// review screen. The expectation when editing a single field is "fix it
|
|
194
|
-
// and return," not "fix it and walk through every later step again."
|
|
195
|
-
if (returningToReview) {
|
|
196
|
-
setReturningToReview(false);
|
|
197
|
-
setBuffer("");
|
|
198
|
-
setStep("review");
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
const i = STEP_ORDER.indexOf(step);
|
|
202
|
-
const next = STEP_ORDER[i + 1];
|
|
203
|
-
// Each forward step starts with a clean buffer; the suggestion shows as
|
|
204
|
-
// a placeholder (TextInput's placeholder prop).
|
|
205
|
-
setBuffer("");
|
|
206
|
-
if (next)
|
|
207
|
-
setStep(next);
|
|
208
|
-
else
|
|
209
|
-
setStep("review");
|
|
210
|
-
}
|
|
211
|
-
function goBack() {
|
|
212
|
-
if (step === "review") {
|
|
213
|
-
setStep("curator");
|
|
214
|
-
setBuffer(draft.curator);
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
// If the user was editing this field from the review screen, esc returns
|
|
218
|
-
// them straight to review — pairs with the goForward bounce-back.
|
|
219
|
-
if (returningToReview) {
|
|
220
|
-
setReturningToReview(false);
|
|
221
|
-
setBuffer("");
|
|
222
|
-
setStep("review");
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
// Leaving jtbd-clarify in either sub-phase resets clarify state and
|
|
226
|
-
// returns to the JTBD field. Captured answers are dropped — they were
|
|
227
|
-
// generated for the previous JTBD and won't apply to a revised one.
|
|
228
|
-
if (step === "jtbd-clarify") {
|
|
229
|
-
setClarifyState({ phase: "idle" });
|
|
230
|
-
setBuffer(draft.jtbd);
|
|
231
|
-
setStep("jtbd");
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
const i = STEP_ORDER.indexOf(step);
|
|
235
|
-
if (i <= 0) {
|
|
236
|
-
// First step → leaving the wizard entirely. back() returns to whatever
|
|
237
|
-
// launched it (splash, browser, …) rather than always landing on browser.
|
|
238
|
-
app.back();
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
const prev = STEP_ORDER[i - 1];
|
|
242
|
-
if (prev) {
|
|
243
|
-
// Going back to a previously-answered step shows the existing answer so
|
|
244
|
-
// the user can tweak rather than retype. The clarify step has its own
|
|
245
|
-
// sub-state so it's not buffer-backed; reset to idle and let the effect
|
|
246
|
-
// re-trigger the agent call on the next forward pass.
|
|
247
|
-
setStep(prev);
|
|
248
|
-
if (prev === "jtbd-clarify") {
|
|
249
|
-
setClarifyState({ phase: "idle" });
|
|
250
|
-
setBuffer("");
|
|
251
|
-
}
|
|
252
|
-
else {
|
|
253
|
-
setBuffer(draft[prev]);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
async function confirm() {
|
|
258
|
-
setStep("creating");
|
|
259
|
-
const folder = join(app.baseDir, draft.slug);
|
|
260
|
-
try {
|
|
261
|
-
await runInit(folder);
|
|
262
|
-
// Read the just-scaffolded tape, apply the wizard's fields, write back.
|
|
263
|
-
const { tape, doc } = readTape(join(folder, "tape.yaml"));
|
|
264
|
-
tape.title = draft.title;
|
|
265
|
-
tape.description = draft.description;
|
|
266
|
-
tape.curator = draft.curator;
|
|
267
|
-
tape.mode = draft.mode;
|
|
268
|
-
tape.jtbd = draft.jtbd;
|
|
269
|
-
// Persist JTBD clarifications onto the tape so Phase 1 (framing) can
|
|
270
|
-
// read them. Empty array → null so unaffected tapes stay clean.
|
|
271
|
-
tape.jtbd_clarifications =
|
|
272
|
-
draft.clarifications.length > 0 ? draft.clarifications : null;
|
|
273
|
-
if (draft.mode === "methodology")
|
|
274
|
-
tape.methodology_version = "2.0";
|
|
275
|
-
writeTape(join(folder, "tape.yaml"), tape, doc);
|
|
276
|
-
// Pre-fill the JTBD into working/01 so the curator doesn't have to copy
|
|
277
|
-
// the same sentence over. The knowledge-map section stays as TODO so the
|
|
278
|
-
// next agent (or the curator) has work to do.
|
|
279
|
-
prefillJtbdIntoWorking01(folder, draft.jtbd);
|
|
280
|
-
// Explicitly initialize the progress cursor to step 0. Without this,
|
|
281
|
-
// the hub falls back to `migrateProgress` which inspects artifact
|
|
282
|
-
// contents — and our JTBD pre-fill above strips the JTBD-section TODO
|
|
283
|
-
// from working/01, which migrateProgress would otherwise misread as
|
|
284
|
-
// "Phase 1 done." Writing step=0 here means the user lands on Framing
|
|
285
|
-
// (the actual current phase) instead of jumping to Gate 0.
|
|
286
|
-
writeProgress(folder, {
|
|
287
|
-
step: 0,
|
|
288
|
-
lastTouched: new Date().toISOString(),
|
|
289
|
-
});
|
|
290
|
-
app.setNotification({ kind: "info", message: `Created ${folder}` });
|
|
291
|
-
await app.refreshProjects();
|
|
292
|
-
app.navigate({ kind: "hub", tape, folder });
|
|
293
|
-
}
|
|
294
|
-
catch (e) {
|
|
295
|
-
setError(e.message);
|
|
296
|
-
setStep("review");
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
const isFieldStep = step === "slug" ||
|
|
300
|
-
step === "jtbd" ||
|
|
301
|
-
step === "title" ||
|
|
302
|
-
step === "description" ||
|
|
303
|
-
step === "curator" ||
|
|
304
|
-
(step === "jtbd-clarify" && clarifyState.phase === "asking");
|
|
305
|
-
// Field-editing steps: TextInput owns letters/numbers. Only escape leaks here.
|
|
306
|
-
useInput((_input, key) => {
|
|
307
|
-
if (key.escape)
|
|
308
|
-
goBack();
|
|
309
|
-
}, { isActive: isFieldStep });
|
|
310
|
-
// While loading the clarifying questions, the wizard is between states —
|
|
311
|
-
// no TextInput is mounted yet. Esc cancels the wait and bounces back to
|
|
312
|
-
// the JTBD step; the in-flight agent call gets ignored via the effect's
|
|
313
|
-
// cleanup flag (the agent process self-exits, audit log captures it).
|
|
314
|
-
useInput((_input, key) => {
|
|
315
|
-
if (key.escape)
|
|
316
|
-
goBack();
|
|
317
|
-
}, { isActive: step === "jtbd-clarify" && clarifyState.phase === "loading" });
|
|
318
|
-
// Review step: full keybinding palette.
|
|
319
|
-
useInput((input, key) => {
|
|
320
|
-
if (step === "review") {
|
|
321
|
-
if (key.return || input === "y") {
|
|
322
|
-
void confirm();
|
|
323
|
-
}
|
|
324
|
-
else if (key.escape || input === "n") {
|
|
325
|
-
goBack();
|
|
326
|
-
}
|
|
327
|
-
else if (input === "1")
|
|
328
|
-
startEditing("slug");
|
|
329
|
-
else if (input === "2")
|
|
330
|
-
startEditing("jtbd");
|
|
331
|
-
else if (input === "3")
|
|
332
|
-
startEditing("title");
|
|
333
|
-
else if (input === "4")
|
|
334
|
-
startEditing("description");
|
|
335
|
-
else if (input === "5")
|
|
336
|
-
startEditing("curator");
|
|
337
|
-
}
|
|
338
|
-
}, { isActive: step === "review" });
|
|
339
|
-
return (React.createElement(Box, { flexDirection: "column" },
|
|
340
|
-
React.createElement(Header, { title: "new mixtape", subtitle: progressLine(step, stepIndex, draft) }),
|
|
341
|
-
React.createElement(StepChip, { step: step, stepIndex: stepIndex, draft: draft }),
|
|
342
|
-
React.createElement(Transcript, { draft: draft, step: step }),
|
|
343
|
-
(step === "slug" ||
|
|
344
|
-
step === "jtbd" ||
|
|
345
|
-
step === "title" ||
|
|
346
|
-
step === "description" ||
|
|
347
|
-
step === "curator") && (React.createElement(FieldStep, { step: step, buffer: buffer, suggestion: suggestionFor(step, draft), onChange: setBuffer, onSubmit: (v) => commitField(v) })),
|
|
348
|
-
step === "jtbd-clarify" ? (React.createElement(ClarifyStep, { state: clarifyState, buffer: buffer, onChange: setBuffer, onSubmit: commitClarifyAnswer })) : null,
|
|
349
|
-
step === "review" ? React.createElement(ReviewStep, { draft: draft }) : null,
|
|
350
|
-
step === "creating" ? (React.createElement(Box, { marginTop: 1 },
|
|
351
|
-
React.createElement(Text, { color: WARNING }, "Creating folder\u2026"))) : null,
|
|
352
|
-
error ? (React.createElement(Box, { marginTop: 1 },
|
|
353
|
-
React.createElement(Text, { color: ERROR }, error))) : null,
|
|
354
|
-
React.createElement(Box, { marginTop: 1 },
|
|
355
|
-
React.createElement(KeyHints, { hints: hintsFor(step) }))));
|
|
356
|
-
}
|
|
357
|
-
function progressLine(step, stepIndex, _draft) {
|
|
358
|
-
if (step === "review")
|
|
359
|
-
return `step ${TOTAL_STEPS}/${TOTAL_STEPS} · review and confirm`;
|
|
360
|
-
if (step === "creating")
|
|
361
|
-
return "creating…";
|
|
362
|
-
const label = stepHeading(step);
|
|
363
|
-
return `step ${stepIndex} of ${TOTAL_STEPS} · ${label}`;
|
|
364
|
-
}
|
|
365
|
-
function stepHeading(step) {
|
|
366
|
-
switch (step) {
|
|
367
|
-
case "slug":
|
|
368
|
-
return "folder name";
|
|
369
|
-
case "jtbd":
|
|
370
|
-
return "job-to-be-done";
|
|
371
|
-
case "jtbd-clarify":
|
|
372
|
-
return "sharpen the JTBD";
|
|
373
|
-
case "title":
|
|
374
|
-
return "title";
|
|
375
|
-
case "description":
|
|
376
|
-
return "one-line description";
|
|
377
|
-
case "curator":
|
|
378
|
-
return "your name";
|
|
379
|
-
default:
|
|
380
|
-
return "";
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
function Transcript({ draft, step }) {
|
|
384
|
-
const entries = [];
|
|
385
|
-
if (afterStep(step, "slug") && draft.slug)
|
|
386
|
-
entries.push({ key: "slug", label: "folder", value: draft.slug });
|
|
387
|
-
if (afterStep(step, "jtbd") && draft.jtbd)
|
|
388
|
-
entries.push({ key: "jtbd", label: "jtbd", value: truncate(draft.jtbd, 80) });
|
|
389
|
-
if (afterStep(step, "jtbd-clarify") && draft.clarifications.length > 0)
|
|
390
|
-
entries.push({
|
|
391
|
-
key: "clarifications",
|
|
392
|
-
label: "clarifications",
|
|
393
|
-
value: `${draft.clarifications.length} captured`,
|
|
394
|
-
});
|
|
395
|
-
if (afterStep(step, "title") && draft.title)
|
|
396
|
-
entries.push({ key: "title", label: "title", value: draft.title });
|
|
397
|
-
if (afterStep(step, "description") && draft.description)
|
|
398
|
-
entries.push({ key: "description", label: "description", value: truncate(draft.description, 80) });
|
|
399
|
-
if (afterStep(step, "curator") && draft.curator)
|
|
400
|
-
entries.push({ key: "curator", label: "curator", value: draft.curator });
|
|
401
|
-
if (entries.length === 0)
|
|
402
|
-
return null;
|
|
403
|
-
return (React.createElement(Box, { flexDirection: "column", marginBottom: 1 }, entries.map((e) => (React.createElement(Box, { key: e.key },
|
|
404
|
-
React.createElement(Text, { color: MUTED }, e.label.padEnd(13, " ")),
|
|
405
|
-
React.createElement(Text, null, e.value))))));
|
|
406
|
-
}
|
|
407
|
-
function StepChip({ step, stepIndex, draft: _draft, }) {
|
|
408
|
-
if (step === "creating")
|
|
409
|
-
return null;
|
|
410
|
-
const total = TOTAL_STEPS;
|
|
411
|
-
// On review, every input step is in the past — no dot is "current".
|
|
412
|
-
const filled = step === "review" ? total + 1 : stepIndex;
|
|
413
|
-
const dots = [];
|
|
414
|
-
for (let i = 1; i <= total; i++) {
|
|
415
|
-
const isPast = i < filled;
|
|
416
|
-
const isCurrent = i === filled;
|
|
417
|
-
dots.push(React.createElement(Text, { key: i, color: isPast ? SUCCESS : isCurrent ? STRUCTURE : MUTED, bold: isCurrent },
|
|
418
|
-
isPast ? "●" : isCurrent ? "●" : "○",
|
|
419
|
-
i < total ? " " : ""));
|
|
420
|
-
}
|
|
421
|
-
return (React.createElement(Box, { marginBottom: 1 },
|
|
422
|
-
React.createElement(Text, { backgroundColor: HEADING, color: HIGHLIGHT, bold: true }, ` step ${step === "review" ? total : stepIndex} / ${total} `),
|
|
423
|
-
React.createElement(Text, null, " "),
|
|
424
|
-
dots));
|
|
425
|
-
}
|
|
426
|
-
function afterStep(current, target) {
|
|
427
|
-
if (current === "review" || current === "creating")
|
|
428
|
-
return true;
|
|
429
|
-
const ci = STEP_ORDER.indexOf(current);
|
|
430
|
-
const ti = STEP_ORDER.indexOf(target);
|
|
431
|
-
return ci > ti;
|
|
432
|
-
}
|
|
433
|
-
/**
|
|
434
|
-
* Render the JTBD-clarify step. Has three visible variants:
|
|
435
|
-
*
|
|
436
|
-
* loading → spinner + "asking the agent for clarifying questions" line.
|
|
437
|
-
* Esc cancels and goes back to JTBD.
|
|
438
|
-
*
|
|
439
|
-
* asking → shows one question at a time with a TextInput. Each answer
|
|
440
|
-
* advances to the next question; the last one advances to title.
|
|
441
|
-
* Enter on an empty input skips the question (and the entry is
|
|
442
|
-
* dropped from the final clarifications list).
|
|
443
|
-
*
|
|
444
|
-
* error → fallback for the helper's promise rejecting; in practice the
|
|
445
|
-
* helper never throws (it falls back to HARDCODED_QUESTIONS),
|
|
446
|
-
* but we keep the variant for completeness.
|
|
447
|
-
*/
|
|
448
|
-
function ClarifyStep({ state, buffer, onChange, onSubmit, }) {
|
|
449
|
-
if (state.phase === "idle" || state.phase === "loading") {
|
|
450
|
-
return (React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
|
|
451
|
-
React.createElement(Text, { color: HEADING, bold: true }, "Sharpening the JTBD"),
|
|
452
|
-
React.createElement(Box, { marginTop: 1 },
|
|
453
|
-
React.createElement(Text, { color: STRUCTURE },
|
|
454
|
-
React.createElement(Spinner, { type: "dots" }),
|
|
455
|
-
" "),
|
|
456
|
-
React.createElement(Text, null, "asking the agent for clarifying questions\u2026")),
|
|
457
|
-
React.createElement(Box, { marginTop: 1 },
|
|
458
|
-
React.createElement(Text, { color: MUTED }, "The corpus is only as good as the input. A few targeted questions here usually beat one vague JTBD downstream.")),
|
|
459
|
-
React.createElement(Box, { marginTop: 1 },
|
|
460
|
-
React.createElement(Text, { color: MUTED },
|
|
461
|
-
"Press ",
|
|
462
|
-
React.createElement(Text, { color: STRUCTURE }, "esc"),
|
|
463
|
-
" to skip this step."))));
|
|
464
|
-
}
|
|
465
|
-
if (state.phase === "error") {
|
|
466
|
-
return (React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
|
|
467
|
-
React.createElement(Text, { color: ERROR },
|
|
468
|
-
"Could not generate questions: ",
|
|
469
|
-
state.message),
|
|
470
|
-
React.createElement(Text, { color: MUTED },
|
|
471
|
-
"Press ",
|
|
472
|
-
React.createElement(Text, { color: STRUCTURE }, "esc"),
|
|
473
|
-
" to skip back to the JTBD step.")));
|
|
474
|
-
}
|
|
475
|
-
const question = state.questions[state.index] ?? "";
|
|
476
|
-
const total = state.questions.length;
|
|
477
|
-
return (React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
|
|
478
|
-
React.createElement(Box, null,
|
|
479
|
-
React.createElement(Text, { color: HEADING, bold: true }, question)),
|
|
480
|
-
React.createElement(Box, { marginTop: 1 },
|
|
481
|
-
React.createElement(Text, { color: MUTED },
|
|
482
|
-
"question ",
|
|
483
|
-
state.index + 1,
|
|
484
|
-
" of ",
|
|
485
|
-
total,
|
|
486
|
-
" \u00B7 enter to advance \u00B7 empty + enter skips this one")),
|
|
487
|
-
React.createElement(Box, { marginTop: 1 },
|
|
488
|
-
React.createElement(Text, { color: CURRENT }, "\u203A "),
|
|
489
|
-
React.createElement(TextInput, { value: buffer, onChange: onChange, onSubmit: onSubmit, placeholder: "your answer (or leave blank to skip)" }))));
|
|
490
|
-
}
|
|
491
|
-
function FieldStep({ step, buffer, suggestion, onChange, onSubmit, }) {
|
|
492
|
-
// The TextInput placeholder is always a short static example. The dynamic
|
|
493
|
-
// JTBD-derived suggestion (often long-form) gets its own labeled card below
|
|
494
|
-
// so it doesn't visually bleed into the input field.
|
|
495
|
-
return (React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
|
|
496
|
-
React.createElement(Text, { color: HEADING, bold: true }, questionFor(step)),
|
|
497
|
-
helpFor(step) ? (React.createElement(Box, { marginTop: 1 },
|
|
498
|
-
React.createElement(Text, { color: MUTED }, helpFor(step)))) : null,
|
|
499
|
-
React.createElement(Box, { marginTop: 1 },
|
|
500
|
-
React.createElement(Text, { color: STRUCTURE }, "\u203A "),
|
|
501
|
-
React.createElement(TextInput, { value: buffer, onChange: onChange, onSubmit: onSubmit, placeholder: placeholderFor(step) })),
|
|
502
|
-
suggestion && !buffer ? (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
|
|
503
|
-
React.createElement(Box, null,
|
|
504
|
-
React.createElement(Text, { color: MUTED }, "suggestion: "),
|
|
505
|
-
React.createElement(Text, null, truncate(suggestion, 72))),
|
|
506
|
-
React.createElement(Text, { color: MUTED },
|
|
507
|
-
"press ",
|
|
508
|
-
React.createElement(Text, { color: STRUCTURE }, "enter"),
|
|
509
|
-
" to accept, or type your own."))) : null,
|
|
510
|
-
step === "slug" && buffer ? (React.createElement(Box, { marginTop: 1 },
|
|
511
|
-
React.createElement(Text, { color: MUTED }, "\u2192 will create folder: "),
|
|
512
|
-
React.createElement(Text, null, slugify(buffer)))) : null));
|
|
513
|
-
}
|
|
514
|
-
function ReviewStep({ draft }) {
|
|
515
|
-
const rows = [
|
|
516
|
-
{ key: "1", label: "folder", value: draft.slug },
|
|
517
|
-
{ key: "2", label: "jtbd", value: truncate(draft.jtbd, 80) },
|
|
518
|
-
{ key: "3", label: "title", value: draft.title },
|
|
519
|
-
{ key: "4", label: "description", value: truncate(draft.description, 80) },
|
|
520
|
-
{ key: "5", label: "curator", value: draft.curator },
|
|
521
|
-
];
|
|
522
|
-
return (React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
|
|
523
|
-
React.createElement(Text, { color: HEADING, bold: true }, "Review:"),
|
|
524
|
-
React.createElement(Box, { marginTop: 1, flexDirection: "column" }, rows.map((r) => (React.createElement(Box, { key: r.key },
|
|
525
|
-
React.createElement(Text, { color: STRUCTURE }, `[${r.key}] `),
|
|
526
|
-
React.createElement(Text, { color: MUTED }, `${r.label}:`.padEnd(14, " ")),
|
|
527
|
-
React.createElement(Text, null, r.value))))),
|
|
528
|
-
React.createElement(Box, { marginTop: 1 },
|
|
529
|
-
React.createElement(Text, null, "Press "),
|
|
530
|
-
React.createElement(Text, { color: STRUCTURE }, "enter"),
|
|
531
|
-
React.createElement(Text, null, " to create, "),
|
|
532
|
-
React.createElement(Text, { color: STRUCTURE }, "1\u20136"),
|
|
533
|
-
React.createElement(Text, null, " to edit a field, "),
|
|
534
|
-
React.createElement(Text, { color: STRUCTURE }, "esc"),
|
|
535
|
-
React.createElement(Text, null, " to go back."))));
|
|
536
|
-
}
|
|
537
|
-
function questionFor(step) {
|
|
538
|
-
switch (step) {
|
|
539
|
-
case "slug":
|
|
540
|
-
return "What's the folder name?";
|
|
541
|
-
case "jtbd":
|
|
542
|
-
return "What's the job-to-be-done?";
|
|
543
|
-
case "title":
|
|
544
|
-
return "What should the title be?";
|
|
545
|
-
case "description":
|
|
546
|
-
return "Give it a one-line description.";
|
|
547
|
-
case "curator":
|
|
548
|
-
return "Who are you (curator name)?";
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
function helpFor(step) {
|
|
552
|
-
switch (step) {
|
|
553
|
-
case "slug":
|
|
554
|
-
return "Lowercase letters, numbers, hyphens. Becomes the project folder under your mixtapes directory.";
|
|
555
|
-
case "jtbd":
|
|
556
|
-
return 'A single Job Story — not a topic. Form: "When [circumstance], I want [motivation], so I can [outcome]." All three slots required, no tool/vendor names, one job per slot. "Mobile design" is a topic; a tight JTBD scopes the circumstance, motivation, and outcome.';
|
|
557
|
-
case "title":
|
|
558
|
-
return "Short human-readable name. Shown in browsers and at the top of MIXTAPE.md.";
|
|
559
|
-
case "description":
|
|
560
|
-
return "One sentence. Helps the AI consuming this context know what the mixtape is for.";
|
|
561
|
-
case "curator":
|
|
562
|
-
return "Your name or handle. Goes into tape.yaml.";
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
function placeholderFor(step) {
|
|
566
|
-
switch (step) {
|
|
567
|
-
case "slug":
|
|
568
|
-
return "mobile-design-foundations";
|
|
569
|
-
case "jtbd":
|
|
570
|
-
return "When I'm designing... I want to... so I can...";
|
|
571
|
-
case "title":
|
|
572
|
-
return "Mobile design foundations";
|
|
573
|
-
case "description":
|
|
574
|
-
return "Designing iOS apps that feel native and distinctive";
|
|
575
|
-
case "curator":
|
|
576
|
-
return "Your name";
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
function hintsFor(step) {
|
|
580
|
-
if (step === "review")
|
|
581
|
-
return [
|
|
582
|
-
{ key: "enter", label: "create" },
|
|
583
|
-
{ key: "1–5", label: "edit field" },
|
|
584
|
-
{ key: "esc", label: "go back" },
|
|
585
|
-
];
|
|
586
|
-
if (step === "creating")
|
|
587
|
-
return [];
|
|
588
|
-
return [
|
|
589
|
-
{ key: "enter", label: "next" },
|
|
590
|
-
{ key: "esc", label: "go back" },
|
|
591
|
-
];
|
|
592
|
-
}
|
|
593
|
-
/**
|
|
594
|
-
* Per-step suggestion — the value Enter accepts when the user submits an
|
|
595
|
-
* empty buffer. Stays empty for steps that have no sensible default (slug,
|
|
596
|
-
* jtbd). For title/description, derives from the user's JTBD answer.
|
|
597
|
-
*/
|
|
598
|
-
function suggestionFor(step, draft) {
|
|
599
|
-
switch (step) {
|
|
600
|
-
case "slug":
|
|
601
|
-
return "";
|
|
602
|
-
case "jtbd":
|
|
603
|
-
return "";
|
|
604
|
-
case "title":
|
|
605
|
-
return suggestTitleFromJtbd(draft.jtbd);
|
|
606
|
-
case "description":
|
|
607
|
-
return suggestDescriptionFromJtbd(draft.jtbd);
|
|
608
|
-
case "curator":
|
|
609
|
-
return defaultCurator();
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
function suggestTitleFromJtbd(jtbd) {
|
|
613
|
-
// Strip leading helper-verb phrases so the title reads as a noun phrase, not
|
|
614
|
-
// an instruction. Then take only the first clause (up to the first sentence
|
|
615
|
-
// boundary or comma) so a long, rambling JTBD still produces a tight title.
|
|
616
|
-
const stripped = jtbd
|
|
617
|
-
.replace(/^(help me|help us|i need (?:help )?(?:to )?|i want to|i'm trying to|i am trying to|let me|allow me to)\s+/i, "")
|
|
618
|
-
.replace(/\.+\s*$/, "")
|
|
619
|
-
.trim();
|
|
620
|
-
if (!stripped)
|
|
621
|
-
return "";
|
|
622
|
-
const firstClause = stripped.split(/[,.;]/)[0]?.trim() ?? stripped;
|
|
623
|
-
// If the first clause is still long, it's not a title — bail rather than
|
|
624
|
-
// surface a bad suggestion.
|
|
625
|
-
if (firstClause.length > 60)
|
|
626
|
-
return "";
|
|
627
|
-
return firstClause[0].toUpperCase() + firstClause.slice(1);
|
|
628
|
-
}
|
|
629
|
-
function suggestDescriptionFromJtbd(jtbd) {
|
|
630
|
-
// Take the first sentence only; descriptions are one line.
|
|
631
|
-
const firstSentence = jtbd.split(/(?<=[.!?])\s+/)[0]?.trim() ?? "";
|
|
632
|
-
const cleaned = firstSentence.replace(/\.+\s*$/, "").trim();
|
|
633
|
-
if (!cleaned)
|
|
634
|
-
return "";
|
|
635
|
-
if (cleaned.length > 100)
|
|
636
|
-
return "";
|
|
637
|
-
return cleaned[0].toUpperCase() + cleaned.slice(1) + ".";
|
|
638
|
-
}
|
|
639
|
-
function defaultCurator() {
|
|
640
|
-
return (process.env["LINER_CURATOR"] ||
|
|
641
|
-
process.env["USER"] ||
|
|
642
|
-
process.env["USERNAME"] ||
|
|
643
|
-
"");
|
|
644
|
-
}
|
|
645
|
-
function prefillJtbdIntoWorking01(folder, jtbd) {
|
|
646
|
-
const path = join(folder, "working", "01-jtbd-and-knowledge-map.md");
|
|
647
|
-
if (!existsSync(path))
|
|
648
|
-
return;
|
|
649
|
-
const text = readFileSync(path, "utf8");
|
|
650
|
-
// Replace the TODO with the curator's JTBD; leave the knowledge-map section
|
|
651
|
-
// as-is so the agent or curator still has explicit work to do.
|
|
652
|
-
const updated = text.replace(/TODO — a single specific sentence\. Not the topic — the use case\. Examples:[\s\S]*?- "[^"]*"\s*\n- "[^"]*"\s*\n/, `${jtbd}\n\n_Set via the new-mixtape wizard. Revise here if your understanding sharpens during research._\n\n`);
|
|
653
|
-
if (updated !== text) {
|
|
654
|
-
const _ = projectFolder; // keep import live for tree-shaking sanity
|
|
655
|
-
void _;
|
|
656
|
-
writeFileSync(path, updated, "utf8");
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
function truncate(s, n) {
|
|
660
|
-
return s.length > n ? s.slice(0, n - 1) + "…" : s;
|
|
661
|
-
}
|
|
662
|
-
//# sourceMappingURL=NewMixtapeWizard.js.map
|