@hanna84/mcp-writing 3.3.1 → 3.4.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/CHANGELOG.md +20 -0
- package/README.md +7 -0
- package/package.json +4 -2
- package/src/index.js +81 -7
- package/src/setup/contracts/styleguide_setup_v1.json +146 -0
- package/src/setup/setup-contract-response-schema.js +59 -0
- package/src/setup/setup-contract.js +315 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,11 +4,31 @@ All notable changes to this project will be documented in this file. Dates are d
|
|
|
4
4
|
|
|
5
5
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
6
6
|
|
|
7
|
+
#### [v3.4.0](https://github.com/hannasdev/mcp-writing.git
|
|
8
|
+
/compare/v3.3.2...v3.4.0)
|
|
9
|
+
|
|
10
|
+
- feat: add client-agnostic styleguide setup contract and VS Code handoff [`#176`](https://github.com/hannasdev/mcp-writing.git
|
|
11
|
+
/pull/176)
|
|
12
|
+
|
|
13
|
+
#### [v3.3.2](https://github.com/hannasdev/mcp-writing.git
|
|
14
|
+
/compare/v3.3.1...v3.3.2)
|
|
15
|
+
|
|
16
|
+
> 5 May 2026
|
|
17
|
+
|
|
18
|
+
- docs: split PRD overview and add client-agnostic setup PRD [`#175`](https://github.com/hannasdev/mcp-writing.git
|
|
19
|
+
/pull/175)
|
|
20
|
+
- Release 3.3.2 [`de30625`](https://github.com/hannasdev/mcp-writing.git
|
|
21
|
+
/commit/de30625c1ff4a25065f124bb26f95397c6db5493)
|
|
22
|
+
|
|
7
23
|
#### [v3.3.1](https://github.com/hannasdev/mcp-writing.git
|
|
8
24
|
/compare/v3.3.0...v3.3.1)
|
|
9
25
|
|
|
26
|
+
> 3 May 2026
|
|
27
|
+
|
|
10
28
|
- fix(review-comments): enforce command-specific flags and add failure-path tests [`#174`](https://github.com/hannasdev/mcp-writing.git
|
|
11
29
|
/pull/174)
|
|
30
|
+
- Release 3.3.1 [`736bc90`](https://github.com/hannasdev/mcp-writing.git
|
|
31
|
+
/commit/736bc90a224885b700b5de331a2f9e333a11f1c8)
|
|
12
32
|
|
|
13
33
|
#### [v3.3.0](https://github.com/hannasdev/mcp-writing.git
|
|
14
34
|
/compare/v3.2.1...v3.3.0)
|
package/README.md
CHANGED
|
@@ -16,6 +16,12 @@ WRITING_SYNC_DIR=/path/to/sync-dir DB_PATH=./writing.db npx -y @hanna84/mcp-writ
|
|
|
16
16
|
|
|
17
17
|
The CLI wrapper defaults to stdio transport and adds the Node 22 SQLite flag automatically when needed.
|
|
18
18
|
|
|
19
|
+
## VS Code extension
|
|
20
|
+
|
|
21
|
+
For VS Code-native setup flows (including prose styleguide setup), use:
|
|
22
|
+
|
|
23
|
+
- [hannasdev/mcp-writing-vscode](https://github.com/hannasdev/mcp-writing-vscode)
|
|
24
|
+
|
|
19
25
|
## What it does
|
|
20
26
|
|
|
21
27
|
Instead of feeding an entire manuscript to an AI and hoping it fits in the context window, `mcp-writing` builds a structured index from your scene files. The AI queries that index first — finding relevant characters, beats, and loglines — then loads only the specific prose it needs.
|
|
@@ -37,6 +43,7 @@ Instead of feeding an entire manuscript to an AI and hoping it fits in the conte
|
|
|
37
43
|
| Guide | Description |
|
|
38
44
|
|---|---|
|
|
39
45
|
| [docs/setup.md](docs/setup.md) | Prerequisites, first-time setup, Scrivener import, native sync format |
|
|
46
|
+
| [mcp-writing-vscode](https://github.com/hannasdev/mcp-writing-vscode) | VS Code extension for client-native setup flows |
|
|
40
47
|
| [docs/docker.md](docs/docker.md) | Docker Compose, OpenClaw integration, SSH hardening |
|
|
41
48
|
| [docs/data-ownership.md](docs/data-ownership.md) | Which tools write which files, import safety rules |
|
|
42
49
|
| [docs/tools.md](docs/tools.md) | Full tool reference — auto-generated from source |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanna84/mcp-writing",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"description": "MCP service for AI-assisted reasoning and editing on long-form fiction projects",
|
|
5
5
|
"homepage": "https://hannasdev.github.io/mcp-writing/",
|
|
6
6
|
"type": "module",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"./prose-styleguide.js": "./src/styleguide/prose-styleguide.js",
|
|
30
30
|
"./prose-styleguide-drift.js": "./src/styleguide/prose-styleguide-drift.js",
|
|
31
31
|
"./prose-styleguide-skill.js": "./src/styleguide/prose-styleguide-skill.js",
|
|
32
|
+
"./setup-contract.js": "./src/setup/setup-contract.js",
|
|
32
33
|
"./tools/*": "./src/tools/*",
|
|
33
34
|
"./scripts/*": "./src/scripts/*",
|
|
34
35
|
"./src/*": "./src/*",
|
|
@@ -45,6 +46,7 @@
|
|
|
45
46
|
"src/core/",
|
|
46
47
|
"src/review-bundles/",
|
|
47
48
|
"src/runtime/",
|
|
49
|
+
"src/setup/",
|
|
48
50
|
"src/scripts/",
|
|
49
51
|
"src/styleguide/",
|
|
50
52
|
"src/sync/",
|
|
@@ -68,7 +70,7 @@
|
|
|
68
70
|
"normalize:scene-characters": "node --experimental-sqlite src/scripts/normalize-scene-characters.mjs",
|
|
69
71
|
"setup:openclaw-env": "sh src/scripts/setup-openclaw-env.sh",
|
|
70
72
|
"release": "release-it",
|
|
71
|
-
"lint": "eslint *.js src/index.js src/core src/review-bundles src/runtime src/scripts src/styleguide src/sync src/tools src/workflows src/world",
|
|
73
|
+
"lint": "eslint *.js src/index.js src/core src/review-bundles src/runtime src/setup src/scripts src/styleguide src/sync src/tools src/workflows src/world",
|
|
72
74
|
"guard:legacy-root-imports": "node src/scripts/check-legacy-root-imports.mjs",
|
|
73
75
|
"docs": "node src/scripts/generate-tool-docs.mjs",
|
|
74
76
|
"lint:metadata": "node src/scripts/lint-metadata.mjs",
|
package/src/index.js
CHANGED
|
@@ -18,7 +18,18 @@ import {
|
|
|
18
18
|
readEntityMetadata,
|
|
19
19
|
resolveBatchTargetScenes,
|
|
20
20
|
} from "./core/helpers.js";
|
|
21
|
-
import { STYLEGUIDE_CONFIG_BASENAME } from "./styleguide/prose-styleguide.js";
|
|
21
|
+
import { STYLEGUIDE_CONFIG_BASENAME, resolveStyleguideConfig } from "./styleguide/prose-styleguide.js";
|
|
22
|
+
import {
|
|
23
|
+
PROSE_STYLEGUIDE_SKILL_BASENAME,
|
|
24
|
+
PROSE_STYLEGUIDE_SKILL_DIRNAME,
|
|
25
|
+
} from "./styleguide/prose-styleguide-skill.js";
|
|
26
|
+
import {
|
|
27
|
+
loadSetupContract,
|
|
28
|
+
deriveStyleguideSetupStatus,
|
|
29
|
+
resolveStyleguideSetupAnswers,
|
|
30
|
+
buildStyleguideSetupArtifactPlan,
|
|
31
|
+
} from "./setup/setup-contract.js";
|
|
32
|
+
import { validateSetupContractContext } from "./setup/setup-contract-response-schema.js";
|
|
22
33
|
import { registerSyncTools } from "./tools/sync.js";
|
|
23
34
|
import { registerSearchTools } from "./tools/search.js";
|
|
24
35
|
import { registerMetadataTools } from "./tools/metadata.js";
|
|
@@ -87,6 +98,7 @@ const MCP_SERVER_VERSION = typeof pkg.version === "string" && pkg.version.trim()
|
|
|
87
98
|
? pkg.version
|
|
88
99
|
: "0.0.0";
|
|
89
100
|
const asyncJobs = new Map();
|
|
101
|
+
const styleguideSetupContract = loadSetupContract({ rootDir: ROOT_DIR, contractId: "styleguide_setup_v1" });
|
|
90
102
|
|
|
91
103
|
function paginateRows(rows, { page, pageSize, forcePagination = false }) {
|
|
92
104
|
const totalCount = rows.length;
|
|
@@ -355,6 +367,70 @@ function createMcpServer() {
|
|
|
355
367
|
const syncRootExists = fs.existsSync(syncRootConfigPath);
|
|
356
368
|
const universeRootExists = universeRootConfigPath !== null && fs.existsSync(universeRootConfigPath);
|
|
357
369
|
const projectRootExists = projectRootConfigPath !== null && fs.existsSync(projectRootConfigPath);
|
|
370
|
+
const styleguideExists = {
|
|
371
|
+
sync_root: syncRootExists,
|
|
372
|
+
universe_root: universeRootExists,
|
|
373
|
+
project_root: projectRootExists,
|
|
374
|
+
};
|
|
375
|
+
const syncRootSkillPath = path.join(
|
|
376
|
+
SYNC_DIR,
|
|
377
|
+
PROSE_STYLEGUIDE_SKILL_DIRNAME,
|
|
378
|
+
PROSE_STYLEGUIDE_SKILL_BASENAME
|
|
379
|
+
);
|
|
380
|
+
const styleguideSkillExists = fs.existsSync(syncRootSkillPath);
|
|
381
|
+
|
|
382
|
+
const styleguideResolution = resolveStyleguideConfig({
|
|
383
|
+
syncDir: SYNC_DIR,
|
|
384
|
+
projectId: project_id ?? undefined,
|
|
385
|
+
});
|
|
386
|
+
const styleguideValid = styleguideResolution.ok;
|
|
387
|
+
const styleguideSetupStatus = deriveStyleguideSetupStatus({
|
|
388
|
+
styleguideExists,
|
|
389
|
+
styleguideValid,
|
|
390
|
+
styleguideSkillExists,
|
|
391
|
+
styleguideEnforcementMode: STYLEGUIDE_ENFORCEMENT_MODE,
|
|
392
|
+
});
|
|
393
|
+
const setupPlanPreview = (() => {
|
|
394
|
+
if (!styleguideSetupContract.ok) return null;
|
|
395
|
+
const resolved = resolveStyleguideSetupAnswers({
|
|
396
|
+
contract: styleguideSetupContract.contract,
|
|
397
|
+
answers: {},
|
|
398
|
+
inferred: { project_id },
|
|
399
|
+
});
|
|
400
|
+
if (!resolved.ok) return null;
|
|
401
|
+
const plan = buildStyleguideSetupArtifactPlan({
|
|
402
|
+
resolvedAnswers: resolved.resolved_answers,
|
|
403
|
+
sceneCount: scene_count,
|
|
404
|
+
setupStatus: styleguideSetupStatus.status,
|
|
405
|
+
});
|
|
406
|
+
return {
|
|
407
|
+
flow_id: "styleguide_setup_v1",
|
|
408
|
+
default_scope: resolved.resolved_answers.scope,
|
|
409
|
+
actions: plan.actions,
|
|
410
|
+
};
|
|
411
|
+
})();
|
|
412
|
+
|
|
413
|
+
const setupContractContext = styleguideSetupContract.ok
|
|
414
|
+
? {
|
|
415
|
+
contract_id: styleguideSetupContract.contract_id,
|
|
416
|
+
schema_version: styleguideSetupContract.contract.schema_version,
|
|
417
|
+
styleguide_setup_status: styleguideSetupStatus.status,
|
|
418
|
+
setup_recommended: styleguideSetupStatus.setup_recommended,
|
|
419
|
+
...(setupPlanPreview ? { plan_preview: setupPlanPreview } : {}),
|
|
420
|
+
}
|
|
421
|
+
: {
|
|
422
|
+
contract_id: "styleguide_setup_v1",
|
|
423
|
+
status: "unavailable",
|
|
424
|
+
error_code: styleguideSetupContract.error.code,
|
|
425
|
+
};
|
|
426
|
+
const setupContractContextCheck = validateSetupContractContext(setupContractContext);
|
|
427
|
+
if (!setupContractContextCheck.ok) {
|
|
428
|
+
return errorResponse(
|
|
429
|
+
setupContractContextCheck.error.code,
|
|
430
|
+
setupContractContextCheck.error.message,
|
|
431
|
+
setupContractContextCheck.error.details
|
|
432
|
+
);
|
|
433
|
+
}
|
|
358
434
|
|
|
359
435
|
return jsonResponse({
|
|
360
436
|
ok: true,
|
|
@@ -362,11 +438,8 @@ function createMcpServer() {
|
|
|
362
438
|
project_id,
|
|
363
439
|
scene_count,
|
|
364
440
|
sync_dir: SYNC_DIR_ABS,
|
|
365
|
-
styleguide_exists:
|
|
366
|
-
|
|
367
|
-
universe_root: universeRootExists,
|
|
368
|
-
project_root: projectRootExists,
|
|
369
|
-
},
|
|
441
|
+
styleguide_exists: styleguideExists,
|
|
442
|
+
setup_contract: setupContractContextCheck.value,
|
|
370
443
|
git_available: GIT_AVAILABLE,
|
|
371
444
|
pending_proposals: pendingProposals.size,
|
|
372
445
|
db_migration_warnings: DB_STARTUP_WARNINGS,
|
|
@@ -378,7 +451,8 @@ function createMcpServer() {
|
|
|
378
451
|
"If a tool returns a next_step field (in a success or error response), follow it before trying anything else.",
|
|
379
452
|
"Use find_scenes without filters to discover what project_ids are indexed.",
|
|
380
453
|
"When calling bootstrap_prose_styleguide_config or check_prose_styleguide_drift, set max_scenes to context.scene_count to avoid the default limit.",
|
|
381
|
-
"
|
|
454
|
+
"Use context.setup_contract.styleguide_setup_status to decide whether styleguide setup is missing/invalid and advisory/blocking.",
|
|
455
|
+
"Styleguide tools resolve config in priority order: project_root > universe_root > sync_root. If any styleguide_exists field is true, a config exists and styleguide tools will work. For invalid setup states, use setup_contract plan preview actions (which may set overwrite=true for repair).",
|
|
382
456
|
...(DB_STARTUP_WARNINGS.length > 0
|
|
383
457
|
? ["Database migration warnings are present in context.db_migration_warnings. Run sync() now, then run enrich_scene(scene_id, project_id) for stale scenes you touch."]
|
|
384
458
|
: []),
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "1.0.0",
|
|
3
|
+
"flows": [
|
|
4
|
+
{
|
|
5
|
+
"id": "styleguide_setup_v1",
|
|
6
|
+
"label": "Set up prose styleguide",
|
|
7
|
+
"questions": [
|
|
8
|
+
"scope",
|
|
9
|
+
"project_id",
|
|
10
|
+
"language",
|
|
11
|
+
"bootstrap_from_scenes",
|
|
12
|
+
"high_impact_overrides",
|
|
13
|
+
"voice_notes"
|
|
14
|
+
],
|
|
15
|
+
"artifact_targets": [
|
|
16
|
+
"prose_styleguide_config",
|
|
17
|
+
"prose_styleguide_skill"
|
|
18
|
+
],
|
|
19
|
+
"completion_rules": [
|
|
20
|
+
"styleguide_config_exists",
|
|
21
|
+
"styleguide_config_valid",
|
|
22
|
+
"skill_present_when_sync_scope"
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"questions": {
|
|
27
|
+
"scope": {
|
|
28
|
+
"label": "Config scope",
|
|
29
|
+
"help_text": "Choose where the prose styleguide config should be written.",
|
|
30
|
+
"blocking": true,
|
|
31
|
+
"ask_mode": "always",
|
|
32
|
+
"input_type": "enum",
|
|
33
|
+
"allowed_values": [
|
|
34
|
+
"project_root",
|
|
35
|
+
"sync_root"
|
|
36
|
+
],
|
|
37
|
+
"write_target": "setup.scope"
|
|
38
|
+
},
|
|
39
|
+
"project_id": {
|
|
40
|
+
"label": "Project ID",
|
|
41
|
+
"help_text": "Required when scope is project_root.",
|
|
42
|
+
"blocking": true,
|
|
43
|
+
"ask_mode": "inferred_with_confirmation",
|
|
44
|
+
"input_type": "string",
|
|
45
|
+
"write_target": "setup.project_id"
|
|
46
|
+
},
|
|
47
|
+
"language": {
|
|
48
|
+
"label": "Primary language",
|
|
49
|
+
"help_text": "Seeds styleguide defaults.",
|
|
50
|
+
"blocking": true,
|
|
51
|
+
"ask_mode": "always",
|
|
52
|
+
"input_type": "enum",
|
|
53
|
+
"allowed_values": [
|
|
54
|
+
"english_us",
|
|
55
|
+
"english_uk",
|
|
56
|
+
"english_au",
|
|
57
|
+
"english_ca",
|
|
58
|
+
"swedish",
|
|
59
|
+
"norwegian",
|
|
60
|
+
"danish",
|
|
61
|
+
"finnish",
|
|
62
|
+
"french",
|
|
63
|
+
"italian",
|
|
64
|
+
"russian",
|
|
65
|
+
"portuguese_pt",
|
|
66
|
+
"portuguese_br",
|
|
67
|
+
"german",
|
|
68
|
+
"dutch",
|
|
69
|
+
"polish",
|
|
70
|
+
"czech",
|
|
71
|
+
"hungarian",
|
|
72
|
+
"spanish",
|
|
73
|
+
"irish",
|
|
74
|
+
"japanese",
|
|
75
|
+
"korean",
|
|
76
|
+
"chinese_traditional",
|
|
77
|
+
"chinese_simplified"
|
|
78
|
+
],
|
|
79
|
+
"write_target": "styleguide.language"
|
|
80
|
+
},
|
|
81
|
+
"bootstrap_from_scenes": {
|
|
82
|
+
"label": "Bootstrap from existing scenes",
|
|
83
|
+
"help_text": "Optionally infer likely conventions from manuscript prose before final confirmation.",
|
|
84
|
+
"blocking": false,
|
|
85
|
+
"ask_mode": "low_risk_keep_change",
|
|
86
|
+
"input_type": "boolean",
|
|
87
|
+
"write_target": "setup.bootstrap_from_scenes"
|
|
88
|
+
},
|
|
89
|
+
"high_impact_overrides": {
|
|
90
|
+
"label": "High-impact convention overrides",
|
|
91
|
+
"help_text": "Confirm or override punctuation, numbers, and formatting conventions.",
|
|
92
|
+
"blocking": false,
|
|
93
|
+
"ask_mode": "inferred_with_confirmation",
|
|
94
|
+
"input_type": "object",
|
|
95
|
+
"write_target": "styleguide.overrides"
|
|
96
|
+
},
|
|
97
|
+
"voice_notes": {
|
|
98
|
+
"label": "Voice notes",
|
|
99
|
+
"help_text": "Optional freeform guidance for tone and craft preferences.",
|
|
100
|
+
"blocking": false,
|
|
101
|
+
"ask_mode": "low_risk_keep_change",
|
|
102
|
+
"input_type": "string",
|
|
103
|
+
"write_target": "styleguide.voice_notes"
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
"defaults": {
|
|
107
|
+
"scope": "project_root",
|
|
108
|
+
"language": "english_us",
|
|
109
|
+
"bootstrap_from_scenes": true
|
|
110
|
+
},
|
|
111
|
+
"validation_rules": [
|
|
112
|
+
{
|
|
113
|
+
"id": "project_id_required_for_project_root",
|
|
114
|
+
"when": {
|
|
115
|
+
"scope": "project_root"
|
|
116
|
+
},
|
|
117
|
+
"rule": "project_id is required"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"id": "language_must_be_supported",
|
|
121
|
+
"rule": "language must be one of allowed_values"
|
|
122
|
+
}
|
|
123
|
+
],
|
|
124
|
+
"artifact_targets": {
|
|
125
|
+
"prose_styleguide_config": {
|
|
126
|
+
"path": "prose-styleguide.config.yaml",
|
|
127
|
+
"owner": "mcp_writing_runtime"
|
|
128
|
+
},
|
|
129
|
+
"prose_styleguide_skill": {
|
|
130
|
+
"path": "skills/prose-styleguide/SKILL.md",
|
|
131
|
+
"owner": "mcp_writing_runtime",
|
|
132
|
+
"condition": "scope == sync_root"
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
"completion_rules": {
|
|
136
|
+
"styleguide_config_exists": {
|
|
137
|
+
"derived_from": "filesystem"
|
|
138
|
+
},
|
|
139
|
+
"styleguide_config_valid": {
|
|
140
|
+
"derived_from": "config_validation"
|
|
141
|
+
},
|
|
142
|
+
"skill_present_when_sync_scope": {
|
|
143
|
+
"derived_from": "filesystem_conditional"
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
const actionSchema = z.object({
|
|
4
|
+
tool: z.string().min(1),
|
|
5
|
+
arguments: z.record(z.string(), z.unknown()),
|
|
6
|
+
mode: z.enum(["preview_or_confirm", "write"]).optional(),
|
|
7
|
+
note: z.string().min(1).optional(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const planPreviewSchema = z.object({
|
|
11
|
+
flow_id: z.literal("styleguide_setup_v1"),
|
|
12
|
+
default_scope: z.enum(["project_root", "sync_root"]),
|
|
13
|
+
actions: z.array(actionSchema),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const setupContractContextSchema = z.union([
|
|
17
|
+
z.object({
|
|
18
|
+
contract_id: z.literal("styleguide_setup_v1"),
|
|
19
|
+
schema_version: z.string().min(1),
|
|
20
|
+
styleguide_setup_status: z.enum([
|
|
21
|
+
"missing_advisory",
|
|
22
|
+
"missing_blocking",
|
|
23
|
+
"invalid_advisory",
|
|
24
|
+
"invalid_blocking",
|
|
25
|
+
"complete",
|
|
26
|
+
]),
|
|
27
|
+
setup_recommended: z.boolean(),
|
|
28
|
+
plan_preview: planPreviewSchema,
|
|
29
|
+
}),
|
|
30
|
+
z.object({
|
|
31
|
+
contract_id: z.literal("styleguide_setup_v1"),
|
|
32
|
+
status: z.literal("unavailable"),
|
|
33
|
+
error_code: z.string().min(1),
|
|
34
|
+
}),
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
export function validateSetupContractContext(payload) {
|
|
38
|
+
const parsed = setupContractContextSchema.safeParse(payload);
|
|
39
|
+
if (!parsed.success) {
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
error: {
|
|
43
|
+
code: "INVALID_SETUP_CONTRACT_CONTEXT",
|
|
44
|
+
message: "describe_workflows produced an invalid setup_contract context payload.",
|
|
45
|
+
details: {
|
|
46
|
+
issues: parsed.error.issues.map((issue) => ({
|
|
47
|
+
path: issue.path.join("."),
|
|
48
|
+
message: issue.message,
|
|
49
|
+
})),
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
ok: true,
|
|
57
|
+
value: parsed.data,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
const questionSchema = z.object({
|
|
6
|
+
label: z.string().min(1),
|
|
7
|
+
help_text: z.string().min(1),
|
|
8
|
+
blocking: z.boolean(),
|
|
9
|
+
ask_mode: z.enum(["always", "inferred_with_confirmation", "low_risk_keep_change"]),
|
|
10
|
+
input_type: z.enum(["enum", "string", "boolean", "object"]),
|
|
11
|
+
allowed_values: z.array(z.string()).optional(),
|
|
12
|
+
write_target: z.string().min(1),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const flowSchema = z.object({
|
|
16
|
+
id: z.string().min(1),
|
|
17
|
+
label: z.string().min(1),
|
|
18
|
+
questions: z.array(z.string()).min(1),
|
|
19
|
+
artifact_targets: z.array(z.string()).min(1),
|
|
20
|
+
completion_rules: z.array(z.string()).min(1),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const setupContractSchema = z.object({
|
|
24
|
+
schema_version: z.string().min(1),
|
|
25
|
+
flows: z.array(flowSchema).min(1),
|
|
26
|
+
questions: z.record(z.string(), questionSchema),
|
|
27
|
+
defaults: z.record(z.string(), z.union([z.string(), z.boolean()])),
|
|
28
|
+
validation_rules: z.array(z.object({
|
|
29
|
+
id: z.string().min(1),
|
|
30
|
+
when: z.record(z.string(), z.string()).optional(),
|
|
31
|
+
rule: z.string().min(1),
|
|
32
|
+
})),
|
|
33
|
+
artifact_targets: z.record(z.string(), z.object({
|
|
34
|
+
path: z.string().min(1),
|
|
35
|
+
owner: z.string().min(1),
|
|
36
|
+
condition: z.string().optional(),
|
|
37
|
+
})),
|
|
38
|
+
completion_rules: z.record(z.string(), z.object({
|
|
39
|
+
derived_from: z.enum(["filesystem", "config_validation", "filesystem_conditional"]),
|
|
40
|
+
})),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const CONTRACT_RELATIVE_PATHS = {
|
|
44
|
+
styleguide_setup_v1: path.join("src", "setup", "contracts", "styleguide_setup_v1.json"),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function getFlowById(contract, flowId) {
|
|
48
|
+
return contract.flows.find((flow) => flow.id === flowId) ?? null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseJsonFile(filePath) {
|
|
52
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
53
|
+
return JSON.parse(raw);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function loadSetupContract({ rootDir, contractId = "styleguide_setup_v1" }) {
|
|
57
|
+
const relativePath = CONTRACT_RELATIVE_PATHS[contractId];
|
|
58
|
+
if (!relativePath) {
|
|
59
|
+
return {
|
|
60
|
+
ok: false,
|
|
61
|
+
error: {
|
|
62
|
+
code: "SETUP_CONTRACT_NOT_FOUND",
|
|
63
|
+
message: `Unknown setup contract id: ${contractId}`,
|
|
64
|
+
details: { contract_id: contractId },
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const contractPath = path.join(rootDir, relativePath);
|
|
70
|
+
if (!fs.existsSync(contractPath)) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
error: {
|
|
74
|
+
code: "SETUP_CONTRACT_FILE_MISSING",
|
|
75
|
+
message: `Setup contract file not found: ${path.resolve(contractPath)}`,
|
|
76
|
+
details: { contract_id: contractId, contract_path: path.resolve(contractPath) },
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let parsed;
|
|
82
|
+
try {
|
|
83
|
+
parsed = parseJsonFile(contractPath);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
error: {
|
|
88
|
+
code: "SETUP_CONTRACT_INVALID_JSON",
|
|
89
|
+
message: `Failed to parse setup contract JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
90
|
+
details: { contract_id: contractId, contract_path: path.resolve(contractPath) },
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const validated = setupContractSchema.safeParse(parsed);
|
|
96
|
+
if (!validated.success) {
|
|
97
|
+
return {
|
|
98
|
+
ok: false,
|
|
99
|
+
error: {
|
|
100
|
+
code: "SETUP_CONTRACT_SCHEMA_INVALID",
|
|
101
|
+
message: "Setup contract does not match required schema.",
|
|
102
|
+
details: {
|
|
103
|
+
contract_id: contractId,
|
|
104
|
+
contract_path: path.resolve(contractPath),
|
|
105
|
+
issues: validated.error.issues.map((issue) => ({
|
|
106
|
+
path: issue.path.join("."),
|
|
107
|
+
message: issue.message,
|
|
108
|
+
})),
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
ok: true,
|
|
116
|
+
contract_id: contractId,
|
|
117
|
+
contract_path: path.resolve(contractPath),
|
|
118
|
+
contract: validated.data,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function resolveStyleguideSetupAnswers({
|
|
123
|
+
contract,
|
|
124
|
+
flowId = "styleguide_setup_v1",
|
|
125
|
+
answers = {},
|
|
126
|
+
inferred = {},
|
|
127
|
+
}) {
|
|
128
|
+
const flow = getFlowById(contract, flowId);
|
|
129
|
+
if (!flow) {
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
error: {
|
|
133
|
+
code: "SETUP_FLOW_NOT_FOUND",
|
|
134
|
+
message: `Flow not found in setup contract: ${flowId}`,
|
|
135
|
+
details: { flow_id: flowId },
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const defaultScope = inferred.project_id
|
|
141
|
+
? (contract.defaults.scope ?? "project_root")
|
|
142
|
+
: "sync_root";
|
|
143
|
+
const scope = answers.scope ?? defaultScope;
|
|
144
|
+
const language = answers.language ?? contract.defaults.language;
|
|
145
|
+
const bootstrapFromScenes = answers.bootstrap_from_scenes ?? contract.defaults.bootstrap_from_scenes;
|
|
146
|
+
const projectId = answers.project_id ?? inferred.project_id ?? null;
|
|
147
|
+
const overrides = answers.high_impact_overrides ?? {};
|
|
148
|
+
const voiceNotes = answers.voice_notes;
|
|
149
|
+
|
|
150
|
+
if (typeof bootstrapFromScenes !== "boolean") {
|
|
151
|
+
return {
|
|
152
|
+
ok: false,
|
|
153
|
+
error: {
|
|
154
|
+
code: "INVALID_SETUP_BOOTSTRAP_FLAG",
|
|
155
|
+
message: "bootstrap_from_scenes must be a boolean.",
|
|
156
|
+
details: {
|
|
157
|
+
bootstrap_from_scenes: bootstrapFromScenes,
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const scopeQuestion = contract.questions.scope;
|
|
164
|
+
if (!scopeQuestion.allowed_values?.includes(scope)) {
|
|
165
|
+
return {
|
|
166
|
+
ok: false,
|
|
167
|
+
error: {
|
|
168
|
+
code: "INVALID_SETUP_SCOPE",
|
|
169
|
+
message: "scope must be one of the setup contract allowed_values.",
|
|
170
|
+
details: {
|
|
171
|
+
scope,
|
|
172
|
+
allowed_values: scopeQuestion.allowed_values ?? [],
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const languageQuestion = contract.questions.language;
|
|
179
|
+
if (!languageQuestion.allowed_values?.includes(language)) {
|
|
180
|
+
return {
|
|
181
|
+
ok: false,
|
|
182
|
+
error: {
|
|
183
|
+
code: "INVALID_SETUP_LANGUAGE",
|
|
184
|
+
message: "language must be one of the setup contract allowed_values.",
|
|
185
|
+
details: {
|
|
186
|
+
language,
|
|
187
|
+
allowed_values: languageQuestion.allowed_values ?? [],
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (scope === "project_root" && !projectId) {
|
|
194
|
+
return {
|
|
195
|
+
ok: false,
|
|
196
|
+
error: {
|
|
197
|
+
code: "SETUP_PROJECT_ID_REQUIRED",
|
|
198
|
+
message: "project_id is required when scope is project_root.",
|
|
199
|
+
details: { scope, project_id: projectId },
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
ok: true,
|
|
206
|
+
flow,
|
|
207
|
+
resolved_answers: {
|
|
208
|
+
scope,
|
|
209
|
+
project_id: projectId,
|
|
210
|
+
language,
|
|
211
|
+
bootstrap_from_scenes: bootstrapFromScenes,
|
|
212
|
+
high_impact_overrides: overrides,
|
|
213
|
+
voice_notes: typeof voiceNotes === "string" && voiceNotes.trim().length > 0
|
|
214
|
+
? voiceNotes
|
|
215
|
+
: undefined,
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function buildStyleguideSetupArtifactPlan({
|
|
221
|
+
resolvedAnswers,
|
|
222
|
+
sceneCount = 0,
|
|
223
|
+
setupStatus = "missing_advisory",
|
|
224
|
+
}) {
|
|
225
|
+
const replaceExistingArtifacts = typeof setupStatus === "string" && setupStatus.startsWith("invalid_");
|
|
226
|
+
const configAction = {
|
|
227
|
+
tool: "setup_prose_styleguide_config",
|
|
228
|
+
arguments: {
|
|
229
|
+
scope: resolvedAnswers.scope,
|
|
230
|
+
language: resolvedAnswers.language,
|
|
231
|
+
...(resolvedAnswers.project_id ? { project_id: resolvedAnswers.project_id } : {}),
|
|
232
|
+
...(Object.keys(resolvedAnswers.high_impact_overrides ?? {}).length > 0
|
|
233
|
+
? { overrides: resolvedAnswers.high_impact_overrides }
|
|
234
|
+
: {}),
|
|
235
|
+
...(resolvedAnswers.voice_notes ? { voice_notes: resolvedAnswers.voice_notes } : {}),
|
|
236
|
+
overwrite: replaceExistingArtifacts,
|
|
237
|
+
},
|
|
238
|
+
mode: "write",
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const actions = [];
|
|
242
|
+
if (resolvedAnswers.bootstrap_from_scenes && resolvedAnswers.project_id) {
|
|
243
|
+
actions.push({
|
|
244
|
+
tool: "bootstrap_prose_styleguide_config",
|
|
245
|
+
arguments: {
|
|
246
|
+
project_id: resolvedAnswers.project_id,
|
|
247
|
+
max_scenes: Math.max(1, sceneCount || 1),
|
|
248
|
+
},
|
|
249
|
+
mode: "preview_or_confirm",
|
|
250
|
+
note: "Use bootstrap suggested_config to populate setup_prose_styleguide_config.overrides before executing write actions.",
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
actions.push(configAction);
|
|
255
|
+
|
|
256
|
+
if (resolvedAnswers.scope === "sync_root") {
|
|
257
|
+
actions.push({
|
|
258
|
+
tool: "setup_prose_styleguide_skill",
|
|
259
|
+
arguments: { overwrite: true },
|
|
260
|
+
mode: "write",
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
ok: true,
|
|
266
|
+
artifact_targets: [
|
|
267
|
+
"prose-styleguide.config.yaml",
|
|
268
|
+
...(resolvedAnswers.scope === "sync_root" ? ["skills/prose-styleguide/SKILL.md"] : []),
|
|
269
|
+
],
|
|
270
|
+
actions,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function deriveStyleguideSetupStatus({
|
|
275
|
+
styleguideExists,
|
|
276
|
+
styleguideValid,
|
|
277
|
+
styleguideSkillExists,
|
|
278
|
+
styleguideEnforcementMode,
|
|
279
|
+
}) {
|
|
280
|
+
const anyStyleguideExists = Boolean(
|
|
281
|
+
styleguideExists?.sync_root
|
|
282
|
+
|| styleguideExists?.universe_root
|
|
283
|
+
|| styleguideExists?.project_root
|
|
284
|
+
);
|
|
285
|
+
const isBlockingMode = styleguideEnforcementMode === "required";
|
|
286
|
+
|
|
287
|
+
if (!anyStyleguideExists) {
|
|
288
|
+
return {
|
|
289
|
+
status: isBlockingMode ? "missing_blocking" : "missing_advisory",
|
|
290
|
+
setup_recommended: true,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (!styleguideValid) {
|
|
295
|
+
return {
|
|
296
|
+
status: isBlockingMode ? "invalid_blocking" : "invalid_advisory",
|
|
297
|
+
setup_recommended: true,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const requiresSyncRootSkill = Boolean(styleguideExists?.sync_root)
|
|
302
|
+
&& !styleguideExists?.universe_root
|
|
303
|
+
&& !styleguideExists?.project_root;
|
|
304
|
+
if (requiresSyncRootSkill && !styleguideSkillExists) {
|
|
305
|
+
return {
|
|
306
|
+
status: isBlockingMode ? "invalid_blocking" : "invalid_advisory",
|
|
307
|
+
setup_recommended: true,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
status: "complete",
|
|
313
|
+
setup_recommended: false,
|
|
314
|
+
};
|
|
315
|
+
}
|