openwriter 0.35.1 → 0.36.0
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/client/assets/{index-Be_l2OOL.css → index-B5p6e-z0.css} +1 -1
- package/dist/client/assets/{index-BPDt3Psd.js → index-BMhKsQ_t.js} +53 -53
- package/dist/client/index.html +2 -2
- package/dist/plugins/authors-voice/skill/LICENSE +21 -0
- package/dist/plugins/authors-voice/skill/README.md +126 -0
- package/dist/plugins/authors-voice/skill/SKILL.md +151 -0
- package/dist/plugins/authors-voice/skill/catalog/ai-tells.md +144 -0
- package/dist/plugins/authors-voice/skill/catalog/anchor-prompt.md +189 -0
- package/dist/plugins/authors-voice/skill/catalog/author-hints.md +119 -0
- package/dist/plugins/authors-voice/skill/catalog/fingerprints.md +175 -0
- package/dist/plugins/authors-voice/skill/catalog/hurdle.md +76 -0
- package/dist/plugins/authors-voice/skill/catalog/post-write-audit.md +105 -0
- package/dist/plugins/authors-voice/skill/docs/analysis.md +31 -0
- package/dist/plugins/authors-voice/skill/docs/anchor-iteration.md +176 -0
- package/dist/plugins/authors-voice/skill/docs/api/import.md +78 -0
- package/dist/plugins/authors-voice/skill/docs/api/protocol.md +140 -0
- package/dist/plugins/authors-voice/skill/docs/api/setup.md +37 -0
- package/dist/plugins/authors-voice/skill/docs/api/tools.md +102 -0
- package/dist/plugins/authors-voice/skill/docs/api/troubleshooting.md +7 -0
- package/dist/plugins/authors-voice/skill/docs/apply-protocol-deep.md +191 -0
- package/dist/plugins/authors-voice/skill/docs/context-hygiene.md +33 -0
- package/dist/plugins/authors-voice/skill/docs/setup.md +74 -0
- package/dist/plugins/authors-voice/skill/docs/tiers.md +13 -0
- package/dist/plugins/authors-voice/skill/package.json +35 -0
- package/dist/plugins/authors-voice/skill/prompts/skeleton.md +29 -0
- package/dist/plugins/authors-voice/skill/voice/README.md +51 -0
- package/dist/plugins/authors-voice/skill/voice/corpus/.gitkeep +0 -0
- package/dist/server/documents.js +7 -10
- package/dist/server/state.js +27 -7
- package/dist/server/title-resolve.js +87 -0
- package/dist/server/workspaces.js +10 -4
- package/package.json +1 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# API Reference
|
|
2
|
+
|
|
3
|
+
## REST Endpoints (recommended)
|
|
4
|
+
|
|
5
|
+
All endpoints require `Authorization: Bearer $AV_API_KEY`. Base URL: `https://api.authors-voice.com`
|
|
6
|
+
|
|
7
|
+
### Core Skill Endpoints
|
|
8
|
+
|
|
9
|
+
**Get voice profile** — full linguistic fingerprint (6 categories + sentence distribution):
|
|
10
|
+
```bash
|
|
11
|
+
curl -s https://api.authors-voice.com/api/voice/profiles/default \
|
|
12
|
+
-H "Authorization: Bearer $AV_API_KEY"
|
|
13
|
+
# Optional: ?format=detailed for profile ID + summary
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
**Apply voice** — rewrite content in the author's voice:
|
|
17
|
+
```bash
|
|
18
|
+
curl -s -X POST https://api.authors-voice.com/api/voice/apply \
|
|
19
|
+
-H "Authorization: Bearer $AV_API_KEY" \
|
|
20
|
+
-H "Content-Type: application/json" \
|
|
21
|
+
-d '{"content": "text to rewrite", "mode": "rewrite", "inputType": "ai", "category": "x"}'
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Additional REST Endpoints
|
|
25
|
+
|
|
26
|
+
| Endpoint | Method | Purpose |
|
|
27
|
+
|----------|--------|---------|
|
|
28
|
+
| `/api/voice/profiles` | GET | List profiles with counts |
|
|
29
|
+
| `/api/voice/profiles/:profileId` | GET | Full voice profile (use `default` for default) |
|
|
30
|
+
| `/api/voice/apply` | POST | Rewrite content in author's voice |
|
|
31
|
+
| `/api/voice/setup` | POST | Analyze samples and build voice profile |
|
|
32
|
+
| `/api/voice/content` | GET | List writing samples (query: profileId, category) |
|
|
33
|
+
| `/api/voice/content` | POST | Upload content chunks |
|
|
34
|
+
| `/api/voice/content/bulk` | POST | Bulk upload documents |
|
|
35
|
+
| `/api/voice/content/:docId` | PATCH | Update doc metadata |
|
|
36
|
+
| `/api/voice/content/:docId` | DELETE | Delete document |
|
|
37
|
+
| `/api/voice/anchor/derive` | POST | Derive a voice anchor from pasted prose |
|
|
38
|
+
| `/api/voice/anchor` | GET/PUT/DELETE | Read / persist / clear the voice anchor |
|
|
39
|
+
| `/api/voice/usage` | GET | Usage stats |
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## MCP Protocol (alternative)
|
|
44
|
+
|
|
45
|
+
For MCP-compatible clients, all tools are also available via JSON-RPC POST:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
curl -s -X POST "https://api.authors-voice.com/api/voice/mcp" \
|
|
49
|
+
-H "Authorization: Bearer $AV_API_KEY" \
|
|
50
|
+
-H "Content-Type: application/json" \
|
|
51
|
+
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"TOOL_NAME","arguments":{...}}}'
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Available Tools (12)
|
|
57
|
+
|
|
58
|
+
### Content Import (3 tools)
|
|
59
|
+
|
|
60
|
+
| Tool | Description |
|
|
61
|
+
|------|-------------|
|
|
62
|
+
| `import_from_url` | Import from any public URL (Medium, Substack, WordPress, .md/.txt). Args: `url`, `categories[]`, `authenticity`. |
|
|
63
|
+
| `bulk_import` | Import multiple docs (max 50). Each item: `{url}` or `{content, docId}` + `categories[]`. Global `authenticity`. |
|
|
64
|
+
| `upload_content` | Upload raw markdown. **One-click ergonomics**: `profileId` is optional — omit it and the server auto-creates/resolves a default profile. `content` can be raw text (server chunks on blank lines) instead of pre-split `chunks[]`, so the minimal payload is `{docId, content}`. Re-uploading the same `docId` replaces (idempotent). This is the contract the OpenWriter filetree right-click ingestion posts against. |
|
|
65
|
+
|
|
66
|
+
### Voice Profiles (3 tools)
|
|
67
|
+
|
|
68
|
+
| Tool | Description |
|
|
69
|
+
|------|-------------|
|
|
70
|
+
| `list_profiles` | List voice profiles, categories, and document counts. Each profile also carries `hasAnchor` (bool) and `anchorAuthors` (the reference authors behind the anchor), so callers can tell which profiles carry an anchor without a per-profile GET. No args. |
|
|
71
|
+
| `get_voice_profile` | Get full voice guidelines — 6 linguistic categories + sentence stats. Use before writing to understand the author's patterns. Optional `profileId`. |
|
|
72
|
+
| `setup_voice` | Analyze samples → create/update voice profile. Args: `profileName`, optional `forceReanalyze`. Call after importing content. |
|
|
73
|
+
|
|
74
|
+
### Voice Anchor (1 tool)
|
|
75
|
+
|
|
76
|
+
| Tool | Description |
|
|
77
|
+
|------|-------------|
|
|
78
|
+
| `set_voice_anchor` | Derive and persist a voice anchor from pasted reference prose. The anchor is V1's lead voice signal — injected ahead of sample retrieval in `rewrite`/`generate` and the OpenWriter editor path. REST equivalents: `POST /api/voice/anchor/derive`, `GET/PUT/DELETE /api/voice/anchor`. |
|
|
79
|
+
|
|
80
|
+
### Voice Application (2 tools)
|
|
81
|
+
|
|
82
|
+
| Tool | Description |
|
|
83
|
+
|------|-------------|
|
|
84
|
+
| `rewrite` | Rewrite existing content in the author's voice. Modes: `rewrite`, `shrink`, `expand`, `custom`. Supports `contextBefore`/`contextAfter`, `inputType`, `category`. |
|
|
85
|
+
| `generate` | Generate NEW content in author's voice. Args: `instruction`, optional `query` (topic for retrieval), `contextBefore`/`contextAfter`, `category`, `targetWords`. |
|
|
86
|
+
|
|
87
|
+
**rewrite parameters**: `content`, `mode`, `contextBefore`, `contextAfter`, `category`, `inputType` (human/ai/ai-assisted), `targetWords` (max 2000), `format` (markdown/plaintext).
|
|
88
|
+
|
|
89
|
+
**inputType** controls how aggressively the rewrite treats the input:
|
|
90
|
+
- `human` — Author's own writing. Preserve word choices and quirks, only polish flow/grammar.
|
|
91
|
+
- `ai` — Generic AI content. Discard phrasing entirely, rewrite from scratch using voice samples.
|
|
92
|
+
- `ai-assisted` (default) — Mixed authorship. Preserve passages matching the author's voice, rewrite generic/formulaic parts.
|
|
93
|
+
|
|
94
|
+
**generate note**: `query` is optional — describes what content is ABOUT for better voice retrieval. When omitted, context + instruction are used for retrieval.
|
|
95
|
+
|
|
96
|
+
### Content Management (3 tools)
|
|
97
|
+
|
|
98
|
+
| Tool | Description |
|
|
99
|
+
|------|-------------|
|
|
100
|
+
| `list_content` | List writing samples with chunk counts. Optional `category` filter. |
|
|
101
|
+
| `update_content` | Retag a doc. Args: `docId`, `categories[]`, `authenticity`. |
|
|
102
|
+
| `delete_content` | Permanently delete a doc and its chunks. Args: `docId`. |
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
**"Unauthorized"** — Check `AV_API_KEY` is set and starts with `av_live_`.
|
|
4
|
+
|
|
5
|
+
**"profileId query parameter is required"** — Some REST endpoints need profileId. For MCP tools, omit profileId to use the default profile.
|
|
6
|
+
|
|
7
|
+
**Empty document list** — Connection is active but no documents match. Try without a query filter.
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Apply Protocol — Deep Reference
|
|
2
|
+
|
|
3
|
+
Loaded when scoping a brief (Apply step 4) or running cross-section coherence review (Apply step 8). Not in context for routing or other turns.
|
|
4
|
+
|
|
5
|
+
## Step 4 — Writing the TASK brief
|
|
6
|
+
|
|
7
|
+
Promoted to `SKILL.md` Apply Protocol step 4. The load-bearing rules (commitments-only default, never meta-references, COMMITMENTS as quasi-verbatim, read-prior-integrated-sections, preservation scope, cadence prescription) live there. This doc holds the edge-case templates and minion taxonomy below.
|
|
8
|
+
|
|
9
|
+
## Writing minion taxonomy
|
|
10
|
+
|
|
11
|
+
| Minion | Scope | Input | Output | When fires |
|
|
12
|
+
|---|---|---|---|---|
|
|
13
|
+
| **Apply** | Generative writing | Commitments + voice + context SUMMARIES (no source prose) | Fresh prose | Initial drafts. Also small reframes when audit prescribes a structural fix (audit's prescription becomes the commitment). |
|
|
14
|
+
| **Rewrite** | Generative writing against updated commitments WITH context awareness | Updated commitments + voice + context layers (summary of preceding + key-term glossary + adjacent seam paragraphs as orientation-only) + cadence prescription. NO source prose for content being written. | Fresh prose | Beat Map commitments changed; surrounding prose is acceptable and should be preserved. Apply brief + context awareness layers. |
|
|
15
|
+
|
|
16
|
+
Plus **Blinder Audit** (Step 8b — paragraph-level substance duplication, critic only) and **Anchor Iteration** (Step 10 — final polish, channels voice anchors as panel, iterates to 90/100; see `anchor-iteration.md`).
|
|
17
|
+
|
|
18
|
+
### Editor's classification job after Audit fires
|
|
19
|
+
|
|
20
|
+
Each finding routes to one of three paths:
|
|
21
|
+
- **Editor direct (no minion)** — cuts, swaps, reorders, sentence-level surgical patches. Per FIRM RULE 1 carve-out.
|
|
22
|
+
- **Rewrite Minion** — paragraph's SUBSTANCE needs to change (structurally redundant with another, or mission needs to shift). Compression alone produces thin output.
|
|
23
|
+
- **Apply Minion (small)** — audit prescribes a specific reframe of a small span and new prose is needed (e.g., "reframe opening sentence to acknowledge pivot from X to Y"). Audit's prescription becomes the commitment.
|
|
24
|
+
|
|
25
|
+
Classification rule: **before flagging anything for a minion, ask "is this a SHAPE problem or a SUBSTANCE problem?"** Different work needed → Rewrite or Cut. New prose into a small span → small Apply. Surgical word/sentence work on existing prose → editor direct.
|
|
26
|
+
|
|
27
|
+
## Rewrite Minion
|
|
28
|
+
|
|
29
|
+
The Rewrite Minion IS the Apply Minion fired with context awareness. The brief shape preserves Apply's no-source-prose-ceiling property (minion brings its own moves) while adding context layers needed to flow into surrounding prose.
|
|
30
|
+
|
|
31
|
+
### Validated brief shape
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
[Voice profile: anchor, NEVER rules, fingerprints, stats, coined terms, examples]
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
TASK:
|
|
39
|
+
|
|
40
|
+
PROJECT: [1-paragraph context — what the doc is, what the section does]
|
|
41
|
+
|
|
42
|
+
CONTEXT — what's already established in the surrounding prose:
|
|
43
|
+
- [Summary of preceding section as bullets — what was named, what was claimed, what threads are live. NOT raw prose. 5-10 bullets.]
|
|
44
|
+
|
|
45
|
+
KEY TERMS already named (available for callback if useful):
|
|
46
|
+
[Glossary list — coined terms, named concepts, distinctive phrasings the prose has established]
|
|
47
|
+
|
|
48
|
+
IMMEDIATELY PRECEDING PARAGRAPH (for seam continuity ONLY; do NOT mirror its cadence):
|
|
49
|
+
[Full paragraph verbatim — 1 only]
|
|
50
|
+
|
|
51
|
+
IMMEDIATELY FOLLOWING PARAGRAPH (for seam continuity ONLY; do NOT mirror its cadence):
|
|
52
|
+
[Full paragraph verbatim — 1 only]
|
|
53
|
+
|
|
54
|
+
COMMITMENT(S) — what your new content must land in the reader:
|
|
55
|
+
[Outcome statement(s) per beat]
|
|
56
|
+
|
|
57
|
+
CADENCE PRESCRIPTION (per paragraph):
|
|
58
|
+
[Explicit rhythm direction — open with X, build with Y, close with Z. Mandatory.]
|
|
59
|
+
|
|
60
|
+
LENGTH: [target word count]
|
|
61
|
+
|
|
62
|
+
Return prose only. No commentary. No headers. No beat labels.
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The two seam paragraphs are full prose (minion needs flow continuity at the join), flagged explicitly as orientation-only. This avoids the source-prose ceiling because the task is "write fresh content that flows from / into these" rather than "match this cadence." **Framing matters more than presence.**
|
|
66
|
+
|
|
67
|
+
### Why these elements
|
|
68
|
+
|
|
69
|
+
- FULL adjacent seam paragraphs flagged "orientation only — do NOT mirror cadence" — flow continuity without ceiling
|
|
70
|
+
- SUMMARIES of preceding section (bullets, not raw prose) + key-term glossary — document-scale awareness
|
|
71
|
+
- OMITTING source prose for content being written — preserves no-ceiling property
|
|
72
|
+
- INCLUDING explicit cadence prescription per paragraph — mandatory; short calls need cadence MORE, not less
|
|
73
|
+
|
|
74
|
+
### Scope
|
|
75
|
+
|
|
76
|
+
Works at any scope: paragraph-level (1 new paragraph), section-level (3-5 paragraphs), beat-level (full beat regeneration). Pick by what changed in commitments — one beat's outcome → regenerate one paragraph; chapter-arc beat's whole outcome shape → regenerate the whole beat; multiple beats → batch per chapter-arc beat.
|
|
77
|
+
|
|
78
|
+
### Preserving specific lines from existing prose
|
|
79
|
+
|
|
80
|
+
List them as MUST-APPEAR-VERBATIM in commitments (Apply's "selective lifts" mode). Rare — usually the writer's training data brings stronger lines than the existing prose anyway.
|
|
81
|
+
|
|
82
|
+
### Audit follow-up MANDATORY
|
|
83
|
+
|
|
84
|
+
Rewrite outputs are subject to the same minion-blinder problem as first-pass Apply. The minion only sees its slice — seam paragraphs + summary bullets, not every paragraph in adjacent sections. It can introduce repetitions with paragraphs it didn't see.
|
|
85
|
+
|
|
86
|
+
**After any Rewrite Minion call, fire the Audit Minion (Step 8b) on the integrated document.** No exceptions. Empirical case: first Rewrite test (B1 close + B2 opening) produced strong individual prose AND 8 audit findings — most notably a visual-roster repetition across 4 paragraphs the rewriter never saw.
|
|
87
|
+
|
|
88
|
+
### Don't conflate rewrites with violation patches
|
|
89
|
+
|
|
90
|
+
Violation patches (FIRM RULE 1 carve-out) are 1-3 sentence local fixes to NEVER violations or brief errors WITHIN otherwise-acceptable prose. Editor work, small scope, surgical. Rewrites here mean re-running the minion against UPDATED commitments — different scenario, different scope decision.
|
|
91
|
+
|
|
92
|
+
## Step 8 — Cross-section coherence review
|
|
93
|
+
|
|
94
|
+
After integrating multiple minion outputs, scan for what individual runs cannot see:
|
|
95
|
+
|
|
96
|
+
- **Cadence repetition** across sections (same opens, same closes, same paragraph counts)
|
|
97
|
+
- **Recurring metaphors / phrases** across sections
|
|
98
|
+
- **Structural sameness** (every section ends with 4-layer enumeration, every section opens with 3 shorts)
|
|
99
|
+
- **Coined term overuse** — coined terms get injected into every minion call as MUST-PRESERVE, producing document-scale repetition (e.g., "territory" every 3 paragraphs). Track heavy-use terms; for subsequent minions, omit heavy-use terms from coined-terms injection OR add "use sparingly" instruction
|
|
100
|
+
|
|
101
|
+
Fix options: re-spawn with varied prescription, surgical post-edit (combine/split/swap), vary commitments + coined-terms per section at brief-assembly time, or accept for low-stakes drafts. The editor owns document-scale coherence; minions are responsible only for section-scale quality.
|
|
102
|
+
|
|
103
|
+
## Step 8b — Blinder Audit Minion (mandatory post-integration)
|
|
104
|
+
|
|
105
|
+
### Purpose
|
|
106
|
+
|
|
107
|
+
Minions write with SURGICAL context — just their slice of the doc — to avoid source-prose ceiling and context pollution. The trade-off: minion A doesn't know what minion B wrote. They can independently produce paragraphs whose ENTIRE SUBSTANCE mirrors another paragraph's entire substance. The **blinder problem**.
|
|
108
|
+
|
|
109
|
+
The Blinder Audit Minion has ONE job: find pairs of paragraphs whose whole substance closely mirrors each other.
|
|
110
|
+
|
|
111
|
+
### Operational test
|
|
112
|
+
|
|
113
|
+
The audit must reduce to an OBJECTIVE pattern-match — not a subjective claim about reader experience. The working question:
|
|
114
|
+
|
|
115
|
+
**"Summarize each paragraph in one sentence. Are two paragraphs' summaries effectively the same?"**
|
|
116
|
+
|
|
117
|
+
Or sharper: **"Could I cut this paragraph entirely and lose only redundancy?"** Answerable by reading; neither requires reader-experience judgment.
|
|
118
|
+
|
|
119
|
+
Empirically validated: the visual-roster case (4 paragraphs each enumerating the same 5 species in different framings) was a real blinder hit. Every other category tested at sentence-level, transition-level, or image-anchor level was a false positive — either intentional craft (image-anchoring, scaffolding, callbacks) or AI baseline patterns the reader doesn't notice.
|
|
120
|
+
|
|
121
|
+
### When to fire (mandatory)
|
|
122
|
+
|
|
123
|
+
- After integrating ≥2 minion outputs into one document
|
|
124
|
+
- After any rewrite cycle touching multiple sections
|
|
125
|
+
- Before showing the integrated document to the user
|
|
126
|
+
|
|
127
|
+
Skip when: single-minion / single-section work; surgical violation patches only.
|
|
128
|
+
|
|
129
|
+
### What to scan for (ONE category)
|
|
130
|
+
|
|
131
|
+
**PARAGRAPH-LEVEL SUBSTANCE DUPLICATION** — two paragraphs whose entire substance closely mirrors each other.
|
|
132
|
+
|
|
133
|
+
Diagnostic for any candidate pair:
|
|
134
|
+
1. Summarize paragraph A in one sentence. ("This paragraph does X.")
|
|
135
|
+
2. Summarize paragraph B in one sentence. ("This paragraph does Y.")
|
|
136
|
+
3. If X and Y are effectively the same job done with different words — finding.
|
|
137
|
+
4. If X and Y are different work — even if paragraphs share images, lexical phrases, or thematic threads — NOT a finding.
|
|
138
|
+
|
|
139
|
+
Cut test: could you delete one paragraph and lose only redundancy (no unique substance, no unique image-anchor, no unique scaffolding move)? Yes → real blinder. Cutting would lose something distinct → NOT a blinder regardless of surface similarity.
|
|
140
|
+
|
|
141
|
+
### What NOT to scan for (explicit exclusions — these produce false positives)
|
|
142
|
+
|
|
143
|
+
The audit must NOT flag any of these, even when surface similarity exists:
|
|
144
|
+
|
|
145
|
+
- **Sentence-level overlap across paragraph boundaries** (P4's closing shares thematic thread with P5's opening). Bridge/transition work. Cutting disconnects.
|
|
146
|
+
- **Image-anchoring across paragraphs** (the same image used in 2-3 paragraphs to thread a concept). Craft. The savanna/lion appearing in opener + body + closing is intentional thread.
|
|
147
|
+
- **Callbacks** (an image returning later as frame device or recognition moment). Craft.
|
|
148
|
+
- **Scaffolding repeats** (a paragraph opening with brief re-statement of previous paragraph's premise to set up its own new move). Premise-restatement is structure-promise, not duplication.
|
|
149
|
+
- **Sentence-level lexical/thematic overlap of any kind** unless part of WHOLE-PARAGRAPH substance duplication. Sharing the word "engine" or "same biology" is sentence-level — not a finding.
|
|
150
|
+
- **Structural sameness** (parallel cadence, repeated openers, declarative thesis + builds + aphorism). Readers don't notice; AI baseline behavior addressed by /anti-ai.
|
|
151
|
+
- **Cadence repetition** (parallel rhythm runs in adjacent paragraphs). Same reason.
|
|
152
|
+
- **Coined-term recurrence**. Coined terms are SUPPOSED to recur as identity markers.
|
|
153
|
+
- **Single-paragraph internal repetition** (triple anaphora within ONE paragraph). Craft, and the audit scans across paragraphs not within.
|
|
154
|
+
|
|
155
|
+
### Expected hit rate
|
|
156
|
+
|
|
157
|
+
Paragraph-level substance duplication is rare in semi-competent writing. On well-written beats the audit should typically return **NO BLINDER ERRORS FOUND** — the correct output, not a list of stretched findings to justify the call.
|
|
158
|
+
|
|
159
|
+
The audit fires as backstop on every integration. It catches gross duplications (visual-roster case, two paragraphs accidentally doing the same teaching beat). Most of the time it correctly returns zero.
|
|
160
|
+
|
|
161
|
+
### Report format
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
Finding #N
|
|
165
|
+
- LOCATION: paragraph references (e.g., "B2 P4 + B2 P5")
|
|
166
|
+
- SUMMARY A: one sentence describing what paragraph A does
|
|
167
|
+
- SUMMARY B: one sentence describing what paragraph B does
|
|
168
|
+
- WHY THE SUMMARIES MATCH: one sentence showing the substance duplication
|
|
169
|
+
- CUT TEST: could one paragraph be deleted entirely with only redundancy lost? (If no, the finding is invalid; do not include.)
|
|
170
|
+
- SEVERITY: moderate / major (paragraph-level duplication does not produce "minor" findings)
|
|
171
|
+
- SUGGESTED FIX: cut paragraph A / cut paragraph B / merge to single paragraph
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
If nothing meets the bar, return exactly: **"NO BLINDER ERRORS FOUND"** — that string, nothing else. Do NOT pad with sentence-level observations, structural notes, or polishing suggestions.
|
|
175
|
+
|
|
176
|
+
### Editor's response
|
|
177
|
+
|
|
178
|
+
For each finding:
|
|
179
|
+
- Apply the cut test independently. Does deleting one paragraph lose only redundancy?
|
|
180
|
+
- Yes → **cut** (editor territory, FIRM RULE 1 carve-out). Delete weaker paragraph via write_to_pad. If both are strong but redundant, merge unique fragments via Rewrite Minion.
|
|
181
|
+
- No → audit got it wrong. Defer.
|
|
182
|
+
|
|
183
|
+
Audit minion does NOT patch. Only reports.
|
|
184
|
+
|
|
185
|
+
Classification failure modes:
|
|
186
|
+
1. Trusting a finding without applying the cut test. If both paragraphs carry distinct substance, the audit was matching surface features. Defer.
|
|
187
|
+
2. Routing a paragraph-level cut to a Rewrite Minion when it should just be a cut. If two paragraphs do the same work, deleting one is the cleanest move.
|
|
188
|
+
|
|
189
|
+
### Brief shape (template)
|
|
190
|
+
|
|
191
|
+
Index every paragraph in the doc (e.g., "B1 P1: ...", "B2 P7: ...") so the audit can reference cleanly. Pass the full indexed document + the ONE category + the explicit exclusions + the output format. No voice profile needed (this isn't writing prose). Use opus.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Context Hygiene
|
|
2
|
+
|
|
3
|
+
Reset context before applying voice to fresh writing — voice profiles fight against active conversation context and lose. Anchor blends, NEVER rules, and first-token cadence all get out-pulled by whatever prose dominates the live session.
|
|
4
|
+
|
|
5
|
+
## Two situations
|
|
6
|
+
|
|
7
|
+
| Situation | Practice |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| First piece of fresh writing in this session | **Reset.** Start a fresh session. Apply Protocol loads voice files cold. |
|
|
10
|
+
| Iteration on already-voice-applied writing (review → revise → review) | **Stay.** The context IS the voice you locked in. |
|
|
11
|
+
|
|
12
|
+
## When to surface the prompt
|
|
13
|
+
|
|
14
|
+
Surface only when ALL THREE hit:
|
|
15
|
+
|
|
16
|
+
1. Voice profile is set up at Tier 1+
|
|
17
|
+
2. Request is fresh writing, not iteration
|
|
18
|
+
3. Session has substantial prior context unrelated to the writing task
|
|
19
|
+
|
|
20
|
+
Skip for brand-new sessions or when prior context IS the writing-task setup.
|
|
21
|
+
|
|
22
|
+
## Prompt to surface
|
|
23
|
+
|
|
24
|
+
> Voice profile is set up at **Tier N**. Context here is polluted with **<one-line summary>**, which will pull output toward that register instead of the locked voice.
|
|
25
|
+
>
|
|
26
|
+
> For best output, start a fresh session and run:
|
|
27
|
+
>
|
|
28
|
+
> ```
|
|
29
|
+
> /writers-voice
|
|
30
|
+
> <then ask for your writing task>
|
|
31
|
+
> ```
|
|
32
|
+
>
|
|
33
|
+
> Or tell me **"write here anyway"** and I'll proceed with the active context.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Setup, Anchor Protocol, Multi-Register
|
|
2
|
+
|
|
3
|
+
Loaded on first run, when generating a new anchor, or when splitting a corpus by register. Not in context during normal writing sessions.
|
|
4
|
+
|
|
5
|
+
## Setup Flow
|
|
6
|
+
|
|
7
|
+
If `voice/anchor.md` doesn't exist or is empty, walk the user through setup:
|
|
8
|
+
|
|
9
|
+
1. **Get the anchor.** Derivation is **fully local** — your own agent analyzes the writing, nothing leaves the machine, no service cost. Run the **Anchor Protocol** below to generate `voice/anchor.md` directly from the corpus on disk. Launch it as a sub-agent (see "Launching the anchor as a sub-agent") so the full stylometry rubric never pollutes the main session. Works offline. If the user has no corpus yet, ask them to seed 2-5 paragraphs (step 2) first — there is no hosted alternative.
|
|
10
|
+
|
|
11
|
+
2. **Seed the corpus** — ask for 2-5 paragraphs, write each to `voice/corpus/sample-NNN.md` with `added: YYYY-MM-DD` frontmatter.
|
|
12
|
+
3. **Run Analysis Protocol** (see `docs/analysis.md`) — populates `stats.md`, `never-rules.md`, `fingerprints.md`, `status.md`.
|
|
13
|
+
4. **Optional: curate examples** — ask the user for 3-5 most-representative paragraphs, write to `voice/examples.md`.
|
|
14
|
+
5. **Optional: populate coined terms** — ask the user for any coined terms / proper-noun concepts they want preserved verbatim, write to `voice/coined-terms.md` as a bare bullet list.
|
|
15
|
+
6. **Report status** — read `voice/status.md` and tell the user their tier + what unlocks next.
|
|
16
|
+
|
|
17
|
+
## Anchor Protocol (fully in-agent)
|
|
18
|
+
|
|
19
|
+
Generates `voice/anchor.md` (lean) and `voice/anchor-analysis.md` (rich).
|
|
20
|
+
|
|
21
|
+
1. Confirm corpus has ≥300 words. Below 300, ask the user to add a few more samples before anchoring.
|
|
22
|
+
2. Read `voice/stats.md`. If missing, run **Analysis Protocol** first.
|
|
23
|
+
3. Read `catalog/anchor-prompt.md` (full stylometry rubric) and `catalog/author-hints.md` (curated training-data authors with prose features).
|
|
24
|
+
4. **Set aside conversational context.** Score the corpus on prose mechanics only — never on themes/topics.
|
|
25
|
+
5. **Per-sample register analysis.** For each sample, record word count, address mode, register, signature moves. Flag samples >25% volume. Cluster by register; if 2+ distinct registers appear, flag as multi-register corpus.
|
|
26
|
+
6. **Score with register-aware feature validation.** Apply the 8 dimensions from `catalog/anchor-prompt.md`. Match against author hints. Assign weights summing to 100. For each cited feature, verify ≥40% sample appearance OR ≥40% volume (if neither, drop the feature; if it was the strongest evidence, drop the author).
|
|
27
|
+
7. **Self-criticism pass.** Strip any thematic reasoning. Set `confidence` and `any_thematic_reasoning` flags.
|
|
28
|
+
8. **Write `voice/anchor.md`** — JUST the lean `- N% Author` lines. No headers, no sub-bullets.
|
|
29
|
+
9. **Write `voice/anchor-analysis.md`** — per-author features, per-sample table, register diversity, self-check, refresh notes. Human-facing only.
|
|
30
|
+
10. **If multi-register corpus detected**, recommend a Multi-Register Split (see below).
|
|
31
|
+
11. Report blend + confidence + caveats to user.
|
|
32
|
+
|
|
33
|
+
## Launching the anchor as a sub-agent
|
|
34
|
+
|
|
35
|
+
The Anchor Protocol loads a large stylometry rubric (`catalog/anchor-prompt.md`),
|
|
36
|
+
the author-hints catalog, and the full corpus. Running it inline floods the main
|
|
37
|
+
session with analysis context the user never needs to see. **Launch it as a
|
|
38
|
+
sub-agent instead** — the sub-agent does the heavy reading and writes the files;
|
|
39
|
+
the main session gets back only a short summary.
|
|
40
|
+
|
|
41
|
+
Use the Agent/Task tool (general-purpose) with a self-contained prompt. The
|
|
42
|
+
sub-agent has no memory of this conversation, so the prompt must name every file
|
|
43
|
+
by absolute path:
|
|
44
|
+
|
|
45
|
+
> Generate a writer's-voice anchor, entirely locally. Do NOT call any network
|
|
46
|
+
> service or API — analyze with your own reasoning only.
|
|
47
|
+
> 1. Read the stylometry rubric at `<skill>/catalog/anchor-prompt.md` and the
|
|
48
|
+
> author hints at `<skill>/catalog/author-hints.md`.
|
|
49
|
+
> 2. Read every sample in `<skill>/voice/corpus/` (strip YAML frontmatter; keep
|
|
50
|
+
> samples separate) and the deterministic stats at `<skill>/voice/stats.md`
|
|
51
|
+
> (run the Analysis Protocol first if it's missing).
|
|
52
|
+
> 3. Follow the rubric exactly: per-sample register analysis → register-aware
|
|
53
|
+
> feature validation → score 8 dimensions → self-criticism pass.
|
|
54
|
+
> 4. Write `<skill>/voice/anchor.md` (lean blend lines only) and
|
|
55
|
+
> `<skill>/voice/anchor-analysis.md` (rich, human-facing).
|
|
56
|
+
> 5. Return ONLY: the blend lines, confidence, and any multi-register warning.
|
|
57
|
+
|
|
58
|
+
Replace `<skill>` with the skill's absolute path. After it returns, read
|
|
59
|
+
`voice/anchor.md`, report the blend + confidence to the user, and offer a
|
|
60
|
+
multi-register split if the sub-agent flagged one.
|
|
61
|
+
|
|
62
|
+
## Multi-Register Anchors
|
|
63
|
+
|
|
64
|
+
If the corpus spans multiple registers (e.g., third-person expository AND direct-you instructional), maintain a separate anchor per register: `voice/anchor-<context>.md` (e.g., `anchor-book.md`, `anchor-essay.md`, `anchor-tweets.md`). Same lean format. Each gets a paired `voice/anchor-<context>-analysis.md`.
|
|
65
|
+
|
|
66
|
+
**Multi-Register Split procedure:**
|
|
67
|
+
|
|
68
|
+
1. Identify registers from the per-sample analysis.
|
|
69
|
+
2. For each register, ask the user for a slug + one-line description.
|
|
70
|
+
3. Filter corpus to samples in that register.
|
|
71
|
+
4. Run the matcher on the subset (same `catalog/anchor-prompt.md` rubric, same variance checks).
|
|
72
|
+
5. Write `voice/anchor-<slug>.md` (lean) + `voice/anchor-<slug>-analysis.md` (rich).
|
|
73
|
+
|
|
74
|
+
**Apply-time anchor selection:** at write time, if multiple anchor files exist, pick by user's request context (explicit naming wins; project the user is working on wins next; ask if ambiguous; fallback to `voice/anchor.md`).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Tier Reference
|
|
2
|
+
|
|
3
|
+
Determined by total word count in `voice/corpus/`. Each tier unlocks additional voice-profile features. Computed during Analysis Protocol.
|
|
4
|
+
|
|
5
|
+
| Tier | Words | Name | Unlocked | Locked |
|
|
6
|
+
| --- | --- | --- | --- | --- |
|
|
7
|
+
| 0 | <300 | Empty | (none) | anchor blend, basic stats, NEVER rules, fingerprints |
|
|
8
|
+
| 1 | 300-999 | Anchor | anchor blend, basic stats | preliminary NEVER rules, fingerprints |
|
|
9
|
+
| 2 | 1000-4999 | Preliminary | anchor blend, basic stats, preliminary NEVER rules, top fingerprints | full NEVER coverage, all fingerprints |
|
|
10
|
+
| 3 | 5000-19999 | Full Coverage | anchor blend, stats, full NEVER rules, full fingerprints | high-confidence em-dash hurdle |
|
|
11
|
+
| 4 | ≥20000 | AV-Grade | anchor blend, stats, full NEVER rules, full fingerprints, em-dash hurdle cleared | (none) |
|
|
12
|
+
|
|
13
|
+
The tier is reported to the user after every Analysis Protocol run, with a "what unlocks next" pointer.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "authors-voice",
|
|
3
|
+
"version": "0.19.1",
|
|
4
|
+
"description": "Author's Voice — constructed-voice skill for AI agents. Anchors writing to a training-data author blend, progressively layers NEVER rules, presentation fingerprints, sentence stats, coined terms, and curated examples from a growing local corpus. Local-first markdown skill; optional paid API for plugin/programmatic flows. Replaces writers-voice + the legacy voice-* skill family.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "travsteward",
|
|
8
|
+
"homepage": "https://openwriter.io/authors-voice",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/travsteward/authors-voice"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"claude",
|
|
15
|
+
"claude-code",
|
|
16
|
+
"skill",
|
|
17
|
+
"voice",
|
|
18
|
+
"writing",
|
|
19
|
+
"ai",
|
|
20
|
+
"openwriter",
|
|
21
|
+
"authors-voice",
|
|
22
|
+
"writers-voice",
|
|
23
|
+
"anti-ai"
|
|
24
|
+
],
|
|
25
|
+
"files": [
|
|
26
|
+
"SKILL.md",
|
|
27
|
+
"catalog/",
|
|
28
|
+
"docs/",
|
|
29
|
+
"prompts/",
|
|
30
|
+
"voice/README.md",
|
|
31
|
+
"voice/corpus/.gitkeep",
|
|
32
|
+
"LICENSE",
|
|
33
|
+
"README.md"
|
|
34
|
+
]
|
|
35
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
You write at this exact training-data blend:
|
|
2
|
+
|
|
3
|
+
{INCLUDE: voice/anchor.md}
|
|
4
|
+
|
|
5
|
+
Maintain these proportions across the output. The blend IS the voice. Do not soften toward generic literary register. Do not default to your own RLHF-trained voice.
|
|
6
|
+
|
|
7
|
+
STYLE REPAIRS — apply these as hard constraints:
|
|
8
|
+
|
|
9
|
+
{INCLUDE: voice/never-rules.md}
|
|
10
|
+
|
|
11
|
+
{INCLUDE: voice/fingerprints.md}
|
|
12
|
+
|
|
13
|
+
{INCLUDE: voice/stats.md}
|
|
14
|
+
|
|
15
|
+
CONTENT — preserve these coined terms verbatim:
|
|
16
|
+
|
|
17
|
+
{INCLUDE: voice/coined-terms.md}
|
|
18
|
+
|
|
19
|
+
REFERENCE EXAMPLES OF AUTHOR'S WRITING (use if helpful — content and style):
|
|
20
|
+
|
|
21
|
+
{INCLUDE: voice/examples.md}
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
TASK:
|
|
26
|
+
|
|
27
|
+
{TASK}
|
|
28
|
+
|
|
29
|
+
Return prose only. No commentary. No diff. No explanation. No headers. No markdown wrapping.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Voice Profile
|
|
2
|
+
|
|
3
|
+
This directory is your **voice profile**. Files in here are read by the agent at write time and applied as style constraints. The skill builds these up progressively over time.
|
|
4
|
+
|
|
5
|
+
## What goes here
|
|
6
|
+
|
|
7
|
+
| File | Purpose | Source |
|
|
8
|
+
|------|---------|--------|
|
|
9
|
+
| `anchor.md` | Anchor blend (3-5 training-data authors with weights) | Pasted from openwriter.io/writers-voice OR generated in-agent by the skill |
|
|
10
|
+
| `anchor-<context>.md` | OPTIONAL per-register anchors (e.g., `anchor-book.md`, `anchor-tweets.md`) — when your corpus spans multiple registers | Generated in-agent via the Multi-Register Split procedure |
|
|
11
|
+
| `stats.md` | Sentence distribution + punctuation density | Agent best-effort from corpus |
|
|
12
|
+
| `never-rules.md` | NEVER rules (kill-list) | Agent + manual additions |
|
|
13
|
+
| `fingerprints.md` | Exact presentation choices (Oxford comma, etc.) | Agent + manual overrides |
|
|
14
|
+
| `examples.md` | Curated reference paragraphs | Manually picked by the user |
|
|
15
|
+
| `status.md` | Current tier + what's locked | Agent-generated |
|
|
16
|
+
| `corpus/` | Raw samples accumulating over time | Manually added (drop files here) |
|
|
17
|
+
|
|
18
|
+
## Multi-Register Anchors
|
|
19
|
+
|
|
20
|
+
If your corpus contains writing in multiple registers — third-person expository AND second-person instructional, or analytical essays AND aphoristic tweets — a single blended anchor will pull toward whichever register has the most volume in your corpus, leaving the others under-weighted.
|
|
21
|
+
|
|
22
|
+
The skill detects this automatically during the Anchor Protocol and offers a multi-register split: one anchor file per register (`anchor-book.md`, `anchor-tweets.md`, etc.). At write-time, the agent picks the right anchor based on what you're writing.
|
|
23
|
+
|
|
24
|
+
NEVER rules and fingerprints stay corpus-wide — only the AUTHOR BLEND varies per register.
|
|
25
|
+
|
|
26
|
+
To generate per-register anchors, ask: *"Split my anchor by register."* See the **Multi-Register Anchors** section in `SKILL.md` for the full procedure.
|
|
27
|
+
|
|
28
|
+
## How it grows
|
|
29
|
+
|
|
30
|
+
The more samples you accumulate in `corpus/`, the richer the analysis. Tiers:
|
|
31
|
+
|
|
32
|
+
- **300-1k words**: anchor blend + basic stats
|
|
33
|
+
- **1k-5k words**: + preliminary NEVER rules + top fingerprints
|
|
34
|
+
- **5k-20k words**: + full NEVER coverage + all fingerprints
|
|
35
|
+
- **20k+ words**: AV-grade (high-confidence profile)
|
|
36
|
+
|
|
37
|
+
## Re-running analysis
|
|
38
|
+
|
|
39
|
+
After adding samples to `corpus/`, tell the agent:
|
|
40
|
+
|
|
41
|
+
> "Re-analyze my voice profile."
|
|
42
|
+
|
|
43
|
+
The agent reads `catalog/*.md` and the corpus, then regenerates `stats.md`, `never-rules.md`, `fingerprints.md`, and `status.md`. No Node script — pure markdown skill, the agent is the extractor.
|
|
44
|
+
|
|
45
|
+
## Manual edits
|
|
46
|
+
|
|
47
|
+
`never-rules.md` and `fingerprints.md` have a `## Manual Additions` / `## Manual Overrides` section at the bottom. The agent preserves anything you put there across regenerations.
|
|
48
|
+
|
|
49
|
+
## Privacy
|
|
50
|
+
|
|
51
|
+
This is your voice profile. The files live on your disk. The skill never uploads anything — analysis is local (agent reasoning over local files). The only thing that leaves your machine is if you use the web tool at openwriter.io/writers-voice for the anchor step (300-800 words pasted in, used once, cached 24h, never trained on). If you use skill mode instead, even the anchor is generated locally.
|
|
File without changes
|
package/dist/server/documents.js
CHANGED
|
@@ -12,6 +12,7 @@ import { resolveTypeMeta } from './content-type-meta.js';
|
|
|
12
12
|
import { parseMarkdownContent } from './compact.js';
|
|
13
13
|
import { getDocument, getTitle, getFilePath, getIsTemp, getMetadata, save, cancelDebouncedSave, setActiveDocument, registerExternalDoc, unregisterExternalDoc, getExternalDocs, cacheActiveDocument, getCachedDocument, invalidateDocCache, removePendingCacheEntry, resetDocVersion, markAsAgentStub, unmarkAgentStub, isAgentStub, } from './state.js';
|
|
14
14
|
import { getDataDir, TEMP_PREFIX, ensureDataDir, filePathForTitle, tempFilePath, generateNodeId, resolveDocPath, isExternalDoc, atomicWriteFileSync, canonicalizePath } from './helpers.js';
|
|
15
|
+
import { resolveListingTitle, getWorkspaceTitleMap } from './title-resolve.js';
|
|
15
16
|
import { ensureDocId } from './versions.js';
|
|
16
17
|
import { renameDocInAllWorkspaces, removeDocFromAllWorkspaces, listWorkspaces, getWorkspace } from './workspaces.js';
|
|
17
18
|
import { collectAllFiles } from './workspace-tree.js';
|
|
@@ -95,6 +96,7 @@ function deriveContentType(data) {
|
|
|
95
96
|
export function listDocuments() {
|
|
96
97
|
ensureDataDir();
|
|
97
98
|
const currentPath = getFilePath();
|
|
99
|
+
const wsTitles = getWorkspaceTitleMap();
|
|
98
100
|
const files = readdirSync(getDataDir())
|
|
99
101
|
.filter((f) => f.endsWith('.md'))
|
|
100
102
|
.map((f) => {
|
|
@@ -104,7 +106,7 @@ export function listDocuments() {
|
|
|
104
106
|
const raw = readFileSync(fullPath, 'utf-8');
|
|
105
107
|
// Use gray-matter directly — skip full TipTap parse for listing
|
|
106
108
|
const { data, content } = matter(raw);
|
|
107
|
-
const title = data.title
|
|
109
|
+
const title = resolveListingTitle({ fmTitle: data.title, workspaceTitle: wsTitles.get(f), content, filename: f });
|
|
108
110
|
// Skip archived docs
|
|
109
111
|
if (data.archivedAt)
|
|
110
112
|
return null;
|
|
@@ -160,13 +162,7 @@ export function listDocuments() {
|
|
|
160
162
|
const stat = statSync(extPath);
|
|
161
163
|
const raw = readFileSync(extPath, 'utf-8');
|
|
162
164
|
const { data, content } = matter(raw);
|
|
163
|
-
|
|
164
|
-
// Title fallback: use filename stem for external files without a title
|
|
165
|
-
if (title === 'Untitled') {
|
|
166
|
-
const stem = extPath.split(/[/\\]/).pop()?.replace(/\.md$/i, '');
|
|
167
|
-
if (stem)
|
|
168
|
-
title = stem;
|
|
169
|
-
}
|
|
165
|
+
const title = resolveListingTitle({ fmTitle: data.title, workspaceTitle: wsTitles.get(extPath), content, filename: extPath });
|
|
170
166
|
const trimmed = content.trim();
|
|
171
167
|
const wordCount = trimmed ? trimmed.split(/\s+/).length : 0;
|
|
172
168
|
files.push({
|
|
@@ -232,7 +228,7 @@ export function listArchivedDocuments() {
|
|
|
232
228
|
const { data, content } = matter(raw);
|
|
233
229
|
if (!data.archivedAt)
|
|
234
230
|
return null;
|
|
235
|
-
const title = data.title
|
|
231
|
+
const title = resolveListingTitle({ fmTitle: data.title, content, filename: f });
|
|
236
232
|
const trimmed = content.trim();
|
|
237
233
|
const wordCount = trimmed ? trimmed.split(/\s+/).length : 0;
|
|
238
234
|
return {
|
|
@@ -672,9 +668,10 @@ export function searchDocuments(query, includeArchived = false) {
|
|
|
672
668
|
catch { /* skip */ }
|
|
673
669
|
}
|
|
674
670
|
const results = [];
|
|
671
|
+
const wsTitles = getWorkspaceTitleMap();
|
|
675
672
|
for (const file of allFiles) {
|
|
676
673
|
const { data, content } = matter(file.raw);
|
|
677
|
-
const title = data.title
|
|
674
|
+
const title = resolveListingTitle({ fmTitle: data.title, workspaceTitle: wsTitles.get(file.filename), content, filename: file.filename });
|
|
678
675
|
const trimmed = content.trim();
|
|
679
676
|
const isArchived = !!data.archivedAt;
|
|
680
677
|
// Skip archived unless requested
|