@xenonbyte/da-vinci-workflow 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.2.9 - 2026-04-05
4
+
5
+ ### Added
6
+ - modular CLI command registration across `lib/cli/command-handlers-*.js`, with dedicated registry regression coverage for core, workflow, design, and pen command families
7
+ - shared CLI test helpers in `scripts/cli-test-helpers.js` to keep `runCli` / JSON parsing behavior consistent across command-facing test suites
8
+
9
+ ### Changed
10
+ - `runCli` now assembles family-scoped handler context instead of wiring a single flat command map payload, reducing coupling between unrelated CLI surfaces
11
+ - release notes in `README.md` and `README.zh-CN.md` now point to the `0.2.9` modular CLI release
12
+
13
+ ### Fixed
14
+ - `da-vinci --help` now keeps long option descriptions readable by wrapping oversized flags instead of collapsing flag and description text together
15
+ - CLI regression suites now reuse shared process-spawn helpers instead of repeating slightly different local implementations
16
+
17
+ ## v0.2.8 - 2026-04-05
18
+
19
+ ### Added
20
+ - optional workflow decision tracing via `.da-vinci/logs/workflow-decisions/YYYY-MM-DD.ndjson`, gated by `DA_VINCI_TRACE_WORKFLOW_DECISIONS=1`
21
+ - compact trace coverage for persisted-state trust, canonical task-group seed fallback, task-group focus override, stale planning-signal fallback, verification freshness downgrade, and worktree-isolation downgrade
22
+ - targeted regression coverage in `scripts/test-workflow-decision-tracing.js` for eligible surfaces, silence rules, sink-write failures, and trace-schema validation behavior
23
+
24
+ ### Changed
25
+ - `workflow-status` and `next-step` now emit bounded decision traces only on the explicitly allowed surfaces, while keeping route/state truth unchanged
26
+ - workflow trace records now reject invalid family/key/outcome combinations with visible diagnostic feedback instead of silently disappearing
27
+ - current release notes in `README.md` and `README.zh-CN.md` now point to the `0.2.8` workflow decision tracing release
28
+
29
+ ### Fixed
30
+ - missing review or verification evidence no longer produces fake `evidenceRefs` in workflow decision traces
31
+ - persisted-state fallback traces no longer imply a fingerprint comparison happened for non-fingerprint fallback paths
32
+ - workflow tracing diagnostics remain non-blocking even when trace persistence fails or candidate trace records are invalid
33
+
3
34
  ## v0.2.7 - 2026-04-05
4
35
 
5
36
  ### Added
package/README.md CHANGED
@@ -34,15 +34,15 @@ Use `da-vinci maintainer-readiness` as the canonical maintainer diagnosis surfac
34
34
 
35
35
  Latest published npm package:
36
36
 
37
- - `@xenonbyte/da-vinci-workflow@0.2.7`
37
+ - `@xenonbyte/da-vinci-workflow@0.2.9`
38
38
 
39
- Release highlights for `0.2.7`:
39
+ Release highlights for `0.2.9`:
40
40
 
41
- - bounded worker isolation is now formalized as a contract-only OpenSpec change, with explicit task-group ownership, isolated workspace, handoff, sequencing, writeback, and downgrade rules
42
- - `task-execution`, `task-review`, and `workflow-state` now align with that contract by blocking out-of-scope writes, preserving review ordering, and rejecting `partial=true` + `DONE`
43
- - the reviewer bridge now runs `codex exec` with closed stdin plus explicit prompt separation, and preserves raw diagnostics when structured reviewer output is missing
44
- - contract CI now exercises bounded-worker-isolation phase 1-4 tests directly instead of relying only on docs/asset consistency checks
45
- - supervisor-review smoke fixtures now use valid exported screenshots, and the real reviewer bridge integration passes end to end
41
+ - CLI command dispatch is now split into core, workflow, design, and pen handler registries instead of one oversized `runCli` branch chain
42
+ - registry-level regression coverage now directly exercises representative commands from every CLI family, reducing the risk of missed command wiring during future refactors
43
+ - command-facing test suites now share a common `runCli` / JSON parsing harness, which removes duplicated spawn helpers and keeps test behavior consistent
44
+ - `da-vinci --help` now wraps long flags such as `--confirm-test-evidence-executed` onto a readable second line instead of collapsing flag and description text together
45
+ - the release keeps runtime behavior unchanged while lowering maintenance cost for future CLI surface expansion
46
46
 
