@xenonbyte/da-vinci-workflow 0.1.25 → 0.2.1

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 (84) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +48 -67
  3. package/README.zh-CN.md +36 -66
  4. package/SKILL.md +3 -0
  5. package/commands/claude/dv/continue.md +5 -0
  6. package/commands/claude/dv/design.md +1 -0
  7. package/commands/codex/prompts/dv-continue.md +6 -1
  8. package/commands/codex/prompts/dv-design.md +1 -0
  9. package/commands/gemini/dv/continue.toml +5 -0
  10. package/commands/gemini/dv/design.toml +1 -0
  11. package/commands/templates/dv-continue.shared.md +33 -0
  12. package/docs/dv-command-reference.md +45 -2
  13. package/docs/execution-chain-migration.md +46 -0
  14. package/docs/execution-chain-plan.md +125 -0
  15. package/docs/pencil-rendering-workflow.md +9 -7
  16. package/docs/prompt-entrypoints.md +6 -0
  17. package/docs/prompt-presets/README.md +4 -0
  18. package/docs/visual-assist-presets/README.md +4 -0
  19. package/docs/workflow-examples.md +23 -11
  20. package/docs/workflow-overview.md +27 -0
  21. package/docs/zh-CN/dv-command-reference.md +45 -2
  22. package/docs/zh-CN/execution-chain-migration.md +46 -0
  23. package/docs/zh-CN/pencil-rendering-workflow.md +9 -7
  24. package/docs/zh-CN/prompt-entrypoints.md +6 -0
  25. package/docs/zh-CN/prompt-presets/README.md +5 -1
  26. package/docs/zh-CN/visual-assist-presets/README.md +5 -1
  27. package/docs/zh-CN/workflow-examples.md +23 -11
  28. package/docs/zh-CN/workflow-overview.md +27 -0
  29. package/examples/greenfield-spec-markupflow/README.md +6 -1
  30. package/lib/artifact-parsers.js +120 -0
  31. package/lib/async-offload-worker.js +26 -0
  32. package/lib/async-offload.js +82 -0
  33. package/lib/audit-parsers.js +152 -32
  34. package/lib/audit.js +145 -23
  35. package/lib/cli.js +1068 -437
  36. package/lib/diff-spec.js +242 -0
  37. package/lib/execution-signals.js +136 -0
  38. package/lib/fs-safety.js +1 -4
  39. package/lib/icon-aliases.js +7 -7
  40. package/lib/icon-search.js +21 -14
  41. package/lib/icon-sync.js +220 -41
  42. package/lib/install.js +128 -60
  43. package/lib/lint-bindings.js +143 -0
  44. package/lib/lint-spec.js +408 -0
  45. package/lib/lint-tasks.js +176 -0
  46. package/lib/mcp-runtime-gate.js +4 -7
  47. package/lib/pen-persistence.js +318 -46
  48. package/lib/pencil-lock.js +237 -25
  49. package/lib/pencil-preflight.js +233 -12
  50. package/lib/pencil-session.js +216 -36
  51. package/lib/planning-parsers.js +567 -0
  52. package/lib/scaffold.js +193 -0
  53. package/lib/scope-check.js +603 -0
  54. package/lib/sidecars.js +369 -0
  55. package/lib/supervisor-review.js +82 -35
  56. package/lib/utils.js +129 -0
  57. package/lib/verify.js +652 -0
  58. package/lib/workflow-bootstrap.js +255 -0
  59. package/lib/workflow-contract.js +107 -0
  60. package/lib/workflow-persisted-state.js +297 -0
  61. package/lib/workflow-state.js +785 -0
  62. package/package.json +21 -3
  63. package/references/artifact-templates.md +26 -0
  64. package/references/checkpoints.md +16 -0
  65. package/references/design-inputs.md +2 -0
  66. package/references/modes.md +10 -0
  67. package/references/pencil-design-to-code.md +2 -0
  68. package/scripts/fixtures/complex-sample.pen +0 -295
  69. package/scripts/fixtures/mock-pencil.js +0 -49
  70. package/scripts/test-audit-context-delta.js +0 -446
  71. package/scripts/test-audit-design-supervisor.js +0 -691
  72. package/scripts/test-audit-safety.js +0 -92
  73. package/scripts/test-icon-aliases.js +0 -96
  74. package/scripts/test-icon-search.js +0 -77
  75. package/scripts/test-icon-sync.js +0 -178
  76. package/scripts/test-mcp-runtime-gate.js +0 -287
  77. package/scripts/test-mode-consistency.js +0 -344
  78. package/scripts/test-pen-persistence.js +0 -403
  79. package/scripts/test-pencil-lock.js +0 -130
  80. package/scripts/test-pencil-preflight.js +0 -169
  81. package/scripts/test-pencil-session.js +0 -192
  82. package/scripts/test-persistence-flows.js +0 -345
  83. package/scripts/test-supervisor-review-cli.js +0 -619
  84. package/scripts/test-supervisor-review-integration.js +0 -115
package/lib/cli.js CHANGED
@@ -16,7 +16,10 @@ const {
16
16
  writePenFromPayloadFiles,
17
17
  snapshotPenFile,
18
18
  ensurePenFile,
19
- comparePenSync
19
+ comparePenSync,
20
+ comparePenBaselineAlignment,
21
+ formatPenBaselineAlignmentReport,
22
+ syncPenSource
20
23
  } = require("./pen-persistence");
