@xenonbyte/da-vinci-workflow 0.1.16 → 0.1.17
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 +9 -3
- package/README.md +15 -4
- package/README.zh-CN.md +15 -5
- package/SKILL.md +11 -2
- package/commands/claude/dv/design.md +3 -2
- package/commands/codex/prompts/dv-design.md +3 -2
- package/commands/gemini/dv/design.toml +3 -2
- package/docs/mode-use-cases.md +2 -2
- package/docs/prompt-presets/README.md +1 -1
- package/docs/prompt-presets/desktop-app.md +4 -4
- package/docs/prompt-presets/mobile-app.md +4 -4
- package/docs/prompt-presets/tablet-app.md +4 -4
- package/docs/prompt-presets/web-app.md +4 -4
- package/docs/workflow-examples.md +5 -5
- package/docs/zh-CN/mode-use-cases.md +7 -6
- package/docs/zh-CN/prompt-presets/README.md +1 -1
- package/docs/zh-CN/prompt-presets/desktop-app.md +4 -4
- package/docs/zh-CN/prompt-presets/mobile-app.md +4 -4
- package/docs/zh-CN/prompt-presets/tablet-app.md +4 -4
- package/docs/zh-CN/prompt-presets/web-app.md +4 -4
- package/docs/zh-CN/workflow-examples.md +3 -3
- package/lib/audit.js +66 -2
- package/lib/cli.js +262 -2
- package/lib/mcp-runtime-gate.js +53 -1
- package/lib/pen-persistence.js +192 -3
- package/lib/pencil-lock.js +128 -0
- package/lib/pencil-session.js +229 -0
- package/package.json +3 -1
- package/references/checkpoints.md +6 -1
- package/references/pencil-design-to-code.md +9 -2
- package/scripts/test-mcp-runtime-gate.js +88 -0
- package/scripts/test-pen-persistence.js +146 -6
- package/scripts/test-pencil-session.js +152 -0
- package/scripts/test-persistence-flows.js +315 -0
|
@@ -28,7 +28,7 @@ Existing code is the behavior source of truth, not the layout truth.
|
|
|
28
28
|
Preserve current behavior, permissions, integrations, validations, and state transitions unless explicitly required otherwise.
|
|
29
29
|
Inventory the current tablet surfaces and important states before Pencil work.
|
|
30
30
|
Use the Visual Assist preferences declared in DA-VINCI.md.
|
|
31
|
-
|
|
31
|
+
在第一次 Pencil 编辑前,优先执行 `da-vinci pencil-session begin --project <project-path> --pen <path>`,让登记好的项目内 `.pen` 先完成 seed 并持有全局 Pencil 锁;只有 session wrapper 不可用时,再退回 `da-vinci ensure-pen --output <path> --verify-open`。
|
|
32
32
|
如果 Pencil MCP 可用,在第一次成功写入 Pencil 后运行 MCP runtime gate,并把结果记录到 `pencil-design.md`。
|
|
33
33
|
在声明 `design complete` 或 `workflow complete` 之前,必须同时通过 MCP runtime gate 和 `da-vinci audit --mode completion --change <change-id> <project-path>`。
|
|
34
34
|
Do not pass design checkpoint if the result ignores tablet-scale composition or collapses into a stretched phone layout.
|
|
@@ -55,7 +55,7 @@ Design 1-3 anchor surfaces first, review screenshots, then expand.
|
|
|
55
55
|
如果同一个 anchor surface 连续两次回滚,就切到每批不超过 6 个操作的微批次,直到拿到干净的 schema-safe pass。
|
|
56
56
|
在第一次成功写入 Pencil 后、继续大范围扩展前,先运行 `da-vinci audit --mode integrity <project-path>`。
|
|
57
57
|
如果 Pencil MCP 可用,在第一次成功写入 Pencil 后运行 MCP runtime gate,并把结果记录到 `pencil-design.md`。
|
|
58
|
-
|
|
58
|
+
在第一次 Pencil 编辑前,优先执行 `da-vinci pencil-session begin --project <project-path> --pen <path>`;发生实质性 live edit 后,优先执行 `da-vinci pencil-session persist --project <project-path> --pen <path> ...`。只有 session wrapper 不可用时,才退回 `ensure-pen + write-pen + check-pen-sync` 这条底层链路。
|
|
59
59
|
截图导出只能写到 `.da-vinci/changes/<change-id>/exports/`,不能写进 `.da-vinci/designs/`。
|
|
60
60
|
截图审查必须记录明确的 `PASS` / `WARN` / `BLOCK`、问题列表和是否回改;“看起来很好”不算审查记录。
|
|
61
61
|
如果登记的 `.pen` 设计源只存在于内存中,或只剩下导出的 PNG,就不能宣布完成。
|
|
@@ -76,7 +76,7 @@ Decompose complex pages into real design surfaces before Pencil work.
|
|
|
76
76
|
Use the Visual Assist preferences declared in DA-VINCI.md.
|
|
77
77
|
If the product is complex, design 1-3 anchor surfaces first, review screenshots, then expand.
|
|
78
78
|
Stop after DA-VINCI.md, design-registry.md, page-map.md, proposal.md, specs, design.md, pencil-design.md, pencil-bindings.md, and tasks.md.
|
|
79
|
-
|
|
79
|
+
在第一次 Pencil 编辑前,优先执行 `da-vinci pencil-session begin --project <project-path> --pen <path>`,让登记好的项目内 `.pen` 先完成 seed 并持有锁;只有 session wrapper 不可用时,再退回 `da-vinci ensure-pen --output <path> --verify-open`。
|
|
80
80
|
如果 Pencil MCP 可用,在第一次成功写入 Pencil 后运行 MCP runtime gate,并把结果记录到 `pencil-design.md`。
|
|
81
81
|
在声明 `design complete` 之前,必须同时通过 MCP runtime gate 和 `da-vinci audit --mode completion --change <change-id> <project-path>`。
|
|
82
82
|
Do not start code changes yet.
|
|
@@ -89,7 +89,7 @@ $da-vinci use continue for this existing tablet-product redesign workflow.
|
|
|
89
89
|
|
|
90
90
|
Use the existing Da Vinci artifacts in this project.
|
|
91
91
|
Do not restart discovery unless an artifact is missing or clearly wrong.
|
|
92
|
-
把登记的项目内 Pencil 源保持在 `.da-vinci/designs/`
|
|
92
|
+
把登记的项目内 Pencil 源保持在 `.da-vinci/designs/` 下,作为设计真相源。继续设计时优先执行 `da-vinci pencil-session begin --project <project-path> --pen <path>`,发生实质性 live edit 后优先执行 `da-vinci pencil-session persist --project <project-path> --pen <path> ...`;只有 session wrapper 不可用时,才退回 `write-pen + check-pen-sync` 这条底层链路。
|
|
93
93
|
If the redesign is complex, continue from the approved anchor surfaces instead of restarting broad scaffolding.
|
|
94
94
|
如果 Pencil MCP 可用,且这一轮继续做了新的 Pencil 写入,就重新运行 MCP runtime gate,并把最新结果记录到 `pencil-design.md`。
|
|
95
95
|
在声明 `design complete` 或 `workflow complete` 之前,必须同时通过 MCP runtime gate 和 `da-vinci audit --mode completion --change <change-id> <project-path>`。
|
|
@@ -28,7 +28,7 @@ Existing code is the behavior source of truth, not the layout truth.
|
|
|
28
28
|
Preserve current business logic, routes, permissions, integrations, validations, and state transitions unless explicitly required otherwise.
|
|
29
29
|
Inventory the current product surfaces and important states before Pencil work.
|
|
30
30
|
Use the Visual Assist preferences declared in DA-VINCI.md.
|
|
31
|
-
|
|
31
|
+
在第一次 Pencil 编辑前,优先执行 `da-vinci pencil-session begin --project <project-path> --pen <path>`,让登记好的项目内 `.pen` 先完成 seed 并持有全局 Pencil 锁;只有 session wrapper 不可用时,再退回 `da-vinci ensure-pen --output <path> --verify-open`。
|
|
32
32
|
如果 Pencil MCP 可用,在第一次成功写入 Pencil 后运行 MCP runtime gate,并把结果记录到 `pencil-design.md`。
|
|
33
33
|
在声明 `design complete` 或 `workflow complete` 之前,必须同时通过 MCP runtime gate 和 `da-vinci audit --mode completion --change <change-id> <project-path>`。
|
|
34
34
|
Do not pass design checkpoint if the result is a generic SaaS card grid or a recolor of the old interface.
|
|
@@ -56,7 +56,7 @@ Design 1-3 anchor surfaces first, review screenshots, then expand.
|
|
|
56
56
|
如果同一个 anchor surface 连续两次回滚,就切到每批不超过 6 个操作的微批次,直到拿到干净的 schema-safe pass。
|
|
57
57
|
在第一次成功写入 Pencil 后、继续大范围扩展前,先运行 `da-vinci audit --mode integrity <project-path>`。
|
|
58
58
|
如果 Pencil MCP 可用,在第一次成功写入 Pencil 后运行 MCP runtime gate,并把结果记录到 `pencil-design.md`。
|
|
59
|
-
|
|
59
|
+
在第一次 Pencil 编辑前,优先执行 `da-vinci pencil-session begin --project <project-path> --pen <path>`;发生实质性 live edit 后,优先执行 `da-vinci pencil-session persist --project <project-path> --pen <path> ...`。只有 session wrapper 不可用时,才退回 `ensure-pen + write-pen + check-pen-sync` 这条底层链路。
|
|
60
60
|
截图导出只能写到 `.da-vinci/changes/<change-id>/exports/`,不能写进 `.da-vinci/designs/`。
|
|
61
61
|
截图审查必须记录明确的 `PASS` / `WARN` / `BLOCK`、问题列表和是否回改;“看起来很好”不算审查记录。
|
|
62
62
|
如果登记的 `.pen` 设计源只存在于内存中,或只剩下导出的 PNG,就不能宣布完成。
|
|
@@ -77,7 +77,7 @@ Decompose complex pages into real design surfaces before Pencil work.
|
|
|
77
77
|
Use the Visual Assist preferences declared in DA-VINCI.md.
|
|
78
78
|
If the product is complex, design 1-3 anchor surfaces first, review screenshots, then expand.
|
|
79
79
|
Stop after DA-VINCI.md, design-registry.md, page-map.md, proposal.md, specs, design.md, pencil-design.md, pencil-bindings.md, and tasks.md.
|
|
80
|
-
|
|
80
|
+
在第一次 Pencil 编辑前,优先执行 `da-vinci pencil-session begin --project <project-path> --pen <path>`,让登记好的项目内 `.pen` 先完成 seed 并持有锁;只有 session wrapper 不可用时,再退回 `da-vinci ensure-pen --output <path> --verify-open`。
|
|
81
81
|
如果 Pencil MCP 可用,在第一次成功写入 Pencil 后运行 MCP runtime gate,并把结果记录到 `pencil-design.md`。
|
|
82
82
|
在声明 `design complete` 之前,必须同时通过 MCP runtime gate 和 `da-vinci audit --mode completion --change <change-id> <project-path>`。
|
|
83
83
|
Do not start code changes yet.
|
|
@@ -90,7 +90,7 @@ $da-vinci use continue for this existing web-product redesign workflow.
|
|
|
90
90
|
|
|
91
91
|
Use the existing Da Vinci artifacts in this project.
|
|
92
92
|
Do not restart discovery unless an artifact is missing or clearly wrong.
|
|
93
|
-
把登记的项目内 Pencil 源保持在 `.da-vinci/designs/`
|
|
93
|
+
把登记的项目内 Pencil 源保持在 `.da-vinci/designs/` 下,作为设计真相源。继续设计时优先执行 `da-vinci pencil-session begin --project <project-path> --pen <path>`,发生实质性 live edit 后优先执行 `da-vinci pencil-session persist --project <project-path> --pen <path> ...`;只有 session wrapper 不可用时,才退回 `write-pen + check-pen-sync` 这条底层链路。
|
|
94
94
|
If the redesign is complex, continue from the approved anchor surfaces instead of restarting broad scaffolding.
|
|
95
95
|
如果 Pencil MCP 可用,且这一轮继续做了新的 Pencil 写入,就重新运行 MCP runtime gate,并把最新结果记录到 `pencil-design.md`。
|
|
96
96
|
在声明 `design complete` 或 `workflow complete` 之前,必须同时通过 MCP runtime gate 和 `da-vinci audit --mode completion --change <change-id> <project-path>`。
|
|
@@ -127,8 +127,8 @@ Do not treat screenshot analysis as an automatic pass if it reports hierarchy, s
|
|
|
127
127
|
Before non-trivial `batch_design` calls, preflight the Pencil operations when shell access is available.
|
|
128
128
|
If the same anchor surface rolls back twice, switch to micro-batches of 6 or fewer operations until a clean schema-safe pass succeeds.
|
|
129
129
|
Use only Pencil-supported properties; do not use web-only props like flex or margin.
|
|
130
|
-
|
|
131
|
-
如果项目里原本已有登记的 `.pen`,继续设计时先打开它,但实质性 live edit
|
|
130
|
+
优先在第一次 Pencil 编辑前执行 `da-vinci pencil-session begin --project <project-path> --pen <path>`,这样会先 seed 登记好的 `.pen` 并持有全局 Pencil 锁。
|
|
131
|
+
如果项目里原本已有登记的 `.pen`,继续设计时先打开它,但实质性 live edit 后优先通过 `da-vinci pencil-session persist` 把当前 live MCP 快照重新覆盖写回同一路径。
|
|
132
132
|
Verify the registered project-local `.pen` file exists as a shell-visible file after the first Pencil write.
|
|
133
133
|
在第一次成功写入 Pencil 后、继续大范围扩展前,先运行 `da-vinci audit --mode integrity <project-path>`。
|
|
134
134
|
Keep `.da-vinci/designs/` reserved for `.pen` files only.
|
|
@@ -174,7 +174,7 @@ Use the visual-adapter preferences declared in DA-VINCI.md.
|
|
|
174
174
|
If frontend-skill is available, use it as the primary visual adapter.
|
|
175
175
|
If it is unavailable, fall back to native Da Vinci design rules and continue.
|
|
176
176
|
Persist project-local Pencil files under .da-vinci/designs/.
|
|
177
|
-
如果当前还没有登记的项目内 `.pen
|
|
177
|
+
如果当前还没有登记的项目内 `.pen`,先在这里 seed 一个登记好的 `.pen`,再开始第一次 Pencil 编辑,并把后续 live 编辑持续绑定到这个路径。
|
|
178
178
|
如果项目里原本已有 `.pen`,继续设计后要把当前 MCP 快照覆盖写回同一路径。
|
|
179
179
|
```
|
|
180
180
|
|
package/lib/audit.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
|
+
const { getStandardPenStatePath, readPenState, hashPenDocument, readPenDocument } = require("./pen-persistence");
|
|
4
|
+
const { getSessionStatePath, readSessionState } = require("./pencil-session");
|
|
3
5
|
|
|
4
6
|
const IMAGE_EXPORT_PATTERN = /\.(png|jpe?g|webp|pdf)$/i;
|
|
5
7
|
|
|
@@ -105,9 +107,12 @@ function resolveScopedChangeDirs(projectRoot, changesDir, options, failures, war
|
|
|
105
107
|
}
|
|
106
108
|
|
|
107
109
|
function addMissingArtifacts(projectRoot, artifactPaths, targetList) {
|
|
108
|
-
for (const artifactPath of artifactPaths) {
|
|
110
|
+
for (const artifactPath of [...new Set(artifactPaths)]) {
|
|
109
111
|
if (!pathExists(artifactPath)) {
|
|
110
|
-
|
|
112
|
+
const message = `Missing required artifact: ${relativeTo(projectRoot, artifactPath)}`;
|
|
113
|
+
if (!targetList.includes(message)) {
|
|
114
|
+
targetList.push(message);
|
|
115
|
+
}
|
|
111
116
|
}
|
|
112
117
|
}
|
|
113
118
|
}
|
|
@@ -119,6 +124,7 @@ function auditProject(projectPathInput, options = {}) {
|
|
|
119
124
|
const designsDir = path.join(daVinciDir, "designs");
|
|
120
125
|
const changesDir = path.join(daVinciDir, "changes");
|
|
121
126
|
const designRegistryPath = path.join(daVinciDir, "design-registry.md");
|
|
127
|
+
const pencilSessionPath = getSessionStatePath(projectRoot);
|
|
122
128
|
|
|
123
129
|
const failures = [];
|
|
124
130
|
const warnings = [];
|
|
@@ -228,6 +234,64 @@ function auditProject(projectPathInput, options = {}) {
|
|
|
228
234
|
failures.push(
|
|
229
235
|
`Registered design source is missing on disk: ${relativeTo(projectRoot, registeredPenPath)}`
|
|
230
236
|
);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const statePath = getStandardPenStatePath(registeredPenPath);
|
|
241
|
+
const state = readPenState(registeredPenPath);
|
|
242
|
+
if (!state) {
|
|
243
|
+
const message = `Registered design source is missing state metadata: ${relativeTo(projectRoot, statePath)}`;
|
|
244
|
+
if (mode === "completion") {
|
|
245
|
+
failures.push(message);
|
|
246
|
+
} else {
|
|
247
|
+
warnings.push(message);
|
|
248
|
+
}
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const persistedDocument = readPenDocument(registeredPenPath);
|
|
253
|
+
const currentHash = hashPenDocument(persistedDocument);
|
|
254
|
+
if (state.snapshotHash !== currentHash) {
|
|
255
|
+
failures.push(
|
|
256
|
+
`Registered design source hash does not match its state metadata: ${relativeTo(projectRoot, registeredPenPath)}`
|
|
257
|
+
);
|
|
258
|
+
} else {
|
|
259
|
+
notes.push(
|
|
260
|
+
`Detected persisted design-source hash for ${relativeTo(projectRoot, registeredPenPath)}.`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const sessionState = readSessionState(projectRoot);
|
|
266
|
+
if (registeredPenPaths.length > 0 || penFiles.length > 0) {
|
|
267
|
+
if (!sessionState) {
|
|
268
|
+
const message = `Missing Pencil session state: ${relativeTo(projectRoot, pencilSessionPath)}`;
|
|
269
|
+
if (mode === "completion") {
|
|
270
|
+
failures.push(message);
|
|
271
|
+
} else {
|
|
272
|
+
warnings.push(message);
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
if (!sessionState.penPath) {
|
|
276
|
+
failures.push("Pencil session state is missing the bound `.pen` path.");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (mode === "completion" && sessionState.status !== "closed") {
|
|
280
|
+
failures.push("Completion audit requires the Pencil session to be ended and recorded as `closed`.");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (sessionState.penPath && pathExists(sessionState.penPath)) {
|
|
284
|
+
const penState = readPenState(sessionState.penPath);
|
|
285
|
+
const persistedHash = penState && penState.snapshotHash ? String(penState.snapshotHash) : "";
|
|
286
|
+
|
|
287
|
+
if (sessionState.lastPersistedHash && persistedHash && sessionState.lastPersistedHash !== persistedHash) {
|
|
288
|
+
failures.push("Pencil session state hash does not match the current persisted `.pen` state hash.");
|
|
289
|
+
} else if (persistedHash) {
|
|
290
|
+
notes.push(
|
|
291
|
+
`Detected Pencil session state for ${relativeTo(projectRoot, sessionState.penPath)}.`
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
231
295
|
}
|
|
232
296
|
}
|
|
233
297
|
|
package/lib/cli.js
CHANGED
|
@@ -14,8 +14,21 @@ const {
|
|
|
14
14
|
} = require("./pencil-preflight");
|
|
15
15
|
const {
|
|
16
16
|
writePenFromPayloadFiles,
|
|
17
|
-
snapshotPenFile
|
|
17
|
+
snapshotPenFile,
|
|
18
|
+
ensurePenFile,
|
|
19
|
+
comparePenSync
|
|
18
20
|
} = require("./pen-persistence");
|
|
21
|
+
const {
|
|
22
|
+
acquirePencilLock,
|
|
23
|
+
releasePencilLock,
|
|
24
|
+
getPencilLockStatus
|
|
25
|
+
} = require("./pencil-lock");
|
|
26
|
+
const {
|
|
27
|
+
beginPencilSession,
|
|
28
|
+
persistPencilSession,
|
|
29
|
+
endPencilSession,
|
|
30
|
+
getPencilSessionStatus
|
|
31
|
+
} = require("./pencil-session");
|
|
19
32
|
|
|
20
33
|
function getOption(args, name) {
|
|
21
34
|
const direct = args.find((arg) => arg.startsWith(`${name}=`));
|
|
@@ -78,14 +91,24 @@ function printHelp() {
|
|
|
78
91
|
" da-vinci validate-assets",
|
|
79
92
|
" da-vinci audit [project-path]",
|
|
80
93
|
" da-vinci preflight-pencil --ops-file <path>",
|
|
94
|
+
" da-vinci ensure-pen --output <path>",
|
|
81
95
|
" da-vinci write-pen --output <path> --nodes-file <path> [--variables-file <path>]",
|
|
96
|
+
" da-vinci check-pen-sync --pen <path> --nodes-file <path> [--variables-file <path>]",
|
|
82
97
|
" da-vinci snapshot-pen --input <path> --output <path>",
|
|
98
|
+
" da-vinci pencil-lock acquire --project <path>",
|
|
99
|
+
" da-vinci pencil-lock release --project <path>",
|
|
100
|
+
" da-vinci pencil-lock status",
|
|
101
|
+
" da-vinci pencil-session begin --project <path> --pen <path>",
|
|
102
|
+
" da-vinci pencil-session persist --project <path> --pen <path> --nodes-file <path> [--variables-file <path>]",
|
|
103
|
+
" da-vinci pencil-session end --project <path> --pen <path> [--nodes-file <path>]",
|
|
104
|
+
" da-vinci pencil-session status --project <path>",
|
|
83
105
|
" da-vinci --version",
|
|
84
106
|
"",
|
|
85
107
|
"Options:",
|
|
86
108
|
" --platform <value> codex, claude, gemini, or all",
|
|
87
109
|
" --home <path> override HOME for installation targets",
|
|
88
110
|
" --project <path> override project path for audit",
|
|
111
|
+
" --pen <path> registered .pen path for sync checks",
|
|
89
112
|
" --ops-file <path> Pencil batch operations file for preflight",
|
|
90
113
|
" --input <path> input .pen file for snapshot-pen",
|
|
91
114
|
" --output <path> output .pen file for write-pen or snapshot-pen",
|
|
@@ -94,7 +117,10 @@ function printHelp() {
|
|
|
94
117
|
" --verify-open reopen the written .pen with Pencil after writing",
|
|
95
118
|
" --version <value> explicit .pen version when writing from MCP payloads",
|
|
96
119
|
" --mode <value> integrity or completion",
|
|
97
|
-
" --change <id> scope completion audit to one change id"
|
|
120
|
+
" --change <id> scope completion audit to one change id",
|
|
121
|
+
" --wait-ms <value> wait for the global Pencil lock before failing",
|
|
122
|
+
" --owner <value> human-readable lock owner label",
|
|
123
|
+
" --force force a lock release held by another project"
|
|
98
124
|
].join("\n")
|
|
99
125
|
);
|
|
100
126
|
}
|
|
@@ -181,6 +207,30 @@ async function runCli(argv) {
|
|
|
181
207
|
return;
|
|
182
208
|
}
|
|
183
209
|
|
|
210
|
+
if (command === "ensure-pen") {
|
|
211
|
+
const outputPath = getOption(argv, "--output");
|
|
212
|
+
const version = getOption(argv, "--version");
|
|
213
|
+
const verifyWithPencil = argv.includes("--verify-open");
|
|
214
|
+
|
|
215
|
+
if (!outputPath) {
|
|
216
|
+
throw new Error("`ensure-pen` requires `--output <path>`.");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const result = ensurePenFile({
|
|
220
|
+
outputPath,
|
|
221
|
+
version,
|
|
222
|
+
verifyWithPencil
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
console.log(`${result.created ? "Created" : "Verified existing"} .pen source at ${result.outputPath}`);
|
|
226
|
+
console.log(`State file: ${result.statePath}`);
|
|
227
|
+
console.log(`Snapshot hash: ${result.state.snapshotHash}`);
|
|
228
|
+
if (result.verification) {
|
|
229
|
+
console.log(`Verified reopen with Pencil (${result.verification.topLevelCount} top-level nodes).`);
|
|
230
|
+
}
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
184
234
|
if (command === "write-pen") {
|
|
185
235
|
const outputPath = getOption(argv, "--output");
|
|
186
236
|
const nodesFile = getOption(argv, "--nodes-file");
|
|
@@ -201,6 +251,8 @@ async function runCli(argv) {
|
|
|
201
251
|
});
|
|
202
252
|
|
|
203
253
|
console.log(`Wrote .pen file to ${result.outputPath}`);
|
|
254
|
+
console.log(`State file: ${result.statePath}`);
|
|
255
|
+
console.log(`Snapshot hash: ${result.state.snapshotHash}`);
|
|
204
256
|
console.log(`Top-level nodes: ${result.document.children.length}`);
|
|
205
257
|
if (result.verification) {
|
|
206
258
|
console.log(`Verified reopen with Pencil (${result.verification.topLevelCount} top-level nodes).`);
|
|
@@ -208,6 +260,44 @@ async function runCli(argv) {
|
|
|
208
260
|
return;
|
|
209
261
|
}
|
|
210
262
|
|
|
263
|
+
if (command === "check-pen-sync") {
|
|
264
|
+
const penPath = getOption(argv, "--pen");
|
|
265
|
+
const nodesFile = getOption(argv, "--nodes-file");
|
|
266
|
+
const variablesFile = getOption(argv, "--variables-file");
|
|
267
|
+
const version = getOption(argv, "--version");
|
|
268
|
+
|
|
269
|
+
if (!penPath || !nodesFile) {
|
|
270
|
+
throw new Error("`check-pen-sync` requires `--pen <path>` and `--nodes-file <path>`.");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const result = comparePenSync({
|
|
274
|
+
penPath,
|
|
275
|
+
nodesFile,
|
|
276
|
+
variablesFile,
|
|
277
|
+
version
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
if (!result.inSync) {
|
|
281
|
+
throw new Error(
|
|
282
|
+
[
|
|
283
|
+
"Registered `.pen` is out of sync with the provided live payload.",
|
|
284
|
+
`Pen: ${result.penPath}`,
|
|
285
|
+
`State file: ${result.statePath}`,
|
|
286
|
+
`Persisted hash: ${result.persistedHash}`,
|
|
287
|
+
`Live hash: ${result.liveHash}`
|
|
288
|
+
].join("\n")
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
console.log(`Registered .pen is in sync: ${result.penPath}`);
|
|
293
|
+
console.log(`State file: ${result.statePath}`);
|
|
294
|
+
console.log(`Snapshot hash: ${result.liveHash}`);
|
|
295
|
+
if (!result.usedStateFile) {
|
|
296
|
+
console.log("State file was missing; sync comparison fell back to hashing the disk .pen file directly.");
|
|
297
|
+
}
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
211
301
|
if (command === "snapshot-pen") {
|
|
212
302
|
const inputPath = getOption(argv, "--input");
|
|
213
303
|
const outputPath = getOption(argv, "--output");
|
|
@@ -226,6 +316,8 @@ async function runCli(argv) {
|
|
|
226
316
|
});
|
|
227
317
|
|
|
228
318
|
console.log(`Snapshotted ${result.inputPath} to ${result.outputPath}`);
|
|
319
|
+
console.log(`State file: ${result.statePath}`);
|
|
320
|
+
console.log(`Snapshot hash: ${result.state.snapshotHash}`);
|
|
229
321
|
console.log(`Top-level nodes: ${result.document.children.length}`);
|
|
230
322
|
if (result.verification) {
|
|
231
323
|
console.log(`Verified reopen with Pencil (${result.verification.topLevelCount} top-level nodes).`);
|
|
@@ -233,6 +325,174 @@ async function runCli(argv) {
|
|
|
233
325
|
return;
|
|
234
326
|
}
|
|
235
327
|
|
|
328
|
+
if (command === "pencil-lock") {
|
|
329
|
+
const subcommand = getPositionalArgs(argv.slice(1), [
|
|
330
|
+
"--home",
|
|
331
|
+
"--project",
|
|
332
|
+
"--owner",
|
|
333
|
+
"--wait-ms"
|
|
334
|
+
])[0];
|
|
335
|
+
const projectPath = getOption(argv, "--project") || process.cwd();
|
|
336
|
+
const owner = getOption(argv, "--owner");
|
|
337
|
+
const waitMs = getOption(argv, "--wait-ms");
|
|
338
|
+
const force = argv.includes("--force");
|
|
339
|
+
|
|
340
|
+
if (!subcommand || ["acquire", "release", "status"].includes(subcommand) === false) {
|
|
341
|
+
throw new Error("`pencil-lock` requires one of: acquire, release, status.");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (subcommand === "acquire") {
|
|
345
|
+
const result = acquirePencilLock({
|
|
346
|
+
projectPath,
|
|
347
|
+
owner,
|
|
348
|
+
waitMs,
|
|
349
|
+
homeDir
|
|
350
|
+
});
|
|
351
|
+
console.log(`${result.alreadyHeld ? "Reused" : "Acquired"} Pencil lock at ${result.lockPath}`);
|
|
352
|
+
console.log(`Project: ${result.lock.projectPath}`);
|
|
353
|
+
console.log(`Owner: ${result.lock.owner}`);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (subcommand === "release") {
|
|
358
|
+
const result = releasePencilLock({
|
|
359
|
+
projectPath,
|
|
360
|
+
force,
|
|
361
|
+
homeDir
|
|
362
|
+
});
|
|
363
|
+
if (!result.hadLock) {
|
|
364
|
+
console.log(`No Pencil lock was present at ${result.lockPath}`);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
console.log(`Released Pencil lock at ${result.lockPath}`);
|
|
368
|
+
console.log(`Previous project: ${result.lock.projectPath}`);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const result = getPencilLockStatus({ homeDir });
|
|
373
|
+
console.log(`Lock path: ${result.lockPath}`);
|
|
374
|
+
if (!result.lock) {
|
|
375
|
+
console.log("Status: unlocked");
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
console.log("Status: locked");
|
|
379
|
+
console.log(`Project: ${result.lock.projectPath}`);
|
|
380
|
+
console.log(`Owner: ${result.lock.owner}`);
|
|
381
|
+
console.log(`PID: ${result.lock.pid}`);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (command === "pencil-session") {
|
|
386
|
+
const subcommand = getPositionalArgs(argv.slice(1), [
|
|
387
|
+
"--home",
|
|
388
|
+
"--project",
|
|
389
|
+
"--pen",
|
|
390
|
+
"--nodes-file",
|
|
391
|
+
"--variables-file",
|
|
392
|
+
"--version",
|
|
393
|
+
"--owner",
|
|
394
|
+
"--wait-ms"
|
|
395
|
+
])[0];
|
|
396
|
+
const projectPath = getOption(argv, "--project") || process.cwd();
|
|
397
|
+
const penPath = getOption(argv, "--pen");
|
|
398
|
+
const nodesFile = getOption(argv, "--nodes-file");
|
|
399
|
+
const variablesFile = getOption(argv, "--variables-file");
|
|
400
|
+
const version = getOption(argv, "--version");
|
|
401
|
+
const verifyWithPencil = argv.includes("--verify-open");
|
|
402
|
+
const owner = getOption(argv, "--owner");
|
|
403
|
+
const waitMs = getOption(argv, "--wait-ms");
|
|
404
|
+
const force = argv.includes("--force");
|
|
405
|
+
|
|
406
|
+
if (!subcommand || ["begin", "persist", "end", "status"].includes(subcommand) === false) {
|
|
407
|
+
throw new Error("`pencil-session` requires one of: begin, persist, end, status.");
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (subcommand === "begin") {
|
|
411
|
+
if (!penPath) {
|
|
412
|
+
throw new Error("`pencil-session begin` requires `--pen <path>`.");
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const result = beginPencilSession({
|
|
416
|
+
projectPath,
|
|
417
|
+
penPath,
|
|
418
|
+
version,
|
|
419
|
+
verifyWithPencil,
|
|
420
|
+
owner,
|
|
421
|
+
waitMs,
|
|
422
|
+
homeDir
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
console.log(`Began Pencil session for ${result.projectRoot}`);
|
|
426
|
+
console.log(`Pen path: ${result.penPath}`);
|
|
427
|
+
console.log(`Session state: ${result.sessionStatePath}`);
|
|
428
|
+
console.log(`Snapshot hash: ${result.session.lastPersistedHash}`);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (subcommand === "persist") {
|
|
433
|
+
if (!penPath || !nodesFile) {
|
|
434
|
+
throw new Error("`pencil-session persist` requires `--pen <path>` and `--nodes-file <path>`.");
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const result = persistPencilSession({
|
|
438
|
+
projectPath,
|
|
439
|
+
penPath,
|
|
440
|
+
nodesFile,
|
|
441
|
+
variablesFile,
|
|
442
|
+
version,
|
|
443
|
+
verifyWithPencil,
|
|
444
|
+
homeDir
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
console.log(`Persisted Pencil session for ${result.projectRoot}`);
|
|
448
|
+
console.log(`Pen path: ${result.penPath}`);
|
|
449
|
+
console.log(`Session state: ${result.sessionStatePath}`);
|
|
450
|
+
console.log(`Snapshot hash: ${result.session.lastPersistedHash}`);
|
|
451
|
+
console.log(`In sync: ${result.syncResult.inSync ? "yes" : "no"}`);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (subcommand === "end") {
|
|
456
|
+
if (!penPath) {
|
|
457
|
+
throw new Error("`pencil-session end` requires `--pen <path>`.");
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const result = endPencilSession({
|
|
461
|
+
projectPath,
|
|
462
|
+
penPath,
|
|
463
|
+
nodesFile,
|
|
464
|
+
variablesFile,
|
|
465
|
+
version,
|
|
466
|
+
homeDir,
|
|
467
|
+
force
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
console.log(`Ended Pencil session for ${result.projectRoot}`);
|
|
471
|
+
console.log(`Pen path: ${result.penPath}`);
|
|
472
|
+
console.log(`Session state: ${result.sessionStatePath}`);
|
|
473
|
+
console.log(`Final status: ${result.session.status}`);
|
|
474
|
+
if (result.syncResult) {
|
|
475
|
+
console.log(`Final sync verified: ${result.syncResult.inSync ? "yes" : "no"}`);
|
|
476
|
+
}
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const result = getPencilSessionStatus({
|
|
481
|
+
projectPath,
|
|
482
|
+
homeDir
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
console.log(`Project: ${result.projectRoot}`);
|
|
486
|
+
console.log(`Session state: ${result.sessionStatePath}`);
|
|
487
|
+
console.log(`Session status: ${result.session ? result.session.status : "missing"}`);
|
|
488
|
+
console.log(`Lock status: ${result.lockStatus.lock ? "locked" : "unlocked"}`);
|
|
489
|
+
if (result.session) {
|
|
490
|
+
console.log(`Pen path: ${result.session.penPath}`);
|
|
491
|
+
console.log(`Last persisted hash: ${result.session.lastPersistedHash || "(missing)"}`);
|
|
492
|
+
}
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
236
496
|
throw new Error(`Unknown command: ${command}`);
|
|
237
497
|
}
|
|
238
498
|
|
package/lib/mcp-runtime-gate.js
CHANGED
|
@@ -52,6 +52,10 @@ function resolveProjectPath(projectRoot, targetPath) {
|
|
|
52
52
|
return path.normalize(path.resolve(projectRoot, targetPath));
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
function normalizeHash(value) {
|
|
56
|
+
return typeof value === "string" ? value.trim() : "";
|
|
57
|
+
}
|
|
58
|
+
|
|
55
59
|
function isUnnamedEditor(activeEditor) {
|
|
56
60
|
const normalized = normalizeEditorName(activeEditor).toLowerCase();
|
|
57
61
|
return normalized === "" || normalized === "new";
|
|
@@ -91,12 +95,17 @@ function derivePhase(snapshot) {
|
|
|
91
95
|
|
|
92
96
|
function evaluateSourceConvergence(snapshot) {
|
|
93
97
|
const notes = [];
|
|
98
|
+
const phase = derivePhase(snapshot);
|
|
94
99
|
const activeEditor = normalizeEditorName(snapshot.activeEditor);
|
|
95
100
|
const registeredPenPath = normalizeRegisteredPath(snapshot.registeredPenPath);
|
|
96
101
|
const projectRoot = typeof snapshot.projectRoot === "string" ? snapshot.projectRoot.trim() : "";
|
|
97
102
|
const shellVisiblePenExists = Boolean(snapshot.shellVisiblePenExists);
|
|
98
103
|
const documentedReconciliation = Boolean(snapshot.documentedReconciliation);
|
|
99
104
|
const noNewPencilEditsYet = Boolean(snapshot.noNewPencilEditsYet);
|
|
105
|
+
const usedEmptyFilePath = Boolean(snapshot.usedEmptyFilePath);
|
|
106
|
+
const penSyncVerified = snapshot.penSyncVerified === true;
|
|
107
|
+
const liveSnapshotHash = normalizeHash(snapshot.liveSnapshotHash);
|
|
108
|
+
const persistedSnapshotHash = normalizeHash(snapshot.persistedSnapshotHash);
|
|
100
109
|
|
|
101
110
|
if (Boolean(snapshot.mcpAvailable) === false) {
|
|
102
111
|
return {
|
|
@@ -119,6 +128,13 @@ function evaluateSourceConvergence(snapshot) {
|
|
|
119
128
|
};
|
|
120
129
|
}
|
|
121
130
|
|
|
131
|
+
if (usedEmptyFilePath) {
|
|
132
|
+
return {
|
|
133
|
+
status: BLOCK,
|
|
134
|
+
notes: ["Pencil operations used an empty `filePath` even though the workflow has a registered project-local `.pen` source."]
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
122
138
|
if (!shellVisiblePenExists) {
|
|
123
139
|
return {
|
|
124
140
|
status: BLOCK,
|
|
@@ -142,6 +158,29 @@ function evaluateSourceConvergence(snapshot) {
|
|
|
142
158
|
};
|
|
143
159
|
}
|
|
144
160
|
|
|
161
|
+
if (phase === "completion") {
|
|
162
|
+
if (!penSyncVerified) {
|
|
163
|
+
return {
|
|
164
|
+
status: BLOCK,
|
|
165
|
+
notes: ["No explicit live-to-disk `.pen` sync verification was recorded before completion."]
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!liveSnapshotHash || !persistedSnapshotHash) {
|
|
170
|
+
return {
|
|
171
|
+
status: BLOCK,
|
|
172
|
+
notes: ["Completion requires both live and persisted `.pen` snapshot hashes."]
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (liveSnapshotHash !== persistedSnapshotHash) {
|
|
177
|
+
return {
|
|
178
|
+
status: BLOCK,
|
|
179
|
+
notes: ["Current live Pencil snapshot does not match the last persisted `.pen` snapshot."]
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
145
184
|
if (noNewPencilEditsYet) {
|
|
146
185
|
notes.push("No new Pencil edits were recorded yet; source convergence is only provisionally confirmed.");
|
|
147
186
|
return {
|
|
@@ -150,9 +189,16 @@ function evaluateSourceConvergence(snapshot) {
|
|
|
150
189
|
};
|
|
151
190
|
}
|
|
152
191
|
|
|
192
|
+
if (penSyncVerified) {
|
|
193
|
+
notes.push("Live Pencil snapshot was explicitly verified against the persisted project-local `.pen` source.");
|
|
194
|
+
}
|
|
195
|
+
|
|
153
196
|
return {
|
|
154
197
|
status: PASS,
|
|
155
|
-
notes: [
|
|
198
|
+
notes: [
|
|
199
|
+
"Active Pencil editor matches the registered project-local `.pen` source and the file exists on disk.",
|
|
200
|
+
...notes
|
|
201
|
+
]
|
|
156
202
|
};
|
|
157
203
|
}
|
|
158
204
|
|
|
@@ -296,6 +342,9 @@ function evaluateMcpRuntimeGate(snapshot = {}) {
|
|
|
296
342
|
shellVisiblePenPath:
|
|
297
343
|
typeof snapshot.shellVisiblePenPath === "string" ? snapshot.shellVisiblePenPath.trim() : "",
|
|
298
344
|
shellVisiblePenExists: Boolean(snapshot.shellVisiblePenExists),
|
|
345
|
+
penSyncVerified: snapshot.penSyncVerified === true,
|
|
346
|
+
liveSnapshotHash: normalizeHash(snapshot.liveSnapshotHash),
|
|
347
|
+
persistedSnapshotHash: normalizeHash(snapshot.persistedSnapshotHash),
|
|
299
348
|
claimedAnchorIds: normalizeArray(snapshot.claimedAnchorIds),
|
|
300
349
|
claimedReviewedScreenIds: normalizeArray(snapshot.claimedReviewedScreenIds),
|
|
301
350
|
reviewTargets: normalizeArray(snapshot.reviewTargets),
|
|
@@ -320,6 +369,9 @@ function formatMcpRuntimeGateSection(snapshot, result, options = {}) {
|
|
|
320
369
|
`- Registered \`.pen\` path: ${result.registeredPenPath || "(missing)"}`,
|
|
321
370
|
`- Shell-visible \`.pen\` path: ${result.shellVisiblePenPath || "(missing)"}`,
|
|
322
371
|
`- Shell-visible \`.pen\` exists: ${result.shellVisiblePenExists ? "yes" : "no"}`,
|
|
372
|
+
`- Pen sync verified: ${result.penSyncVerified ? "yes" : "no"}`,
|
|
373
|
+
`- Live snapshot hash: ${result.liveSnapshotHash || "(missing)"}`,
|
|
374
|
+
`- Persisted snapshot hash: ${result.persistedSnapshotHash || "(missing)"}`,
|
|
323
375
|
`- Claimed anchor ids: ${result.claimedAnchorIds.length > 0 ? result.claimedAnchorIds.join(", ") : "(none)"}`,
|
|
324
376
|
`- Reviewed screen ids: ${result.claimedReviewedScreenIds.length > 0 ? result.claimedReviewedScreenIds.join(", ") : "(none)"}`,
|
|
325
377
|
`- Screenshot target ids: ${result.reviewTargets.length > 0 ? result.reviewTargets.join(", ") : "(none)"}`,
|