@xenonbyte/da-vinci-workflow 0.1.19 → 0.1.21

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 (56) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +40 -54
  3. package/README.zh-CN.md +34 -54
  4. package/SKILL.md +4 -0
  5. package/commands/claude/dv/build.md +6 -0
  6. package/commands/claude/dv/continue.md +5 -0
  7. package/commands/codex/prompts/dv-build.md +4 -0
  8. package/commands/codex/prompts/dv-continue.md +5 -0
  9. package/commands/gemini/dv/build.toml +4 -0
  10. package/commands/gemini/dv/continue.toml +5 -0
  11. package/docs/codex-natural-language-usage.md +3 -0
  12. package/docs/dv-command-reference.md +10 -0
  13. package/docs/mode-use-cases.md +2 -0
  14. package/docs/pencil-rendering-workflow.md +16 -0
  15. package/docs/prompt-entrypoints.md +7 -0
  16. package/docs/prompt-presets/README.md +2 -0
  17. package/docs/prompt-presets/desktop-app.md +4 -0
  18. package/docs/prompt-presets/mobile-app.md +4 -0
  19. package/docs/prompt-presets/tablet-app.md +4 -0
  20. package/docs/prompt-presets/web-app.md +4 -0
  21. package/docs/visual-adapters.md +24 -80
  22. package/docs/visual-assist-presets/desktop-app.md +20 -68
  23. package/docs/visual-assist-presets/mobile-app.md +20 -68
  24. package/docs/visual-assist-presets/tablet-app.md +20 -68
  25. package/docs/visual-assist-presets/web-app.md +20 -68
  26. package/docs/workflow-examples.md +2 -0
  27. package/docs/workflow-overview.md +11 -0
  28. package/docs/zh-CN/codex-natural-language-usage.md +3 -0
  29. package/docs/zh-CN/dv-command-reference.md +10 -0
  30. package/docs/zh-CN/mode-use-cases.md +2 -0
  31. package/docs/zh-CN/pencil-rendering-workflow.md +16 -0
  32. package/docs/zh-CN/prompt-entrypoints.md +7 -0
  33. package/docs/zh-CN/prompt-presets/README.md +2 -0
  34. package/docs/zh-CN/prompt-presets/desktop-app.md +3 -0
  35. package/docs/zh-CN/prompt-presets/mobile-app.md +3 -0
  36. package/docs/zh-CN/prompt-presets/tablet-app.md +3 -0
  37. package/docs/zh-CN/prompt-presets/web-app.md +3 -0
  38. package/docs/zh-CN/visual-adapters.md +24 -80
  39. package/docs/zh-CN/visual-assist-presets/desktop-app.md +20 -68
  40. package/docs/zh-CN/visual-assist-presets/mobile-app.md +20 -68
  41. package/docs/zh-CN/visual-assist-presets/tablet-app.md +20 -68
  42. package/docs/zh-CN/visual-assist-presets/web-app.md +20 -68
  43. package/docs/zh-CN/workflow-examples.md +2 -0
  44. package/docs/zh-CN/workflow-overview.md +11 -0
  45. package/examples/greenfield-spec-markupflow/DA-VINCI.md +4 -13
  46. package/lib/audit.js +455 -0
  47. package/lib/cli.js +6 -1
  48. package/lib/pencil-session.js +6 -0
  49. package/package.json +2 -1
  50. package/references/artifact-templates.md +38 -0
  51. package/references/checkpoints.md +16 -0
  52. package/references/prompt-recipes.md +5 -0
  53. package/scripts/test-audit-context-delta.js +446 -0
  54. package/scripts/test-mode-consistency.js +50 -0
  55. package/scripts/test-pencil-session.js +40 -0
  56. package/scripts/test-persistence-flows.js +31 -1
@@ -18,20 +18,10 @@
18
18
 