47
47
  ## Discipline And Orchestration Upgrade
48
48
 
package/README.zh-CN.md CHANGED
@@ -37,15 +37,15 @@ Da Vinci 是一个把产品需求一路推进到结构化规格、Pencil 设计
37
37
 
38
38
  最新已发布 npm 包:
39
39
 
40
- - `@xenonbyte/da-vinci-workflow@0.2.7`
40
+ - `@xenonbyte/da-vinci-workflow@0.2.9`
41
41
 
42
- `0.2.7` 版本重点:
42
+ `0.2.9` 版本重点:
43
43
 
44
- - `bounded-worker-isolation-contract` 现已作为 contract-only OpenSpec 变更落地,明确 task-group ownershipisolated workspacehandoffsequencing、writeback downgrade 规则
45
- - `task-execution`、`task-review`、`workflow-state` 现已与该 contract 对齐:会阻断 out-of-scope writes,保持 review 顺序,并拒绝 `partial=true` 与 `DONE` 组合
46
- - reviewer bridge 现在会以显式 prompt 分隔并关闭 stdin 的方式运行 `codex exec`,同时在 reviewer 缺失结构化输出时保留原始诊断
47
- - contract CI 现在会直接跑 bounded-worker-isolation phase 1-4 回归,不再只依赖文档/命令资产一致性检查
48
- - supervisor-review smoke fixture 现在使用有效截图,真实 reviewer bridge integration 已能端到端通过
44
+ - CLI 命令分发现在拆成 coreworkflowdesignpen 四组 handler registry,不再把所有命令逻辑都堆在一个超长 `runCli` 分支链里
45
+ - registry 层回归测试现在直接覆盖每个 CLI family 的代表性命令,后续做命令面重构时更容易及时发现 wiring 漏洞
46
+ - 多个命令测试脚本现在共用统一的 `runCli` / JSON 解析 helper,减少重复的 spawn 逻辑并保持测试行为一致
47
+ - `da-vinci --help` 现在会把 `--confirm-test-evidence-executed` 这类超长 flag 正确换行显示,不再把 flag 和说明挤成一行
48
+ - 这次发布不改变既有运行时契约,但显著降低了 CLI 表面的维护成本和后续扩展风险
49
49
 
50
50
  ## Discipline And Orchestration 升级
51
51
 
@@ -2,6 +2,7 @@ const { Worker } = require("worker_threads");
2
2
  const path = require("path");
3
3
 
4
4
  const WORKER_SCRIPT_PATH = path.join(__dirname, "async-offload-worker.js");
5
+ const DEFAULT_WORKER_TIMEOUT_MS = 5 * 60 * 1000;
5
6
 
