mirror of
https://github.com/khodges42/nightShift.git
synced 2026-06-14 10:08:37 +00:00
Refactor of writing tool
This commit is contained in:
parent
e1e6803eb1
commit
78dcf911d6
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -29,6 +29,9 @@ MANIFEST
|
||||||
# Codex working notes and generated analysis docs
|
# Codex working notes and generated analysis docs
|
||||||
docs/codex/
|
docs/codex/
|
||||||
|
|
||||||
|
# Locally preserved pre-pivot writing template
|
||||||
|
tutorial-writing-complex/
|
||||||
|
|
||||||
# PyInstaller
|
# PyInstaller
|
||||||
# Usually these files are written by a python script from a template
|
# Usually these files are written by a python script from a template
|
||||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ from .runlog import RunLogger
|
||||||
from .stages import StageResult
|
from .stages import StageResult
|
||||||
from .tasks import Task, mark_task_completed
|
from .tasks import Task, mark_task_completed
|
||||||
from .telemetry import TelemetryEntry, format_telemetry_summary, telemetry_from_stage_output
|
from .telemetry import TelemetryEntry, format_telemetry_summary, telemetry_from_stage_output
|
||||||
from .writing_validators import validate_writing_file_updates
|
from .writing_validators import collect_writing_warnings, validate_writing_file_updates
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
|
@ -767,6 +767,7 @@ class PipelineRunner:
|
||||||
stdout = self._read_agent_stdout(result.output_path)
|
stdout = self._read_agent_stdout(result.output_path)
|
||||||
invalid_rerun_done = False
|
invalid_rerun_done = False
|
||||||
candidate_index_path: Path | None = None
|
candidate_index_path: Path | None = None
|
||||||
|
warning_path: Path | None = None
|
||||||
while True:
|
while True:
|
||||||
updates: tuple[FileUpdate, ...] = ()
|
updates: tuple[FileUpdate, ...] = ()
|
||||||
try:
|
try:
|
||||||
|
|
@ -779,6 +780,7 @@ class PipelineRunner:
|
||||||
)
|
)
|
||||||
if _is_writing_file_writer_stage(stage):
|
if _is_writing_file_writer_stage(stage):
|
||||||
validate_writing_file_updates(updates, self.config.project.root)
|
validate_writing_file_updates(updates, self.config.project.root)
|
||||||
|
warning_path = self._write_file_writer_warnings(task.id, stage, updates, retry_count)
|
||||||
patch = generate_patch_from_file_updates(
|
patch = generate_patch_from_file_updates(
|
||||||
updates,
|
updates,
|
||||||
self.config.project.root,
|
self.config.project.root,
|
||||||
|
|
@ -799,6 +801,12 @@ class PipelineRunner:
|
||||||
):
|
):
|
||||||
if _is_writing_file_writer_stage(stage):
|
if _is_writing_file_writer_stage(stage):
|
||||||
validate_writing_file_updates(allowed_updates, self.config.project.root)
|
validate_writing_file_updates(allowed_updates, self.config.project.root)
|
||||||
|
warning_path = self._write_file_writer_warnings(
|
||||||
|
task.id,
|
||||||
|
stage,
|
||||||
|
allowed_updates,
|
||||||
|
retry_count,
|
||||||
|
)
|
||||||
patch = generate_patch_from_file_updates(
|
patch = generate_patch_from_file_updates(
|
||||||
allowed_updates,
|
allowed_updates,
|
||||||
self.config.project.root,
|
self.config.project.root,
|
||||||
|
|
@ -910,7 +918,11 @@ class PipelineRunner:
|
||||||
"pass",
|
"pass",
|
||||||
patch_reason,
|
patch_reason,
|
||||||
output_path=str(proposed_path.relative_to(self.config.project.root)),
|
output_path=str(proposed_path.relative_to(self.config.project.root)),
|
||||||
context_update=f"Implementation summary: {summary_path.relative_to(self.config.project.root).as_posix()}",
|
context_update=_format_writer_context_update(
|
||||||
|
self.config.project.root,
|
||||||
|
summary_path,
|
||||||
|
warning_path,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _write_file_writer_candidates(
|
def _write_file_writer_candidates(
|
||||||
|
|
@ -954,6 +966,31 @@ class PipelineRunner:
|
||||||
lines.append("")
|
lines.append("")
|
||||||
return self.artifacts.write_stage_output(task_id, f"{base}/index.md", "\n".join(lines))
|
return self.artifacts.write_stage_output(task_id, f"{base}/index.md", "\n".join(lines))
|
||||||
|
|
||||||
|
def _write_file_writer_warnings(
|
||||||
|
self,
|
||||||
|
task_id: str,
|
||||||
|
stage: StageConfig,
|
||||||
|
updates: tuple[FileUpdate, ...],
|
||||||
|
retry_count: int,
|
||||||
|
) -> Path | None:
|
||||||
|
warnings = collect_writing_warnings(updates, self.config.project.root)
|
||||||
|
if not warnings:
|
||||||
|
return None
|
||||||
|
filename = _attempt_filename(f"{stage.id}-warnings.md", retry_count)
|
||||||
|
lines = [
|
||||||
|
"# Writing Warnings",
|
||||||
|
"",
|
||||||
|
f"Stage: `{stage.id}`",
|
||||||
|
"",
|
||||||
|
"These are soft writing concerns. They do not block artifact creation.",
|
||||||
|
"",
|
||||||
|
"## Warnings",
|
||||||
|
"",
|
||||||
|
*[f"- {warning}" for warning in warnings],
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
return self.artifacts.write_stage_output(task_id, filename, "\n".join(lines))
|
||||||
|
|
||||||
def _allowed_file_contents(self, stage: StageConfig, max_chars: int = 2400) -> str:
|
def _allowed_file_contents(self, stage: StageConfig, max_chars: int = 2400) -> str:
|
||||||
sections: list[str] = []
|
sections: list[str] = []
|
||||||
for path_text in stage.allowed_paths:
|
for path_text in stage.allowed_paths:
|
||||||
|
|
@ -1677,6 +1714,18 @@ def format_implementation_summary(
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _format_writer_context_update(
|
||||||
|
project_root: Path,
|
||||||
|
summary_path: Path,
|
||||||
|
warning_path: Path | None,
|
||||||
|
) -> str:
|
||||||
|
summary = summary_path.relative_to(project_root).as_posix()
|
||||||
|
if warning_path is None:
|
||||||
|
return f"Implementation summary: {summary}"
|
||||||
|
warnings = warning_path.relative_to(project_root).as_posix()
|
||||||
|
return f"Implementation summary: {summary}; writing warnings: {warnings}"
|
||||||
|
|
||||||
|
|
||||||
def _latest_patch_like_output(previous_outputs: dict[str, str]) -> str:
|
def _latest_patch_like_output(previous_outputs: dict[str, str]) -> str:
|
||||||
for name in ("normalized.patch", "applied.patch", "proposed.patch", "patch_input"):
|
for name in ("normalized.patch", "applied.patch", "proposed.patch", "patch_input"):
|
||||||
if name in previous_outputs and previous_outputs[name].strip():
|
if name in previous_outputs and previous_outputs[name].strip():
|
||||||
|
|
|
||||||
|
|
@ -16,3 +16,7 @@ story/chapters/*.md
|
||||||
.nightshift/project-context.md
|
.nightshift/project-context.md
|
||||||
.nightshift/project-context-chart.md
|
.nightshift/project-context-chart.md
|
||||||
.nightshift/nightshift.log
|
.nightshift/nightshift.log
|
||||||
|
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
.pytest_cache/
|
||||||
|
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
You are the continuity reviewer for a NightShift novel-writing workflow.
|
|
||||||
|
|
||||||
Review the drafted scene against:
|
|
||||||
- the current task
|
|
||||||
- `story/worldbuilding.md`
|
|
||||||
- `story/characters.md`
|
|
||||||
- `story/plot-state.md`
|
|
||||||
- `story/timeline.md`
|
|
||||||
- `story/unresolved-threads.md`
|
|
||||||
- `story/continuity-rules.md`
|
|
||||||
- prior scene context provided in artifacts
|
|
||||||
|
|
||||||
Check for:
|
|
||||||
- contradictions
|
|
||||||
- wrong character knowledge
|
|
||||||
- wrong character pronouns or narrative reference, using `Pronouns / Reference` in `story/characters.md` as hard canon
|
|
||||||
- impossible locations or timing
|
|
||||||
- accidental resolution of future threads
|
|
||||||
- missing required beats from the task
|
|
||||||
- invented lore that should have been added deliberately
|
|
||||||
|
|
||||||
Do not fail the scene because durable state files are not updated yet. State files are updated by a later `update_state` stage after review. If the task lists `Updates:`, treat those as future state-update requirements and mention them only as `context_update` guidance.
|
|
||||||
|
|
||||||
Wrong pronouns are a continuity failure. If a drafted scene uses non-canonical pronouns for a named character, return `status: fail` and explain which character drifted. Do not pass the scene with only `context_update` guidance.
|
|
||||||
|
|
||||||
Pronoun canon quick reference:
|
|
||||||
- Proxy: she/her
|
|
||||||
- BLOODMONEY: narrative default they/them; he/him allowed only when dialogue or close character voice has a specific reason
|
|
||||||
- Cricket: she/her
|
|
||||||
- Saint: he/him
|
|
||||||
- Miette: she/her
|
|
||||||
|
|
||||||
If retry notes, previous reviewer output, or generated scene text conflict with `story/characters.md`, obey `story/characters.md`. Do not infer pronouns from a previous failure note. Before failing a pronoun issue, verify the character's `Pronouns / Reference` section.
|
|
||||||
|
|
||||||
Output exactly:
|
|
||||||
|
|
||||||
status: pass | fail | retry | escalate
|
|
||||||
reason: <short explanation>
|
|
||||||
next_stage: <optional stage id>
|
|
||||||
context_update: <compact useful note>
|
|
||||||
|
|
||||||
When `status: pass`, leave `next_stage` blank. Use `retry` when the scene can be repaired by drafting again. For retryable scene issues, leave `next_stage` blank; NightShift will route back to the configured drafting stage.
|
|
||||||
|
|
@ -1,25 +1,22 @@
|
||||||
You are the drafting agent for a NightShift novel-writing workflow.
|
You are the scene writer for a NightShift fiction workflow.
|
||||||
|
|
||||||
Draft only the current scene or section requested by the task.
|
Write the scene requested by the current task.
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
- Write prose only under `story/chapters/`.
|
- Write prose only under `story/chapters/`.
|
||||||
- Do not edit `story/worldbuilding.md`, `story/characters.md`, `story/style-guide.md`, `story/plot-state.md`, `story/timeline.md`, `story/unresolved-threads.md`, `story/continuity-rules.md`, or `story/outline.md`.
|
|
||||||
- Use `story/style-guide.md` for POV, tense, tone, and prose rules.
|
- Use `story/style-guide.md` for POV, tense, tone, and prose rules.
|
||||||
- Use `story/plot-state.md` and `story/timeline.md` as current state.
|
- Use `story/characters.md`, especially `Pronouns / Reference`, as canon.
|
||||||
- Use the `Pronouns / Reference` sections in `story/characters.md` as hard canon.
|
- Use `story/plot-state.md`, `story/timeline.md`, and `story/unresolved-threads.md` as current state.
|
||||||
- Do not infer, vary, or "smooth out" character pronouns. Use canonical narrative reference exactly.
|
|
||||||
- Keep the scene bounded to the task acceptance criteria.
|
- Keep the scene bounded to the task acceptance criteria.
|
||||||
- Do not resolve future plot threads unless the task explicitly asks for that.
|
- Do not update state files, character files, worldbuilding, outline, continuity rules, or style guide.
|
||||||
- Do not include author notes, TODOs, bracket placeholders, or analysis in the scene file.
|
- Do not include author notes, TODOs, bracket placeholders, or analysis in the scene file.
|
||||||
|
|
||||||
Output only one complete file block using this delimiter format:
|
Output only one complete file block:
|
||||||
|
|
||||||
FILE: <the exact story/chapters path listed under Writes in the current task>
|
FILE: <the exact story/chapters path listed under Writes in the current task>
|
||||||
---CONTENT---
|
---CONTENT---
|
||||||
<complete scene prose>
|
<complete scene prose>
|
||||||
---END---
|
---END---
|
||||||
|
|
||||||
Do not use markdown code fences for scene prose output.
|
Do not use markdown code fences. Do not output any text outside the file block.
|
||||||
Do not output a plan, notes, analysis, or any text outside the delimiter block.
|
|
||||||
|
|
||||||
If the task does not specify a scene path, choose the next obvious path under `story/chapters/` and keep it stable.
|
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
You are the scene editor for a NightShift novel-writing workflow.
|
|
||||||
|
|
||||||
Edit an already drafted scene after a continuity or style review failure.
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
- Preserve the existing scene's structure, voice, events, pacing, and best lines.
|
|
||||||
- Make the smallest changes needed to satisfy the review failure and task acceptance criteria.
|
|
||||||
- Do not restart, summarize, replace the scene premise, or change scene direction.
|
|
||||||
- Use `story/style-guide.md` for POV, tense, tone, and prose rules.
|
|
||||||
- Use `story/characters.md`, especially `Pronouns / Reference`, as hard canon.
|
|
||||||
- Wrong pronouns are mandatory fixes.
|
|
||||||
- If retry notes or reviewer feedback conflict with `story/characters.md`, obey `story/characters.md`.
|
|
||||||
- Never change correct canonical pronouns because a review note claims a different canon.
|
|
||||||
- Do not edit state files, worldbuilding, outline, continuity rules, or style guide.
|
|
||||||
- Do not resolve future plot threads unless the task explicitly asks for that.
|
|
||||||
- Do not include author notes, TODOs, bracket placeholders, or analysis in the scene file.
|
|
||||||
|
|
||||||
Use the `current_scene_file` context as the source text to edit.
|
|
||||||
Use the retry notes and latest review output to identify the required repair.
|
|
||||||
|
|
||||||
Output only one complete file block using this delimiter format:
|
|
||||||
FILE: <the exact story/chapters path listed under Writes in the current task>
|
|
||||||
---CONTENT---
|
|
||||||
<complete edited scene prose>
|
|
||||||
---END---
|
|
||||||
|
|
||||||
Do not use markdown code fences for scene prose output.
|
|
||||||
Do not output a plan, notes, analysis, or any text outside the delimiter block.
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
You are the planning agent for a NightShift novel-writing workflow.
|
|
||||||
|
|
||||||
Create a concise scene plan for the current task.
|
|
||||||
|
|
||||||
Use the story files as source of truth:
|
|
||||||
- `story/worldbuilding.md`
|
|
||||||
- `story/characters.md`
|
|
||||||
- `story/style-guide.md`
|
|
||||||
- `story/plot-state.md`
|
|
||||||
- `story/timeline.md`
|
|
||||||
- `story/unresolved-threads.md`
|
|
||||||
- `story/continuity-rules.md`
|
|
||||||
- `story/outline.md`
|
|
||||||
|
|
||||||
Plan in this order:
|
|
||||||
1. scene purpose
|
|
||||||
2. POV and tone constraints
|
|
||||||
3. relevant characters, locations, timeline facts, and unresolved threads
|
|
||||||
4. concrete beats for this scene
|
|
||||||
5. state files likely to need updates after drafting
|
|
||||||
|
|
||||||
Do not write prose. Do not invent large new plot arcs unless the task asks for them.
|
|
||||||
If repository context is needed, request it with `lookup_requests`.
|
|
||||||
|
|
@ -1,46 +1,37 @@
|
||||||
You are the state updater for a NightShift novel-writing workflow.
|
You are the state updater for a NightShift fiction workflow.
|
||||||
|
|
||||||
Update durable story state after an accepted scene.
|
Read the accepted scene and update durable story state conservatively.
|
||||||
|
|
||||||
You may edit only:
|
You may edit only:
|
||||||
- `story/plot-state.md`
|
- `story/plot-state.md`
|
||||||
- `story/characters.md`
|
|
||||||
- `story/timeline.md`
|
- `story/timeline.md`
|
||||||
- `story/unresolved-threads.md`
|
- `story/unresolved-threads.md`
|
||||||
|
|
||||||
Do not edit scene prose. Do not edit worldbuilding, style guide, continuity rules, or outline unless a later template explicitly allows it.
|
Do not edit:
|
||||||
|
- scene prose
|
||||||
|
- `story/characters.md`
|
||||||
|
- `story/worldbuilding.md`
|
||||||
|
- `story/style-guide.md`
|
||||||
|
- `story/continuity-rules.md`
|
||||||
|
- `story/outline.md`
|
||||||
|
|
||||||
State updates should reflect only what happened in the accepted scene:
|
State updates should be extractive. Reflect only facts/events/thread changes that are actually present in the accepted scene.
|
||||||
- current character locations
|
|
||||||
- what each important character knows
|
|
||||||
- relationship changes
|
|
||||||
- injuries, resources, items, and commitments
|
|
||||||
- timeline movement
|
|
||||||
- unresolved questions opened or resolved
|
|
||||||
- promises made to the reader
|
|
||||||
|
|
||||||
Do not invent events that are not in the scene.
|
Prefer additive updates:
|
||||||
|
- record completed scene events
|
||||||
|
- add timeline bullets
|
||||||
|
- add or update unresolved threads
|
||||||
|
- update current story moment or character locations only when clearly changed
|
||||||
|
- keep all existing sections and bullets unless they directly contradict the accepted scene
|
||||||
|
|
||||||
Preserve existing durable state. Make minimal additive edits:
|
Do not invent events, compress existing state, rewrite for style, or alter canon.
|
||||||
- append new scene facts, timeline bullets, character knowledge, and unresolved threads
|
If a full-file update would require removing or reorganizing existing material, do less. Add a small `Recently Changed` or scene-specific bullet instead.
|
||||||
- update current locations/status only where the accepted scene changes them
|
|
||||||
- do not remove or compress existing character profiles, faction notes, world notes, or open threads
|
|
||||||
- do not rewrite whole files for style, brevity, or cleanup
|
|
||||||
- if a section already contains useful detail, keep it and add only the new facts needed
|
|
||||||
|
|
||||||
Protect character canon:
|
Output complete file blocks only:
|
||||||
- Never change any `Pronouns / Reference` section.
|
|
||||||
- Never change a character's canonical pronouns, narrative reference, identity, or core wound.
|
|
||||||
- Prefer updating `story/plot-state.md`, `story/timeline.md`, and `story/unresolved-threads.md`.
|
|
||||||
- Edit `story/characters.md` only when the accepted scene adds a small current-status fact or introduces a new named character.
|
|
||||||
- If editing `story/characters.md`, preserve all existing sections and add only the minimal new status/detail needed.
|
|
||||||
|
|
||||||
Output only complete file content blocks.
|
|
||||||
Use this delimiter format for each state file you update:
|
|
||||||
|
|
||||||
FILE: story/plot-state.md
|
FILE: story/plot-state.md
|
||||||
---CONTENT---
|
---CONTENT---
|
||||||
<complete updated state file>
|
<complete updated file>
|
||||||
---END---
|
---END---
|
||||||
|
|
||||||
Do not use markdown code fences. Do not include prose outside FILE blocks.
|
Do not use markdown code fences. Do not output any text outside file blocks.
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
You are the style reviewer for a NightShift novel-writing workflow.
|
|
||||||
|
|
||||||
Review the drafted scene against:
|
|
||||||
- the current task
|
|
||||||
- `story/style-guide.md`
|
|
||||||
- the scene plan
|
|
||||||
- the applied scene file
|
|
||||||
|
|
||||||
Check for:
|
|
||||||
- POV discipline
|
|
||||||
- tense consistency
|
|
||||||
- tone match
|
|
||||||
- pacing
|
|
||||||
- excessive exposition
|
|
||||||
- dialogue that violates established voice
|
|
||||||
- placeholders such as TODO, TBD, `[insert]`, or author notes
|
|
||||||
- scene length far outside the requested range
|
|
||||||
|
|
||||||
Do not fail the scene because durable state files are not updated yet. State files are updated by a later `update_state` stage after review.
|
|
||||||
|
|
||||||
Output exactly:
|
|
||||||
|
|
||||||
status: pass | fail | retry | escalate
|
|
||||||
reason: <short explanation>
|
|
||||||
next_stage: <optional stage id>
|
|
||||||
context_update: <compact useful note>
|
|
||||||
|
|
||||||
When `status: pass`, leave `next_stage` blank. Use `retry` when the drafter should revise the scene. For retryable scene issues, leave `next_stage` blank; NightShift will route back to the configured drafting stage.
|
|
||||||
|
|
@ -9,6 +9,8 @@ safety:
|
||||||
scoped_paths:
|
scoped_paths:
|
||||||
- story
|
- story
|
||||||
- README.md
|
- README.md
|
||||||
|
- STORY_FILES.md
|
||||||
|
- pyproject.toml
|
||||||
allowed_commands:
|
allowed_commands:
|
||||||
- python -m pytest -q
|
- python -m pytest -q
|
||||||
forbidden_commands:
|
forbidden_commands:
|
||||||
|
|
@ -17,18 +19,10 @@ safety:
|
||||||
- curl | bash
|
- curl | bash
|
||||||
|
|
||||||
experiment:
|
experiment:
|
||||||
label: tutorial-novel
|
label: tutorial-novel-simple
|
||||||
prompt_variant: scene-state-workflow-v1
|
prompt_variant: scene-state-simple-v1
|
||||||
|
|
||||||
agents:
|
agents:
|
||||||
planner:
|
|
||||||
backend: ollama
|
|
||||||
model: nightshift-base
|
|
||||||
temperature: 0.4
|
|
||||||
num_ctx: 8192
|
|
||||||
num_predict: 4096
|
|
||||||
system_prompt: .nightshift/agents/planner.md
|
|
||||||
|
|
||||||
drafter:
|
drafter:
|
||||||
backend: ollama
|
backend: ollama
|
||||||
model: nightshift-writer
|
model: nightshift-writer
|
||||||
|
|
@ -37,30 +31,6 @@ agents:
|
||||||
num_predict: 8192
|
num_predict: 8192
|
||||||
system_prompt: .nightshift/agents/drafter.md
|
system_prompt: .nightshift/agents/drafter.md
|
||||||
|
|
||||||
editor:
|
|
||||||
backend: ollama
|
|
||||||
model: nightshift-writer
|
|
||||||
temperature: 0.3
|
|
||||||
num_ctx: 16384
|
|
||||||
num_predict: 8192
|
|
||||||
system_prompt: .nightshift/agents/editor.md
|
|
||||||
|
|
||||||
continuity_reviewer:
|
|
||||||
backend: ollama
|
|
||||||
model: nightshift-base
|
|
||||||
temperature: 0.2
|
|
||||||
num_ctx: 8192
|
|
||||||
num_predict: 4096
|
|
||||||
system_prompt: .nightshift/agents/continuity-reviewer.md
|
|
||||||
|
|
||||||
style_reviewer:
|
|
||||||
backend: ollama
|
|
||||||
model: nightshift-base
|
|
||||||
temperature: 0.3
|
|
||||||
num_ctx: 8192
|
|
||||||
num_predict: 4096
|
|
||||||
system_prompt: .nightshift/agents/style-reviewer.md
|
|
||||||
|
|
||||||
state_updater:
|
state_updater:
|
||||||
backend: ollama
|
backend: ollama
|
||||||
model: nightshift-writer
|
model: nightshift-writer
|
||||||
|
|
@ -74,16 +44,7 @@ pipeline:
|
||||||
stop_on_repeated_failure_signature_after: 3
|
stop_on_repeated_failure_signature_after: 3
|
||||||
continue_on_task_failure: false
|
continue_on_task_failure: false
|
||||||
stages:
|
stages:
|
||||||
- id: plan
|
- id: build_context
|
||||||
type: agent
|
|
||||||
agent: planner
|
|
||||||
output: scene-plan.md
|
|
||||||
|
|
||||||
- id: semantic_context
|
|
||||||
type: semantic_context
|
|
||||||
output: semantic-context.md
|
|
||||||
|
|
||||||
- id: context
|
|
||||||
type: repo_context
|
type: repo_context
|
||||||
output: context-pack.md
|
output: context-pack.md
|
||||||
|
|
||||||
|
|
@ -93,101 +54,56 @@ pipeline:
|
||||||
output: scene-draft.patch
|
output: scene-draft.patch
|
||||||
allowed_paths:
|
allowed_paths:
|
||||||
- story/chapters
|
- story/chapters
|
||||||
|
on_fail: draft_scene
|
||||||
|
|
||||||
- id: normalize_draft
|
- id: normalize_patch
|
||||||
type: patch_normalizer
|
type: patch_normalizer
|
||||||
output: normalized-draft.patch
|
output: normalized-scene.patch
|
||||||
|
|
||||||
- id: validate_draft
|
- id: hard_validate_patch
|
||||||
type: patch_validator
|
type: patch_validator
|
||||||
output: draft-validation.md
|
output: scene-validation.md
|
||||||
max_files: 2
|
max_files: 1
|
||||||
max_lines: 4000
|
max_lines: 4000
|
||||||
max_delete_ratio: 0.50
|
max_delete_ratio: 0.50
|
||||||
allowed_paths:
|
allowed_paths:
|
||||||
- story/chapters
|
- story/chapters
|
||||||
on_fail: draft_scene
|
on_fail: draft_scene
|
||||||
|
|
||||||
- id: apply_draft
|
- id: apply_scene
|
||||||
type: patch_apply
|
type: patch_apply
|
||||||
mode: apply
|
mode: apply
|
||||||
output: draft-apply-output.txt
|
output: scene-apply-output.txt
|
||||||
on_fail: draft_scene
|
on_fail: draft_scene
|
||||||
|
|
||||||
- id: continuity_review
|
|
||||||
type: agent_review
|
|
||||||
agent: continuity_reviewer
|
|
||||||
output: continuity-review.md
|
|
||||||
on_fail: edit_scene
|
|
||||||
|
|
||||||
- id: style_review
|
|
||||||
type: agent_review
|
|
||||||
agent: style_reviewer
|
|
||||||
output: style-review.md
|
|
||||||
on_fail: edit_scene
|
|
||||||
on_pass: update_state
|
|
||||||
|
|
||||||
- id: edit_scene
|
|
||||||
type: file_writer
|
|
||||||
agent: editor
|
|
||||||
output: scene-edit.patch
|
|
||||||
allowed_paths:
|
|
||||||
- story/chapters
|
|
||||||
|
|
||||||
- id: normalize_edit
|
|
||||||
type: patch_normalizer
|
|
||||||
output: normalized-edit.patch
|
|
||||||
|
|
||||||
- id: validate_edit
|
|
||||||
type: patch_validator
|
|
||||||
output: edit-validation.md
|
|
||||||
max_files: 2
|
|
||||||
max_lines: 1200
|
|
||||||
max_delete_ratio: 0.50
|
|
||||||
allowed_paths:
|
|
||||||
- story/chapters
|
|
||||||
on_fail: edit_scene
|
|
||||||
|
|
||||||
- id: apply_edit
|
|
||||||
type: patch_apply
|
|
||||||
mode: apply
|
|
||||||
output: edit-apply-output.txt
|
|
||||||
on_fail: edit_scene
|
|
||||||
on_pass: continuity_review
|
|
||||||
|
|
||||||
- id: update_state
|
- id: update_state
|
||||||
type: file_writer
|
type: file_writer
|
||||||
agent: state_updater
|
agent: state_updater
|
||||||
output: state-update.patch
|
output: state-update.patch
|
||||||
allowed_paths:
|
allowed_paths:
|
||||||
- story/plot-state.md
|
- story/plot-state.md
|
||||||
- story/characters.md
|
|
||||||
- story/timeline.md
|
- story/timeline.md
|
||||||
- story/unresolved-threads.md
|
- story/unresolved-threads.md
|
||||||
|
on_fail: summarize_warnings
|
||||||
|
|
||||||
- id: normalize_state
|
- id: hard_validate_state
|
||||||
type: patch_normalizer
|
|
||||||
output: normalized-state.patch
|
|
||||||
|
|
||||||
- id: validate_state
|
|
||||||
type: patch_validator
|
type: patch_validator
|
||||||
output: state-validation.md
|
output: state-validation.md
|
||||||
max_files: 5
|
max_files: 3
|
||||||
max_lines: 1200
|
max_lines: 1200
|
||||||
max_delete_ratio: 0.35
|
max_delete_ratio: 0.35
|
||||||
allowed_paths:
|
allowed_paths:
|
||||||
- story/plot-state.md
|
- story/plot-state.md
|
||||||
- story/characters.md
|
|
||||||
- story/timeline.md
|
- story/timeline.md
|
||||||
- story/unresolved-threads.md
|
- story/unresolved-threads.md
|
||||||
on_fail: update_state
|
on_fail: summarize_warnings
|
||||||
|
|
||||||
- id: apply_state
|
- id: apply_state
|
||||||
type: patch_apply
|
type: patch_apply
|
||||||
mode: apply
|
mode: apply
|
||||||
output: state-apply-output.txt
|
output: state-apply-output.txt
|
||||||
on_fail: update_state
|
on_fail: summarize_warnings
|
||||||
|
|
||||||
- id: summarize
|
- id: summarize_warnings
|
||||||
type: summarize
|
type: summarize
|
||||||
output: final-notes.md
|
output: final-notes.md
|
||||||
|
|
|
||||||
|
|
@ -7,3 +7,4 @@ name = "nightshift-novel-target"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = []
|
dependencies = []
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ from .patches import FileUpdate
|
||||||
|
|
||||||
|
|
||||||
def validate_writing_file_updates(updates: tuple[FileUpdate, ...], project_root: Path) -> None:
|
def validate_writing_file_updates(updates: tuple[FileUpdate, ...], project_root: Path) -> None:
|
||||||
"""Validate writing-specific invariants for novel scene/state file updates."""
|
"""Validate hard writing-specific invariants for novel scene/state updates."""
|
||||||
|
|
||||||
root = Path(project_root)
|
root = Path(project_root)
|
||||||
characters_path = root / "story" / "characters.md"
|
characters_path = root / "story" / "characters.md"
|
||||||
|
|
@ -27,8 +27,24 @@ def validate_writing_file_updates(updates: tuple[FileUpdate, ...], project_root:
|
||||||
normalized_path = update.path.replace("\\", "/").strip().strip("/")
|
normalized_path = update.path.replace("\\", "/").strip().strip("/")
|
||||||
if normalized_path == "story/characters.md":
|
if normalized_path == "story/characters.md":
|
||||||
_validate_protected_character_canon(normalized_path, character_sections, update.content)
|
_validate_protected_character_canon(normalized_path, character_sections, update.content)
|
||||||
|
|
||||||
|
|
||||||
|
def collect_writing_warnings(updates: tuple[FileUpdate, ...], project_root: Path) -> tuple[str, ...]:
|
||||||
|
"""Collect soft writing concerns without blocking artifact creation."""
|
||||||
|
|
||||||
|
root = Path(project_root)
|
||||||
|
characters_path = root / "story" / "characters.md"
|
||||||
|
character_sections = (
|
||||||
|
_pronoun_reference_sections(characters_path.read_text(encoding="utf-8", errors="replace"))
|
||||||
|
if characters_path.is_file()
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
warnings: list[str] = []
|
||||||
|
for update in updates:
|
||||||
|
normalized_path = update.path.replace("\\", "/").strip().strip("/")
|
||||||
if normalized_path.startswith("story/chapters/") and normalized_path.endswith(".md"):
|
if normalized_path.startswith("story/chapters/") and normalized_path.endswith(".md"):
|
||||||
_validate_scene_pronoun_canon(normalized_path, update.content, character_sections)
|
warnings.extend(_scene_pronoun_canon_warnings(normalized_path, update.content, character_sections))
|
||||||
|
return tuple(warnings)
|
||||||
|
|
||||||
|
|
||||||
def _validate_protected_character_canon(
|
def _validate_protected_character_canon(
|
||||||
|
|
@ -52,18 +68,19 @@ def _validate_protected_character_canon(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _validate_scene_pronoun_canon(
|
def _scene_pronoun_canon_warnings(
|
||||||
path_text: str,
|
path_text: str,
|
||||||
scene_text: str,
|
scene_text: str,
|
||||||
sections: dict[str, str],
|
sections: dict[str, str],
|
||||||
) -> None:
|
) -> tuple[str, ...]:
|
||||||
if not sections:
|
if not sections:
|
||||||
return
|
return ()
|
||||||
rules = _pronoun_rules_from_sections(sections)
|
rules = _pronoun_rules_from_sections(sections)
|
||||||
if not rules:
|
if not rules:
|
||||||
return
|
return ()
|
||||||
aliases = {alias: character for character in rules for alias in _character_aliases(character)}
|
aliases = {alias: character for character in rules for alias in _character_aliases(character)}
|
||||||
active_character: str | None = None
|
active_character: str | None = None
|
||||||
|
warnings: list[str] = []
|
||||||
for sentence in _scene_sentences(scene_text):
|
for sentence in _scene_sentences(scene_text):
|
||||||
present = {
|
present = {
|
||||||
character
|
character
|
||||||
|
|
@ -78,7 +95,7 @@ def _validate_scene_pronoun_canon(
|
||||||
continue
|
continue
|
||||||
forbidden = rules[character]
|
forbidden = rules[character]
|
||||||
if present:
|
if present:
|
||||||
bad = _first_forbidden_pronoun(sentence, forbidden)
|
bad = _first_forbidden_pronoun_after_alias(sentence, character, forbidden)
|
||||||
active_character = character
|
active_character = character
|
||||||
else:
|
else:
|
||||||
bad = _leading_forbidden_pronoun(sentence, forbidden)
|
bad = _leading_forbidden_pronoun(sentence, forbidden)
|
||||||
|
|
@ -88,10 +105,12 @@ def _validate_scene_pronoun_canon(
|
||||||
excerpt = sentence.strip()
|
excerpt = sentence.strip()
|
||||||
if len(excerpt) > 160:
|
if len(excerpt) > 160:
|
||||||
excerpt = excerpt[:157].rstrip() + "..."
|
excerpt = excerpt[:157].rstrip() + "..."
|
||||||
raise PipelineError(
|
warnings.append(
|
||||||
"File writer error: scene pronoun canon violation for "
|
"Scene pronoun canon warning in "
|
||||||
f"{character}: found `{bad}` near character reference. Excerpt: {excerpt}"
|
f"`{path_text}` for {character}: found `{bad}` near character reference. "
|
||||||
|
f"Excerpt: {excerpt}"
|
||||||
)
|
)
|
||||||
|
return tuple(warnings)
|
||||||
|
|
||||||
|
|
||||||
def _first_forbidden_pronoun(sentence: str, forbidden: tuple[str, ...]) -> str | None:
|
def _first_forbidden_pronoun(sentence: str, forbidden: tuple[str, ...]) -> str | None:
|
||||||
|
|
@ -105,6 +124,26 @@ def _first_forbidden_pronoun(sentence: str, forbidden: tuple[str, ...]) -> str |
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _first_forbidden_pronoun_after_alias(
|
||||||
|
sentence: str,
|
||||||
|
character: str,
|
||||||
|
forbidden: tuple[str, ...],
|
||||||
|
) -> str | None:
|
||||||
|
alias_match = _first_alias_match(sentence, character)
|
||||||
|
if alias_match is None:
|
||||||
|
return _first_forbidden_pronoun(sentence, forbidden)
|
||||||
|
return _first_forbidden_pronoun(sentence[alias_match.end() :], forbidden)
|
||||||
|
|
||||||
|
|
||||||
|
def _first_alias_match(sentence: str, character: str) -> re.Match[str] | None:
|
||||||
|
matches = [
|
||||||
|
match
|
||||||
|
for alias in _character_aliases(character)
|
||||||
|
for match in re.finditer(rf"\b{re.escape(alias)}\b", sentence)
|
||||||
|
]
|
||||||
|
return min(matches, key=lambda match: match.start()) if matches else None
|
||||||
|
|
||||||
|
|
||||||
def _leading_forbidden_pronoun(sentence: str, forbidden: tuple[str, ...]) -> str | None:
|
def _leading_forbidden_pronoun(sentence: str, forbidden: tuple[str, ...]) -> str | None:
|
||||||
stripped = sentence.strip()
|
stripped = sentence.strip()
|
||||||
return next(
|
return next(
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,8 @@ class InitProjectTests(unittest.TestCase):
|
||||||
self.assertTrue((root / "story" / "chapters" / ".gitkeep").exists())
|
self.assertTrue((root / "story" / "chapters" / ".gitkeep").exists())
|
||||||
self.assertIn("type: file_writer", config)
|
self.assertIn("type: file_writer", config)
|
||||||
self.assertIn("model: nightshift-writer", config)
|
self.assertIn("model: nightshift-writer", config)
|
||||||
self.assertIn("model: nightshift-base", config)
|
self.assertIn("summarize_warnings", config)
|
||||||
|
self.assertNotIn("model: nightshift-base", config)
|
||||||
self.assertIn("story/chapters", config)
|
self.assertIn("story/chapters", config)
|
||||||
self.assertIn("story/worldbuilding.md", gitignore)
|
self.assertIn("story/worldbuilding.md", gitignore)
|
||||||
self.assertIn("story/chapters/**/*.md", gitignore)
|
self.assertIn("story/chapters/**/*.md", gitignore)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import unittest
|
||||||
|
|
||||||
from nightshift.errors import PipelineError
|
from nightshift.errors import PipelineError
|
||||||
from nightshift.patches import FileUpdate
|
from nightshift.patches import FileUpdate
|
||||||
from nightshift.writing_validators import validate_writing_file_updates
|
from nightshift.writing_validators import collect_writing_warnings, validate_writing_file_updates
|
||||||
|
|
||||||
|
|
||||||
class WritingValidatorTests(unittest.TestCase):
|
class WritingValidatorTests(unittest.TestCase):
|
||||||
|
|
@ -44,7 +44,7 @@ Scavenger.
|
||||||
with self.assertRaisesRegex(PipelineError, "protected character pronoun canon changed"):
|
with self.assertRaisesRegex(PipelineError, "protected character pronoun canon changed"):
|
||||||
validate_writing_file_updates(updates, root)
|
validate_writing_file_updates(updates, root)
|
||||||
|
|
||||||
def test_rejects_scene_pronoun_drift(self) -> None:
|
def test_reports_scene_pronoun_drift_as_warning(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
root = Path(directory)
|
root = Path(directory)
|
||||||
(root / "story" / "chapters").mkdir(parents=True)
|
(root / "story" / "chapters").mkdir(parents=True)
|
||||||
|
|
@ -66,8 +66,12 @@ Scavenger.
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
with self.assertRaisesRegex(PipelineError, "scene pronoun canon violation for Proxy"):
|
|
||||||
validate_writing_file_updates(updates, root)
|
validate_writing_file_updates(updates, root)
|
||||||
|
warnings = collect_writing_warnings(updates, root)
|
||||||
|
|
||||||
|
self.assertEqual(len(warnings), 1)
|
||||||
|
self.assertIn("Proxy", warnings[0])
|
||||||
|
self.assertIn("found `he`", warnings[0])
|
||||||
|
|
||||||
def test_allows_scene_pronouns_when_multiple_characters_make_ambiguous_sentence(self) -> None:
|
def test_allows_scene_pronouns_when_multiple_characters_make_ambiguous_sentence(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
|
@ -99,6 +103,40 @@ Scavenger.
|
||||||
|
|
||||||
validate_writing_file_updates(updates, root)
|
validate_writing_file_updates(updates, root)
|
||||||
|
|
||||||
|
def test_allows_pronoun_before_other_character_reference(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
(root / "story" / "chapters" / "chapter-001").mkdir(parents=True)
|
||||||
|
(root / "story" / "characters.md").write_text(
|
||||||
|
"""# Characters
|
||||||
|
|
||||||
|
## Proxy
|
||||||
|
|
||||||
|
### Pronouns / Reference
|
||||||
|
- Pronouns: she/her
|
||||||
|
- Narrative reference: Proxy; she/her
|
||||||
|
|
||||||
|
## DJ BLOODMONEY
|
||||||
|
|
||||||
|
### Pronouns / Reference
|
||||||
|
- Pronouns: they/them or he/him
|
||||||
|
- Narrative default: BLOODMONEY; they/them
|
||||||
|
""",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
updates = (
|
||||||
|
FileUpdate(
|
||||||
|
path="story/chapters/chapter-001/scene-001.md",
|
||||||
|
content=(
|
||||||
|
"BLOODMONEY stood behind the turntables. "
|
||||||
|
"He adjusted the EQ with one hand, let his hair fall into his eyes, "
|
||||||
|
"and glanced over at Proxy without breaking the groove.\n"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
validate_writing_file_updates(updates, root)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user