21
24
  const {
22
25
  acquirePencilLock,
@@ -42,37 +45,349 @@ const {
42
45
  loadIconAliases,
43
46
  expandQueryWithAliases
44
47
  } = require("./icon-aliases");
48
+ const { sleepSync } = require("./utils");
45
49
  const {
46
50
  runDesignSupervisorReview,
47
51
  formatDesignSupervisorReviewReport
48
52
  } = require("./supervisor-review");
53
+ const {
54
+ bootstrapProjectArtifacts,
55
+ formatBootstrapProjectReport
56
+ } = require("./workflow-bootstrap");
57
+ const {
58
+ deriveWorkflowStatus,
59
+ formatWorkflowStatusReport,
60
+ formatNextStepReport
61
+ } = require("./workflow-state");
62
+ const { lintRuntimeSpecs, formatLintSpecReport } = require("./lint-spec");
63
+ const { runScopeCheck, formatScopeCheckReport } = require("./scope-check");
64
+ const { lintTasks, formatLintTasksReport } = require("./lint-tasks");
65
+ const { lintBindings, formatLintBindingsReport } = require("./lint-bindings");
66
+ const { generatePlanningSidecars, formatGenerateSidecarsReport } = require("./sidecars");
67
+ const {
68
+ verifyBindings,
69
+ verifyImplementation,
70
+ verifyStructure,
71
+ verifyCoverage,
72
+ formatVerifyReport
73
+ } = require("./verify");
74
+ const { diffSpec, formatDiffSpecReport } = require("./diff-spec");
75
+ const { scaffoldFromBindings, formatScaffoldReport } = require("./scaffold");
76
+ const { writeExecutionSignal } = require("./execution-signals");
77
+
78
+ const DEFAULT_MAX_PREFLIGHT_STDIN_BYTES = 1024 * 1024;
79
+ const DEFAULT_MAX_STDIN_TRANSIENT_RETRIES = 2000;
80
+ const DEFAULT_MAX_STDIN_TRANSIENT_BACKOFF_MS = 25;
81
+ const OPTION_FLAGS_WITH_VALUES = new Set([
82
+ "--home",
83
+ "--platform",
84
+ "--project",
85
+ "--mode",
86
+ "--change",
87
+ "--query",
88
+ "--baseline",
89
+ "--prefer-source",
90
+ "--family",
91
+ "--top",
92
+ "--catalog",
93
+ "--aliases",
94
+ "--pencil-design",
95
+ "--status",
96
+ "--source",
97
+ "--executed-reviewers",
98
+ "--codex-bin",
99
+ "--max-images",
100
+ "--review-timeout-ms",
101
+ "--review-concurrency",
102
+ "--review-retries",
103
+ "--review-retry-delay-ms",
104
+ "--review-retry-max-delay-ms",
105
+ "--issue-list",
106
+ "--revision-outcome",
107
+ "--timeout-ms",
108
+ "--ops-file",
109
+ "--input",
110
+ "--output",
111
+ "--from",
112
+ "--to",
113
+ "--pen",
114
+ "--nodes-file",
115
+ "--variables-file",
116
+ "--version",
117
+ "--owner",
118
+ "--wait-ms"
119
+ ]);
120
+
121
+ const HELP_OPTION_SPECS = [
122
+ { flag: "--platform <value>", description: "codex, claude, gemini, or all" },
123
+ { flag: "--home <path>", description: "override HOME for installation targets" },
124
+ { flag: "--project <path>", description: "override project path for audit" },
125
+ {
126
+ flag: "--catalog <path>",
127
+ description: "icon catalog path for icon-search/icon-sync (default: ~/.da-vinci/icon-catalog.json)"
128
+ },
129
+ {
130
+ flag: "--aliases <path>",
131
+ description: "icon alias mapping file for icon-search (default: ~/.da-vinci/icon-aliases.json)"
132
+ },
133
+ {
134
+ flag: "--pencil-design <path>",
135
+ description: "explicit pencil-design.md path for supervisor-review"
136
+ },
137
+ { flag: "--query <text>", description: "icon-search query text" },
138
+ {
139
+ flag: "--baseline <path>",
140
+ description: "comparison .pen path for multi-source baseline alignment checks (repeatable, supports comma-separated values)"
141
+ },
142
+ {
143
+ flag: "--prefer-source <path>",
144
+ description: "explicit baseline source of truth when compared .pen hashes diverge"
145
+ },
146
+ {
147
+ flag: "--sync-preferred-source",
148
+ description: "copy `--prefer-source` into project `--pen` when hashes diverge (begin command only)"
149
+ },
150
+ {
151
+ flag: "--family <value>",
152
+ description: "icon-search family filter: all, material, rounded, outlined, sharp, lucide, feather, phosphor"
153
+ },
154
+ { flag: "--status <value>", description: "PASS, WARN, or BLOCK for supervisor-review" },
155
+ { flag: "--source <value>", description: "review source: skill, manual, inferred" },
156
+ { flag: "--executed-reviewers <csv>", description: "reviewer skills that executed this review" },
157
+ { flag: "--run-reviewers", description: "execute configured reviewer skills through codex exec" },
158
+ {
159
+ flag: "--review-concurrency <value>",
160
+ description: "max parallel reviewer executions (default 2)"
161
+ },
162
+ {
163
+ flag: "--review-retries <value>",
164
+ description: "retry count per reviewer before failing (default 1)"
165
+ },
166
+ {
167
+ flag: "--review-retry-delay-ms <value>",
168
+ description: "initial retry delay in milliseconds (default 400, exponential backoff)"
169
+ },
170
+ {
171
+ flag: "--review-retry-max-delay-ms <value>",
172
+ description: "cap reviewer retry backoff delay in milliseconds (default 5000)"
173
+ },
174
+ {
175
+ flag: "--codex-bin <path>",
176
+ description: "codex executable path for --run-reviewers (default: codex)"
177
+ },
178
+ { flag: "--max-images <value>", description: "max screenshots attached to reviewer runs (default 6)" },
179
+ { flag: "--review-timeout-ms <value>", description: "timeout per reviewer run in milliseconds" },
180
+ { flag: "--issue-list <text>", description: "supervisor-review issue summary" },
181
+ { flag: "--revision-outcome <text>", description: "supervisor-review revision result summary" },
182
+ { flag: "--top <value>", description: "icon-search result count (1-50, default 8)" },
183
+ { flag: "--timeout-ms <value>", description: "network timeout for icon-sync requests" },
184
+ {
185
+ flag: "--strict",
186
+ description: "enable strict failure mode for commands that support advisory defaults (for example icon-sync, lint-spec)"
187
+ },
188
+ {
189
+ flag: "--continue-on-error",
190
+ description: "print BLOCK/FAIL command results without throwing process errors"
191
+ },
192
+ { flag: "--json", description: "print structured JSON output when supported by the command" },
193
+ { flag: "--pen <path>", description: "registered .pen path for sync checks" },
194
+ {
195
+ flag: "--from <path>",
196
+ description: "source path for sync-pen-source, or baseline sidecars directory for diff-spec"
197
+ },
198
+ { flag: "--to <path>", description: "destination .pen path for sync-pen-source" },
199
+ { flag: "--ops-file <path>", description: "Pencil batch operations file for preflight" },
200
+ { flag: "--input <path>", description: "input .pen file for snapshot-pen" },
201
+ { flag: "--output <path>", description: "output .pen file for write-pen or snapshot-pen" },
202
+ { flag: "--nodes-file <path>", description: "JSON payload from batch_get for write-pen" },
203
+ { flag: "--variables-file <path>", description: "JSON payload from get_variables for write-pen" },
204
+ { flag: "--verify-open", description: "reopen the written .pen with Pencil after writing" },
205
+ { flag: "--version <value>", description: "explicit .pen version when writing from MCP payloads" },
206
+ { flag: "--mode <value>", description: "integrity or completion" },
207
+ { flag: "--change <id>", description: "scope completion audit to one change id" },
208
+ { flag: "--wait-ms <value>", description: "wait for the global Pencil lock before failing" },
209
+ { flag: "--owner <value>", description: "human-readable lock owner label" },
210
+ { flag: "--force", description: "overwrite bootstrap placeholders or force commands that explicitly support it" }
211
+ ];
212
+
213
+ function readLimitedStdin(maxBytes = DEFAULT_MAX_PREFLIGHT_STDIN_BYTES, options = {}) {
214
+ const limit =
215
+ Number.isFinite(Number(maxBytes)) && Number(maxBytes) > 0
216
+ ? Number(maxBytes)
217
+ : DEFAULT_MAX_PREFLIGHT_STDIN_BYTES;
218
+ const maxTransientRetries =
219
+ Number.isFinite(Number(options.maxTransientRetries)) && Number(options.maxTransientRetries) >= 0
220
+ ? Number(options.maxTransientRetries)
221
+ : DEFAULT_MAX_STDIN_TRANSIENT_RETRIES;
222
+ const maxBackoffMs =
223
+ Number.isFinite(Number(options.maxBackoffMs)) && Number(options.maxBackoffMs) > 0
224
+ ? Number(options.maxBackoffMs)
225
+ : DEFAULT_MAX_STDIN_TRANSIENT_BACKOFF_MS;
226
+ const chunks = [];
227
+ let totalBytes = 0;
228
+ let transientReadRetries = 0;
229
+
230
+ while (true) {
231
+ const chunk = Buffer.allocUnsafe(64 * 1024);
232
+ let bytesRead;
233
+ try {
234
+ bytesRead = fs.readSync(0, chunk, 0, chunk.length, null);
235
+ } catch (error) {
236
+ if (error && (error.code === "EAGAIN" || error.code === "EINTR")) {
237
+ transientReadRetries += 1;
238
+ if (transientReadRetries > maxTransientRetries) {
239
+ throw new Error(
240
+ `Unable to read stdin after ${transientReadRetries} transient retry attempts (${error.code}).`
241
+ );
242
+ }
243
+ sleepSyncMs(Math.min(5 * transientReadRetries, maxBackoffMs));
244
+ continue;
245
+ }
246
+ throw error;
247
+ }
248
+ transientReadRetries = 0;
249
+ if (bytesRead === 0) {
250
+ break;
251
+ }
252
+
253
+ totalBytes += bytesRead;
254
+ if (totalBytes > limit) {
255
+ throw new Error(
256
+ `Piped stdin payload exceeds ${limit} bytes. Use \`--ops-file <path>\` for larger preflight batches.`
257
+ );
258
+ }
259
+
260
+ chunks.push(Buffer.from(chunk.subarray(0, bytesRead)));
261
+ }
262
+
263
+ return Buffer.concat(chunks, totalBytes).toString("utf8");
264
+ }
265
+
266
+ function sleepSyncMs(durationMs) {
267
+ sleepSync(durationMs, "back off stdin retries");
268
+ }
269
+
270
+ function collectOptionEntries(args, name) {
271
+ const entries = [];
272
+
273
+ for (let index = 0; index < args.length; index += 1) {
274
+ const arg = args[index];
275
+ if (arg === name) {
276
+ const next = args[index + 1];
277
+ if (next === undefined || String(next).startsWith("-")) {
278
+ entries.push({
279
+ form: "split",
280
+ hasValue: false,
281
+ value: ""
282
+ });
283
+ continue;
284
+ }
285
+ entries.push({
286
+ form: "split",
287
+ hasValue: true,
288
+ value: next
289
+ });
290
+ index += 1;
291
+ continue;
292
+ }
293
+
294
+ if (String(arg).startsWith(`${name}=`)) {
295
+ entries.push({
296
+ form: "inline",
297
+ hasValue: true,
298
+ value: String(arg).slice(name.length + 1)
299
+ });
300
+ }
301
+ }
302
+
303
+ return entries;
304
+ }
49
305
 
50
306
  function getOption(args, name) {
51
- const direct = args.find((arg) => arg.startsWith(`${name}=`));
52
- if (direct) {
53
- return direct.slice(name.length + 1);
307
+ const entries = collectOptionEntries(args, name);
308
+ if (entries.length === 0) {
309
+ return undefined;
54
310
  }
55
311
 
56
- const index = args.indexOf(name);
57
- if (index >= 0) {
58
- return args[index + 1];
312
+ const values = entries.filter((entry) => entry.hasValue).map((entry) => String(entry.value));
313
+ if (values.length === 0) {
314
+ return undefined;
59
315
  }
316
+ const uniqueValues = Array.from(new Set(values));
317
+ if (uniqueValues.length > 1) {
318
+ throw new Error(
319
+ `\`${name}\` was provided with conflicting values (${uniqueValues.join(", ")}). ` +
320
+ "Use a single value format (`--flag=value` or `--flag value`)."
321
+ );
322
+ }
323
+
324
+ return values[values.length - 1];
325
+ }
60
326
 
61
- return undefined;
327
+ function getOptionValues(args, name) {
328
+ return collectOptionEntries(args, name)
329
+ .filter((entry) => entry.hasValue)
330
+ .map((entry) => entry.value);
62
331
  }
63
332
 
64
- function getPositionalArgs(args, optionsWithValues = []) {
333
+ function shouldContinueOnError(args) {
334
+ return Array.isArray(args) && args.includes("--continue-on-error");
335
+ }
336
+
337
+ function emitOrThrowOnStatus(status, blockedStatuses, output, continueOnError) {
338
+ if (!Array.isArray(blockedStatuses) || !blockedStatuses.includes(status)) {
339
+ return false;
340
+ }
341
+ if (continueOnError) {
342
+ console.log(output);
343
+ return true;
344
+ }
345
+ throw new Error(output);
346
+ }
347
+
348
+ function getIntegerOption(args, name, options = {}) {
349
+ const raw = getOption(args, name);
350
+ if (raw === undefined) {
351
+ return undefined;
352
+ }
353
+
354
+ const parsed = Number.parseInt(String(raw), 10);
355
+ if (!Number.isFinite(parsed)) {
356
+ throw new Error(`\`${name}\` must be an integer.`);
357
+ }
358
+
359
+ if (Number.isFinite(options.min) && parsed < options.min) {
360
+ throw new Error(`\`${name}\` must be >= ${options.min}.`);
361
+ }
362
+ if (Number.isFinite(options.max) && parsed > options.max) {
363
+ throw new Error(`\`${name}\` must be <= ${options.max}.`);
364
+ }
365
+ return parsed;
366
+ }
367
+
368
+ function getCommaSeparatedOptionValues(args, name) {
369
+ return getOptionValues(args, name)
370
+ .flatMap((value) => String(value || "").split(","))
371
+ .map((value) => value.trim())
372
+ .filter(Boolean);
373
+ }
374
+
375
+ function getPositionalArgs(args, optionsWithValues = OPTION_FLAGS_WITH_VALUES) {
376
+ const optionNames =
377
+ optionsWithValues instanceof Set ? Array.from(optionsWithValues) : [...optionsWithValues];
378
+ const optionsWithValuesSet =
379
+ optionsWithValues instanceof Set ? optionsWithValues : new Set(optionsWithValues);
65
380
  const positional = [];
66
381
 
67
382
  for (let index = 0; index < args.length; index += 1) {
68
383
  const arg = args[index];
69
384
 
70
- if (optionsWithValues.includes(arg)) {
385
+ if (optionsWithValuesSet.has(arg)) {
71
386
  index += 1;
72
387
  continue;
73
388
  }
74
389
 
75
- if (optionsWithValues.some((name) => arg.startsWith(`${name}=`))) {
390
+ if (optionNames.some((name) => String(arg).startsWith(`${name}=`))) {
76
391
  continue;
77
392
  }
78
393
 
@@ -87,16 +402,68 @@ function getPositionalArgs(args, optionsWithValues = []) {
87
402
  }
88
403
 
89
404
  function formatStatus(status) {
90
- return [
405
+ const lines = [
91
406
  `Da Vinci v${status.version}`,
92
407
  `Home: ${status.homeDir}`,
93
408
  `Codex: prompt=${status.codex.prompt ? "yes" : "no"} skill=${status.codex.skill ? "yes" : "no"}`,
94
409
  `Claude: command=${status.claude.command ? "yes" : "no"} actions=${status.claude.actionSet ? "yes" : "no"}`,
95
410
  `Gemini: command=${status.gemini.command ? "yes" : "no"} actions=${status.gemini.actionSet ? "yes" : "no"}`
96
- ].join("\n");
411
+ ];
412
+
413
+ appendStatusIssues(lines, "codex prompt", status.codex.promptMissing, status.codex.promptMismatched, status.codex.promptUnreadable);
414
+ appendStatusIssues(lines, "codex skill", status.codex.skillMissing, status.codex.skillMismatched, status.codex.skillUnreadable);
415
+ appendStatusIssues(lines, "claude command", status.claude.commandMissing, status.claude.commandMismatched, status.claude.commandUnreadable);
416
+ appendStatusIssues(lines, "claude actions", status.claude.actionSetMissing, status.claude.actionSetMismatched, status.claude.actionSetUnreadable);
417
+ appendStatusIssues(lines, "gemini command", status.gemini.commandMissing, status.gemini.commandMismatched, status.gemini.commandUnreadable);
418
+ appendStatusIssues(lines, "gemini actions", status.gemini.actionSetMissing, status.gemini.actionSetMismatched, status.gemini.actionSetUnreadable);
419
+
420
+ return lines.join("\n");
421
+ }
422
+
423
+ function appendStatusIssues(lines, label, missing = [], mismatched = [], unreadable = []) {
424
+ if (missing.length > 0) {
425
+ lines.push(` ${label} missing: ${missing.join(", ")}`);
426
+ }
427
+ if (mismatched.length > 0) {
428
+ lines.push(` ${label} stale/mismatched: ${mismatched.join(", ")}`);
429
+ }
430
+ if (unreadable.length > 0) {
431
+ lines.push(` ${label} unreadable: ${unreadable.join(", ")}`);
432
+ }
433
+ }
434
+
435
+ function persistExecutionSignal(projectPath, changeId, surface, result, strict = false) {
436
+ try {
437
+ writeExecutionSignal(projectPath, {
438
+ changeId: changeId || "global",
439
+ surface,
440
+ status: result.status,
441
+ advisory: strict ? false : true,
442
+ strict,
443
+ failures: result.failures || [],
444
+ warnings: result.warnings || [],
445
+ notes: result.notes || []
446
+ });
447
+ } catch (error) {
448
+ // Signals are advisory metadata and should not break command execution.
449
+ const code = error && error.code ? String(error.code).toUpperCase() : "";
450
+ if (code === "EACCES" || code === "ENOSPC") {
451
+ return;
452
+ }
453
+
454
+ const message = error && error.message ? error.message : String(error);
455
+ console.error(
456
+ `Warning: failed to persist execution signal (${surface}) for change ${changeId || "global"}: ${message}`
457
+ );
458
+ }
97
459
  }
98
460
 
99
461
  function printHelp() {
462
+ const optionLines = HELP_OPTION_SPECS.map((optionSpec) => {
463
+ const paddedFlag = optionSpec.flag.padEnd(30, " ");
464
+ return ` ${paddedFlag}${optionSpec.description}`;
465
+ });
466
+
100
467
  console.log(
101
468
  [
102
469
  "Da Vinci CLI",
@@ -105,104 +472,437 @@ function printHelp() {
105
472
  " da-vinci install --platform codex,claude,gemini",
106
473
  " da-vinci uninstall --platform codex,claude,gemini",
107
474
  " da-vinci status",
475
+ " da-vinci workflow-status [--project <path>] [--change <id>] [--json]",
476
+ " da-vinci next-step [--project <path>] [--change <id>] [--json]",
477
+ " da-vinci lint-spec [--project <path>] [--change <id>] [--strict] [--json]",
478
+ " da-vinci scope-check [--project <path>] [--change <id>] [--strict] [--json]",
479
+ " da-vinci lint-tasks [--project <path>] [--change <id>] [--strict] [--json]",
480
+ " da-vinci lint-bindings [--project <path>] [--change <id>] [--strict] [--json]",
481
+ " da-vinci generate-sidecars [--project <path>] [--change <id>] [--json]",
482
+ " da-vinci verify-bindings [--project <path>] [--change <id>] [--strict] [--json]",
483
+ " da-vinci verify-implementation [--project <path>] [--change <id>] [--strict] [--json]",
484
+ " da-vinci verify-structure [--project <path>] [--change <id>] [--strict] [--json]",
485
+ " da-vinci verify-coverage [--project <path>] [--change <id>] [--strict] [--json]",
486
+ " da-vinci diff-spec [--project <path>] [--change <id>] [--from <sidecars-dir>] [--json]",
487
+ " da-vinci scaffold [--project <path>] [--change <id>] [--output <path>] [--json]",
108
488
  " da-vinci validate-assets",
489
+ " da-vinci bootstrap-project --project <path> [--change <id>] [--force]",
109
490
  " da-vinci audit [project-path]",
110
491
  " da-vinci icon-sync [--output <path>] [--timeout-ms <value>] [--strict]",
111
492
  " da-vinci icon-search --query <text> [--family <value>] [--top <value>] [--catalog <path>] [--aliases <path>] [--json]",
112
- " da-vinci supervisor-review --project <path> --change <id> [--run-reviewers] [--review-concurrency <value>] [--review-retries <value>] [--review-retry-delay-ms <value>] [--source <skill|manual|inferred>] [--executed-reviewers <csv>] [--status <PASS|WARN|BLOCK>] [--issue-list <text>] [--revision-outcome <text>] [--write] [--json]",
493
+ " da-vinci supervisor-review --project <path> --change <id> [--run-reviewers] [--review-concurrency <value>] [--review-retries <value>] [--review-retry-delay-ms <value>] [--review-retry-max-delay-ms <value>] [--source <skill|manual|inferred>] [--executed-reviewers <csv>] [--status <PASS|WARN|BLOCK>] [--issue-list <text>] [--revision-outcome <text>] [--write] [--json]",
113
494
  " da-vinci preflight-pencil --ops-file <path>",
114
495
  " da-vinci ensure-pen --output <path>",
115
496
  " da-vinci write-pen --output <path> --nodes-file <path> [--variables-file <path>]",
116
497
  " da-vinci check-pen-sync --pen <path> --nodes-file <path> [--variables-file <path>]",
498
+ " da-vinci check-pen-baseline --pen <path> --baseline <path>[,<path>...] [--baseline <path>] [--prefer-source <path>]",
499
+ " da-vinci sync-pen-source --from <path> --to <path>",
117
500
  " da-vinci snapshot-pen --input <path> --output <path>",
118
501
  " da-vinci pencil-lock acquire --project <path>",
119
502
  " da-vinci pencil-lock release --project <path>",
120
503
  " da-vinci pencil-lock status",
121
- " da-vinci pencil-session begin --project <path> --pen <path>",
504
+ " da-vinci pencil-session begin --project <path> --pen <path> [--baseline <path>[,<path>...]] [--prefer-source <path>] [--sync-preferred-source]",
122
505
  " da-vinci pencil-session persist --project <path> --pen <path> --nodes-file <path> [--variables-file <path>]",
123
506
  " da-vinci pencil-session end --project <path> --pen <path> --nodes-file <path> [--variables-file <path>]",
124
507
  " da-vinci pencil-session status --project <path>",
125
508
  " da-vinci --version",
126
509
  "",
127
510
  "Options:",
128
- " --platform <value> codex, claude, gemini, or all",
129
- " --home <path> override HOME for installation targets",
130
- " --project <path> override project path for audit",
131
- " --catalog <path> icon catalog path for icon-search/icon-sync (default: ~/.da-vinci/icon-catalog.json)",
132
- " --aliases <path> icon alias mapping file for icon-search (default: ~/.da-vinci/icon-aliases.json)",
133
- " --pencil-design <path> explicit pencil-design.md path for supervisor-review",
134
- " --query <text> icon-search query text",
135
- " --family <value> icon-search family filter: all, material, rounded, outlined, sharp, lucide, feather, phosphor",
136
- " --status <value> PASS, WARN, or BLOCK for supervisor-review",
137
- " --source <value> review source: skill, manual, inferred",
138
- " --executed-reviewers <csv> reviewer skills that executed this review",
139
- " --run-reviewers execute configured reviewer skills through codex exec",
140
- " --review-concurrency <value> max parallel reviewer executions (default 2)",
141
- " --review-retries <value> retry count per reviewer before failing (default 1)",
142
- " --review-retry-delay-ms <value> initial retry delay in milliseconds (default 400, exponential backoff)",
143
- " --codex-bin <path> codex executable path for --run-reviewers (default: codex)",
144
- " --max-images <value> max screenshots attached to reviewer runs (default 6)",
145
- " --review-timeout-ms <value> timeout per reviewer run in milliseconds",
146
- " --issue-list <text> supervisor-review issue summary",
147
- " --revision-outcome <text> supervisor-review revision result summary",
148
- " --top <value> icon-search result count (1-50, default 8)",
149
- " --timeout-ms <value> network timeout for icon-sync requests",
150
- " --strict fail icon-sync when any upstream source request fails",
151
- " --json print structured JSON for icon-search",
152
- " --pen <path> registered .pen path for sync checks",
153
- " --ops-file <path> Pencil batch operations file for preflight",
154
- " --input <path> input .pen file for snapshot-pen",
155
- " --output <path> output .pen file for write-pen or snapshot-pen",
156
- " --nodes-file <path> JSON payload from batch_get for write-pen",
157
- " --variables-file <path> JSON payload from get_variables for write-pen",
158
- " --verify-open reopen the written .pen with Pencil after writing",
159
- " --version <value> explicit .pen version when writing from MCP payloads",
160
- " --mode <value> integrity or completion",
161
- " --change <id> scope completion audit to one change id",
162
- " --wait-ms <value> wait for the global Pencil lock before failing",
163
- " --owner <value> human-readable lock owner label",
164
- " --force force a lock release held by another project"
511
+ ...optionLines
165
512
  ].join("\n")
166
513
  );
167
514
  }
168
515
 
169
- async function runCli(argv) {
170
- const [command] = argv;
171
- const homeDir = getOption(argv, "--home");
172
- const positionalArgs = getPositionalArgs(argv.slice(1), [
516
+ async function handleIconSearchCommand(argv, homeDir) {
517
+ const family = getOption(argv, "--family") || "all";
518
+ const top = getIntegerOption(argv, "--top", { min: 1, max: 50 });
519
+ const queryOption = getOption(argv, "--query");
520
+ const catalogPath = getOption(argv, "--catalog");
521
+ const aliasesPath = getOption(argv, "--aliases");
522
+ const iconPositional = getPositionalArgs(argv.slice(1), [
173
523
  "--home",
174
- "--platform",
175
- "--project",
176
- "--mode",
177
- "--change",
178
524
  "--query",
179
525
  "--family",
180
526
  "--top",
181
527
  "--catalog",
182
- "--aliases",
183
- "--pencil-design",
184
- "--status",
185
- "--source",
186
- "--executed-reviewers",
187
- "--codex-bin",
188
- "--max-images",
189
- "--review-timeout-ms",
190
- "--review-concurrency",
191
- "--review-retries",
192
- "--review-retry-delay-ms",
193
- "--issue-list",
194
- "--revision-outcome",
195
- "--timeout-ms",
196
- "--ops-file",
197
- "--input",
198
- "--output",
528
+ "--aliases"
529
+ ]);
530
+ const query = queryOption || iconPositional.join(" ").trim();
531
+
532
+ if (!query) {
533
+ throw new Error("`icon-search` requires `--query <text>` or positional query text.");
534
+ }
535
+
536
+ let loadedCatalog = null;
537
+ let loadedCatalogPath = null;
538
+ let catalogLoadError = null;
539
+ let loadedAliases = null;
540
+ let loadedAliasesPath = null;
541
+ let aliasesLoadError = null;
542
+ let aliasExpansion = {
543
+ extraTokens: [],
544
+ matchedAliases: []
545
+ };
546
+
547
+ try {
548
+ const loaded = loadIconCatalog({
549
+ catalogPath,
550
+ homeDir
551
+ });
552
+ loadedCatalog = loaded.catalog;
553
+ loadedCatalogPath = loaded.catalogPath;
554
+ } catch (error) {
555
+ catalogLoadError = error.message || String(error);
556
+ }
557
+
558
+ try {
559
+ const loaded = loadIconAliases({
560
+ aliasPath: aliasesPath,
561
+ homeDir
562
+ });
563
+ loadedAliases = loaded;
564
+ loadedAliasesPath = loaded.aliasPath;
565
+ aliasExpansion = expandQueryWithAliases(query, loaded.aliases);
566
+ } catch (error) {
567
+ aliasesLoadError = error.message || String(error);
568
+ }
569
+
570
+ const result = searchIconLibrary(query, {
571
+ family,
572
+ top,
573
+ catalog: loadedCatalog ? loadedCatalog.icons : [],
574
+ extraQueryTokens: aliasExpansion.extraTokens
575
+ });
576
+ const jsonOutput = argv.includes("--json");
577
+
578
+ const resultWithMeta = {
579
+ ...result,
580
+ catalog: {
581
+ path: loadedCatalogPath || "(unresolved)",
582
+ loaded: Boolean(loadedCatalog),
583
+ iconCount: loadedCatalog ? loadedCatalog.iconCount : 0,
584
+ generatedAt: loadedCatalog ? loadedCatalog.generatedAt : null,
585
+ error: catalogLoadError
586
+ },
587
+ aliases: {
588
+ path: loadedAliasesPath || "(unresolved)",
589
+ loaded: loadedAliases ? Boolean(loadedAliases.loaded) : false,
590
+ available: Boolean(loadedAliases),
591
+ source: loadedAliases ? loadedAliases.source : null,
592
+ matched: aliasExpansion.matchedAliases.length,
593
+ extraTokens: aliasExpansion.extraTokens,
594
+ error: aliasesLoadError
595
+ }
596
+ };
597
+
598
+ if (jsonOutput) {
599
+ console.log(JSON.stringify(resultWithMeta, null, 2));
600
+ return;
601
+ }
602
+
603
+ if (resultWithMeta.catalog.loaded) {
604
+ console.log(
605
+ `Icon catalog: ${resultWithMeta.catalog.path} (${resultWithMeta.catalog.iconCount} icons, ${resultWithMeta.catalog.generatedAt})`
606
+ );
607
+ } else if (resultWithMeta.catalog.error) {
608
+ console.log(
609
+ `Icon catalog: ${resultWithMeta.catalog.path} (load failed: ${resultWithMeta.catalog.error}; using built-in fallback index)`
610
+ );
611
+ } else {
612
+ console.log(
613
+ `Icon catalog: ${resultWithMeta.catalog.path} (not found; using built-in fallback index; run \`da-vinci icon-sync\`)`
614
+ );
615
+ }
616
+
617
+ if (resultWithMeta.aliases.available) {
618
+ console.log(
619
+ `Icon aliases: ${resultWithMeta.aliases.path} (${resultWithMeta.aliases.source}, matched ${resultWithMeta.aliases.matched})`
620
+ );
621
+ } else if (resultWithMeta.aliases.error) {
622
+ console.log(
623
+ `Icon aliases: ${resultWithMeta.aliases.path} (load failed: ${resultWithMeta.aliases.error})`
624
+ );
625
+ } else {
626
+ console.log(
627
+ `Icon aliases: ${resultWithMeta.aliases.path} (not found; using built-in defaults only)`
628
+ );
629
+ }
630
+
631
+ console.log(formatIconSearchReport(result));
632
+ }
633
+
634
+ async function handleSupervisorReviewCommand(argv) {
635
+ if (argv.includes("--help") || argv.includes("-h")) {
636
+ console.log(
637
+ [
638
+ "da-vinci supervisor-review",
639
+ "",
640
+ "Usage:",
641
+ " da-vinci supervisor-review --project <path> --change <id> [--run-reviewers] [--review-concurrency <value>] [--review-retries <value>] [--review-retry-delay-ms <value>] [--review-retry-max-delay-ms <value>] [--source <skill|manual|inferred>] [--executed-reviewers <csv>] [--status <PASS|WARN|BLOCK>] [--issue-list <text>] [--revision-outcome <text>] [--write] [--json]",
642
+ " da-vinci supervisor-review --project <path> --pencil-design <path> [--run-reviewers] [--review-concurrency <value>] [--review-retries <value>] [--review-retry-delay-ms <value>] [--review-retry-max-delay-ms <value>] [--source <skill|manual|inferred>] [--executed-reviewers <csv>] [--status <PASS|WARN|BLOCK>] [--issue-list <text>] [--revision-outcome <text>] [--write] [--json]",
643
+ "",
644
+ "Notes:",
645
+ " - omit --status to infer a conservative review status from current design artifacts",
646
+ " - use --run-reviewers to execute configured reviewer skills automatically via codex exec",
647
+ " - `design-supervisor review` is a compatibility alias for this command"
648
+ ].join("\n")
649
+ );
650
+ return;
651
+ }
652
+
653
+ const projectPath = getOption(argv, "--project") || process.cwd();
654
+ const changeId = getOption(argv, "--change");
655
+ const pencilDesignPath = getOption(argv, "--pencil-design");
656
+ const status = getOption(argv, "--status");
657
+ const source = getOption(argv, "--source");
658
+ const executedReviewers = getOption(argv, "--executed-reviewers");
659
+ const codexBin = getOption(argv, "--codex-bin");
660
+ const maxImages = getIntegerOption(argv, "--max-images", { min: 0 });
661
+ const reviewerTimeoutMs = getIntegerOption(argv, "--review-timeout-ms", { min: 1 });
662
+ const reviewConcurrency = getIntegerOption(argv, "--review-concurrency", { min: 1 });
663
+ const reviewerRetries = getIntegerOption(argv, "--review-retries", { min: 0 });
664
+ const reviewerRetryDelayMs = getIntegerOption(argv, "--review-retry-delay-ms", { min: 0 });
665
+ const reviewerRetryMaxDelayMs = getIntegerOption(argv, "--review-retry-max-delay-ms", {
666
+ min: 0
667
+ });
668
+ const issueList = getOption(argv, "--issue-list");
669
+ const revisionOutcome = getOption(argv, "--revision-outcome");
670
+ const write = argv.includes("--write");
671
+ const acceptWarn = argv.includes("--accept-warn");
672
+ const runReviewers = argv.includes("--run-reviewers");
673
+ const jsonOutput = argv.includes("--json");
674
+
675
+ const result = await runDesignSupervisorReview({
676
+ projectPath,
677
+ changeId,
678
+ pencilDesignPath,
679
+ status,
680
+ source,
681
+ executedReviewers,
682
+ codexBin,
683
+ maxImages,
684
+ reviewerTimeoutMs,
685
+ reviewConcurrency,
686
+ reviewerRetries,
687
+ reviewerRetryDelayMs,
688
+ reviewerRetryMaxDelayMs,
689
+ issueList,
690
+ revisionOutcome,
691
+ write,
692
+ acceptWarn,
693
+ runReviewers
694
+ });
695
+
696
+ if (jsonOutput) {
697
+ console.log(JSON.stringify(result, null, 2));
698
+ return;
699
+ }
700
+
701
+ console.log(formatDesignSupervisorReviewReport(result));
702
+ }
703
+
704
+ function handlePencilLockCommand(argv, homeDir) {
705
+ const subcommand = getPositionalArgs(argv.slice(1), [
706
+ "--home",
707
+ "--project",
708
+ "--owner",
709
+ "--wait-ms"
710
+ ])[0];
711
+ const projectPath = getOption(argv, "--project") || process.cwd();
712
+ const owner = getOption(argv, "--owner");
713
+ const waitMs = getIntegerOption(argv, "--wait-ms", { min: 0 });
714
+ const force = argv.includes("--force");
715
+
716
+ if (!subcommand || ["acquire", "release", "status"].includes(subcommand) === false) {
717
+ throw new Error("`pencil-lock` requires one of: acquire, release, status.");
718
+ }
719
+
720
+ if (subcommand === "acquire") {
721
+ const result = acquirePencilLock({
722
+ projectPath,
723
+ owner,
724
+ waitMs,
725
+ homeDir
726
+ });
727
+ console.log(`${result.alreadyHeld ? "Reused" : "Acquired"} Pencil lock at ${result.lockPath}`);
728
+ console.log(`Project: ${result.lock.projectPath}`);
729
+ console.log(`Owner: ${result.lock.owner}`);
730
+ return;
731
+ }
732
+
733
+ if (subcommand === "release") {
734
+ const result = releasePencilLock({
735
+ projectPath,
736
+ force,
737
+ homeDir
738
+ });
739
+ if (!result.hadLock) {
740
+ console.log(`No Pencil lock was present at ${result.lockPath}`);
741
+ return;
742
+ }
743
+ console.log(`Released Pencil lock at ${result.lockPath}`);
744
+ console.log(`Previous project: ${result.lock.projectPath}`);
745
+ return;
746
+ }
747
+
748
+ const result = getPencilLockStatus({ homeDir });
749
+ console.log(`Lock path: ${result.lockPath}`);
750
+ if (!result.lock) {
751
+ console.log("Status: unlocked");
752
+ return;
753
+ }
754
+ console.log("Status: locked");
755
+ console.log(`Project: ${result.lock.projectPath}`);
756
+ console.log(`Owner: ${result.lock.owner}`);
757
+ console.log(`PID: ${result.lock.pid}`);
758
+ }
759
+
760
+ function handlePencilSessionCommand(argv, homeDir) {
761
+ const subcommand = getPositionalArgs(argv.slice(1), [
762
+ "--home",
763
+ "--project",
199
764
  "--pen",
765
+ "--baseline",
766
+ "--prefer-source",
200
767
  "--nodes-file",
201
768
  "--variables-file",
202
769
  "--version",
203
770
  "--owner",
204
771
  "--wait-ms"
205
- ]);
772
+ ])[0];
773
+ const projectPath = getOption(argv, "--project") || process.cwd();
774
+ const penPath = getOption(argv, "--pen");
775
+ const baselinePaths = getCommaSeparatedOptionValues(argv, "--baseline");
776
+ const preferredSource = getOption(argv, "--prefer-source");
777
+ const nodesFile = getOption(argv, "--nodes-file");
778
+ const variablesFile = getOption(argv, "--variables-file");
779
+ const version = getOption(argv, "--version");
780
+ const verifyWithPencil = argv.includes("--verify-open");
781
+ const owner = getOption(argv, "--owner");
782
+ const waitMs = getIntegerOption(argv, "--wait-ms", { min: 0 });
783
+ const force = argv.includes("--force");
784
+ const syncPreferredSource = argv.includes("--sync-preferred-source");
785
+
786
+ if (!subcommand || ["begin", "persist", "end", "status"].includes(subcommand) === false) {
787
+ throw new Error("`pencil-session` requires one of: begin, persist, end, status.");
788
+ }
789
+
790
+ if (subcommand === "begin") {
791
+ if (!penPath) {
792
+ throw new Error("`pencil-session begin` requires `--pen <path>`.");
793
+ }
794
+ if (preferredSource && baselinePaths.length === 0) {
795
+ throw new Error("`pencil-session begin --prefer-source` requires at least one `--baseline <path>`.");
796
+ }
797
+ if (syncPreferredSource && !preferredSource) {
798
+ throw new Error("`pencil-session begin --sync-preferred-source` requires `--prefer-source <path>`.");
799
+ }
800
+ if (syncPreferredSource && baselinePaths.length === 0) {
801
+ throw new Error("`pencil-session begin --sync-preferred-source` requires at least one `--baseline <path>`.");
802
+ }
803
+
804
+ const result = beginPencilSession({
805
+ projectPath,
806
+ penPath,
807
+ baselinePaths,
808
+ preferredSource,
809
+ syncPreferredSource,
810
+ version,
811
+ verifyWithPencil,
812
+ owner,
813
+ waitMs,
814
+ homeDir
815
+ });
816
+
817
+ console.log(`Began Pencil session for ${result.projectRoot}`);
818
+ console.log(`Pen path: ${result.penPath}`);
819
+ console.log(`Session state: ${result.sessionStatePath}`);
820
+ console.log(`Snapshot hash: ${result.session.lastPersistedHash}`);
821
+ if (result.session.baselineCheck) {
822
+ console.log(`Baseline status: ${result.session.baselineCheck.status}`);
823
+ console.log(`Baseline decision: ${result.session.baselineCheck.decision}`);
824
+ }
825
+ if (result.session.baselineSync) {
826
+ console.log(`Baseline synced from: ${result.session.baselineSync.sourcePath}`);
827
+ }
828
+ return;
829
+ }
830
+
831
+ if (subcommand === "persist") {
832
+ if (!penPath || !nodesFile) {
833
+ throw new Error("`pencil-session persist` requires `--pen <path>` and `--nodes-file <path>`.");
834
+ }
835
+
836
+ const result = persistPencilSession({
837
+ projectPath,
838
+ penPath,
839
+ nodesFile,
840
+ variablesFile,
841
+ version,
842
+ verifyWithPencil,
843
+ homeDir
844
+ });
845
+
846
+ console.log(`Persisted Pencil session for ${result.projectRoot}`);
847
+ console.log(`Pen path: ${result.penPath}`);
848
+ console.log(`Session state: ${result.sessionStatePath}`);
849
+ console.log(`Snapshot hash: ${result.session.lastPersistedHash}`);
850
+ console.log(`In sync: ${result.syncResult.inSync ? "yes" : "no"}`);
851
+ return;
852
+ }
853
+
854
+ if (subcommand === "end") {
855
+ if (!penPath) {
856
+ throw new Error("`pencil-session end` requires `--pen <path>`.");
857
+ }
858
+ if (!nodesFile && !force) {
859
+ throw new Error(
860
+ "`pencil-session end` requires `--nodes-file <path>` (and `--variables-file <path>` when available). Use `--force` only for emergency lock release."
861
+ );
862
+ }
863
+
864
+ const result = endPencilSession({
865
+ projectPath,
866
+ penPath,
867
+ nodesFile,
868
+ variablesFile,
869
+ version,
870
+ homeDir,
871
+ force
872
+ });
873
+
874
+ console.log(`Ended Pencil session for ${result.projectRoot}`);
875
+ console.log(`Pen path: ${result.penPath}`);
876
+ console.log(`Session state: ${result.sessionStatePath}`);
877
+ console.log(`Final status: ${result.session.status}`);
878
+ if (result.syncResult) {
879
+ console.log(`Final sync verified: ${result.syncResult.inSync ? "yes" : "no"}`);
880
+ } else if (force) {
881
+ console.log("Final sync verified: skipped (force shutdown)");
882
+ }
883
+ return;
884
+ }
885
+
886
+ const result = getPencilSessionStatus({
887
+ projectPath,
888
+ homeDir
889
+ });
890
+
891
+ console.log(`Project: ${result.projectRoot}`);
892
+ console.log(`Session state: ${result.sessionStatePath}`);
893
+ console.log(`Session status: ${result.session ? result.session.status : "missing"}`);
894
+ console.log(`Lock status: ${result.lockStatus.lock ? "locked" : "unlocked"}`);
895
+ if (result.session) {
896
+ console.log(`Pen path: ${result.session.penPath}`);
897
+ console.log(`Last persisted hash: ${result.session.lastPersistedHash || "(missing)"}`);
898
+ }
899
+ }
900
+
901
+ async function runCli(argv) {
902
+ const [command] = argv;
903
+ const homeDir = getOption(argv, "--home");
904
+ const positionalArgs = getPositionalArgs(argv.slice(1), OPTION_FLAGS_WITH_VALUES);
905
+ const continueOnError = shouldContinueOnError(argv);
206
906
 
207
907
  if (!command || command === "help" || command === "--help" || command === "-h") {
208
908
  printHelp();
@@ -237,6 +937,216 @@ async function runCli(argv) {
237
937
  return;
238
938
  }
239
939
 
940
+ if (command === "workflow-status") {
941
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
942
+ const changeId = getOption(argv, "--change");
943
+ const result = deriveWorkflowStatus(projectPath, { changeId });
944
+
945
+ if (argv.includes("--json")) {
946
+ console.log(JSON.stringify(result, null, 2));
947
+ return;
948
+ }
949
+
950
+ console.log(formatWorkflowStatusReport(result));
951
+ return;
952
+ }
953
+
954
+ if (command === "next-step") {
955
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
956
+ const changeId = getOption(argv, "--change");
957
+ const result = deriveWorkflowStatus(projectPath, { changeId });
958
+
959
+ if (argv.includes("--json")) {
960
+ console.log(JSON.stringify(result.nextStep || null, null, 2));
961
+ return;
962
+ }
963
+
964
+ console.log(formatNextStepReport(result));
965
+ return;
966
+ }
967
+
968
+ if (command === "lint-spec") {
969
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
970
+ const changeId = getOption(argv, "--change");
971
+ const strict = argv.includes("--strict");
972
+ const result = lintRuntimeSpecs(projectPath, { changeId, strict });
973
+ persistExecutionSignal(projectPath, result.changeId || changeId, "lint-spec", result, strict);
974
+ const useJson = argv.includes("--json");
975
+ const output = useJson ? JSON.stringify(result, null, 2) : formatLintSpecReport(result);
976
+
977
+ if (emitOrThrowOnStatus(result.status, ["BLOCK"], output, continueOnError)) {
978
+ return;
979
+ }
980
+
981
+ console.log(output);
982
+ return;
983
+ }
984
+
985
+ if (command === "scope-check") {
986
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
987
+ const changeId = getOption(argv, "--change");
988
+ const strict = argv.includes("--strict");
989
+ const result = runScopeCheck(projectPath, { changeId, strict });
990
+ persistExecutionSignal(projectPath, result.changeId || changeId, "scope-check", result, strict);
991
+ const useJson = argv.includes("--json");
992
+ const output = useJson ? JSON.stringify(result, null, 2) : formatScopeCheckReport(result);
993
+
994
+ if (emitOrThrowOnStatus(result.status, ["BLOCK"], output, continueOnError)) {
995
+ return;
996
+ }
997
+
998
+ console.log(output);
999
+ return;
1000
+ }
1001
+
1002
+ if (command === "lint-tasks") {
1003
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
1004
+ const changeId = getOption(argv, "--change");
1005
+ const strict = argv.includes("--strict");
1006
+ const result = lintTasks(projectPath, { changeId, strict });
1007
+ persistExecutionSignal(projectPath, result.changeId || changeId, "lint-tasks", result, strict);
1008
+ const useJson = argv.includes("--json");
1009
+ const output = useJson ? JSON.stringify(result, null, 2) : formatLintTasksReport(result);
1010
+
1011
+ if (emitOrThrowOnStatus(result.status, ["BLOCK"], output, continueOnError)) {
1012
+ return;
1013
+ }
1014
+
1015
+ console.log(output);
1016
+ return;
1017
+ }
1018
+
1019
+ if (command === "lint-bindings") {
1020
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
1021
+ const changeId = getOption(argv, "--change");
1022
+ const strict = argv.includes("--strict");
1023
+ const result = lintBindings(projectPath, { changeId, strict });
1024
+ persistExecutionSignal(projectPath, result.changeId || changeId, "lint-bindings", result, strict);
1025
+ const useJson = argv.includes("--json");
1026
+ const output = useJson ? JSON.stringify(result, null, 2) : formatLintBindingsReport(result);
1027
+
1028
+ if (emitOrThrowOnStatus(result.status, ["BLOCK"], output, continueOnError)) {
1029
+ return;
1030
+ }
1031
+
1032
+ console.log(output);
1033
+ return;
1034
+ }
1035
+
1036
+ if (command === "generate-sidecars") {
1037
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
1038
+ const changeId = getOption(argv, "--change");
1039
+ const result = generatePlanningSidecars(projectPath, { changeId, write: true });
1040
+ persistExecutionSignal(projectPath, result.changeId || changeId, "generate-sidecars", result, false);
1041
+ const useJson = argv.includes("--json");
1042
+ const output = useJson ? JSON.stringify(result, null, 2) : formatGenerateSidecarsReport(result);
1043
+
1044
+ if (emitOrThrowOnStatus(result.status, ["BLOCK"], output, continueOnError)) {
1045
+ return;
1046
+ }
1047
+
1048
+ console.log(output);
1049
+ return;
1050
+ }
1051
+
1052
+ if (command === "verify-bindings") {
1053
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
1054
+ const changeId = getOption(argv, "--change");
1055
+ const strict = argv.includes("--strict");
1056
+ const result = verifyBindings(projectPath, { changeId, strict });
1057
+ persistExecutionSignal(projectPath, result.changeId || changeId, "verify-bindings", result, strict);
1058
+ const useJson = argv.includes("--json");
1059
+ const output = useJson
1060
+ ? JSON.stringify(result, null, 2)
1061
+ : formatVerifyReport(result, "Da Vinci verify-bindings");
1062
+ if (emitOrThrowOnStatus(result.status, ["BLOCK"], output, continueOnError)) {
1063
+ return;
1064
+ }
1065
+ console.log(output);
1066
+ return;
1067
+ }
1068
+
1069
+ if (command === "verify-implementation") {
1070
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
1071
+ const changeId = getOption(argv, "--change");
1072
+ const strict = argv.includes("--strict");
1073
+ const result = verifyImplementation(projectPath, { changeId, strict });
1074
+ persistExecutionSignal(projectPath, result.changeId || changeId, "verify-implementation", result, strict);
1075
+ const useJson = argv.includes("--json");
1076
+ const output = useJson
1077
+ ? JSON.stringify(result, null, 2)
1078
+ : formatVerifyReport(result, "Da Vinci verify-implementation");
1079
+ if (emitOrThrowOnStatus(result.status, ["BLOCK"], output, continueOnError)) {
1080
+ return;
1081
+ }
1082
+ console.log(output);
1083
+ return;
1084
+ }
1085
+
1086
+ if (command === "verify-structure") {
1087
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
1088
+ const changeId = getOption(argv, "--change");
1089
+ const strict = argv.includes("--strict");
1090
+ const result = verifyStructure(projectPath, { changeId, strict });
1091
+ persistExecutionSignal(projectPath, result.changeId || changeId, "verify-structure", result, strict);
1092
+ const useJson = argv.includes("--json");
1093
+ const output = useJson
1094
+ ? JSON.stringify(result, null, 2)
1095
+ : formatVerifyReport(result, "Da Vinci verify-structure");
1096
+ if (emitOrThrowOnStatus(result.status, ["BLOCK"], output, continueOnError)) {
1097
+ return;
1098
+ }
1099
+ console.log(output);
1100
+ return;
1101
+ }
1102
+
1103
+ if (command === "verify-coverage") {
1104
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
1105
+ const changeId = getOption(argv, "--change");
1106
+ const strict = argv.includes("--strict");
1107
+ const result = verifyCoverage(projectPath, { changeId, strict });
1108
+ persistExecutionSignal(projectPath, result.changeId || changeId, "verify-coverage", result, strict);
1109
+ const useJson = argv.includes("--json");
1110
+ const output = useJson
1111
+ ? JSON.stringify(result, null, 2)
1112
+ : formatVerifyReport(result, "Da Vinci verify-coverage");
1113
+ if (emitOrThrowOnStatus(result.status, ["BLOCK"], output, continueOnError)) {
1114
+ return;
1115
+ }
1116
+ console.log(output);
1117
+ return;
1118
+ }
1119
+
1120
+ if (command === "diff-spec") {
1121
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
1122
+ const changeId = getOption(argv, "--change");
1123
+ const fromDir = getOption(argv, "--from");
1124
+ const result = diffSpec(projectPath, { changeId, fromDir });
1125
+ persistExecutionSignal(projectPath, result.changeId || changeId, "diff-spec", result, false);
1126
+ const useJson = argv.includes("--json");
1127
+ const output = useJson ? JSON.stringify(result, null, 2) : formatDiffSpecReport(result);
1128
+ if (emitOrThrowOnStatus(result.status, ["BLOCK"], output, continueOnError)) {
1129
+ return;
1130
+ }
1131
+ console.log(output);
1132
+ return;
1133
+ }
1134
+
1135
+ if (command === "scaffold") {
1136
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
1137
+ const changeId = getOption(argv, "--change");
1138
+ const outputDir = getOption(argv, "--output");
1139
+ const result = scaffoldFromBindings(projectPath, { changeId, outputDir });
1140
+ persistExecutionSignal(projectPath, result.changeId || changeId, "scaffold", result, false);
1141
+ const useJson = argv.includes("--json");
1142
+ const output = useJson ? JSON.stringify(result, null, 2) : formatScaffoldReport(result);
1143
+ if (emitOrThrowOnStatus(result.status, ["BLOCK"], output, continueOnError)) {
1144
+ return;
1145
+ }
1146
+ console.log(output);
1147
+ return;
1148
+ }
1149
+
240
1150
  if (command === "validate-assets") {
241
1151
  const result = validateAssets();
242
1152
  console.log(`Da Vinci v${result.version} assets are complete (${result.requiredAssets} required files).`);
@@ -250,17 +1160,26 @@ async function runCli(argv) {
250
1160
  const result = auditProject(projectPath, { mode, changeId });
251
1161
  const report = formatAuditReport(result);
252
1162
 
253
- if (result.status === "FAIL") {
254
- throw new Error(report);
1163
+ if (emitOrThrowOnStatus(result.status, ["FAIL"], report, continueOnError)) {
1164
+ return;
255
1165
  }
256
1166
 
257
1167
  console.log(report);
258
1168
  return;
259
1169
  }
260
1170
 
1171
+ if (command === "bootstrap-project") {
1172
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
1173
+ const changeId = getOption(argv, "--change");
1174
+ const force = argv.includes("--force");
1175
+ const result = bootstrapProjectArtifacts(projectPath, { changeId, force });
1176
+ console.log(formatBootstrapProjectReport(result));
1177
+ return;
1178
+ }
1179
+
261
1180
  if (command === "icon-sync") {
262
1181
  const outputPath = getOption(argv, "--output") || getOption(argv, "--catalog");
263
- const timeoutMs = getOption(argv, "--timeout-ms");
1182
+ const timeoutMs = getIntegerOption(argv, "--timeout-ms", { min: 1 });
264
1183
  const strict = argv.includes("--strict");
265
1184
 
266
1185
  const result = await syncIconCatalog({
@@ -275,129 +1194,7 @@ async function runCli(argv) {
275
1194
  }
276
1195
 
277
1196
  if (command === "icon-search") {
278
- const family = getOption(argv, "--family") || "all";
279
- const topRaw = getOption(argv, "--top");
280
- const queryOption = getOption(argv, "--query");
281
- const catalogPath = getOption(argv, "--catalog");
282
- const aliasesPath = getOption(argv, "--aliases");
283
- const iconPositional = getPositionalArgs(argv.slice(1), [
284
- "--home",
285
- "--query",
286
- "--family",
287
- "--top",
288
- "--catalog",
289
- "--aliases"
290
- ]);
291
- const query = queryOption || iconPositional.join(" ").trim();
292
-
293
- if (!query) {
294
- throw new Error("`icon-search` requires `--query <text>` or positional query text.");
295
- }
296
-
297
- let top;
298
- if (topRaw !== undefined) {
299
- top = Number.parseInt(topRaw, 10);
300
- if (!Number.isFinite(top) || top < 1 || top > 50) {
301
- throw new Error("`icon-search --top` must be an integer between 1 and 50.");
302
- }
303
- }
304
-
305
- let loadedCatalog = null;
306
- let loadedCatalogPath = null;
307
- let catalogLoadError = null;
308
- let loadedAliases = null;
309
- let loadedAliasesPath = null;
310
- let aliasesLoadError = null;
311
- let aliasExpansion = {
312
- extraTokens: [],
313
- matchedAliases: []
314
- };
315
-
316
- try {
317
- const loaded = loadIconCatalog({
318
- catalogPath,
319
- homeDir
320
- });
321
- loadedCatalog = loaded.catalog;
322
- loadedCatalogPath = loaded.catalogPath;
323
- } catch (error) {
324
- catalogLoadError = error.message || String(error);
325
- }
326
-
327
- try {
328
- const loaded = loadIconAliases({
329
- aliasPath: aliasesPath,
330
- homeDir
331
- });
332
- loadedAliases = loaded;
333
- loadedAliasesPath = loaded.aliasPath;
334
- aliasExpansion = expandQueryWithAliases(query, loaded.aliases);
335
- } catch (error) {
336
- aliasesLoadError = error.message || String(error);
337
- }
338
-
339
- const result = searchIconLibrary(query, {
340
- family,
341
- top,
342
- catalog: loadedCatalog ? loadedCatalog.icons : [],
343
- extraQueryTokens: aliasExpansion.extraTokens
344
- });
345
- const jsonOutput = argv.includes("--json");
346
-
347
- const resultWithMeta = {
348
- ...result,
349
- catalog: {
350
- path: loadedCatalogPath || "(unresolved)",
351
- loaded: Boolean(loadedCatalog),
352
- iconCount: loadedCatalog ? loadedCatalog.iconCount : 0,
353
- generatedAt: loadedCatalog ? loadedCatalog.generatedAt : null,
354
- error: catalogLoadError
355
- },
356
- aliases: {
357
- path: loadedAliasesPath || "(unresolved)",
358
- loaded: loadedAliases ? Boolean(loadedAliases.loaded) : false,
359
- available: Boolean(loadedAliases),
360
- source: loadedAliases ? loadedAliases.source : null,
361
- matched: aliasExpansion.matchedAliases.length,
362
- extraTokens: aliasExpansion.extraTokens,
363
- error: aliasesLoadError
364
- }
365
- };
366
-
367
- if (jsonOutput) {
368
- console.log(JSON.stringify(resultWithMeta, null, 2));
369
- return;
370
- }
371
-
372
- if (resultWithMeta.catalog.loaded) {
373
- console.log(
374
- `Icon catalog: ${resultWithMeta.catalog.path} (${resultWithMeta.catalog.iconCount} icons, ${resultWithMeta.catalog.generatedAt})`
375
- );
376
- } else if (resultWithMeta.catalog.error) {
377
- console.log(
378
- `Icon catalog: ${resultWithMeta.catalog.path} (load failed: ${resultWithMeta.catalog.error}; using built-in fallback index)`
379
- );
380
- } else {
381
- console.log(
382
- `Icon catalog: ${resultWithMeta.catalog.path} (not found; using built-in fallback index; run \`da-vinci icon-sync\`)`
383
- );
384
- }
385
-
386
- if (resultWithMeta.aliases.available) {
387
- console.log(
388
- `Icon aliases: ${resultWithMeta.aliases.path} (${resultWithMeta.aliases.source}, matched ${resultWithMeta.aliases.matched})`
389
- );
390
- } else if (resultWithMeta.aliases.error) {
391
- console.log(
392
- `Icon aliases: ${resultWithMeta.aliases.path} (load failed: ${resultWithMeta.aliases.error})`
393
- );
394
- } else {
395
- console.log(
396
- `Icon aliases: ${resultWithMeta.aliases.path} (not found; using built-in defaults only)`
397
- );
398
- }
399
-
400
- console.log(formatIconSearchReport(result));
1197
+ await handleIconSearchCommand(argv, homeDir);
401
1198
  return;
402
1199
  }
403
1200
 
@@ -408,7 +1205,7 @@ async function runCli(argv) {
408
1205
  if (opsFile) {
409
1206
  operations = readOperations(opsFile);
410
1207
  } else if (!process.stdin.isTTY) {
411
- operations = fs.readFileSync(0, "utf8");
1208
+ operations = readLimitedStdin();
412
1209
  } else {
413
1210
  throw new Error("`preflight-pencil` requires `--ops-file <path>` or piped stdin input.");
414
1211
  }
@@ -416,8 +1213,8 @@ async function runCli(argv) {
416
1213
  const result = preflightPencilBatch(operations);
417
1214
  const report = formatPencilPreflightReport(result);
418
1215
 
419
- if (result.status === "FAIL") {
420
- throw new Error(report);
1216
+ if (emitOrThrowOnStatus(result.status, ["FAIL"], report, continueOnError)) {
1217
+ return;
421
1218
  }
422
1219
 
423
1220
  console.log(report);
@@ -425,69 +1222,7 @@ async function runCli(argv) {
425
1222
  }
426
1223
 
427
1224
  if (command === "supervisor-review") {
428
- if (argv.includes("--help") || argv.includes("-h")) {
429
- console.log(
430
- [
431
- "da-vinci supervisor-review",
432
- "",
433
- "Usage:",
434
- " da-vinci supervisor-review --project <path> --change <id> [--run-reviewers] [--review-concurrency <value>] [--review-retries <value>] [--review-retry-delay-ms <value>] [--source <skill|manual|inferred>] [--executed-reviewers <csv>] [--status <PASS|WARN|BLOCK>] [--issue-list <text>] [--revision-outcome <text>] [--write] [--json]",
435
- " da-vinci supervisor-review --project <path> --pencil-design <path> [--run-reviewers] [--review-concurrency <value>] [--review-retries <value>] [--review-retry-delay-ms <value>] [--source <skill|manual|inferred>] [--executed-reviewers <csv>] [--status <PASS|WARN|BLOCK>] [--issue-list <text>] [--revision-outcome <text>] [--write] [--json]",
436
- "",
437
- "Notes:",
438
- " - omit --status to infer a conservative review status from current design artifacts",
439
- " - use --run-reviewers to execute configured reviewer skills automatically via codex exec",
440
- " - `design-supervisor review` is a compatibility alias for this command"
441
- ].join("\n")
442
- );
443
- return;
444
- }
445
-
446
- const projectPath = getOption(argv, "--project") || process.cwd();
447
- const changeId = getOption(argv, "--change");
448
- const pencilDesignPath = getOption(argv, "--pencil-design");
449
- const status = getOption(argv, "--status");
450
- const source = getOption(argv, "--source");
451
- const executedReviewers = getOption(argv, "--executed-reviewers");
452
- const codexBin = getOption(argv, "--codex-bin");
453
- const maxImages = getOption(argv, "--max-images");
454
- const reviewerTimeoutMs = getOption(argv, "--review-timeout-ms");
455
- const reviewConcurrency = getOption(argv, "--review-concurrency");
456
- const reviewerRetries = getOption(argv, "--review-retries");
457
- const reviewerRetryDelayMs = getOption(argv, "--review-retry-delay-ms");
458
- const issueList = getOption(argv, "--issue-list");
459
- const revisionOutcome = getOption(argv, "--revision-outcome");
460
- const write = argv.includes("--write");
461
- const acceptWarn = argv.includes("--accept-warn");
462
- const runReviewers = argv.includes("--run-reviewers");
463
- const jsonOutput = argv.includes("--json");
464
-
465
- const result = await runDesignSupervisorReview({
466
- projectPath,
467
- changeId,
468
- pencilDesignPath,
469
- status,
470
- source,
471
- executedReviewers,
472
- codexBin,
473
- maxImages,
474
- reviewerTimeoutMs,
475
- reviewConcurrency,
476
- reviewerRetries,
477
- reviewerRetryDelayMs,
478
- issueList,
479
- revisionOutcome,
480
- write,
481
- acceptWarn,
482
- runReviewers
483
- });
484
-
485
- if (jsonOutput) {
486
- console.log(JSON.stringify(result, null, 2));
487
- return;
488
- }
489
-
490
- console.log(formatDesignSupervisorReviewReport(result));
1225
+ await handleSupervisorReviewCommand(argv);
491
1226
  return;
492
1227
  }
493
1228
 
@@ -577,11 +1312,69 @@ async function runCli(argv) {
577
1312
  console.log(`State file: ${result.statePath}`);
578
1313
  console.log(`Snapshot hash: ${result.liveHash}`);
579
1314
  if (!result.usedStateFile) {
580
- console.log("State file was missing; sync comparison fell back to hashing the disk .pen file directly.");
1315
+ console.log(
1316
+ result.stateHash
1317
+ ? "State file hash was stale; sync comparison fell back to hashing the disk .pen file directly."
1318
+ : "State file was missing; sync comparison fell back to hashing the disk .pen file directly."
1319
+ );
581
1320
  }
582
1321
  return;
583
1322
  }
584
1323
 
1324
+ if (command === "check-pen-baseline") {
1325
+ const penPath = getOption(argv, "--pen");
1326
+ const baselinePaths = getCommaSeparatedOptionValues(argv, "--baseline");
1327
+ const preferredSource = getOption(argv, "--prefer-source");
1328
+
1329
+ if (!penPath || baselinePaths.length === 0) {
1330
+ throw new Error(
1331
+ "`check-pen-baseline` requires `--pen <path>` and at least one `--baseline <path>`."
1332
+ );
1333
+ }
1334
+
1335
+ const result = comparePenBaselineAlignment({
1336
+ penPath,
1337
+ baselinePaths,
1338
+ preferredSource
1339
+ });
1340
+
1341
+ if (
1342
+ emitOrThrowOnStatus(
1343
+ result.status,
1344
+ ["BLOCK"],
1345
+ [
1346
+ "Baseline alignment check failed.",
1347
+ formatPenBaselineAlignmentReport(result)
1348
+ ].join("\n"),
1349
+ continueOnError
1350
+ )
1351
+ ) {
1352
+ return;
1353
+ }
1354
+
1355
+ console.log(formatPenBaselineAlignmentReport(result));
1356
+ return;
1357
+ }
1358
+
1359
+ if (command === "sync-pen-source") {
1360
+ const sourcePath = getOption(argv, "--from");
1361
+ const targetPath = getOption(argv, "--to");
1362
+
1363
+ if (!sourcePath || !targetPath) {
1364
+ throw new Error("`sync-pen-source` requires `--from <path>` and `--to <path>`.");
1365
+ }
1366
+
1367
+ const result = syncPenSource({
1368
+ sourcePath,
1369
+ targetPath
1370
+ });
1371
+
1372
+ console.log(`Synced .pen source from ${result.sourcePath} to ${result.targetPath}`);
1373
+ console.log(`State file: ${result.statePath}`);
1374
+ console.log(`Snapshot hash: ${result.state.snapshotHash}`);
1375
+ return;
1376
+ }
1377
+
585
1378
  if (command === "snapshot-pen") {
586
1379
  const inputPath = getOption(argv, "--input");
587
1380
  const outputPath = getOption(argv, "--output");
@@ -610,175 +1403,12 @@ async function runCli(argv) {
610
1403
  }
611
1404
 
612
1405
  if (command === "pencil-lock") {
613
- const subcommand = getPositionalArgs(argv.slice(1), [
614
- "--home",
615
- "--project",
616
- "--owner",
617
- "--wait-ms"
618
- ])[0];
619
- const projectPath = getOption(argv, "--project") || process.cwd();
620
- const owner = getOption(argv, "--owner");
621
- const waitMs = getOption(argv, "--wait-ms");
622
- const force = argv.includes("--force");
623
-
624
- if (!subcommand || ["acquire", "release", "status"].includes(subcommand) === false) {
625
- throw new Error("`pencil-lock` requires one of: acquire, release, status.");
626
- }
627
-
628
- if (subcommand === "acquire") {
629
- const result = acquirePencilLock({
630
- projectPath,
631
- owner,
632
- waitMs,
633
- homeDir
634
- });
635
- console.log(`${result.alreadyHeld ? "Reused" : "Acquired"} Pencil lock at ${result.lockPath}`);
636
- console.log(`Project: ${result.lock.projectPath}`);
637
- console.log(`Owner: ${result.lock.owner}`);
638
- return;
639
- }
640
-
641
- if (subcommand === "release") {
642
- const result = releasePencilLock({
643
- projectPath,
644
- force,
645
- homeDir
646
- });
647
- if (!result.hadLock) {
648
- console.log(`No Pencil lock was present at ${result.lockPath}`);
649
- return;
650
- }
651
- console.log(`Released Pencil lock at ${result.lockPath}`);
652
- console.log(`Previous project: ${result.lock.projectPath}`);
653
- return;
654
- }
655
-
656
- const result = getPencilLockStatus({ homeDir });
657
- console.log(`Lock path: ${result.lockPath}`);
658
- if (!result.lock) {
659
- console.log("Status: unlocked");
660
- return;
661
- }
662
- console.log("Status: locked");
663
- console.log(`Project: ${result.lock.projectPath}`);
664
- console.log(`Owner: ${result.lock.owner}`);
665
- console.log(`PID: ${result.lock.pid}`);
1406
+ handlePencilLockCommand(argv, homeDir);
666
1407
  return;
667
1408
  }
668
1409
 
669
1410
  if (command === "pencil-session") {
670
- const subcommand = getPositionalArgs(argv.slice(1), [
671
- "--home",
672
- "--project",
673
- "--pen",
674
- "--nodes-file",
675
- "--variables-file",
676
- "--version",
677
- "--owner",
678
- "--wait-ms"
679
- ])[0];
680
- const projectPath = getOption(argv, "--project") || process.cwd();
681
- const penPath = getOption(argv, "--pen");
682
- const nodesFile = getOption(argv, "--nodes-file");
683
- const variablesFile = getOption(argv, "--variables-file");
684
- const version = getOption(argv, "--version");
685
- const verifyWithPencil = argv.includes("--verify-open");
686
- const owner = getOption(argv, "--owner");
687
- const waitMs = getOption(argv, "--wait-ms");
688
- const force = argv.includes("--force");
689
-
690
- if (!subcommand || ["begin", "persist", "end", "status"].includes(subcommand) === false) {
691
- throw new Error("`pencil-session` requires one of: begin, persist, end, status.");
692
- }
693
-
694
- if (subcommand === "begin") {
695
- if (!penPath) {
696
- throw new Error("`pencil-session begin` requires `--pen <path>`.");
697
- }
698
-
699
- const result = beginPencilSession({
700
- projectPath,
701
- penPath,
702
- version,
703
- verifyWithPencil,
704
- owner,
705
- waitMs,
706
- homeDir
707
- });
708
-
709
- console.log(`Began Pencil session for ${result.projectRoot}`);
710
- console.log(`Pen path: ${result.penPath}`);
711
- console.log(`Session state: ${result.sessionStatePath}`);
712
- console.log(`Snapshot hash: ${result.session.lastPersistedHash}`);
713
- return;
714
- }
715
-
716
- if (subcommand === "persist") {
717
- if (!penPath || !nodesFile) {
718
- throw new Error("`pencil-session persist` requires `--pen <path>` and `--nodes-file <path>`.");
719
- }
720
-
721
- const result = persistPencilSession({
722
- projectPath,
723
- penPath,
724
- nodesFile,
725
- variablesFile,
726
- version,
727
- verifyWithPencil,
728
- homeDir
729
- });
730
-
731
- console.log(`Persisted Pencil session for ${result.projectRoot}`);
732
- console.log(`Pen path: ${result.penPath}`);
733
- console.log(`Session state: ${result.sessionStatePath}`);
734
- console.log(`Snapshot hash: ${result.session.lastPersistedHash}`);
735
- console.log(`In sync: ${result.syncResult.inSync ? "yes" : "no"}`);
736
- return;
737
- }
738
-
739
- if (subcommand === "end") {
740
- if (!penPath) {
741
- throw new Error("`pencil-session end` requires `--pen <path>`.");
742
- }
743
- if (!nodesFile && !force) {
744
- throw new Error(
745
- "`pencil-session end` requires `--nodes-file <path>` (and `--variables-file <path>` when available). Use `--force` only for emergency lock release."
746
- );
747
- }
748
-
749
- const result = endPencilSession({
750
- projectPath,
751
- penPath,
752
- nodesFile,
753
- variablesFile,
754
- version,
755
- homeDir,
756
- force
757
- });
758
-
759
- console.log(`Ended Pencil session for ${result.projectRoot}`);
760
- console.log(`Pen path: ${result.penPath}`);
761
- console.log(`Session state: ${result.sessionStatePath}`);
762
- console.log(`Final status: ${result.session.status}`);
763
- if (result.syncResult) {
764
- console.log(`Final sync verified: ${result.syncResult.inSync ? "yes" : "no"}`);
765
- }
766
- return;
767
- }
768
-
769
- const result = getPencilSessionStatus({
770
- projectPath,
771
- homeDir
772
- });
773
-
774
- console.log(`Project: ${result.projectRoot}`);
775
- console.log(`Session state: ${result.sessionStatePath}`);
776
- console.log(`Session status: ${result.session ? result.session.status : "missing"}`);
777
- console.log(`Lock status: ${result.lockStatus.lock ? "locked" : "unlocked"}`);
778
- if (result.session) {
779
- console.log(`Pen path: ${result.session.penPath}`);
780
- console.log(`Last persisted hash: ${result.session.lastPersistedHash || "(missing)"}`);
781
- }
1411
+ handlePencilSessionCommand(argv, homeDir);
782
1412
  return;
783
1413
  }
784
1414
 
@@ -786,5 +1416,6 @@ async function runCli(argv) {
786
1416
  }
787
1417
 
788
1418
  module.exports = {
789
- runCli
1419
+ runCli,
1420
+ readLimitedStdin
790
1421
  };