@xenonbyte/da-vinci-workflow 0.1.15 → 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.
Files changed (40) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/README.md +35 -1
  3. package/README.zh-CN.md +35 -1
  4. package/SKILL.md +18 -0
  5. package/commands/claude/dv/design.md +7 -0
  6. package/commands/codex/prompts/dv-design.md +7 -0
  7. package/commands/gemini/dv/design.toml +7 -0
  8. package/docs/mode-use-cases.md +5 -1
  9. package/docs/prompt-presets/README.md +3 -0
  10. package/docs/prompt-presets/desktop-app.md +16 -1
  11. package/docs/prompt-presets/mobile-app.md +16 -1
  12. package/docs/prompt-presets/tablet-app.md +16 -1
  13. package/docs/prompt-presets/web-app.md +16 -1
  14. package/docs/visual-assist-presets/README.md +5 -0
  15. package/docs/workflow-examples.md +22 -8
  16. package/docs/zh-CN/mode-use-cases.md +13 -4
  17. package/docs/zh-CN/prompt-presets/README.md +3 -0
  18. package/docs/zh-CN/prompt-presets/desktop-app.md +16 -1
  19. package/docs/zh-CN/prompt-presets/mobile-app.md +16 -1
  20. package/docs/zh-CN/prompt-presets/tablet-app.md +16 -1
  21. package/docs/zh-CN/prompt-presets/web-app.md +16 -1
  22. package/docs/zh-CN/visual-assist-presets/README.md +5 -0
  23. package/docs/zh-CN/workflow-examples.md +22 -8
  24. package/lib/audit.js +66 -2
  25. package/lib/cli.js +356 -1
  26. package/lib/mcp-runtime-gate.js +53 -1
  27. package/lib/pen-persistence.js +515 -0
  28. package/lib/pencil-lock.js +128 -0
  29. package/lib/pencil-preflight.js +438 -0
  30. package/lib/pencil-session.js +229 -0
  31. package/package.json +6 -2
  32. package/references/artifact-templates.md +2 -0
  33. package/references/checkpoints.md +14 -0
  34. package/references/pencil-design-to-code.md +15 -0
  35. package/scripts/fixtures/complex-sample.pen +295 -0
  36. package/scripts/test-mcp-runtime-gate.js +88 -0
  37. package/scripts/test-pen-persistence.js +250 -0
  38. package/scripts/test-pencil-preflight.js +153 -0
  39. package/scripts/test-pencil-session.js +152 -0
  40. package/scripts/test-persistence-flows.js +315 -0
package/lib/cli.js CHANGED
@@ -1,3 +1,4 @@
1
+ const fs = require("fs");
1
2
  const {
2
3
  VERSION,
3
4
  installPlatforms,
@@ -6,6 +7,28 @@ const {
6
7
  validateAssets
7
8
  } = require("./install");
8
9
  const { auditProject, formatAuditReport } = require("./audit");
10
+ const {
11
+ preflightPencilBatch,
12
+ formatPencilPreflightReport,
13
+ readOperations
14
+ } = require("./pencil-preflight");
15
+ const {
16
+ writePenFromPayloadFiles,
17
+ snapshotPenFile,
18
+ ensurePenFile,
19
+ comparePenSync
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");
9
32
 
10
33
  function getOption(args, name) {
11
34
  const direct = args.find((arg) => arg.startsWith(`${name}=`));
@@ -67,14 +90,37 @@ function printHelp() {
67
90
  " da-vinci status",
68
91
  " da-vinci validate-assets",
69
92
  " da-vinci audit [project-path]",
93
+ " da-vinci preflight-pencil --ops-file <path>",
94
+ " da-vinci ensure-pen --output <path>",
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>]",
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>",
70
105
  " da-vinci --version",
71
106
  "",
72
107
  "Options:",
73
108
  " --platform <value> codex, claude, gemini, or all",
74
109
  " --home <path> override HOME for installation targets",
75
110
  " --project <path> override project path for audit",
111
+ " --pen <path> registered .pen path for sync checks",
112
+ " --ops-file <path> Pencil batch operations file for preflight",
113
+ " --input <path> input .pen file for snapshot-pen",
114
+ " --output <path> output .pen file for write-pen or snapshot-pen",
115
+ " --nodes-file <path> JSON payload from batch_get for write-pen",
116
+ " --variables-file <path> JSON payload from get_variables for write-pen",
117
+ " --verify-open reopen the written .pen with Pencil after writing",
118
+ " --version <value> explicit .pen version when writing from MCP payloads",
76
119
  " --mode <value> integrity or completion",
77
- " --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"
78
124
  ].join("\n")
79
125
  );
80
126
  }