19
19
  ```md
20
20
  ## Visual Assist
21
- - Preferred adapters:
22
- - ui-ux-pro-max
23
- - frontend-skill
24
- - Scope:
25
- - visual contract refinement
26
- - page composition
27
- - hierarchy and spacing
28
- - responsive motion guidance
29
- - anchor-surface composition
30
- - Pencil design refinement
31
- - Fallback:
32
- - native-da-vinci
33
- - Require Adapter:
34
- - false
21
+ - Preferred adapters: ui-ux-pro-max, frontend-skill
22
+ - Scope: visual contract refinement, page composition, hierarchy and spacing, responsive motion guidance, anchor-surface composition, Pencil design refinement
23
+ - Fallback: native-da-vinci
24
+ - Require Adapter: false
35
25
  ```
36
26
 
37
27
  ### 变体 2:reviewer 建议性审查
@@ -43,33 +33,14 @@
43
33
 
44
34
  ```md
45
35
  ## Visual Assist
46
- - Preferred adapters:
47
- - ui-ux-pro-max
48
- - frontend-skill
49
- - Design-supervisor reviewers:
50
- - frontend-skill
51
- - ui-ux-pro-max
52
- - Design-supervisor review mode:
53
- - screenshot-and-theme
54
- - Design-supervisor review inputs:
55
- - screenshots
56
- - pencil variables
57
- - visual thesis
58
- - content plan
59
- - interaction thesis
60
- - Scope:
61
- - visual contract refinement
62
- - page composition
63
- - hierarchy and spacing
64
- - responsive motion guidance
65
- - anchor-surface composition
66
- - Pencil design refinement
67
- - Fallback:
68
- - native-da-vinci
69
- - Require Adapter:
70
- - false
71
- - Require Supervisor Review:
72
- - false
36
+ - Preferred adapters: ui-ux-pro-max, frontend-skill
37
+ - Design-supervisor reviewers: frontend-skill, ui-ux-pro-max
38
+ - Design-supervisor review mode: screenshot-and-theme
39
+ - Design-supervisor review inputs: screenshots, pencil variables, visual thesis, content plan, interaction thesis
40
+ - Scope: visual contract refinement, page composition, hierarchy and spacing, responsive motion guidance, anchor-surface composition, Pencil design refinement
41
+ - Fallback: native-da-vinci
42
+ - Require Adapter: false
43
+ - Require Supervisor Review: false
73
44
  ```
74
45
 
75
46
  ### 变体 3:reviewer 硬签字
@@ -82,33 +53,14 @@
82
53
 
83
54
  ```md
84
55
  ## Visual Assist
85
- - Preferred adapters:
86
- - frontend-skill
87
- - ui-ux-pro-max
88
- - Design-supervisor reviewers:
89
- - frontend-skill
90
- - ui-ux-pro-max
91
- - Design-supervisor review mode:
92
- - screenshot-and-theme
93
- - Design-supervisor review inputs:
94
- - screenshots
95
- - pencil variables
96
- - visual thesis
97
- - content plan
98
- - interaction thesis
99
- - Scope:
100
- - visual contract refinement
101
- - page composition
102
- - hierarchy and spacing
103
- - responsive motion guidance
104
- - anchor-surface composition
105
- - Pencil design refinement
106
- - Fallback:
107
- - native-da-vinci
108
- - Require Adapter:
109
- - true
110
- - Require Supervisor Review:
111
- - true
56
+ - Preferred adapters: frontend-skill, ui-ux-pro-max
57
+ - Design-supervisor reviewers: frontend-skill, ui-ux-pro-max
58
+ - Design-supervisor review mode: screenshot-and-theme
59
+ - Design-supervisor review inputs: screenshots, pencil variables, visual thesis, content plan, interaction thesis
60
+ - Scope: visual contract refinement, page composition, hierarchy and spacing, responsive motion guidance, anchor-surface composition, Pencil design refinement
61
+ - Fallback: native-da-vinci
62
+ - Require Adapter: true
63
+ - Require Supervisor Review: true
112
64
  ```