6
7
  function normalizeError(error) {
7
8
  if (!error || typeof error !== "object") {
@@ -24,22 +25,51 @@ function normalizeError(error) {
24
25
  return wrapped;
25
26
  }
26
27
 
27
- function runModuleExportInWorker(modulePath, exportName, args = []) {
28
+ function resolveWorkerTimeoutMs(options = {}) {
29
+ const optionValue = Number(options.timeoutMs);
30
+ if (Number.isFinite(optionValue) && optionValue > 0) {
31
+ return optionValue;
32
+ }
33
+
34
+ const envValue = Number(process.env.DA_VINCI_WORKER_TIMEOUT_MS);
35
+ if (Number.isFinite(envValue) && envValue > 0) {
36
+ return envValue;
37
+ }
38
+
39
+ return DEFAULT_WORKER_TIMEOUT_MS;
40
+ }
41
+
42
+ function buildWorkerTimeoutError(modulePath, exportName, timeoutMs) {
43
+ const error = new Error(
44
+ `Worker timed out after ${timeoutMs}ms while running ${path.basename(modulePath)}#${exportName}.`
45
+ );
46
+ error.code = "ETIMEDOUT";
47
+ return error;
48
+ }
49
+
50
+ function runModuleExportInWorker(modulePath, exportName, args = [], options = {}) {
28
51
  return new Promise((resolve, reject) => {
52
+ const resolvedModulePath = path.resolve(modulePath);
53
+ const timeoutMs = resolveWorkerTimeoutMs(options);
29
54
  const worker = new Worker(WORKER_SCRIPT_PATH, {
30
55
  workerData: {
31
- modulePath: path.resolve(modulePath),
56
+ modulePath: resolvedModulePath,
32
57
  exportName,
33
58
  args
34
59
  }
35
60
  });
36
61
 
37
62
  let settled = false;
63
+ let timeoutHandle = null;
38
64
  function settle(callback, value) {
39
65
  if (settled) {
40
66
  return;
41
67
  }
42
68
  settled = true;
69
+ if (timeoutHandle) {
70
+ clearTimeout(timeoutHandle);
71
+ timeoutHandle = null;
72
+ }
43
73
 
44
74
  const termination = worker.terminate();
45
75
  if (termination && typeof termination.then === "function") {
@@ -54,6 +84,13 @@ function runModuleExportInWorker(modulePath, exportName, args = []) {
54
84
  callback(value);
55
85
  }
56
86
 
87
+ timeoutHandle = setTimeout(() => {
88
+ settle(reject, buildWorkerTimeoutError(resolvedModulePath, exportName, timeoutMs));
89
+ }, timeoutMs);
90
+ if (typeof timeoutHandle.unref === "function") {
91
+ timeoutHandle.unref();
92
+ }
93
+
57
94
  worker.once("message", (message) => {
58
95
  if (message && message.ok) {
59
96
  settle(resolve, message.result);
@@ -0,0 +1,132 @@
1
+ const { registerCommand } = require("./helpers");
2
+
3
+ /**
4
+ * @typedef {Object} CliCoreCommandContext
5
+ * @property {string[]} argv
6
+ * @property {string} homeDir
7
+ * @property {string[]} positionalArgs
8
+ * @property {boolean} continueOnError
9
+ * @property {(args: string[], name: string) => string | undefined} getOption
10
+ * @property {string} VERSION
11
+ * @property {Function} installPlatforms
12
+ * @property {Function} uninstallPlatforms
13
+ * @property {Function} getStatus
14
+ * @property {Function} verifyInstall
15
+ * @property {Function} validateAssets
16
+ * @property {Function} runMaintainerReadinessCheck
17
+ * @property {Function} formatMaintainerReadinessReport
18
+ * @property {Function} formatStatus
19
+ * @property {Function} formatVerifyInstallReport
20
+ * @property {Function} emitCommandOutput
21
+ * @property {Function} formatTuiHelp
22
+ * @property {Function} launchTui
23
+ */
24
+
25
+ /**
26
+ * Register platform/bootstrap-adjacent CLI commands plus the TUI entrypoint.
27
+ *
28
+ * @param {import("./helpers").CliCommandHandlerMap} handlers
29
+ * @param {CliCoreCommandContext} context
30
+ * @returns {void}
31
+ */
32
+ function registerCoreCommands(handlers, context) {
33
+ const {
34
+ argv,
35
+ homeDir,
36
+ positionalArgs,
37
+ continueOnError,
38
+ getOption,
39
+ VERSION,
40
+ installPlatforms,
41
+ uninstallPlatforms,
42
+ getStatus,
43
+ verifyInstall,
44
+ validateAssets,
45
+ runMaintainerReadinessCheck,
46
+ formatMaintainerReadinessReport,
47
+ formatStatus,
48
+ formatVerifyInstallReport,
49
+ emitCommandOutput,
50
+ formatTuiHelp,
51
+ launchTui
52
+ } = context;
53
+
54
+ registerCommand(handlers, ["--version", "-v", "version"], async () => {
55
+ console.log(`Da Vinci v${VERSION}`);
56
+ });
57
+
58
+ registerCommand(handlers, "install", async () => {
59
+ const platformValue = getOption(argv, "--platform") || "all";
60
+ const result = installPlatforms(platformValue, { homeDir });
61
+ console.log(
62
+ `Installed Da Vinci v${result.version} for ${result.platforms.join(", ")} at ${result.homeDir}`
63
+ );
64
+ });
65
+
66
+ registerCommand(handlers, "uninstall", async () => {
67
+ const platformValue = getOption(argv, "--platform") || "all";
68
+ const result = uninstallPlatforms(platformValue, { homeDir });
69
+ console.log(
70
+ `Uninstalled Da Vinci v${result.version} for ${result.platforms.join(", ")} from ${result.homeDir}`
71
+ );
72
+ });
73
+
74
+ registerCommand(handlers, "status", async () => {
75
+ console.log(formatStatus(getStatus({ homeDir })));
76
+ });
77
+
78
+ registerCommand(handlers, "verify-install", async () => {
79
+ const platformValue = getOption(argv, "--platform") || "";
80
+ const result = verifyInstall({
81
+ homeDir,
82
+ platforms: platformValue
83
+ });
84
+ emitCommandOutput(result, argv, formatVerifyInstallReport, ["BLOCK"], continueOnError);
85
+ });
86
+
87
+ registerCommand(handlers, "maintainer-readiness", async () => {
88
+ const platformValue = getOption(argv, "--platform") || "";
89
+ const repoRoot = getOption(argv, "--project") || process.cwd();
90
+ const result = runMaintainerReadinessCheck({
91
+ repoRoot,
92
+ homeDir,
93
+ platforms: platformValue
94
+ });
95
+ emitCommandOutput(result, argv, formatMaintainerReadinessReport, ["BLOCK"], continueOnError);
96
+ });
97
+
98
+ registerCommand(handlers, "tui", async () => {
99
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
100
+ const changeId = getOption(argv, "--change");
101
+ const lang = getOption(argv, "--lang");
102
+ const tuiWidth = getOption(argv, "--tui-width");
103
+ const altScreen = argv.includes("--alt-screen")
104
+ ? true
105
+ : argv.includes("--no-alt-screen")
106
+ ? false
107
+ : undefined;
108
+ if (argv.includes("--help") || argv.includes("-h")) {
109
+ console.log(formatTuiHelp(lang));
110
+ return;
111
+ }
112
+ await launchTui({
113
+ projectPath,
114
+ changeId,
115
+ lang,
116
+ tuiWidth,
117
+ altScreen,
118
+ strict: argv.includes("--strict"),
119
+ jsonOutput: argv.includes("--json"),
120
+ continueOnError
121
+ });
122
+ });
123
+
124
+ registerCommand(handlers, "validate-assets", async () => {
125
+ const result = validateAssets();
126
+ console.log(`Da Vinci v${result.version} assets are complete (${result.requiredAssets} required files).`);
127
+ });
128
+ }
129
+
130
+ module.exports = {
131
+ registerCoreCommands
132
+ };
@@ -0,0 +1,129 @@
1
+ const { registerCommand } = require("./helpers");
2
+
3
+ /**
4
+ * @typedef {Object} CliDesignCommandContext
5
+ * @property {string[]} argv
6
+ * @property {string} homeDir
7
+ * @property {string[]} positionalArgs
8
+ * @property {boolean} continueOnError
9
+ * @property {(args: string[], name: string) => string | undefined} getOption
10
+ * @property {(args: string[], name: string, options?: object) => number | undefined} getIntegerOption
11
+ * @property {Function} syncIconCatalog
12
+ * @property {Function} formatIconSyncReport
13
+ * @property {Function} handleIconSearchCommand
14
+ * @property {Function} readOperations
15
+ * @property {Function} readLimitedStdin
16
+ * @property {Function} preflightPencilBatch
17
+ * @property {Function} formatPencilPreflightReport
18
+ * @property {Function} emitOrThrowOnStatus
19
+ * @property {Function} handleSupervisorReviewCommand
20
+ * @property {Function} saveCurrentDesign
21
+ * @property {Function} formatSaveCurrentDesignReport
22
+ * @property {{ BLOCKED: string, UNAVAILABLE: string }} SAVE_STATUS
23
+ */
24
+
25
+ /**
26
+ * Register design-tooling and design-review command handlers.
27
+ *
28
+ * @param {import("./helpers").CliCommandHandlerMap} handlers
29
+ * @param {CliDesignCommandContext} context
30
+ * @returns {void}
31
+ */
32
+ function registerDesignCommands(handlers, context) {
33
+ const {
34
+ argv,
35
+ homeDir,
36
+ positionalArgs,
37
+ continueOnError,
38
+ getOption,
39
+ getIntegerOption,
40
+ syncIconCatalog,
41
+ formatIconSyncReport,
42
+ handleIconSearchCommand,
43
+ readOperations,
44
+ readLimitedStdin,
45
+ preflightPencilBatch,
46
+ formatPencilPreflightReport,
47
+ emitOrThrowOnStatus,
48
+ handleSupervisorReviewCommand,
49
+ saveCurrentDesign,
50
+ formatSaveCurrentDesignReport,
51
+ SAVE_STATUS
52
+ } = context;
53
+
54
+ registerCommand(handlers, "icon-sync", async () => {
55
+ const outputPath = getOption(argv, "--output") || getOption(argv, "--catalog");
56
+ const timeoutMs = getIntegerOption(argv, "--timeout-ms", { min: 1 });
57
+ const strict = argv.includes("--strict");
58
+
59
+ const result = await syncIconCatalog({
60
+ outputPath,
61
+ timeoutMs,
62
+ strict,
63
+ homeDir
64
+ });
65
+
66
+ console.log(formatIconSyncReport(result));
67
+ });
68
+
69
+ registerCommand(handlers, "icon-search", async () => {
70
+ await handleIconSearchCommand(argv, homeDir);
71
+ });
72
+
73
+ registerCommand(handlers, "preflight-pencil", async () => {
74
+ const opsFile = getOption(argv, "--ops-file");
75
+ let operations = "";
76
+
77
+ if (opsFile) {
78
+ operations = readOperations(opsFile);
79
+ } else if (!process.stdin.isTTY) {
80
+ operations = readLimitedStdin();
81
+ } else {
82
+ throw new Error("`preflight-pencil` requires `--ops-file <path>` or piped stdin input.");
83
+ }
84
+
85
+ const result = preflightPencilBatch(operations);
86
+ const report = formatPencilPreflightReport(result);
87
+
88
+ if (emitOrThrowOnStatus(result.status, ["FAIL"], report, continueOnError)) {
89
+ return;
90
+ }
91
+
92
+ console.log(report);
93
+ });
94
+
95
+ registerCommand(handlers, "supervisor-review", async () => {
96
+ await handleSupervisorReviewCommand(argv);
97
+ });
98
+
99
+ registerCommand(handlers, "save-current-design", async () => {
100
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
101
+ const pencilBin = getOption(argv, "--pencil-bin");
102
+ const result = await saveCurrentDesign({
103
+ projectPath,
104
+ homeDir,
105
+ pencilBin,
106
+ allowLocalBridge: true
107
+ });
108
+ const output = argv.includes("--json")
109
+ ? JSON.stringify(result, null, 2)
110
+ : formatSaveCurrentDesignReport(result);
111
+
112
+ if (
113
+ emitOrThrowOnStatus(
114
+ result.status,
115
+ [SAVE_STATUS.BLOCKED, SAVE_STATUS.UNAVAILABLE],
116
+ output,
117
+ continueOnError
118
+ )
119
+ ) {
120
+ return;
121
+ }
122
+
123
+ console.log(output);
124
+ });
125
+ }
126
+
127
+ module.exports = {
128
+ registerDesignCommands
129
+ };
@@ -0,0 +1,231 @@
1
+ const { registerCommand } = require("./helpers");
2
+
3
+ /**
4
+ * @typedef {Object} CliPenCommandContext
5
+ * @property {string[]} argv
6
+ * @property {string} homeDir
7
+ * @property {string[]} positionalArgs
8
+ * @property {boolean} continueOnError
9
+ * @property {(args: string[], name: string) => string | undefined} getOption
10
+ * @property {(args: string[], name: string) => string[]} getCommaSeparatedOptionValues
11
+ * @property {Function} ensurePenFile
12
+ * @property {Function} writePenFromPayloadFiles
13
+ * @property {Function} comparePenSync
14
+ * @property {Function} comparePenBaselineAlignment
15
+ * @property {Function} formatPenBaselineAlignmentReport
16
+ * @property {Function} syncPenSource
17
+ * @property {Function} snapshotPenFile
18
+ * @property {Function} handlePencilLockCommand
19
+ * @property {Function} handlePencilSessionCommand
20
+ * @property {Function} emitOrThrowOnStatus
21
+ */
22
+
23
+ /**
24
+ * Register `.pen`, Pencil lock, and Pencil session command handlers.
25
+ *
26
+ * @param {import("./helpers").CliCommandHandlerMap} handlers
27
+ * @param {CliPenCommandContext} context
28
+ * @returns {void}
29
+ */
30
+ function registerPenCommands(handlers, context) {
31
+ const {
32
+ argv,
33
+ homeDir,
34
+ positionalArgs,
35
+ continueOnError,
36
+ getOption,
37
+ getCommaSeparatedOptionValues,
38
+ ensurePenFile,
39
+ writePenFromPayloadFiles,
40
+ comparePenSync,
41
+ comparePenBaselineAlignment,
42
+ formatPenBaselineAlignmentReport,
43
+ syncPenSource,
44
+ snapshotPenFile,
45
+ handlePencilLockCommand,
46
+ handlePencilSessionCommand,
47
+ emitOrThrowOnStatus
48
+ } = context;
49
+
50
+ registerCommand(handlers, "ensure-pen", async () => {
51
+ const outputPath = getOption(argv, "--output");
52
+ const version = getOption(argv, "--version");
53
+ const verifyWithPencil = argv.includes("--verify-open");
54
+
55
+ if (!outputPath) {
56
+ throw new Error("`ensure-pen` requires `--output <path>`.");
57
+ }
58
+
59
+ const result = ensurePenFile({
60
+ outputPath,
61
+ version,
62
+ verifyWithPencil
63
+ });
64
+
65
+ console.log(`${result.created ? "Created" : "Verified existing"} .pen source at ${result.outputPath}`);
66
+ console.log(`State file: ${result.statePath}`);
67
+ console.log(`Snapshot hash: ${result.state.snapshotHash}`);
68
+ if (result.verification) {
69
+ console.log(`Verified reopen with Pencil (${result.verification.topLevelCount} top-level nodes).`);
70
+ }
71
+ });
72
+
73
+ registerCommand(handlers, "write-pen", async () => {
74
+ const outputPath = getOption(argv, "--output");
75
+ const nodesFile = getOption(argv, "--nodes-file");
76
+ const variablesFile = getOption(argv, "--variables-file");
77
+ const version = getOption(argv, "--version");
78
+ const verifyWithPencil = argv.includes("--verify-open");
79
+
80
+ if (!outputPath || !nodesFile) {
81
+ throw new Error("`write-pen` requires `--output <path>` and `--nodes-file <path>`.");
82
+ }
83
+
84
+ const result = writePenFromPayloadFiles({
85
+ outputPath,
86
+ nodesFile,
87
+ variablesFile,
88
+ version,
89
+ verifyWithPencil
90
+ });
91
+
92
+ console.log(`Wrote .pen file to ${result.outputPath}`);
93
+ console.log(`State file: ${result.statePath}`);
94
+ console.log(`Snapshot hash: ${result.state.snapshotHash}`);
95
+ console.log(`Top-level nodes: ${result.document.children.length}`);
96
+ if (result.verification) {
97
+ console.log(`Verified reopen with Pencil (${result.verification.topLevelCount} top-level nodes).`);
98
+ }
99
+ });
100
+
101
+ registerCommand(handlers, "check-pen-sync", async () => {
102
+ const penPath = getOption(argv, "--pen");
103
+ const nodesFile = getOption(argv, "--nodes-file");
104
+ const variablesFile = getOption(argv, "--variables-file");
105
+ const version = getOption(argv, "--version");
106
+
107
+ if (!penPath || !nodesFile) {
108
+ throw new Error("`check-pen-sync` requires `--pen <path>` and `--nodes-file <path>`.");
109
+ }
110
+
111
+ const result = comparePenSync({
112
+ penPath,
113
+ nodesFile,
114
+ variablesFile,
115
+ version
116
+ });
117
+
118
+ if (!result.inSync) {
119
+ throw new Error(
120
+ [
121
+ "Registered `.pen` is out of sync with the provided live payload.",
122
+ `Pen: ${result.penPath}`,
123
+ `State file: ${result.statePath}`,
124
+ `Persisted hash: ${result.persistedHash}`,
125
+ `Live hash: ${result.liveHash}`
126
+ ].join("\n")
127
+ );
128
+ }
129
+
130
+ console.log(`Registered .pen is in sync: ${result.penPath}`);
131
+ console.log(`State file: ${result.statePath}`);
132
+ console.log(`Snapshot hash: ${result.liveHash}`);
133
+ if (!result.usedStateFile) {
134
+ console.log(
135
+ result.stateHash
136
+ ? "State file hash was stale; sync comparison fell back to hashing the disk .pen file directly."
137
+ : "State file was missing; sync comparison fell back to hashing the disk .pen file directly."
138
+ );
139
+ }
140
+ });
141
+
142
+ registerCommand(handlers, "check-pen-baseline", async () => {
143
+ const penPath = getOption(argv, "--pen");
144
+ const baselinePaths = getCommaSeparatedOptionValues(argv, "--baseline");
145
+ const preferredSource = getOption(argv, "--prefer-source");
146
+
147
+ if (!penPath || baselinePaths.length === 0) {
148
+ throw new Error(
149
+ "`check-pen-baseline` requires `--pen <path>` and at least one `--baseline <path>`."
150
+ );
151
+ }
152
+
153
+ const result = comparePenBaselineAlignment({
154
+ penPath,
155
+ baselinePaths,
156
+ preferredSource
157
+ });
158
+
159
+ if (
160
+ emitOrThrowOnStatus(
161
+ result.status,
162
+ ["BLOCK"],
163
+ [
164
+ "Baseline alignment check failed.",
165
+ formatPenBaselineAlignmentReport(result)
166
+ ].join("\n"),
167
+ continueOnError
168
+ )
169
+ ) {
170
+ return;
171
+ }
172
+
173
+ console.log(formatPenBaselineAlignmentReport(result));
174
+ });
175
+
176
+ registerCommand(handlers, "sync-pen-source", async () => {
177
+ const sourcePath = getOption(argv, "--from");
178
+ const targetPath = getOption(argv, "--to");
179
+
180
+ if (!sourcePath || !targetPath) {
181
+ throw new Error("`sync-pen-source` requires `--from <path>` and `--to <path>`.");
182
+ }
183
+
184
+ const result = syncPenSource({
185
+ sourcePath,
186
+ targetPath
187
+ });
188
+
189
+ console.log(`Synced .pen source from ${result.sourcePath} to ${result.targetPath}`);
190
+ console.log(`State file: ${result.statePath}`);
191
+ console.log(`Snapshot hash: ${result.state.snapshotHash}`);
192
+ });
193
+
194
+ registerCommand(handlers, "snapshot-pen", async () => {
195
+ const inputPath = getOption(argv, "--input");
196
+ const outputPath = getOption(argv, "--output");
197
+ const version = getOption(argv, "--version");
198
+ const verifyWithPencil = argv.includes("--verify-open");
199
+
200
+ if (!inputPath || !outputPath) {
201
+ throw new Error("`snapshot-pen` requires `--input <path>` and `--output <path>`.");
202
+ }
203
+
204
+ const result = snapshotPenFile({
205
+ inputPath,
206
+ outputPath,
207
+ version,
208
+ verifyWithPencil
209
+ });
210
+
211
+ console.log(`Snapshotted ${result.inputPath} to ${result.outputPath}`);
212
+ console.log(`State file: ${result.statePath}`);
213
+ console.log(`Snapshot hash: ${result.state.snapshotHash}`);
214
+ console.log(`Top-level nodes: ${result.document.children.length}`);
215
+ if (result.verification) {
216
+ console.log(`Verified reopen with Pencil (${result.verification.topLevelCount} top-level nodes).`);
217
+ }
218
+ });
219
+
220
+ registerCommand(handlers, "pencil-lock", async () => {
221
+ handlePencilLockCommand(argv, homeDir);
222
+ });
223
+
224
+ registerCommand(handlers, "pencil-session", async () => {
225
+ handlePencilSessionCommand(argv, homeDir);
226
+ });
227
+ }
228
+
229
+ module.exports = {
230
+ registerPenCommands
231
+ };