@@ -138,6 +184,315 @@ async function runCli(argv) {
138
184
  return;
139
185
  }
140
186
 
187
+ if (command === "preflight-pencil") {
188
+ const opsFile = getOption(argv, "--ops-file");
189
+ let operations = "";
190
+
191
+ if (opsFile) {
192
+ operations = readOperations(opsFile);
193
+ } else if (!process.stdin.isTTY) {
194
+ operations = fs.readFileSync(0, "utf8");
195
+ } else {
196
+ throw new Error("`preflight-pencil` requires `--ops-file <path>` or piped stdin input.");
197
+ }
198
+
199
+ const result = preflightPencilBatch(operations);
200
+ const report = formatPencilPreflightReport(result);
201
+
202
+ if (result.status === "FAIL") {
203
+ throw new Error(report);
204
+ }
205
+
206
+ console.log(report);
207
+ return;
208
+ }
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
+
234
+ if (command === "write-pen") {
235
+ const outputPath = getOption(argv, "--output");
236
+ const nodesFile = getOption(argv, "--nodes-file");
237
+ const variablesFile = getOption(argv, "--variables-file");
238
+ const version = getOption(argv, "--version");
239
+ const verifyWithPencil = argv.includes("--verify-open");
240
+
241
+ if (!outputPath || !nodesFile) {
242
+ throw new Error("`write-pen` requires `--output <path>` and `--nodes-file <path>`.");
243
+ }
244
+
245
+ const result = writePenFromPayloadFiles({
246
+ outputPath,
247
+ nodesFile,
248
+ variablesFile,
249
+ version,
250
+ verifyWithPencil
251
+ });
252
+
253
+ console.log(`Wrote .pen file to ${result.outputPath}`);
254
+ console.log(`State file: ${result.statePath}`);
255
+ console.log(`Snapshot hash: ${result.state.snapshotHash}`);
256
+ console.log(`Top-level nodes: ${result.document.children.length}`);
257
+ if (result.verification) {
258
+ console.log(`Verified reopen with Pencil (${result.verification.topLevelCount} top-level nodes).`);
259
+ }
260
+ return;
261
+ }
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
+
301
+ if (command === "snapshot-pen") {
302
+ const inputPath = getOption(argv, "--input");
303
+ const outputPath = getOption(argv, "--output");
304
+ const version = getOption(argv, "--version");
305
+ const verifyWithPencil = argv.includes("--verify-open");
306
+
307
+ if (!inputPath || !outputPath) {
308
+ throw new Error("`snapshot-pen` requires `--input <path>` and `--output <path>`.");
309
+ }
310
+
311
+ const result = snapshotPenFile({
312
+ inputPath,
313
+ outputPath,
314
+ version,
315
+ verifyWithPencil
316
+ });
317
+
318
+ console.log(`Snapshotted ${result.inputPath} to ${result.outputPath}`);
319
+ console.log(`State file: ${result.statePath}`);
320
+ console.log(`Snapshot hash: ${result.state.snapshotHash}`);
321
+ console.log(`Top-level nodes: ${result.document.children.length}`);
322
+ if (result.verification) {
323
+ console.log(`Verified reopen with Pencil (${result.verification.topLevelCount} top-level nodes).`);
324
+ }
325
+ return;
326
+ }
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
+
141
496
  throw new Error(`Unknown command: ${command}`);
142
497
  }
143
498
 
@@ -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: ["Active Pencil editor matches the registered project-local `.pen` source and the file exists on disk."]
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)"}`,