113
65
 
114
66
  通用说明:
@@ -16,6 +16,8 @@
16
16
 
17
17
  - `intake` 和 `continue` 通常应该回到主工作流入口
18
18
  - 不应默认把用户导向 `build`
19
+ - `continue` 选路先看工件/checkpoint 真相,再把 Context Delta 当作辅助恢复信息
20
+ - 如果 Context Delta 与当前工件冲突,选路时应忽略冲突内容并记录冲突
19
21
 
20
22
  ## 1. `greenfield-spec`
21
23
 
@@ -130,6 +130,7 @@ anchor 通过后,再抽 shared primitives,然后再扩更多页面。
130
130
  - `design-source checkpoint`
131
131
  - 如果启用了 Pencil MCP,则跑 `MCP runtime gate`
132
132
  - 工作中期跑 `da-vinci audit --mode integrity <project-path>`
133
+ - 在现有 change 工件里记录 checkpoint 邻近的 `Context Delta`
133
134
 
134
135
  这些检查用来确认:
135
136
 
@@ -137,6 +138,7 @@ anchor 通过后,再抽 shared primitives,然后再扩更多页面。
137
138
  - active editor 是正确的设计源
138
139
  - shell 上确实有 `.pen`
139
140
  - live snapshot 和 persisted snapshot 已同步
141
+ - 最近关键执行上下文可恢复,但不会替代工件真相的选路权
140
142
 
141
143
  ### 6. Mapping
142
144
 
@@ -165,6 +167,10 @@ mapping 通过后:
165
167
  - 如果用了 Pencil MCP,runtime gate 结果可接受
166
168
  - `da-vinci audit --mode completion --change <change-id> <project-path>` 通过
167
169
 
170
+ 补充说明:
171
+
172
+ - Context Delta 的告警用于提升续跑质量,本身不会单独形成新的 completion 阻断
173
+
168
174
  ## 门禁与审计
169
175
 
170
176
  ### Design Checkpoint
@@ -219,6 +225,11 @@ mapping 通过后:
219
225
 
220
226
  在任何终态完成声明前跑。
221
227
 
228
+ audit 中对 Context Delta 的处理:
229
+
230
+ - 缺失、字段不完整、或过期冲突都属于告警级信号
231
+ - 选路和完成真相仍以工件、checkpoint、runtime gate 与文件系统审计为准
232
+
222
233
  ## 流程图
223
234
 
