novel-writer-cli 0.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/LICENSE +21 -0
- package/README.md +103 -0
- package/agents/chapter-writer.md +142 -0
- package/agents/character-weaver.md +117 -0
- package/agents/consistency-auditor.md +85 -0
- package/agents/plot-architect.md +128 -0
- package/agents/quality-judge.md +232 -0
- package/agents/style-analyzer.md +109 -0
- package/agents/style-refiner.md +97 -0
- package/agents/summarizer.md +128 -0
- package/agents/world-builder.md +161 -0
- package/dist/__tests__/character-voice.test.js +445 -0
- package/dist/__tests__/commit-prototype-pollution.test.js +45 -0
- package/dist/__tests__/engagement.test.js +382 -0
- package/dist/__tests__/foreshadow-visibility.test.js +131 -0
- package/dist/__tests__/hook-ledger.test.js +1028 -0
- package/dist/__tests__/naming-lint.test.js +132 -0
- package/dist/__tests__/narrative-health-injection.test.js +359 -0
- package/dist/__tests__/next-step-prejudge-guardrails.test.js +325 -0
- package/dist/__tests__/next-step-title-fix.test.js +153 -0
- package/dist/__tests__/platform-profile.test.js +274 -0
- package/dist/__tests__/promise-ledger.test.js +189 -0
- package/dist/__tests__/readability-lint.test.js +209 -0
- package/dist/__tests__/text-utils.test.js +39 -0
- package/dist/__tests__/title-policy.test.js +147 -0
- package/dist/advance.js +75 -0
- package/dist/character-voice.js +805 -0
- package/dist/checkpoint.js +126 -0
- package/dist/cli.js +563 -0
- package/dist/cliche-lint.js +515 -0
- package/dist/commit.js +1460 -0
- package/dist/consistency-auditor.js +684 -0
- package/dist/engagement.js +687 -0
- package/dist/errors.js +7 -0
- package/dist/fingerprint.js +16 -0
- package/dist/foreshadow-visibility.js +214 -0
- package/dist/fs-utils.js +68 -0
- package/dist/hook-ledger.js +721 -0
- package/dist/hook-policy.js +107 -0
- package/dist/instruction-gates.js +51 -0
- package/dist/instructions.js +406 -0
- package/dist/latest-summary-loader.js +29 -0
- package/dist/lock.js +121 -0
- package/dist/naming-lint.js +531 -0
- package/dist/ner.js +73 -0
- package/dist/next-step.js +408 -0
- package/dist/novel-ask.js +270 -0
- package/dist/output.js +9 -0
- package/dist/platform-constraints.js +518 -0
- package/dist/platform-profile.js +325 -0
- package/dist/prejudge-guardrails.js +370 -0
- package/dist/project.js +40 -0
- package/dist/promise-ledger.js +723 -0
- package/dist/readability-lint.js +555 -0
- package/dist/safe-parse.js +36 -0
- package/dist/safe-path.js +29 -0
- package/dist/scoring-weights.js +290 -0
- package/dist/steps.js +60 -0
- package/dist/text-utils.js +18 -0
- package/dist/title-policy.js +251 -0
- package/dist/type-guards.js +6 -0
- package/dist/validate.js +131 -0
- package/docs/user/README.md +17 -0
- package/docs/user/guardrails.md +179 -0
- package/docs/user/interactive-gates.md +124 -0
- package/docs/user/novel-cli.md +289 -0
- package/docs/user/ops.md +123 -0
- package/docs/user/quick-start.md +97 -0
- package/docs/user/spec-system.md +166 -0
- package/docs/user/storylines.md +144 -0
- package/package.json +48 -0
- package/schemas/README.md +18 -0
- package/schemas/character-voice-drift.schema.json +135 -0
- package/schemas/character-voice-profiles.schema.json +141 -0
- package/schemas/engagement-metrics.schema.json +38 -0
- package/schemas/hook-ledger.schema.json +108 -0
- package/schemas/platform-profile.schema.json +235 -0
- package/schemas/promise-ledger.schema.json +97 -0
- package/scripts/calibrate-quality-judge.sh +91 -0
- package/scripts/compare-regression-runs.sh +86 -0
- package/scripts/lib/_common.py +131 -0
- package/scripts/lib/calibrate_quality_judge.py +312 -0
- package/scripts/lib/compare_regression_runs.py +142 -0
- package/scripts/lib/run_regression.py +621 -0
- package/scripts/lint-blacklist.sh +201 -0
- package/scripts/lint-cliche.sh +370 -0
- package/scripts/lint-readability.sh +404 -0
- package/scripts/query-foreshadow.sh +252 -0
- package/scripts/run-ner.sh +669 -0
- package/scripts/run-regression.sh +122 -0
- package/skills/cli-step/SKILL.md +158 -0
- package/skills/continue/SKILL.md +348 -0
- package/skills/continue/references/context-contracts.md +169 -0
- package/skills/continue/references/continuity-checks.md +187 -0
- package/skills/continue/references/file-protocols.md +64 -0
- package/skills/continue/references/foreshadowing.md +130 -0
- package/skills/continue/references/gate-decision.md +53 -0
- package/skills/continue/references/periodic-maintenance.md +46 -0
- package/skills/novel-writing/SKILL.md +77 -0
- package/skills/novel-writing/references/quality-rubric.md +140 -0
- package/skills/novel-writing/references/style-guide.md +145 -0
- package/skills/start/SKILL.md +458 -0
- package/skills/start/references/quality-review.md +86 -0
- package/skills/start/references/setting-update.md +44 -0
- package/skills/start/references/vol-planning.md +61 -0
- package/skills/start/references/vol-review.md +58 -0
- package/skills/status/SKILL.md +116 -0
- package/skills/status/references/sample-output.md +60 -0
- package/templates/ai-blacklist.json +79 -0
- package/templates/brief-template.md +46 -0
- package/templates/genre-weight-profiles.json +90 -0
- package/templates/novel-ask/example.answer.json +12 -0
- package/templates/novel-ask/example.question.json +51 -0
- package/templates/platform-profile.json +148 -0
- package/templates/style-profile-template.json +58 -0
- package/templates/web-novel-cliche-lint.json +41 -0
package/dist/ner.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { NovelCliError } from "./errors.js";
|
|
5
|
+
import { isPlainObject } from "./type-guards.js";
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
export function parseNerOutput(raw) {
|
|
8
|
+
if (!isPlainObject(raw))
|
|
9
|
+
throw new NovelCliError(`Invalid NER output: expected JSON object.`, 2);
|
|
10
|
+
const obj = raw;
|
|
11
|
+
const schema = obj.schema_version;
|
|
12
|
+
if (typeof schema !== "number" || !Number.isInteger(schema))
|
|
13
|
+
throw new NovelCliError(`Invalid NER output: schema_version must be an int.`, 2);
|
|
14
|
+
if (schema !== 1)
|
|
15
|
+
throw new NovelCliError(`Invalid NER output: unsupported schema_version=${schema} (expected 1).`, 2);
|
|
16
|
+
const entitiesRaw = obj.entities;
|
|
17
|
+
if (!isPlainObject(entitiesRaw))
|
|
18
|
+
throw new NovelCliError(`Invalid NER output: missing entities object.`, 2);
|
|
19
|
+
const entitiesObj = entitiesRaw;
|
|
20
|
+
const parseList = (key) => {
|
|
21
|
+
const listRaw = entitiesObj[key];
|
|
22
|
+
if (!Array.isArray(listRaw))
|
|
23
|
+
return [];
|
|
24
|
+
const out = [];
|
|
25
|
+
for (const it of listRaw) {
|
|
26
|
+
if (!isPlainObject(it))
|
|
27
|
+
continue;
|
|
28
|
+
const rec = it;
|
|
29
|
+
const text = typeof rec.text === "string" ? rec.text.trim() : "";
|
|
30
|
+
const confidence = typeof rec.confidence === "string" ? rec.confidence : "unknown";
|
|
31
|
+
const mentionsRaw = rec.mentions;
|
|
32
|
+
const mentions = [];
|
|
33
|
+
if (Array.isArray(mentionsRaw)) {
|
|
34
|
+
for (const m of mentionsRaw) {
|
|
35
|
+
if (!isPlainObject(m))
|
|
36
|
+
continue;
|
|
37
|
+
const mo = m;
|
|
38
|
+
const line = typeof mo.line === "number" && Number.isInteger(mo.line) ? mo.line : null;
|
|
39
|
+
const snippet = typeof mo.snippet === "string" ? mo.snippet : null;
|
|
40
|
+
if (line !== null && snippet !== null)
|
|
41
|
+
mentions.push({ line, snippet });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (text.length === 0)
|
|
45
|
+
continue;
|
|
46
|
+
out.push({ text, confidence, mentions });
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
};
|
|
50
|
+
return {
|
|
51
|
+
schema_version: 1,
|
|
52
|
+
entities: {
|
|
53
|
+
characters: parseList("characters"),
|
|
54
|
+
locations: parseList("locations"),
|
|
55
|
+
time_markers: parseList("time_markers"),
|
|
56
|
+
events: parseList("events")
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function runNerScriptPath() {
|
|
61
|
+
return fileURLToPath(new URL("../scripts/run-ner.sh", import.meta.url));
|
|
62
|
+
}
|
|
63
|
+
export async function runNer(chapterAbs) {
|
|
64
|
+
const script = runNerScriptPath();
|
|
65
|
+
const { stdout } = await execFileAsync("bash", [script, chapterAbs], {
|
|
66
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
67
|
+
timeout: 60_000,
|
|
68
|
+
killSignal: "SIGKILL"
|
|
69
|
+
});
|
|
70
|
+
const trimmed = stdout.trim();
|
|
71
|
+
const raw = JSON.parse(trimmed);
|
|
72
|
+
return parseNerOutput(raw);
|
|
73
|
+
}
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { pathExists, readJsonFile, readTextFile } from "./fs-utils.js";
|
|
3
|
+
import { checkHookPolicy } from "./hook-policy.js";
|
|
4
|
+
import { loadPlatformProfile } from "./platform-profile.js";
|
|
5
|
+
import { computePrejudgeGuardrailsReport, loadPrejudgeGuardrailsReportIfFresh, prejudgeGuardrailsRelPath } from "./prejudge-guardrails.js";
|
|
6
|
+
import { summarizeNamingIssues } from "./naming-lint.js";
|
|
7
|
+
import { summarizeReadabilityIssues } from "./readability-lint.js";
|
|
8
|
+
import { computeTitlePolicyReport } from "./title-policy.js";
|
|
9
|
+
import { chapterRelPaths, formatStepId } from "./steps.js";
|
|
10
|
+
function normalizeStage(stage) {
|
|
11
|
+
if (stage === null || stage === undefined)
|
|
12
|
+
return null;
|
|
13
|
+
if (typeof stage === "string")
|
|
14
|
+
return stage;
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
async function checkHookPolicyForStage(args) {
|
|
18
|
+
const loadedProfile = await loadPlatformProfile(args.projectRootDir);
|
|
19
|
+
const hookPolicy = loadedProfile?.profile.hook_policy;
|
|
20
|
+
if (!hookPolicy?.required)
|
|
21
|
+
return null;
|
|
22
|
+
let evalRaw;
|
|
23
|
+
try {
|
|
24
|
+
evalRaw = await readJsonFile(join(args.projectRootDir, args.evalRelPath));
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
28
|
+
return {
|
|
29
|
+
step: formatStepId({ kind: "chapter", chapter: args.inflightChapter, stage: "judge" }),
|
|
30
|
+
reason: `${args.stagePrefix}:hook_eval_read_failed`,
|
|
31
|
+
inflight: { chapter: args.inflightChapter, pipeline_stage: args.pipelineStage },
|
|
32
|
+
evidence: { ...args.evidence, hookFixCount: args.hookFixCount, error: message }
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const check = checkHookPolicy({ hookPolicy, evalRaw });
|
|
36
|
+
if (check.status === "invalid_eval") {
|
|
37
|
+
return {
|
|
38
|
+
step: formatStepId({ kind: "chapter", chapter: args.inflightChapter, stage: "judge" }),
|
|
39
|
+
reason: `${args.stagePrefix}:hook_eval_invalid:${check.reason}`,
|
|
40
|
+
inflight: { chapter: args.inflightChapter, pipeline_stage: args.pipelineStage },
|
|
41
|
+
evidence: { ...args.evidence, hookFixCount: args.hookFixCount, hook_check: check }
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (check.status === "fail") {
|
|
45
|
+
if (args.hookFixCount < 1) {
|
|
46
|
+
return {
|
|
47
|
+
step: formatStepId({ kind: "chapter", chapter: args.inflightChapter, stage: "hook-fix" }),
|
|
48
|
+
reason: `${args.stagePrefix}:hook_policy_fail:hook-fix:${check.reason}`,
|
|
49
|
+
inflight: { chapter: args.inflightChapter, pipeline_stage: args.pipelineStage },
|
|
50
|
+
evidence: { ...args.evidence, hookFixCount: args.hookFixCount, hook_check: check }
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
step: formatStepId({ kind: "chapter", chapter: args.inflightChapter, stage: "review" }),
|
|
55
|
+
reason: `${args.stagePrefix}:hook_policy_fail:manual_review:${check.reason}`,
|
|
56
|
+
inflight: { chapter: args.inflightChapter, pipeline_stage: args.pipelineStage },
|
|
57
|
+
evidence: { ...args.evidence, hookFixCount: args.hookFixCount, hook_check: check }
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
async function checkTitlePolicyForStage(args) {
|
|
63
|
+
const loadedProfile = await loadPlatformProfile(args.projectRootDir);
|
|
64
|
+
if (!loadedProfile)
|
|
65
|
+
return null;
|
|
66
|
+
const titlePolicy = loadedProfile.profile.retention?.title_policy;
|
|
67
|
+
if (!titlePolicy?.enabled)
|
|
68
|
+
return null;
|
|
69
|
+
if (!args.hasChapter) {
|
|
70
|
+
return {
|
|
71
|
+
step: formatStepId({ kind: "chapter", chapter: args.inflightChapter, stage: "draft" }),
|
|
72
|
+
reason: `${args.stagePrefix}:missing_chapter`,
|
|
73
|
+
inflight: { chapter: args.inflightChapter, pipeline_stage: args.pipelineStage },
|
|
74
|
+
evidence: { ...args.evidence, titleFixCount: args.titleFixCount }
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
let chapterText;
|
|
78
|
+
try {
|
|
79
|
+
chapterText = await readTextFile(join(args.projectRootDir, args.chapterRelPath));
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
83
|
+
return {
|
|
84
|
+
step: formatStepId({ kind: "chapter", chapter: args.inflightChapter, stage: "review" }),
|
|
85
|
+
reason: `${args.stagePrefix}:title_read_failed`,
|
|
86
|
+
inflight: { chapter: args.inflightChapter, pipeline_stage: args.pipelineStage },
|
|
87
|
+
evidence: { ...args.evidence, titleFixCount: args.titleFixCount, error: message }
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const report = computeTitlePolicyReport({ chapter: args.inflightChapter, chapterText, platformProfile: loadedProfile.profile });
|
|
91
|
+
if (report.status === "pass" || report.status === "skipped")
|
|
92
|
+
return null;
|
|
93
|
+
if (!report.has_hard_violations && !titlePolicy.auto_fix)
|
|
94
|
+
return null;
|
|
95
|
+
const primaryIssue = report.issues.find((i) => i.severity === "hard") ?? report.issues[0] ?? null;
|
|
96
|
+
const issueSummary = primaryIssue?.summary ?? "title policy failing";
|
|
97
|
+
if (titlePolicy.auto_fix) {
|
|
98
|
+
if (args.titleFixCount < 1) {
|
|
99
|
+
return {
|
|
100
|
+
step: formatStepId({ kind: "chapter", chapter: args.inflightChapter, stage: "title-fix" }),
|
|
101
|
+
reason: `${args.stagePrefix}:title_policy_fail:title-fix`,
|
|
102
|
+
inflight: { chapter: args.inflightChapter, pipeline_stage: args.pipelineStage },
|
|
103
|
+
evidence: { ...args.evidence, titleFixCount: args.titleFixCount, title_policy: { status: report.status, issue: issueSummary } }
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
step: formatStepId({ kind: "chapter", chapter: args.inflightChapter, stage: "review" }),
|
|
108
|
+
reason: `${args.stagePrefix}:title_policy_fail:manual_review`,
|
|
109
|
+
inflight: { chapter: args.inflightChapter, pipeline_stage: args.pipelineStage },
|
|
110
|
+
evidence: { ...args.evidence, titleFixCount: args.titleFixCount, title_policy: { status: report.status, issue: issueSummary } }
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
step: formatStepId({ kind: "chapter", chapter: args.inflightChapter, stage: "review" }),
|
|
115
|
+
reason: `${args.stagePrefix}:title_policy_fail:manual_fix_required`,
|
|
116
|
+
inflight: { chapter: args.inflightChapter, pipeline_stage: args.pipelineStage },
|
|
117
|
+
evidence: { ...args.evidence, titleFixCount: args.titleFixCount, title_policy: { status: report.status, issue: issueSummary } }
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
async function checkPrejudgeGuardrailsForStage(args) {
|
|
121
|
+
const loadedProfile = await loadPlatformProfile(args.projectRootDir);
|
|
122
|
+
if (!loadedProfile)
|
|
123
|
+
return null;
|
|
124
|
+
const chapterAbsPath = join(args.projectRootDir, args.chapterRelPath);
|
|
125
|
+
const cacheRelPath = prejudgeGuardrailsRelPath(args.inflightChapter);
|
|
126
|
+
let cacheStatus = "miss";
|
|
127
|
+
let report = await loadPrejudgeGuardrailsReportIfFresh({
|
|
128
|
+
rootDir: args.projectRootDir,
|
|
129
|
+
chapter: args.inflightChapter,
|
|
130
|
+
chapterAbsPath,
|
|
131
|
+
platformProfileRelPath: loadedProfile.relPath,
|
|
132
|
+
platformProfile: loadedProfile.profile
|
|
133
|
+
});
|
|
134
|
+
if (report)
|
|
135
|
+
cacheStatus = "hit";
|
|
136
|
+
if (!report) {
|
|
137
|
+
try {
|
|
138
|
+
report = await computePrejudgeGuardrailsReport({
|
|
139
|
+
rootDir: args.projectRootDir,
|
|
140
|
+
chapter: args.inflightChapter,
|
|
141
|
+
chapterAbsPath,
|
|
142
|
+
platformProfileRelPath: loadedProfile.relPath,
|
|
143
|
+
platformProfile: loadedProfile.profile
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
148
|
+
return {
|
|
149
|
+
step: formatStepId({ kind: "chapter", chapter: args.inflightChapter, stage: "review" }),
|
|
150
|
+
reason: `${args.stagePrefix}:prejudge_guardrails_error`,
|
|
151
|
+
inflight: { chapter: args.inflightChapter, pipeline_stage: args.pipelineStage },
|
|
152
|
+
evidence: { ...args.evidence, prejudge_guardrails: { cache: { status: cacheStatus, rel_path: cacheRelPath }, error: message } }
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (!report.has_blocking_issues)
|
|
157
|
+
return null;
|
|
158
|
+
const readabilityBlocking = report.readability_lint.has_blocking_issues;
|
|
159
|
+
const namingBlocking = report.naming_lint.has_blocking_issues;
|
|
160
|
+
const readabilitySummary = readabilityBlocking ? summarizeReadabilityIssues(report.readability_lint.issues, 3) : null;
|
|
161
|
+
const namingSummary = namingBlocking ? summarizeNamingIssues(report.naming_lint.issues, 3) : null;
|
|
162
|
+
const reasons = [];
|
|
163
|
+
if (readabilityBlocking)
|
|
164
|
+
reasons.push("readability_lint");
|
|
165
|
+
if (namingBlocking)
|
|
166
|
+
reasons.push("naming_lint");
|
|
167
|
+
const label = reasons.length > 0 ? reasons.join("+") : report.blocking_reasons.join("+");
|
|
168
|
+
return {
|
|
169
|
+
step: formatStepId({ kind: "chapter", chapter: args.inflightChapter, stage: "review" }),
|
|
170
|
+
reason: `${args.stagePrefix}:prejudge_guardrails_blocking:${label}`,
|
|
171
|
+
inflight: { chapter: args.inflightChapter, pipeline_stage: args.pipelineStage },
|
|
172
|
+
evidence: {
|
|
173
|
+
...args.evidence,
|
|
174
|
+
prejudge_guardrails: {
|
|
175
|
+
cache: { status: cacheStatus, rel_path: cacheRelPath },
|
|
176
|
+
status: report.status,
|
|
177
|
+
has_blocking_issues: report.has_blocking_issues,
|
|
178
|
+
blocking_reasons: report.blocking_reasons,
|
|
179
|
+
platform_profile: report.platform_profile,
|
|
180
|
+
readability: {
|
|
181
|
+
status: report.readability_lint.status,
|
|
182
|
+
issues_total: report.readability_lint.issues.length,
|
|
183
|
+
has_blocking_issues: report.readability_lint.has_blocking_issues,
|
|
184
|
+
...(readabilitySummary ? { blocking_summary: readabilitySummary } : {})
|
|
185
|
+
},
|
|
186
|
+
naming: {
|
|
187
|
+
status: report.naming_lint.status,
|
|
188
|
+
issues_total: report.naming_lint.issues.length,
|
|
189
|
+
has_blocking_issues: report.naming_lint.has_blocking_issues,
|
|
190
|
+
...(namingSummary ? { blocking_summary: namingSummary } : {})
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
export async function computeNextStep(projectRootDir, checkpoint) {
|
|
197
|
+
const inflightChapter = typeof checkpoint.inflight_chapter === "number" ? checkpoint.inflight_chapter : null;
|
|
198
|
+
const stage = normalizeStage(checkpoint.pipeline_stage);
|
|
199
|
+
const hookFixCount = typeof checkpoint.hook_fix_count === "number" ? checkpoint.hook_fix_count : 0;
|
|
200
|
+
const titleFixCount = typeof checkpoint.title_fix_count === "number" ? checkpoint.title_fix_count : 0;
|
|
201
|
+
// Fresh start.
|
|
202
|
+
if (inflightChapter === null || stage === null || stage === "committed") {
|
|
203
|
+
const nextChapter = checkpoint.last_completed_chapter + 1;
|
|
204
|
+
return {
|
|
205
|
+
step: formatStepId({ kind: "chapter", chapter: nextChapter, stage: "draft" }),
|
|
206
|
+
reason: "fresh",
|
|
207
|
+
inflight: { chapter: null, pipeline_stage: stage }
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const rel = chapterRelPaths(inflightChapter);
|
|
211
|
+
const hasChapter = await pathExists(join(projectRootDir, rel.staging.chapterMd));
|
|
212
|
+
const hasSummary = await pathExists(join(projectRootDir, rel.staging.summaryMd));
|
|
213
|
+
const hasDelta = await pathExists(join(projectRootDir, rel.staging.deltaJson));
|
|
214
|
+
const hasCrossref = await pathExists(join(projectRootDir, rel.staging.crossrefJson));
|
|
215
|
+
const hasEval = await pathExists(join(projectRootDir, rel.staging.evalJson));
|
|
216
|
+
const evidence = { hasChapter, hasSummary, hasDelta, hasCrossref, hasEval };
|
|
217
|
+
// Resume rules (aligned with skills/continue).
|
|
218
|
+
// Revision loop: restart from ChapterWriter regardless of existing staging artifacts.
|
|
219
|
+
if (stage === "revising") {
|
|
220
|
+
return {
|
|
221
|
+
step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "draft" }),
|
|
222
|
+
reason: "revising:restart_draft",
|
|
223
|
+
inflight: { chapter: inflightChapter, pipeline_stage: stage },
|
|
224
|
+
evidence
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
if (stage === "drafting") {
|
|
228
|
+
if (!hasChapter) {
|
|
229
|
+
return {
|
|
230
|
+
step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "draft" }),
|
|
231
|
+
reason: `${stage}:missing_chapter`,
|
|
232
|
+
inflight: { chapter: inflightChapter, pipeline_stage: stage },
|
|
233
|
+
evidence
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
if (!hasSummary || !hasDelta || !hasCrossref) {
|
|
237
|
+
return {
|
|
238
|
+
step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "summarize" }),
|
|
239
|
+
reason: `${stage}:missing_summary`,
|
|
240
|
+
inflight: { chapter: inflightChapter, pipeline_stage: stage },
|
|
241
|
+
evidence
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "refine" }),
|
|
246
|
+
reason: `${stage}:ready_refine`,
|
|
247
|
+
inflight: { chapter: inflightChapter, pipeline_stage: stage },
|
|
248
|
+
evidence
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
if (stage === "drafted") {
|
|
252
|
+
if (!hasChapter) {
|
|
253
|
+
return {
|
|
254
|
+
step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "draft" }),
|
|
255
|
+
reason: "drafted:missing_chapter",
|
|
256
|
+
inflight: { chapter: inflightChapter, pipeline_stage: stage },
|
|
257
|
+
evidence
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
if (!hasSummary || !hasDelta || !hasCrossref) {
|
|
261
|
+
return {
|
|
262
|
+
step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "summarize" }),
|
|
263
|
+
reason: "drafted:missing_summary",
|
|
264
|
+
inflight: { chapter: inflightChapter, pipeline_stage: stage },
|
|
265
|
+
evidence
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "refine" }),
|
|
270
|
+
reason: "drafted:resume_refine",
|
|
271
|
+
inflight: { chapter: inflightChapter, pipeline_stage: stage },
|
|
272
|
+
evidence
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
if (stage === "refined") {
|
|
276
|
+
if (!hasChapter) {
|
|
277
|
+
return {
|
|
278
|
+
step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "draft" }),
|
|
279
|
+
reason: "refined:missing_chapter",
|
|
280
|
+
inflight: { chapter: inflightChapter, pipeline_stage: stage },
|
|
281
|
+
evidence
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
if (!hasEval) {
|
|
285
|
+
const titleGate = await checkTitlePolicyForStage({
|
|
286
|
+
projectRootDir,
|
|
287
|
+
stagePrefix: "refined",
|
|
288
|
+
inflightChapter,
|
|
289
|
+
pipelineStage: stage,
|
|
290
|
+
evidence,
|
|
291
|
+
titleFixCount,
|
|
292
|
+
hasChapter,
|
|
293
|
+
chapterRelPath: rel.staging.chapterMd
|
|
294
|
+
});
|
|
295
|
+
if (titleGate)
|
|
296
|
+
return titleGate;
|
|
297
|
+
return {
|
|
298
|
+
step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "judge" }),
|
|
299
|
+
reason: "refined:missing_eval",
|
|
300
|
+
inflight: { chapter: inflightChapter, pipeline_stage: stage },
|
|
301
|
+
evidence
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
const titleGate = await checkTitlePolicyForStage({
|
|
305
|
+
projectRootDir,
|
|
306
|
+
stagePrefix: "refined",
|
|
307
|
+
inflightChapter,
|
|
308
|
+
pipelineStage: stage,
|
|
309
|
+
evidence,
|
|
310
|
+
titleFixCount,
|
|
311
|
+
hasChapter,
|
|
312
|
+
chapterRelPath: rel.staging.chapterMd
|
|
313
|
+
});
|
|
314
|
+
if (titleGate)
|
|
315
|
+
return titleGate;
|
|
316
|
+
const hookGate = await checkHookPolicyForStage({
|
|
317
|
+
projectRootDir,
|
|
318
|
+
stagePrefix: "refined",
|
|
319
|
+
inflightChapter,
|
|
320
|
+
pipelineStage: stage,
|
|
321
|
+
evidence,
|
|
322
|
+
hookFixCount,
|
|
323
|
+
evalRelPath: rel.staging.evalJson
|
|
324
|
+
});
|
|
325
|
+
if (hookGate)
|
|
326
|
+
return hookGate;
|
|
327
|
+
const guardrailsGate = await checkPrejudgeGuardrailsForStage({
|
|
328
|
+
projectRootDir,
|
|
329
|
+
stagePrefix: "refined",
|
|
330
|
+
inflightChapter,
|
|
331
|
+
pipelineStage: stage,
|
|
332
|
+
evidence,
|
|
333
|
+
chapterRelPath: rel.staging.chapterMd
|
|
334
|
+
});
|
|
335
|
+
if (guardrailsGate)
|
|
336
|
+
return guardrailsGate;
|
|
337
|
+
return {
|
|
338
|
+
step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "commit" }),
|
|
339
|
+
reason: "refined:ready_commit",
|
|
340
|
+
inflight: { chapter: inflightChapter, pipeline_stage: stage },
|
|
341
|
+
evidence
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
if (stage === "judged") {
|
|
345
|
+
if (!hasChapter) {
|
|
346
|
+
return {
|
|
347
|
+
step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "draft" }),
|
|
348
|
+
reason: "judged:missing_chapter",
|
|
349
|
+
inflight: { chapter: inflightChapter, pipeline_stage: stage },
|
|
350
|
+
evidence
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
if (!hasEval) {
|
|
354
|
+
return {
|
|
355
|
+
step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "judge" }),
|
|
356
|
+
reason: "judged:missing_eval",
|
|
357
|
+
inflight: { chapter: inflightChapter, pipeline_stage: stage },
|
|
358
|
+
evidence
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
const titleGate = await checkTitlePolicyForStage({
|
|
362
|
+
projectRootDir,
|
|
363
|
+
stagePrefix: "judged",
|
|
364
|
+
inflightChapter,
|
|
365
|
+
pipelineStage: stage,
|
|
366
|
+
evidence,
|
|
367
|
+
titleFixCount,
|
|
368
|
+
hasChapter,
|
|
369
|
+
chapterRelPath: rel.staging.chapterMd
|
|
370
|
+
});
|
|
371
|
+
if (titleGate)
|
|
372
|
+
return titleGate;
|
|
373
|
+
const hookGate = await checkHookPolicyForStage({
|
|
374
|
+
projectRootDir,
|
|
375
|
+
stagePrefix: "judged",
|
|
376
|
+
inflightChapter,
|
|
377
|
+
pipelineStage: stage,
|
|
378
|
+
evidence,
|
|
379
|
+
hookFixCount,
|
|
380
|
+
evalRelPath: rel.staging.evalJson
|
|
381
|
+
});
|
|
382
|
+
if (hookGate)
|
|
383
|
+
return hookGate;
|
|
384
|
+
const guardrailsGate = await checkPrejudgeGuardrailsForStage({
|
|
385
|
+
projectRootDir,
|
|
386
|
+
stagePrefix: "judged",
|
|
387
|
+
inflightChapter,
|
|
388
|
+
pipelineStage: stage,
|
|
389
|
+
evidence,
|
|
390
|
+
chapterRelPath: rel.staging.chapterMd
|
|
391
|
+
});
|
|
392
|
+
if (guardrailsGate)
|
|
393
|
+
return guardrailsGate;
|
|
394
|
+
return {
|
|
395
|
+
step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "commit" }),
|
|
396
|
+
reason: "judged:ready_commit",
|
|
397
|
+
inflight: { chapter: inflightChapter, pipeline_stage: stage },
|
|
398
|
+
evidence
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
// Unknown stage: fall back to safest.
|
|
402
|
+
return {
|
|
403
|
+
step: formatStepId({ kind: "chapter", chapter: inflightChapter, stage: "draft" }),
|
|
404
|
+
reason: `unknown_stage:${stage}`,
|
|
405
|
+
inflight: { chapter: inflightChapter, pipeline_stage: stage },
|
|
406
|
+
evidence
|
|
407
|
+
};
|
|
408
|
+
}
|