novel-writer-cli 0.1.0 → 0.2.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/dist/__tests__/checkpoint-quickstart-phase.test.js +49 -0
- package/dist/__tests__/cli-instructions-novel-ask-gate.test.js +83 -0
- package/dist/__tests__/cli-repair-reset-quickstart.test.js +194 -0
- package/dist/__tests__/cli-version-flag.test.js +38 -0
- package/dist/__tests__/init.test.js +9 -6
- package/dist/__tests__/instructions-review-novel-ask-gate.test.js +31 -0
- package/dist/__tests__/orchestrator-state-routing.test.js +10 -6
- package/dist/__tests__/quickstart-pipeline.test.js +346 -0
- package/dist/__tests__/safe-path-symlink.test.js +41 -0
- package/dist/__tests__/validate-quickstart-prereqs.test.js +73 -0
- package/dist/advance.js +88 -3
- package/dist/checkpoint.js +25 -4
- package/dist/cli.js +130 -4
- package/dist/init.js +2 -1
- package/dist/instructions.js +162 -1
- package/dist/next-step.js +227 -4
- package/dist/quickstart-validators.js +84 -0
- package/dist/quickstart.js +16 -0
- package/dist/safe-path.js +23 -1
- package/dist/validate.js +72 -1
- package/docs/user/README.md +0 -1
- package/package.json +1 -1
- package/scripts/sync-final-spec-skills.mjs +65 -0
- package/skills/cli-step/SKILL.md +186 -32
- package/skills/continue/SKILL.md +30 -326
- package/skills/shared/thin-adapter-loop.md +67 -0
- package/skills/start/SKILL.md +23 -440
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command, CommanderError } from "commander";
|
|
3
3
|
import { realpathSync } from "node:fs";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
4
5
|
import { join, resolve } from "node:path";
|
|
5
6
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
7
|
import { buildCharacterVoiceProfiles, clearCharacterVoiceDriftFile, computeCharacterVoiceDrift, loadActiveCharacterVoiceDriftIds, loadCharacterVoiceProfiles, writeCharacterVoiceDriftFile, writeCharacterVoiceProfilesFile } from "./character-voice.js";
|
|
@@ -8,7 +9,7 @@ import { NovelCliError } from "./errors.js";
|
|
|
8
9
|
import { errJson, okJson, printJson } from "./output.js";
|
|
9
10
|
import { pathExists, readJsonFile, writeJsonFile, writeTextFile } from "./fs-utils.js";
|
|
10
11
|
import { resolveProjectRoot } from "./project.js";
|
|
11
|
-
import { readCheckpoint } from "./checkpoint.js";
|
|
12
|
+
import { readCheckpoint, writeCheckpoint } from "./checkpoint.js";
|
|
12
13
|
import { initProject, normalizePlatformId, resolveInitRootDir } from "./init.js";
|
|
13
14
|
import { advanceCheckpointForStep } from "./advance.js";
|
|
14
15
|
import { commitChapter } from "./commit.js";
|
|
@@ -17,16 +18,37 @@ import { buildInstructionPacket } from "./instructions.js";
|
|
|
17
18
|
import { getLockStatus, clearStaleLock, withWriteLock } from "./lock.js";
|
|
18
19
|
import { computeNextStep } from "./next-step.js";
|
|
19
20
|
import { computeEngagementReport, loadEngagementMetricsStream, writeEngagementLogs } from "./engagement.js";
|
|
21
|
+
import { parseNovelAskQuestionSpec } from "./novel-ask.js";
|
|
20
22
|
import { computePromiseLedgerReport, ensurePromiseLedgerInitialized, loadPromiseLedger, writePromiseLedgerLogs } from "./promise-ledger.js";
|
|
21
23
|
import { pad2, pad3, parseStepId } from "./steps.js";
|
|
24
|
+
import { resolveProjectRelativePath } from "./safe-path.js";
|
|
22
25
|
import { isPlainObject } from "./type-guards.js";
|
|
23
26
|
import { validateStep } from "./validate.js";
|
|
24
27
|
import { VOL_REVIEW_RELS, collectVolumeData, computeBridgeCheck, computeForeshadowingAudit, computeStorylineRhythm } from "./volume-review.js";
|
|
25
28
|
import { tryResolveVolumeChapterRange } from "./consistency-auditor.js";
|
|
29
|
+
const require = createRequire(import.meta.url);
|
|
30
|
+
function resolveCliVersion() {
|
|
31
|
+
try {
|
|
32
|
+
const pkg = require("../package.json");
|
|
33
|
+
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return "0.0.0";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const CLI_VERSION = resolveCliVersion();
|
|
26
40
|
function detectCommandName(argv) {
|
|
27
|
-
for (
|
|
41
|
+
for (let i = 0; i < argv.length; i++) {
|
|
42
|
+
const token = argv[i];
|
|
28
43
|
if (token === "--")
|
|
29
44
|
return "unknown";
|
|
45
|
+
// Global option: consumes the next token as a value.
|
|
46
|
+
if (token === "--project") {
|
|
47
|
+
i++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (token.startsWith("--project="))
|
|
51
|
+
continue;
|
|
30
52
|
if (!token.startsWith("-"))
|
|
31
53
|
return token;
|
|
32
54
|
}
|
|
@@ -39,6 +61,7 @@ function buildProgram(argv) {
|
|
|
39
61
|
const jsonMode = isJsonMode(argv);
|
|
40
62
|
const program = new Command();
|
|
41
63
|
program.name("novel").description("Executor-agnostic novel orchestration CLI.");
|
|
64
|
+
program.version(CLI_VERSION);
|
|
42
65
|
program.option("--json", "Emit machine-readable JSON (single object).");
|
|
43
66
|
program.option("--project <dir>", "Project root directory (defaults to auto-detect via .checkpoint.json).");
|
|
44
67
|
program.configureOutput({
|
|
@@ -124,6 +147,16 @@ function buildProgram(argv) {
|
|
|
124
147
|
printJson(okJson("next", { rootDir, ...next }));
|
|
125
148
|
return;
|
|
126
149
|
}
|
|
150
|
+
if (isPlainObject(next.evidence)) {
|
|
151
|
+
const blocked = next.evidence.recovery_blocked;
|
|
152
|
+
if (isPlainObject(blocked)) {
|
|
153
|
+
const obj = blocked;
|
|
154
|
+
const checkpointPhase = typeof obj.checkpoint_phase === "string" ? obj.checkpoint_phase : "unknown";
|
|
155
|
+
const inferredPhase = typeof obj.inferred_phase === "string" ? obj.inferred_phase : "unknown";
|
|
156
|
+
const expectedPath = typeof obj.expected_path === "string" ? obj.expected_path : "unknown";
|
|
157
|
+
process.stderr.write(`WARN: quickstart recovery blocked (checkpoint_phase=${checkpointPhase}, inferred_phase=${inferredPhase}, expected=${expectedPath}). Run 'novel status' for details.\n`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
127
160
|
process.stdout.write(`${next.step}\n`);
|
|
128
161
|
});
|
|
129
162
|
program
|
|
@@ -132,18 +165,34 @@ function buildProgram(argv) {
|
|
|
132
165
|
.argument("<step>", "Step id, e.g. chapter:048:draft")
|
|
133
166
|
.option("--write-manifest", "Persist packet under staging/manifests/.")
|
|
134
167
|
.option("--embed <mode>", "Optional embed mode (off by default). Example: --embed brief")
|
|
168
|
+
.option("--novel-ask-file <path>", "Project-relative path to a NOVEL_ASK QuestionSpec JSON file (enables gate).")
|
|
169
|
+
.option("--answer-path <path>", "Project-relative path to write the NOVEL_ASK AnswerSpec JSON record.")
|
|
135
170
|
.action(async (step, localOpts) => {
|
|
136
171
|
const opts = program.opts();
|
|
137
172
|
const json = Boolean(opts.json);
|
|
138
173
|
const rootDir = await resolveProjectRoot({ cwd: process.cwd(), projectOverride: opts.project });
|
|
139
174
|
const checkpoint = await readCheckpoint(rootDir);
|
|
140
175
|
const parsedStep = parseStepId(step);
|
|
176
|
+
const novelAskFile = localOpts.novelAskFile ?? null;
|
|
177
|
+
const answerPath = localOpts.answerPath ?? null;
|
|
178
|
+
let novelAskGate = null;
|
|
179
|
+
if (novelAskFile !== null || answerPath !== null) {
|
|
180
|
+
if (!novelAskFile || !answerPath) {
|
|
181
|
+
throw new NovelCliError(`Invalid NOVEL_ASK gate: provide both --novel-ask-file and --answer-path.`, 2);
|
|
182
|
+
}
|
|
183
|
+
const absAsk = resolveProjectRelativePath(rootDir, novelAskFile, "--novel-ask-file");
|
|
184
|
+
const rawSpec = await readJsonFile(absAsk);
|
|
185
|
+
const spec = parseNovelAskQuestionSpec(rawSpec);
|
|
186
|
+
resolveProjectRelativePath(rootDir, answerPath, "--answer-path");
|
|
187
|
+
novelAskGate = { novel_ask: spec, answer_path: answerPath };
|
|
188
|
+
}
|
|
141
189
|
const packet = await buildInstructionPacket({
|
|
142
190
|
rootDir,
|
|
143
191
|
checkpoint,
|
|
144
192
|
step: parsedStep,
|
|
145
193
|
embedMode: localOpts.embed ?? null,
|
|
146
|
-
writeManifest: Boolean(localOpts.writeManifest)
|
|
194
|
+
writeManifest: Boolean(localOpts.writeManifest),
|
|
195
|
+
novelAskGate
|
|
147
196
|
});
|
|
148
197
|
if (json) {
|
|
149
198
|
printJson(okJson("instructions", packet));
|
|
@@ -703,6 +752,83 @@ function buildProgram(argv) {
|
|
|
703
752
|
else if (!localOpts.apply)
|
|
704
753
|
process.stdout.write(`\nPreview-only. Use --apply to write character-voice-drift.json.\n`);
|
|
705
754
|
});
|
|
755
|
+
program
|
|
756
|
+
.command("repair")
|
|
757
|
+
.description("Repair project state (checkpoint recovery helpers).")
|
|
758
|
+
.option("--reset-quickstart", "Set .checkpoint.json.quickstart_phase to null.")
|
|
759
|
+
.option("--force", "Apply the repair (otherwise preview only).")
|
|
760
|
+
.action(async (localOpts) => {
|
|
761
|
+
const opts = program.opts();
|
|
762
|
+
const json = Boolean(opts.json);
|
|
763
|
+
const rootDir = await resolveProjectRoot({ cwd: process.cwd(), projectOverride: opts.project });
|
|
764
|
+
if (!localOpts.resetQuickstart) {
|
|
765
|
+
throw new NovelCliError("Invalid repair: no actions specified. Use --reset-quickstart.", 2);
|
|
766
|
+
}
|
|
767
|
+
if (!localOpts.force) {
|
|
768
|
+
const checkpoint = await readCheckpoint(rootDir);
|
|
769
|
+
const beforePresent = Object.prototype.hasOwnProperty.call(checkpoint, "quickstart_phase");
|
|
770
|
+
const before = (checkpoint.quickstart_phase ?? null);
|
|
771
|
+
const wouldChange = !beforePresent || before !== null;
|
|
772
|
+
if (json) {
|
|
773
|
+
printJson(okJson("repair", {
|
|
774
|
+
rootDir,
|
|
775
|
+
applied: false,
|
|
776
|
+
actions: ["reset_quickstart"],
|
|
777
|
+
before_present: beforePresent,
|
|
778
|
+
after_present: true,
|
|
779
|
+
changed: false,
|
|
780
|
+
would_change: wouldChange,
|
|
781
|
+
before,
|
|
782
|
+
after: before,
|
|
783
|
+
target_after: null
|
|
784
|
+
}));
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
if (!wouldChange) {
|
|
788
|
+
process.stdout.write("No changes needed: quickstart_phase is already null.\n");
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
process.stdout.write(`Preview: set quickstart_phase ${beforePresent ? before : "<missing>"} -> null\n`);
|
|
792
|
+
process.stdout.write("Re-run with --force to apply.\n");
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
const applied = await withWriteLock(rootDir, {}, async () => {
|
|
796
|
+
const checkpoint = await readCheckpoint(rootDir);
|
|
797
|
+
const beforePresent = Object.prototype.hasOwnProperty.call(checkpoint, "quickstart_phase");
|
|
798
|
+
const before = (checkpoint.quickstart_phase ?? null);
|
|
799
|
+
const wouldChange = !beforePresent || before !== null;
|
|
800
|
+
if (!wouldChange)
|
|
801
|
+
return { beforePresent, before, wouldChange, wrote: false, checkpoint };
|
|
802
|
+
const updated = { ...checkpoint, quickstart_phase: null, last_checkpoint_time: new Date().toISOString() };
|
|
803
|
+
await writeCheckpoint(rootDir, updated);
|
|
804
|
+
return { beforePresent, before, wouldChange, wrote: true, checkpoint: await readCheckpoint(rootDir) };
|
|
805
|
+
});
|
|
806
|
+
const afterPresent = Object.prototype.hasOwnProperty.call(applied.checkpoint, "quickstart_phase");
|
|
807
|
+
const after = applied.checkpoint.quickstart_phase ?? null;
|
|
808
|
+
if (!afterPresent || after !== null) {
|
|
809
|
+
throw new NovelCliError(`Repair failed: quickstart_phase is still ${String(after)} (expected null).`, 2);
|
|
810
|
+
}
|
|
811
|
+
if (json) {
|
|
812
|
+
printJson(okJson("repair", {
|
|
813
|
+
rootDir,
|
|
814
|
+
applied: true,
|
|
815
|
+
actions: ["reset_quickstart"],
|
|
816
|
+
before_present: applied.beforePresent,
|
|
817
|
+
after_present: afterPresent,
|
|
818
|
+
changed: applied.wrote,
|
|
819
|
+
would_change: applied.wouldChange,
|
|
820
|
+
before: applied.before,
|
|
821
|
+
after: null,
|
|
822
|
+
target_after: null
|
|
823
|
+
}));
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
if (!applied.wrote) {
|
|
827
|
+
process.stdout.write("No changes needed: quickstart_phase is already null.\n");
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
process.stdout.write(`Set quickstart_phase ${applied.beforePresent ? applied.before : "<missing>"} -> null\n`);
|
|
831
|
+
});
|
|
706
832
|
const lock = program.command("lock").description("Manage project lock (.novel.lock).");
|
|
707
833
|
lock
|
|
708
834
|
.command("status")
|
|
@@ -757,7 +883,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
757
883
|
return err.exitCode;
|
|
758
884
|
}
|
|
759
885
|
if (err instanceof CommanderError) {
|
|
760
|
-
if (err.code === "commander.helpDisplayed") {
|
|
886
|
+
if (err.code === "commander.helpDisplayed" || err.code === "commander.version") {
|
|
761
887
|
return 0;
|
|
762
888
|
}
|
|
763
889
|
if (jsonMode) {
|
package/dist/init.js
CHANGED
|
@@ -108,7 +108,8 @@ const STAGING_SUBDIRS = [
|
|
|
108
108
|
"staging/storylines",
|
|
109
109
|
"staging/volumes",
|
|
110
110
|
"staging/foreshadowing",
|
|
111
|
-
"staging/manifests"
|
|
111
|
+
"staging/manifests",
|
|
112
|
+
"staging/quickstart"
|
|
112
113
|
];
|
|
113
114
|
export async function initProject(args) {
|
|
114
115
|
const force = Boolean(args.force);
|
package/dist/instructions.js
CHANGED
|
@@ -10,6 +10,7 @@ import { parseNovelAskQuestionSpec } from "./novel-ask.js";
|
|
|
10
10
|
import { loadPlatformProfile } from "./platform-profile.js";
|
|
11
11
|
import { computePrejudgeGuardrailsReport, writePrejudgeGuardrailsReport } from "./prejudge-guardrails.js";
|
|
12
12
|
import { loadPromiseLedgerLatestSummary } from "./promise-ledger.js";
|
|
13
|
+
import { QUICKSTART_STAGING_RELS } from "./quickstart.js";
|
|
13
14
|
import { resolveProjectRelativePath } from "./safe-path.js";
|
|
14
15
|
import { computeTitlePolicyReport } from "./title-policy.js";
|
|
15
16
|
import { chapterRelPaths, formatStepId, pad2, pad3, titleFixSnapshotRel } from "./steps.js";
|
|
@@ -187,10 +188,169 @@ async function buildReviewInstructionPacket(args) {
|
|
|
187
188
|
...(writtenPath ? { written_manifest_path: writtenPath } : {})
|
|
188
189
|
};
|
|
189
190
|
}
|
|
191
|
+
async function buildQuickStartInstructionPacket(args) {
|
|
192
|
+
const stepId = formatStepId(args.step);
|
|
193
|
+
if (args.step.kind !== "quickstart")
|
|
194
|
+
throw new NovelCliError(`Unsupported quickstart step: ${stepId}`, 2);
|
|
195
|
+
const step = args.step;
|
|
196
|
+
const embedMode = safeEmbedMode(args.embedMode);
|
|
197
|
+
const embed = {};
|
|
198
|
+
if (embedMode === "brief") {
|
|
199
|
+
const briefAbs = join(args.rootDir, "brief.md");
|
|
200
|
+
if (await pathExists(briefAbs)) {
|
|
201
|
+
const content = await readTextFile(briefAbs);
|
|
202
|
+
embed.brief_preview = content.slice(0, 2000);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
embed.brief_preview = null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const trialChapter = Math.max(1, args.checkpoint.last_completed_chapter + 1);
|
|
209
|
+
const isTrialMode = step.phase === "trial" || step.phase === "results";
|
|
210
|
+
const inline = {
|
|
211
|
+
quickstart_phase: step.phase,
|
|
212
|
+
trial_mode: isTrialMode,
|
|
213
|
+
volume: args.checkpoint.current_volume,
|
|
214
|
+
...(isTrialMode ? { chapter: trialChapter } : {})
|
|
215
|
+
};
|
|
216
|
+
const paths = {};
|
|
217
|
+
const maybeAddPath = async (key, relPath) => {
|
|
218
|
+
const absPath = join(args.rootDir, relPath);
|
|
219
|
+
if (await pathExists(absPath))
|
|
220
|
+
paths[key] = relPath;
|
|
221
|
+
};
|
|
222
|
+
await maybeAddPath("project_brief", "brief.md");
|
|
223
|
+
await maybeAddPath("platform_profile", "platform-profile.json");
|
|
224
|
+
await maybeAddPath("style_profile_template", "style-profile.json");
|
|
225
|
+
// Attach staging quickstart artifacts when present (for resume/debug).
|
|
226
|
+
await maybeAddPath("quickstart_rules", QUICKSTART_STAGING_RELS.rulesJson);
|
|
227
|
+
await maybeAddPath("quickstart_contracts_dir", QUICKSTART_STAGING_RELS.contractsDir);
|
|
228
|
+
await maybeAddPath("quickstart_style_profile", QUICKSTART_STAGING_RELS.styleProfileJson);
|
|
229
|
+
await maybeAddPath("quickstart_trial_chapter", QUICKSTART_STAGING_RELS.trialChapterMd);
|
|
230
|
+
await maybeAddPath("quickstart_evaluation", QUICKSTART_STAGING_RELS.evaluationJson);
|
|
231
|
+
const asString = (value) => (typeof value === "string" ? value : null);
|
|
232
|
+
const qsRules = asString(paths.quickstart_rules);
|
|
233
|
+
const qsContractsDir = asString(paths.quickstart_contracts_dir);
|
|
234
|
+
const qsStyleProfile = asString(paths.quickstart_style_profile);
|
|
235
|
+
const qsTrialChapter = asString(paths.quickstart_trial_chapter);
|
|
236
|
+
const styleTemplate = asString(paths.style_profile_template);
|
|
237
|
+
// Provide canonical manifest keys in addition to quickstart-scoped aliases.
|
|
238
|
+
if (qsRules)
|
|
239
|
+
paths.world_rules = qsRules;
|
|
240
|
+
if (qsContractsDir)
|
|
241
|
+
paths.character_contracts_dir = qsContractsDir;
|
|
242
|
+
if (qsStyleProfile)
|
|
243
|
+
paths.style_profile = qsStyleProfile;
|
|
244
|
+
else if (styleTemplate)
|
|
245
|
+
paths.style_profile = styleTemplate;
|
|
246
|
+
if (qsTrialChapter)
|
|
247
|
+
paths.chapter_draft = qsTrialChapter;
|
|
248
|
+
let agent;
|
|
249
|
+
const expected_outputs = [];
|
|
250
|
+
const next_actions = [];
|
|
251
|
+
if (step.phase === "world") {
|
|
252
|
+
agent = { kind: "subagent", name: "world-builder" };
|
|
253
|
+
expected_outputs.push({ path: QUICKSTART_STAGING_RELS.rulesJson, required: true });
|
|
254
|
+
next_actions.push({ kind: "command", command: `novel validate ${stepId}` });
|
|
255
|
+
next_actions.push({ kind: "command", command: `novel advance ${stepId}` });
|
|
256
|
+
next_actions.push({ kind: "command", command: `novel next`, note: "Compute next deterministic step (skips already-generated artifacts)." });
|
|
257
|
+
next_actions.push({
|
|
258
|
+
kind: "command",
|
|
259
|
+
command: `novel instructions quickstart:characters --json`,
|
|
260
|
+
note: "After advance, proceed to create initial characters/contracts."
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
else if (step.phase === "characters") {
|
|
264
|
+
agent = { kind: "subagent", name: "character-weaver" };
|
|
265
|
+
inline.contracts_output_dir = QUICKSTART_STAGING_RELS.contractsDir;
|
|
266
|
+
expected_outputs.push({
|
|
267
|
+
path: QUICKSTART_STAGING_RELS.contractsDir,
|
|
268
|
+
required: true,
|
|
269
|
+
note: "Write at least 1 character contract JSON under this dir."
|
|
270
|
+
});
|
|
271
|
+
next_actions.push({ kind: "command", command: `novel validate ${stepId}` });
|
|
272
|
+
next_actions.push({ kind: "command", command: `novel advance ${stepId}` });
|
|
273
|
+
next_actions.push({ kind: "command", command: `novel next`, note: "Compute next deterministic step (skips already-generated artifacts)." });
|
|
274
|
+
next_actions.push({ kind: "command", command: `novel instructions quickstart:style --json`, note: "After advance, proceed to style extraction." });
|
|
275
|
+
}
|
|
276
|
+
else if (step.phase === "style") {
|
|
277
|
+
agent = { kind: "subagent", name: "style-analyzer" };
|
|
278
|
+
expected_outputs.push({ path: QUICKSTART_STAGING_RELS.styleProfileJson, required: true });
|
|
279
|
+
next_actions.push({ kind: "command", command: `novel validate ${stepId}` });
|
|
280
|
+
next_actions.push({ kind: "command", command: `novel advance ${stepId}` });
|
|
281
|
+
next_actions.push({ kind: "command", command: `novel next`, note: "Compute next deterministic step (skips already-generated artifacts)." });
|
|
282
|
+
next_actions.push({ kind: "command", command: `novel instructions quickstart:trial --json`, note: "After advance, proceed to trial chapter." });
|
|
283
|
+
}
|
|
284
|
+
else if (step.phase === "trial") {
|
|
285
|
+
agent = { kind: "subagent", name: "chapter-writer" };
|
|
286
|
+
expected_outputs.push({ path: QUICKSTART_STAGING_RELS.trialChapterMd, required: true });
|
|
287
|
+
next_actions.push({ kind: "command", command: `novel validate ${stepId}` });
|
|
288
|
+
next_actions.push({ kind: "command", command: `novel advance ${stepId}` });
|
|
289
|
+
next_actions.push({ kind: "command", command: `novel next`, note: "Compute next deterministic step (skips already-generated artifacts)." });
|
|
290
|
+
next_actions.push({ kind: "command", command: `novel instructions quickstart:results --json`, note: "After advance, evaluate trial results." });
|
|
291
|
+
}
|
|
292
|
+
else if (step.phase === "results") {
|
|
293
|
+
agent = { kind: "subagent", name: "quality-judge" };
|
|
294
|
+
expected_outputs.push({
|
|
295
|
+
path: QUICKSTART_STAGING_RELS.evaluationJson,
|
|
296
|
+
required: true,
|
|
297
|
+
note: "QualityJudge returns JSON; the executor should write it to this path."
|
|
298
|
+
});
|
|
299
|
+
next_actions.push({ kind: "command", command: `novel validate ${stepId}` });
|
|
300
|
+
next_actions.push({ kind: "command", command: `novel advance ${stepId}` });
|
|
301
|
+
next_actions.push({ kind: "command", command: `novel next`, note: "After advance, the pipeline transitions to VOL_PLANNING." });
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
const _exhaustive = step.phase;
|
|
305
|
+
throw new NovelCliError(`Unsupported quickstart phase: ${String(_exhaustive)}`, 2);
|
|
306
|
+
}
|
|
307
|
+
const gate = args.novelAskGate ?? null;
|
|
308
|
+
const gateSpec = gate ? parseNovelAskQuestionSpec(gate.novel_ask) : null;
|
|
309
|
+
if (gate) {
|
|
310
|
+
resolveProjectRelativePath(args.rootDir, gate.answer_path, "novelAskGate.answer_path");
|
|
311
|
+
expected_outputs.unshift({
|
|
312
|
+
path: gate.answer_path,
|
|
313
|
+
required: true,
|
|
314
|
+
note: "AnswerSpec JSON record for the NOVEL_ASK gate (written before main step execution)."
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
const packet = {
|
|
318
|
+
version: 1,
|
|
319
|
+
step: stepId,
|
|
320
|
+
agent,
|
|
321
|
+
...(gate ? { novel_ask: gateSpec, answer_path: gate.answer_path } : {}),
|
|
322
|
+
manifest: {
|
|
323
|
+
mode: embedMode === "off" ? "paths" : "paths+embed",
|
|
324
|
+
inline,
|
|
325
|
+
paths,
|
|
326
|
+
...(embedMode === "off" ? {} : { embed })
|
|
327
|
+
},
|
|
328
|
+
expected_outputs,
|
|
329
|
+
next_actions
|
|
330
|
+
};
|
|
331
|
+
let writtenPath = null;
|
|
332
|
+
if (args.writeManifest) {
|
|
333
|
+
const manifestsDir = join(args.rootDir, "staging/manifests");
|
|
334
|
+
await ensureDir(manifestsDir);
|
|
335
|
+
const fileName = `${stepId.replaceAll(":", "-")}.packet.json`;
|
|
336
|
+
writtenPath = `staging/manifests/${fileName}`;
|
|
337
|
+
await writeJsonFile(join(args.rootDir, writtenPath), packet);
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
packet,
|
|
341
|
+
...(writtenPath ? { written_manifest_path: writtenPath } : {})
|
|
342
|
+
};
|
|
343
|
+
}
|
|
190
344
|
export async function buildInstructionPacket(args) {
|
|
191
345
|
const stepId = formatStepId(args.step);
|
|
192
|
-
if (args.step.kind === "review")
|
|
346
|
+
if (args.step.kind === "review") {
|
|
347
|
+
if (args.novelAskGate) {
|
|
348
|
+
throw new NovelCliError(`NOVEL_ASK gate is not supported for review steps: ${stepId}`, 2);
|
|
349
|
+
}
|
|
193
350
|
return await buildReviewInstructionPacket(args);
|
|
351
|
+
}
|
|
352
|
+
if (args.step.kind === "quickstart")
|
|
353
|
+
return await buildQuickStartInstructionPacket(args);
|
|
194
354
|
if (args.step.kind === "volume") {
|
|
195
355
|
const step = args.step;
|
|
196
356
|
const volume = args.checkpoint.current_volume;
|
|
@@ -661,6 +821,7 @@ export async function buildInstructionPacket(args) {
|
|
|
661
821
|
agent = { kind: "cli", name: "novel" };
|
|
662
822
|
expected_outputs.push({ path: `chapters/chapter-${String(step.chapter).padStart(3, "0")}.md`, required: true });
|
|
663
823
|
next_actions.push({ kind: "command", command: `novel commit --chapter ${step.chapter}` });
|
|
824
|
+
next_actions.push({ kind: "command", command: `novel next`, note: "After commit, compute next step." });
|
|
664
825
|
}
|
|
665
826
|
else {
|
|
666
827
|
throw new NovelCliError(`Unsupported step stage: ${step.stage}`, 2);
|
package/dist/next-step.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
1
2
|
import { join } from "node:path";
|
|
2
3
|
import { tryResolveVolumeChapterRange } from "./consistency-auditor.js";
|
|
3
4
|
import { NovelCliError } from "./errors.js";
|
|
@@ -5,12 +6,14 @@ import { pathExists, readJsonFile, readTextFile } from "./fs-utils.js";
|
|
|
5
6
|
import { computeGateDecision, detectHighConfidenceViolation } from "./gate-decision.js";
|
|
6
7
|
import { checkHookPolicy } from "./hook-policy.js";
|
|
7
8
|
import { loadPlatformProfile } from "./platform-profile.js";
|
|
9
|
+
import { QUICKSTART_STAGING_RELS } from "./quickstart.js";
|
|
10
|
+
import { validateQuickstartRulesSchema, validateQuickstartStyleProfileSchema } from "./quickstart-validators.js";
|
|
8
11
|
import { computeReviewNext } from "./volume-review.js";
|
|
9
12
|
import { computePrejudgeGuardrailsReport, loadPrejudgeGuardrailsReportIfFresh, prejudgeGuardrailsRelPath } from "./prejudge-guardrails.js";
|
|
10
13
|
import { summarizeNamingIssues } from "./naming-lint.js";
|
|
11
14
|
import { summarizeReadabilityIssues } from "./readability-lint.js";
|
|
12
15
|
import { computeTitlePolicyReport } from "./title-policy.js";
|
|
13
|
-
import { chapterRelPaths, formatStepId } from "./steps.js";
|
|
16
|
+
import { QUICKSTART_PHASES, chapterRelPaths, formatStepId } from "./steps.js";
|
|
14
17
|
import { isPlainObject } from "./type-guards.js";
|
|
15
18
|
import { computeVolumeNextStep } from "./volume-planning.js";
|
|
16
19
|
function normalizeStage(stage) {
|
|
@@ -536,6 +539,224 @@ async function computeChapterNextStep(projectRootDir, checkpoint) {
|
|
|
536
539
|
function notImplementedState(state) {
|
|
537
540
|
throw new NovelCliError(`Not implemented: orchestrator_state=${state}`, 2);
|
|
538
541
|
}
|
|
542
|
+
async function countContractArtifacts(rootDir) {
|
|
543
|
+
const absDir = join(rootDir, QUICKSTART_STAGING_RELS.contractsDir);
|
|
544
|
+
const hasDir = await pathExists(absDir);
|
|
545
|
+
if (!hasDir)
|
|
546
|
+
return { hasDir, fileCount: 0, sample: [], degraded: false };
|
|
547
|
+
try {
|
|
548
|
+
const entries = await readdir(absDir, { withFileTypes: true });
|
|
549
|
+
const files = entries.filter((e) => e.isFile()).map((e) => e.name);
|
|
550
|
+
const sample = files
|
|
551
|
+
.filter((n) => n.endsWith(".json"))
|
|
552
|
+
.sort()
|
|
553
|
+
.slice(0, 3);
|
|
554
|
+
const fileCount = files.filter((n) => n.endsWith(".json")).length;
|
|
555
|
+
return { hasDir, fileCount, sample, degraded: false };
|
|
556
|
+
}
|
|
557
|
+
catch (err) {
|
|
558
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
559
|
+
// If the dir exists but is unreadable, treat as present but degraded.
|
|
560
|
+
return { hasDir, fileCount: 0, sample: [], degraded: true, error: message };
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
async function computeQuickStartNextStep(projectRootDir, checkpoint) {
|
|
564
|
+
const stage = normalizeStage(checkpoint.pipeline_stage);
|
|
565
|
+
const inflight = typeof checkpoint.inflight_chapter === "number" ? checkpoint.inflight_chapter : null;
|
|
566
|
+
if ((stage === null || stage === "committed") && inflight !== null) {
|
|
567
|
+
throw new NovelCliError(`Checkpoint inconsistent for QUICK_START: pipeline_stage=${stage ?? "null"} but inflight_chapter=${inflight}. Set inflight_chapter to null.`, 2);
|
|
568
|
+
}
|
|
569
|
+
if (stage !== null && stage !== "committed") {
|
|
570
|
+
throw new NovelCliError(`Checkpoint inconsistent for QUICK_START: pipeline_stage=${stage} (expected null or committed). Finish the chapter pipeline or repair .checkpoint.json.`, 2);
|
|
571
|
+
}
|
|
572
|
+
const rulesAbs = join(projectRootDir, QUICKSTART_STAGING_RELS.rulesJson);
|
|
573
|
+
const styleAbs = join(projectRootDir, QUICKSTART_STAGING_RELS.styleProfileJson);
|
|
574
|
+
const trialAbs = join(projectRootDir, QUICKSTART_STAGING_RELS.trialChapterMd);
|
|
575
|
+
const evalAbs = join(projectRootDir, QUICKSTART_STAGING_RELS.evaluationJson);
|
|
576
|
+
const rulesExists = await pathExists(rulesAbs);
|
|
577
|
+
const contracts = await countContractArtifacts(projectRootDir);
|
|
578
|
+
const styleExists = await pathExists(styleAbs);
|
|
579
|
+
const trialExists = await pathExists(trialAbs);
|
|
580
|
+
const evalExists = await pathExists(evalAbs);
|
|
581
|
+
let rulesOk = false;
|
|
582
|
+
let rulesError = null;
|
|
583
|
+
if (rulesExists) {
|
|
584
|
+
try {
|
|
585
|
+
await validateQuickstartRulesSchema(rulesAbs, { trimRequiredStrings: true });
|
|
586
|
+
rulesOk = true;
|
|
587
|
+
}
|
|
588
|
+
catch (err) {
|
|
589
|
+
rulesError = err instanceof Error ? err.message : String(err);
|
|
590
|
+
rulesOk = false;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
let styleOk = false;
|
|
594
|
+
let styleError = null;
|
|
595
|
+
if (styleExists) {
|
|
596
|
+
try {
|
|
597
|
+
await validateQuickstartStyleProfileSchema(styleAbs);
|
|
598
|
+
styleOk = true;
|
|
599
|
+
}
|
|
600
|
+
catch (err) {
|
|
601
|
+
styleError = err instanceof Error ? err.message : String(err);
|
|
602
|
+
styleOk = false;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
let trialOk = false;
|
|
606
|
+
let trialError = null;
|
|
607
|
+
if (trialExists) {
|
|
608
|
+
try {
|
|
609
|
+
const text = await readTextFile(trialAbs);
|
|
610
|
+
if (text.trim().length === 0)
|
|
611
|
+
throw new Error("empty trial chapter");
|
|
612
|
+
trialOk = true;
|
|
613
|
+
}
|
|
614
|
+
catch (err) {
|
|
615
|
+
trialError = err instanceof Error ? err.message : String(err);
|
|
616
|
+
trialOk = false;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
let evalOk = false;
|
|
620
|
+
let evalError = null;
|
|
621
|
+
if (evalExists) {
|
|
622
|
+
try {
|
|
623
|
+
const raw = await readJsonFile(evalAbs);
|
|
624
|
+
if (!isPlainObject(raw))
|
|
625
|
+
throw new Error("expected JSON object");
|
|
626
|
+
evalOk = true;
|
|
627
|
+
}
|
|
628
|
+
catch (err) {
|
|
629
|
+
evalError = err instanceof Error ? err.message : String(err);
|
|
630
|
+
evalOk = false;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
const evidence = {
|
|
634
|
+
checkpoint: {
|
|
635
|
+
quickstart_phase: (checkpoint.quickstart_phase ?? null)
|
|
636
|
+
},
|
|
637
|
+
staging: {
|
|
638
|
+
rulesExists,
|
|
639
|
+
rulesOk,
|
|
640
|
+
...(rulesError ? { rulesError } : {}),
|
|
641
|
+
contracts: {
|
|
642
|
+
hasDir: contracts.hasDir,
|
|
643
|
+
jsonFileCount: contracts.fileCount,
|
|
644
|
+
sample: contracts.sample,
|
|
645
|
+
degraded: contracts.degraded,
|
|
646
|
+
...(contracts.error ? { error: contracts.error } : {})
|
|
647
|
+
},
|
|
648
|
+
styleExists,
|
|
649
|
+
styleOk,
|
|
650
|
+
...(styleError ? { styleError } : {}),
|
|
651
|
+
trialExists,
|
|
652
|
+
trialOk,
|
|
653
|
+
...(trialError ? { trialError } : {}),
|
|
654
|
+
evalExists,
|
|
655
|
+
evalOk,
|
|
656
|
+
...(evalError ? { evalError } : {})
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
let selected;
|
|
660
|
+
let selectedPhase;
|
|
661
|
+
if (!rulesOk) {
|
|
662
|
+
selectedPhase = "world";
|
|
663
|
+
selected = {
|
|
664
|
+
step: formatStepId({ kind: "quickstart", phase: "world" }),
|
|
665
|
+
reason: "quickstart:world",
|
|
666
|
+
inflight: { chapter: null, pipeline_stage: null },
|
|
667
|
+
evidence
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
else if (!contracts.hasDir || contracts.fileCount === 0) {
|
|
671
|
+
selectedPhase = "characters";
|
|
672
|
+
selected = {
|
|
673
|
+
step: formatStepId({ kind: "quickstart", phase: "characters" }),
|
|
674
|
+
reason: "quickstart:characters",
|
|
675
|
+
inflight: { chapter: null, pipeline_stage: null },
|
|
676
|
+
evidence
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
else if (!styleOk) {
|
|
680
|
+
selectedPhase = "style";
|
|
681
|
+
selected = {
|
|
682
|
+
step: formatStepId({ kind: "quickstart", phase: "style" }),
|
|
683
|
+
reason: "quickstart:style",
|
|
684
|
+
inflight: { chapter: null, pipeline_stage: null },
|
|
685
|
+
evidence
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
else if (!trialOk) {
|
|
689
|
+
selectedPhase = "trial";
|
|
690
|
+
selected = {
|
|
691
|
+
step: formatStepId({ kind: "quickstart", phase: "trial" }),
|
|
692
|
+
reason: "quickstart:trial",
|
|
693
|
+
inflight: { chapter: null, pipeline_stage: null },
|
|
694
|
+
evidence
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
else if (!evalOk) {
|
|
698
|
+
selectedPhase = "results";
|
|
699
|
+
selected = {
|
|
700
|
+
step: formatStepId({ kind: "quickstart", phase: "results" }),
|
|
701
|
+
reason: "quickstart:results",
|
|
702
|
+
inflight: { chapter: null, pipeline_stage: null },
|
|
703
|
+
evidence
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
else {
|
|
707
|
+
selectedPhase = "results";
|
|
708
|
+
selected = {
|
|
709
|
+
step: formatStepId({ kind: "quickstart", phase: "results" }),
|
|
710
|
+
reason: "quickstart:results:artifacts_present",
|
|
711
|
+
inflight: { chapter: null, pipeline_stage: null },
|
|
712
|
+
evidence
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
const selectedPhaseIdx = QUICKSTART_PHASES.indexOf(selectedPhase);
|
|
716
|
+
if (selectedPhaseIdx < 0) {
|
|
717
|
+
throw new NovelCliError(`Internal error: invalid quickstart phase=${selectedPhase}`, 2);
|
|
718
|
+
}
|
|
719
|
+
const checkpointPhase = checkpoint.quickstart_phase ?? null;
|
|
720
|
+
if (checkpointPhase) {
|
|
721
|
+
const checkpointIdx = QUICKSTART_PHASES.indexOf(checkpointPhase);
|
|
722
|
+
if (checkpointIdx >= 0 && selectedPhaseIdx < checkpointIdx) {
|
|
723
|
+
const expectedPath = (() => {
|
|
724
|
+
switch (selectedPhase) {
|
|
725
|
+
case "world":
|
|
726
|
+
return QUICKSTART_STAGING_RELS.rulesJson;
|
|
727
|
+
case "characters":
|
|
728
|
+
return QUICKSTART_STAGING_RELS.contractsDir;
|
|
729
|
+
case "style":
|
|
730
|
+
return QUICKSTART_STAGING_RELS.styleProfileJson;
|
|
731
|
+
case "trial":
|
|
732
|
+
return QUICKSTART_STAGING_RELS.trialChapterMd;
|
|
733
|
+
case "results":
|
|
734
|
+
return QUICKSTART_STAGING_RELS.evaluationJson;
|
|
735
|
+
default: {
|
|
736
|
+
const _exhaustive = selectedPhase;
|
|
737
|
+
return String(_exhaustive);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
})();
|
|
741
|
+
return {
|
|
742
|
+
...selected,
|
|
743
|
+
reason: `quickstart:recovery_blocked:${checkpointPhase}`,
|
|
744
|
+
evidence: {
|
|
745
|
+
...evidence,
|
|
746
|
+
recovery_blocked: {
|
|
747
|
+
checkpoint_phase: checkpointPhase,
|
|
748
|
+
checkpoint_phase_idx: checkpointIdx,
|
|
749
|
+
inferred_phase: selectedPhase,
|
|
750
|
+
inferred_phase_idx: selectedPhaseIdx,
|
|
751
|
+
inferred_reason: selected.reason,
|
|
752
|
+
expected_path: expectedPath
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return selected;
|
|
759
|
+
}
|
|
539
760
|
export async function computeNextStep(projectRootDir, checkpoint) {
|
|
540
761
|
switch (checkpoint.orchestrator_state) {
|
|
541
762
|
case "WRITING":
|
|
@@ -558,10 +779,12 @@ export async function computeNextStep(projectRootDir, checkpoint) {
|
|
|
558
779
|
const next = await computeChapterNextStep(projectRootDir, normalizedCheckpoint);
|
|
559
780
|
return { ...next, reason: `error_retry:${healPrefix}${next.reason}` };
|
|
560
781
|
}
|
|
561
|
-
case "INIT":
|
|
562
|
-
|
|
782
|
+
case "INIT": {
|
|
783
|
+
const next = await computeQuickStartNextStep(projectRootDir, checkpoint);
|
|
784
|
+
return { ...next, reason: `init:${next.reason}` };
|
|
785
|
+
}
|
|
563
786
|
case "QUICK_START":
|
|
564
|
-
return
|
|
787
|
+
return await computeQuickStartNextStep(projectRootDir, checkpoint);
|
|
565
788
|
case "VOL_PLANNING":
|
|
566
789
|
return await computeVolumeNextStep(projectRootDir, checkpoint);
|
|
567
790
|
case "VOL_REVIEW":
|