224
235
  ```mermaid
@@ -35,19 +35,10 @@
35
35
  - CTA regions should preserve the same dark product language
36
36
 
37
37
  ## Visual Assist
38
- - Preferred adapters:
39
- - frontend-skill
40
- - ui-ux-pro-max
41
- - Scope:
42
- - visual contract refinement
43
- - page composition
44
- - hierarchy and spacing
45
- - anchor-surface composition
46
- - Pencil design refinement
47
- - Fallback:
48
- - native-da-vinci
49
- - Require Adapter:
50
- - false
38
+ - Preferred adapters: frontend-skill, ui-ux-pro-max
39
+ - Scope: visual contract refinement, page composition, hierarchy and spacing, anchor-surface composition, Pencil design refinement
40
+ - Fallback: native-da-vinci
41
+ - Require Adapter: false
51
42
 
52
43
  ## Do
53
44
  - keep page sections clearly bounded
package/lib/audit.js CHANGED
@@ -121,6 +121,12 @@ function addMissingArtifacts(projectRoot, artifactPaths, targetList) {
121
121
  }
122
122
  }
123
123
 
124
+ function pushUnique(targetList, message) {
125
+ if (!targetList.includes(message)) {
126
+ targetList.push(message);
127
+ }
128
+ }
129
+
124
130
  function getMarkdownSection(text, heading) {
125
131
  if (!text) {
126
132
  return "";
@@ -151,6 +157,323 @@ function getMarkdownSection(text, heading) {
151
157
  return sectionLines.join("\n").trim();
152
158
  }
153
159
 
160
+ function normalizeCheckpointLabel(value) {
161
+ return String(value || "")
162
+ .toLowerCase()
163
+ .replace(/`/g, "")
164
+ .replace(/[_-]+/g, " ")
165
+ .replace(/\s+/g, " ")
166
+ .trim();
167
+ }
168
+
169
+ function parseCheckpointStatusMap(markdownText) {
170
+ const section = getMarkdownSection(markdownText, "Checkpoint Status");
171
+ if (!section) {
172
+ return {};
173
+ }
174
+
175
+ const statuses = {};
176
+ const matches = section.matchAll(/(?:^|\n)\s*-\s*`?([^`:\n]+?)`?\s*:\s*(PASS|WARN|BLOCK)\b/gi);
177
+ for (const match of matches) {
178
+ const label = normalizeCheckpointLabel(match[1]);
179
+ if (!label) {
180
+ continue;
181
+ }
182
+ statuses[label] = String(match[2]).toUpperCase();
183
+ }
184
+
185
+ return statuses;
186
+ }
187
+
188
+ function hasContextDeltaExpectationSignals(markdownText) {
189
+ const text = String(markdownText || "");
190
+ return (
191
+ /##\s+(Checkpoint Status|MCP Runtime Gate)\b/i.test(text) ||
192
+ /(?:^|\n)\s*(?:[-*]\s*)?`?Context Delta Required`?\s*:\s*(?:true|yes|on|1)\b/i.test(text)
193
+ );
194
+ }
195
+
196
+ function parseSupersedesTokens(value) {
197
+ return String(value || "")
198
+ .split(/[,\n;]/)
199
+ .map((token) => token.trim())
200
+ .filter(Boolean);
201
+ }
202
+
203
+ function normalizeTimeToken(value) {
204
+ const raw = String(value || "").trim();
205
+ if (!raw) {
206
+ return "";
207
+ }
208
+
209
+ const parseCandidates = [];
210
+ const rawWithT = raw.includes(" ") ? raw.replace(/\s+/, "T") : raw;
211
+ const hasExplicitTimezone = /(?:Z|[+-]\d{2}:?\d{2})$/i.test(rawWithT);
212
+
213
+ if (!hasExplicitTimezone && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(rawWithT)) {
214
+ parseCandidates.push(`${rawWithT}Z`);
215
+ }
216
+
217
+ parseCandidates.push(rawWithT);
218
+ parseCandidates.push(raw);
219
+
220
+ for (const candidate of parseCandidates) {
221
+ const timestamp = Date.parse(candidate);
222
+ if (!Number.isFinite(timestamp)) {
223
+ continue;
224
+ }
225
+ return new Date(timestamp).toISOString();
226
+ }
227
+
228
+ return "";
229
+ }
230
+
231
+ function getTimeReferenceKeys(value) {
232
+ const raw = String(value || "").trim();
233
+ if (!raw) {
234
+ return [];
235
+ }
236
+
237
+ const keys = new Set([raw]);
238
+
239
+ if (raw.includes(" ")) {
240
+ keys.add(raw.replace(/\s+/, "T"));
241
+ }
242
+
243
+ if (raw.endsWith(".000Z")) {
244
+ keys.add(raw.replace(".000Z", "Z"));
245
+ }
246
+
247
+ const normalized = normalizeTimeToken(raw);
248
+ if (normalized) {
249
+ keys.add(normalized);
250
+ if (normalized.endsWith(".000Z")) {
251
+ keys.add(normalized.replace(".000Z", "Z"));
252
+ }
253
+ }
254
+
255
+ return [...keys];
256
+ }
257
+
258
+ function buildContextDeltaReferenceIndex(entries) {
259
+ const referenceIndex = new Map();
260
+
261
+ function addReference(key, entryIndex) {
262
+ const normalizedKey = String(key || "").trim();
263
+ if (!normalizedKey) {
264
+ return;
265
+ }
266
+
267
+ const existing = referenceIndex.get(normalizedKey) || [];
268
+ existing.push(entryIndex);
269
+ referenceIndex.set(normalizedKey, existing);
270
+ }
271
+
272
+ entries.forEach((entry, entryIndex) => {
273
+ if (entry.time) {
274
+ for (const timeKey of getTimeReferenceKeys(entry.time)) {
275
+ addReference(timeKey, entryIndex);
276
+ addReference(`time:${timeKey}`, entryIndex);
277
+ }
278
+ }
279
+
280
+ if (entry.checkpointType && entry.time) {
281
+ const normalizedCheckpointType = normalizeCheckpointLabel(entry.checkpointType);
282
+ for (const timeKey of getTimeReferenceKeys(entry.time)) {
283
+ addReference(`${entry.checkpointType}@${timeKey}`, entryIndex);
284
+ addReference(`${normalizedCheckpointType}@${timeKey}`, entryIndex);
285
+ }
286
+ }
287
+ });
288
+
289
+ return referenceIndex;
290
+ }
291
+
292
+ function getSupersedesCandidateKeys(token) {
293
+ const trimmed = String(token || "").trim();
294
+ if (!trimmed) {
295
+ return [];
296
+ }
297
+
298
+ const keys = new Set([trimmed]);
299
+ const prefixedTimeMatch = trimmed.match(/^time\s*:\s*(.+)$/i);
300
+ if (prefixedTimeMatch) {
301
+ for (const timeKey of getTimeReferenceKeys(prefixedTimeMatch[1])) {
302
+ keys.add(timeKey);
303
+ keys.add(`time:${timeKey}`);
304
+ }
305
+ } else {
306
+ keys.add(`time:${trimmed}`);
307
+ for (const timeKey of getTimeReferenceKeys(trimmed)) {
308
+ keys.add(timeKey);
309
+ keys.add(`time:${timeKey}`);
310
+ }
311
+ }
312
+
313
+ const atIndex = trimmed.indexOf("@");
314
+ if (atIndex > 0 && atIndex < trimmed.length - 1) {
315
+ const checkpointType = trimmed.slice(0, atIndex).trim();
316
+ const time = trimmed.slice(atIndex + 1).trim();
317
+ if (checkpointType && time) {
318
+ const normalizedCheckpointType = normalizeCheckpointLabel(checkpointType);
319
+ for (const timeKey of getTimeReferenceKeys(time)) {
320
+ keys.add(`${checkpointType}@${timeKey}`);
321
+ keys.add(`${normalizedCheckpointType}@${timeKey}`);
322
+ }
323
+ }
324
+ }
325
+
326
+ return [...keys];
327
+ }
328
+
329
+ function resolveSupersedesReferenceIndices(token, referenceIndex) {
330
+ const indices = new Set();
331
+ for (const key of getSupersedesCandidateKeys(token)) {
332
+ const matches = referenceIndex.get(key);
333
+ if (!matches) {
334
+ continue;
335
+ }
336
+ for (const entryIndex of matches) {
337
+ indices.add(entryIndex);
338
+ }
339
+ }
340
+ return [...indices].sort((a, b) => a - b);
341
+ }
342
+
343
+ function inspectContextDelta(markdownText) {
344
+ const section = getMarkdownSection(markdownText, "Context Delta");
345
+ if (!section) {
346
+ return {
347
+ found: false,
348
+ hasConcreteEntry: false,
349
+ entries: [],
350
+ incompleteEntryCount: 0
351
+ };
352
+ }
353
+
354
+ const lines = String(section).replace(/\r\n?/g, "\n").split("\n");
355
+ const entries = [];
356
+ let current = null;
357
+
358
+ function ensureCurrent() {
359
+ if (!current) {
360
+ current = {};
361
+ }
362
+ }
363
+
364
+ function flushCurrent() {
365
+ if (!current) {
366
+ return;
367
+ }
368
+
369
+ const hasAnyValue = [
370
+ current.time,
371
+ current.checkpointType,
372
+ current.goal,
373
+ current.decision,
374
+ current.constraints,
375
+ current.impact,
376
+ current.status,
377
+ current.nextAction,
378
+ current.supersedes
379
+ ].some((value) => Boolean(value));
380
+
381
+ if (hasAnyValue) {
382
+ entries.push(current);
383
+ }
384
+ current = null;
385
+ }
386
+
387
+ for (const rawLine of lines) {
388
+ const line = rawLine.trim();
389
+
390
+ if (!line) {
391
+ continue;
392
+ }
393
+
394
+ const timeMatch = line.match(/^-+\s*`?time`?\s*:\s*(.+)$/i);
395
+ if (timeMatch) {
396
+ flushCurrent();
397
+ current = {
398
+ time: timeMatch[1].trim()
399
+ };
400
+ continue;
401
+ }
402
+
403
+ const checkpointTypeMatch = line.match(/^-+\s*`?checkpoint(?:[_ -]?type)?`?\s*:\s*(.+)$/i);
404
+ if (checkpointTypeMatch) {
405
+ ensureCurrent();
406
+ current.checkpointType = checkpointTypeMatch[1].trim();
407
+ continue;
408
+ }
409
+
410
+ const goalMatch = line.match(/^-+\s*`?goal`?\s*:\s*(.+)$/i);
411
+ if (goalMatch) {
412
+ ensureCurrent();
413
+ current.goal = goalMatch[1].trim();
414
+ continue;
415
+ }
416
+
417
+ const decisionMatch = line.match(/^-+\s*`?decision`?\s*:\s*(.+)$/i);
418
+ if (decisionMatch) {
419
+ ensureCurrent();
420
+ current.decision = decisionMatch[1].trim();
421
+ continue;
422
+ }
423
+
424
+ const constraintsMatch = line.match(/^-+\s*`?constraints`?\s*:\s*(.+)$/i);
425
+ if (constraintsMatch) {
426
+ ensureCurrent();
427
+ current.constraints = constraintsMatch[1].trim();
428
+ continue;
429
+ }
430
+
431
+ const impactMatch = line.match(/^-+\s*`?impact`?\s*:\s*(.+)$/i);
432
+ if (impactMatch) {
433
+ ensureCurrent();
434
+ current.impact = impactMatch[1].trim();
435
+ continue;
436
+ }
437
+
438
+ const statusMatch = line.match(/^-+\s*`?status`?\s*:\s*(PASS|WARN|BLOCK)\b/i);
439
+ if (statusMatch) {
440
+ ensureCurrent();
441
+ current.status = String(statusMatch[1]).toUpperCase();
442
+ continue;
443
+ }
444
+
445
+ const nextActionMatch = line.match(/^-+\s*`?next(?:[_ -]?action)?`?\s*:\s*(.+)$/i);
446
+ if (nextActionMatch) {
447
+ ensureCurrent();
448
+ current.nextAction = nextActionMatch[1].trim();
449
+ continue;
450
+ }
451
+
452
+ const supersedesMatch = line.match(/^-+\s*`?supersedes`?\s*:\s*(.+)$/i);
453
+ if (supersedesMatch) {
454
+ ensureCurrent();
455
+ current.supersedes = supersedesMatch[1].trim();
456
+ continue;
457
+ }
458
+ }
459
+
460
+ flushCurrent();
461
+
462
+ const hasConcreteEntry = entries.some(
463
+ (entry) => entry.time || entry.checkpointType || entry.status || entry.decision || entry.nextAction
464
+ );
465
+ const incompleteEntryCount = entries.filter(
466
+ (entry) => !entry.time || !entry.checkpointType || !entry.status
467
+ ).length;
468
+
469
+ return {
470
+ found: true,
471
+ hasConcreteEntry,
472
+ entries,
473
+ incompleteEntryCount
474
+ };
475
+ }
476
+
154
477
  function getVisualAssistFieldValues(daVinciText, fieldName) {
155
478
  const section = getMarkdownSection(daVinciText, "Visual Assist");
156
479
  if (!section) {
@@ -547,6 +870,138 @@ function auditProject(projectPathInput, options = {}) {
547
870
  }
548
871
  }
549
872
 
873
+ const tasksPath = path.join(changeDir, "tasks.md");
874
+ const verificationPath = path.join(changeDir, "verification.md");
875
+ const contextArtifacts = [pencilDesignPath, tasksPath, verificationPath]
876
+ .filter((artifactPath) => pathExists(artifactPath))
877
+ .map((artifactPath) => ({
878
+ path: artifactPath,
879
+ text: readTextIfExists(artifactPath)
880
+ }));
881
+ const contextCandidates = contextArtifacts.filter((artifact) =>
882
+ hasContextDeltaExpectationSignals(artifact.text)
883
+ );
884
+
885
+ if (contextCandidates.length > 0) {
886
+ const contextInspections = contextCandidates.map((artifact) => ({
887
+ path: artifact.path,
888
+ inspection: inspectContextDelta(artifact.text),
889
+ checkpointStatuses: parseCheckpointStatusMap(artifact.text)
890
+ }));
891
+
892
+ const hasAnyConcreteContextDelta = contextInspections.some(
893
+ (item) => item.inspection.hasConcreteEntry
894
+ );
895
+
896
+ if (!hasAnyConcreteContextDelta) {
897
+ pushUnique(
898
+ warnings,
899
+ `Checkpoint-bearing artifacts in ${changeRel} do not record any concrete \`## Context Delta\` entries.`
900
+ );
901
+ } else {
902
+ notes.push(`Detected context-delta recovery notes in ${changeRel}.`);
903
+ }
904
+
905
+ for (const item of contextInspections) {
906
+ if (!item.inspection.found || item.inspection.incompleteEntryCount === 0) {
907
+ continue;
908
+ }
909
+
910
+ pushUnique(
911
+ warnings,
912
+ `${relativeTo(projectRoot, item.path)} has ${item.inspection.incompleteEntryCount} context-delta entr` +
913
+ `${item.inspection.incompleteEntryCount === 1 ? "y" : "ies"} missing one of: time/checkpoint_type/status.`
914
+ );
915
+ }
916
+
917
+ for (const item of contextInspections) {
918
+ if (!item.inspection.found || item.inspection.entries.length === 0) {
919
+ continue;
920
+ }
921
+
922
+ const referenceIndex = buildContextDeltaReferenceIndex(item.inspection.entries);
923
+
924
+ item.inspection.entries.forEach((entry, entryIndex) => {
925
+ if (!entry.supersedes) {
926
+ return;
927
+ }
928
+
929
+ const entryLabel = entry.time ? `time \`${entry.time}\`` : `entry #${entryIndex + 1}`;
930
+ const supersedesTokens = parseSupersedesTokens(entry.supersedes);
931
+
932
+ if (supersedesTokens.length === 0) {
933
+ pushUnique(
934
+ warnings,
935
+ `${relativeTo(projectRoot, item.path)} has ${entryLabel} with an empty \`supersedes\` reference.`
936
+ );
937
+ return;
938
+ }
939
+
940
+ for (const token of supersedesTokens) {
941
+ const resolvedIndices = resolveSupersedesReferenceIndices(token, referenceIndex);
942
+
943
+ if (resolvedIndices.length === 0) {
944
+ pushUnique(
945
+ warnings,
946
+ `${relativeTo(projectRoot, item.path)} has ${entryLabel} superseding \`${token}\`, ` +
947
+ "but no referenced context-delta entry exists in the same artifact."
948
+ );
949
+ continue;
950
+ }
951
+
952
+ const hasOlderMatch = resolvedIndices.some((resolvedIndex) => resolvedIndex < entryIndex);
953
+ if (!hasOlderMatch) {
954
+ pushUnique(
955
+ warnings,
956
+ `${relativeTo(projectRoot, item.path)} has ${entryLabel} superseding \`${token}\`, ` +
957
+ "but the reference does not point to an earlier context-delta entry."
958
+ );
959
+ }
960
+ }
961
+ });
962
+ }
963
+
964
+ if (mode === "completion" && scopedChangeDirs.includes(changeDir)) {
965
+ for (const item of contextInspections) {
966
+ if (!item.inspection.hasConcreteEntry || Object.keys(item.checkpointStatuses).length === 0) {
967
+ continue;
968
+ }
969
+
970
+ const entriesByType = new Map();
971
+ for (const entry of item.inspection.entries) {
972
+ if (!entry.checkpointType || !entry.status) {
973
+ continue;
974
+ }
975
+ const checkpointType = normalizeCheckpointLabel(entry.checkpointType);
976
+ const existing = entriesByType.get(checkpointType) || [];
977
+ existing.push(entry);
978
+ entriesByType.set(checkpointType, existing);
979
+ }
980
+
981
+ for (const [checkpointType, entries] of entriesByType.entries()) {
982
+ const currentStatus = item.checkpointStatuses[checkpointType];
983
+ if (!currentStatus || entries.length === 0) {
984
+ continue;
985
+ }
986
+
987
+ const latestEntry = entries[entries.length - 1];
988
+ if (currentStatus === latestEntry.status) {
989
+ continue;
990
+ }
991
+
992
+ const statusHistory = entries.map((entry) => entry.status).join(" -> ");
993
+ pushUnique(
994
+ warnings,
995
+ `Context-delta/checkpoint status mismatch in ${relativeTo(projectRoot, item.path)} for ` +
996
+ `\`${latestEntry.checkpointType}\`: latest context-delta status is ${latestEntry.status}, ` +
997
+ `current checkpoint status is ${currentStatus} (history: ${statusHistory}). ` +
998
+ "This may indicate stale checkpoint status or stale context-delta notes."
999
+ );
1000
+ }
1001
+ }
1002
+ }
1003
+ }
1004
+
550
1005
  const specDirs = listChildDirs(path.join(changeDir, "specs"));
551
1006
  for (const specDir of specDirs) {
552
1007
  const specFile = path.join(specDir, "spec.md");
package/lib/cli.js CHANGED
@@ -100,7 +100,7 @@ function printHelp() {
100
100
  " da-vinci pencil-lock status",
101
101
  " da-vinci pencil-session begin --project <path> --pen <path>",
102
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>]",
103
+ " da-vinci pencil-session end --project <path> --pen <path> --nodes-file <path> [--variables-file <path>]",
104
104
  " da-vinci pencil-session status --project <path>",
105
105
  " da-vinci --version",
106
106
  "",
@@ -456,6 +456,11 @@ async function runCli(argv) {
456
456
  if (!penPath) {
457
457
  throw new Error("`pencil-session end` requires `--pen <path>`.");
458
458
  }
459
+ if (!nodesFile && !force) {
460
+ throw new Error(
461
+ "`pencil-session end` requires `--nodes-file <path>` (and `--variables-file <path>` when available). Use `--force` only for emergency lock release."
462
+ );
463
+ }
459
464
 
460
465
  const result = endPencilSession({
461
466
  projectPath,
@@ -168,6 +168,12 @@ function endPencilSession(options) {
168
168
  throw new Error("A registered `.pen` path is required for session shutdown.");
169
169
  }
170
170
 
171
+ if (!options.nodesFile && !options.force) {
172
+ throw new Error(
173
+ "Cannot end Pencil session without a live MCP snapshot. Provide `--nodes-file` (and `--variables-file` when available), or use `--force` for emergency shutdown."
174
+ );
175
+ }
176
+
171
177
  let syncResult = null;
172
178
  if (options.nodesFile) {
173
179
  syncResult = comparePenSync({