@xenonbyte/da-vinci-workflow 0.2.2 → 0.2.4

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 (72) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +49 -14
  3. package/README.zh-CN.md +169 -14
  4. package/commands/claude/dv/breakdown.md +8 -0
  5. package/commands/claude/dv/build.md +16 -0
  6. package/commands/claude/dv/continue.md +4 -0
  7. package/commands/claude/dv/design.md +5 -2
  8. package/commands/claude/dv/tasks.md +14 -0
  9. package/commands/claude/dv/verify.md +11 -0
  10. package/commands/codex/prompts/dv-breakdown.md +8 -0
  11. package/commands/codex/prompts/dv-build.md +16 -0
  12. package/commands/codex/prompts/dv-continue.md +4 -0
  13. package/commands/codex/prompts/dv-design.md +5 -2
  14. package/commands/codex/prompts/dv-tasks.md +14 -0
  15. package/commands/codex/prompts/dv-verify.md +10 -0
  16. package/commands/gemini/dv/breakdown.toml +8 -0
  17. package/commands/gemini/dv/build.toml +16 -0
  18. package/commands/gemini/dv/continue.toml +4 -0
  19. package/commands/gemini/dv/design.toml +5 -2
  20. package/commands/gemini/dv/tasks.toml +14 -0
  21. package/commands/gemini/dv/verify.toml +10 -0
  22. package/commands/templates/dv-continue.shared.md +4 -0
  23. package/docs/discipline-and-orchestration-upgrade.md +83 -0
  24. package/docs/dv-command-reference.md +61 -2
  25. package/docs/execution-chain-migration.md +23 -0
  26. package/docs/execution-chain-plan.md +10 -3
  27. package/docs/mode-use-cases.md +2 -1
  28. package/docs/pencil-rendering-workflow.md +15 -12
  29. package/docs/prompt-entrypoints.md +5 -0
  30. package/docs/prompt-presets/README.md +1 -1
  31. package/docs/prompt-presets/desktop-app.md +3 -3
  32. package/docs/prompt-presets/mobile-app.md +3 -3
  33. package/docs/prompt-presets/tablet-app.md +3 -3
  34. package/docs/prompt-presets/web-app.md +3 -3
  35. package/docs/skill-usage.md +61 -38
  36. package/docs/workflow-examples.md +16 -13
  37. package/docs/workflow-overview.md +19 -0
  38. package/docs/zh-CN/dv-command-reference.md +59 -2
  39. package/docs/zh-CN/execution-chain-migration.md +23 -0
  40. package/docs/zh-CN/mode-use-cases.md +2 -1
  41. package/docs/zh-CN/pencil-rendering-workflow.md +15 -12
  42. package/docs/zh-CN/prompt-entrypoints.md +5 -0
  43. package/docs/zh-CN/prompt-presets/README.md +1 -1
  44. package/docs/zh-CN/prompt-presets/desktop-app.md +3 -3
  45. package/docs/zh-CN/prompt-presets/mobile-app.md +3 -3
  46. package/docs/zh-CN/prompt-presets/tablet-app.md +3 -3
  47. package/docs/zh-CN/prompt-presets/web-app.md +3 -3
  48. package/docs/zh-CN/skill-usage.md +61 -38
  49. package/docs/zh-CN/workflow-examples.md +15 -13
  50. package/docs/zh-CN/workflow-overview.md +19 -0
  51. package/examples/greenfield-spec-markupflow/.da-vinci/state/execution-signals/demo__lint-tasks.json +16 -0
  52. package/lib/audit-parsers.js +166 -10
  53. package/lib/audit.js +3 -26
  54. package/lib/cli.js +156 -2
  55. package/lib/design-source-registry.js +146 -0
  56. package/lib/execution-profile.js +143 -0
  57. package/lib/execution-signals.js +19 -1
  58. package/lib/lint-tasks.js +86 -2
  59. package/lib/planning-parsers.js +255 -18
  60. package/lib/save-current-design.js +790 -0
  61. package/lib/supervisor-review.js +3 -2
  62. package/lib/task-execution.js +160 -0
  63. package/lib/task-review.js +197 -0
  64. package/lib/verify.js +152 -1
  65. package/lib/workflow-bootstrap.js +2 -13
  66. package/lib/workflow-persisted-state.js +3 -1
  67. package/lib/workflow-state.js +503 -33
  68. package/lib/worktree-preflight.js +214 -0
  69. package/package.json +1 -1
  70. package/references/artifact-templates.md +56 -6
  71. package/tui/catalog.js +103 -0
  72. package/tui/index.js +2274 -418
@@ -244,18 +244,27 @@ function normalizeCheckpointLabel(value) {
244
244
 
245
245
  function parseCheckpointStatusMap(markdownText) {
246
246
  const section = getMarkdownSection(markdownText, "Checkpoint Status");
247
- if (!section) {
248
- return {};
247
+ const statuses = {};
248
+
249
+ if (section) {
250
+ const matches = section.matchAll(/(?:^|\n)\s*-\s*`?([^`:\n]+?)`?\s*:\s*(PASS|WARN|BLOCK)\b/gi);
251
+ for (const match of matches) {
252
+ const label = normalizeCheckpointLabel(match[1]);
253
+ if (!label) {
254
+ continue;
255
+ }
256
+ statuses[label] = String(match[2]).toUpperCase();
257
+ }
249
258
  }
250
259
 
251
- const statuses = {};
252
- const matches = section.matchAll(/(?:^|\n)\s*-\s*`?([^`:\n]+?)`?\s*:\s*(PASS|WARN|BLOCK)\b/gi);
253
- for (const match of matches) {
254
- const label = normalizeCheckpointLabel(match[1]);
255
- if (!label) {
256
- continue;
260
+ const runtimeGateSection = getMarkdownSection(markdownText, "MCP Runtime Gate");
261
+ if (runtimeGateSection) {
262
+ const runtimeGateMatch = String(runtimeGateSection).match(
263
+ /(?:^|\n)\s*-\s*(?:Final runtime gate status|Status|状态)\s*:\s*`?(PASS|WARN|BLOCK)`?\b/i
264
+ );
265
+ if (runtimeGateMatch) {
266
+ statuses[normalizeCheckpointLabel("mcp runtime gate")] = String(runtimeGateMatch[1]).toUpperCase();
257
267
  }
258
- statuses[label] = String(match[2]).toUpperCase();
259
268
  }
260
269
 
261
270
  return statuses;
@@ -751,6 +760,149 @@ function inspectDesignSupervisorReview(pencilDesignText) {
751
760
  };
752
761
  }
753
762
 
763
+ const DISCIPLINE_MARKER_NAMES = Object.freeze({
764
+ designApproval: "design_approval",
765
+ planSelfReview: "plan_self_review",
766
+ operatorReviewAck: "operator_review_ack"
767
+ });
768
+
769
+ const DISCIPLINE_MARKER_ALIASES = Object.freeze({
770
+ [DISCIPLINE_MARKER_NAMES.designApproval]: [
771
+ "design approval",
772
+ "design-approved",
773
+ "design approved",
774
+ "design_approval",
775
+ "design-approval"
776
+ ],
777
+ [DISCIPLINE_MARKER_NAMES.planSelfReview]: [
778
+ "plan self review",
779
+ "plan_self_review",
780
+ "plan-self-review",
781
+ "plan review",
782
+ "plan-review"
783
+ ],
784
+ [DISCIPLINE_MARKER_NAMES.operatorReviewAck]: [
785
+ "operator review ack",
786
+ "operator review acknowledgement",
787
+ "operator review acknowledgment",
788
+ "operator_ack",
789
+ "operator-review-ack",
790
+ "operator-review-acknowledgement",
791
+ "operator-review-acknowledgment"
792
+ ]
793
+ });
794
+
795
+ const DISCIPLINE_MARKER_KEYWORDS = Object.freeze(
796
+ Object.values(DISCIPLINE_MARKER_ALIASES)
797
+ .flat()
798
+ .map((token) => String(token).replace(/[_-]+/g, " ").toLowerCase())
799
+ );
800
+
801
+ function normalizeDisciplineMarkerName(value) {
802
+ const normalized = String(value || "")
803
+ .toLowerCase()
804
+ .replace(/[`*_~]/g, "")
805
+ .replace(/[_-]+/g, " ")
806
+ .replace(/\s+/g, " ")
807
+ .trim();
808
+ if (!normalized) {
809
+ return "";
810
+ }
811
+ for (const [canonicalName, aliases] of Object.entries(DISCIPLINE_MARKER_ALIASES)) {
812
+ if (aliases.some((alias) => normalized === String(alias).replace(/[_-]+/g, " ").toLowerCase())) {
813
+ return canonicalName;
814
+ }
815
+ }
816
+ return "";
817
+ }
818
+
819
+ function normalizeDisciplineMarkerStatus(value) {
820
+ return String(value || "")
821
+ .toUpperCase()
822
+ .replace(/[`*_~]/g, "")
823
+ .replace(/[()]/g, "")
824
+ .replace(/\s+/g, "_")
825
+ .replace(/[^A-Z0-9_]+/g, "_")
826
+ .replace(/^_+|_+$/g, "");
827
+ }
828
+
829
+ function parseDisciplineMarkers(markdownText) {
830
+ const markers = {};
831
+ const ordered = [];
832
+ const malformed = [];
833
+ const lines = String(markdownText || "").replace(/\r\n?/g, "\n").split("\n");
834
+ const markerPattern =
835
+ /^\s*-\s*`?([A-Za-z][A-Za-z0-9 _-]{1,80})`?\s*:\s*`?([A-Za-z0-9 _-]{1,80})`?(?:\s*@\s*`?([^`]+?)`?)?\s*$/;
836
+
837
+ for (let index = 0; index < lines.length; index += 1) {
838
+ const rawLine = lines[index];
839
+ const match = rawLine.match(markerPattern);
840
+ if (!match) {
841
+ const normalizedLine = String(rawLine || "")
842
+ .toLowerCase()
843
+ .replace(/[_-]+/g, " ")
844
+ .replace(/\s+/g, " ")
845
+ .trim();
846
+ if (!normalizedLine.startsWith("-")) {
847
+ continue;
848
+ }
849
+ if (DISCIPLINE_MARKER_KEYWORDS.some((keyword) => normalizedLine.includes(keyword))) {
850
+ malformed.push({
851
+ line: index + 1,
852
+ reason: "Malformed discipline marker syntax.",
853
+ raw: rawLine
854
+ });
855
+ }
856
+ continue;
857
+ }
858
+
859
+ const markerName = normalizeDisciplineMarkerName(match[1]);
860
+ if (!markerName) {
861
+ continue;
862
+ }
863
+
864
+ const status = normalizeDisciplineMarkerStatus(match[2]);
865
+ if (!status) {
866
+ malformed.push({
867
+ line: index + 1,
868
+ reason: "Missing marker status token.",
869
+ raw: rawLine
870
+ });
871
+ continue;
872
+ }
873
+
874
+ const rawTime = String(match[3] || "").trim();
875
+ let time = "";
876
+ if (rawTime) {
877
+ time = normalizeTimeToken(rawTime);
878
+ if (!time) {
879
+ malformed.push({
880
+ line: index + 1,
881
+ reason: "Invalid marker timestamp.",
882
+ raw: rawLine
883
+ });
884
+ }
885
+ }
886
+
887
+ const record = {
888
+ name: markerName,
889
+ status,
890
+ time,
891
+ rawTime,
892
+ line: index + 1,
893
+ raw: rawLine
894
+ };
895
+ ordered.push(record);
896
+ markers[markerName] = record;
897
+ }
898
+
899
+ return {
900
+ markers,
901
+ ordered,
902
+ malformed
903
+ };
904
+ }
905
+
754
906
  module.exports = {
755
907
  hasMarkdownHeading,
756
908
  getMarkdownSection,
@@ -766,5 +918,9 @@ module.exports = {
766
918
  getConfiguredDesignSupervisorReviewers,
767
919
  hasConfiguredDesignSupervisorReview,
768
920
  isDesignSupervisorReviewRequired,
769
- inspectDesignSupervisorReview
921
+ inspectDesignSupervisorReview,
922
+ DISCIPLINE_MARKER_NAMES,
923
+ normalizeDisciplineMarkerName,
924
+ normalizeDisciplineMarkerStatus,
925
+ parseDisciplineMarkers
770
926
  };
package/lib/audit.js CHANGED
@@ -8,7 +8,8 @@ const {
8
8
  assertPenDocumentShape
9
9
  } = require("./pen-persistence");
10
10
  const { getSessionStatePath, readSessionState } = require("./pencil-session");
11
- const { isPathInside, listFilesRecursiveSafe } = require("./fs-safety");
11
+ const { listFilesRecursiveSafe } = require("./fs-safety");
12
+ const { collectRegisteredPenPaths } = require("./design-source-registry");
12
13
  const { pathExists, readTextIfExists } = require("./utils");
13
14
  const { runModuleExportInWorker } = require("./async-offload");
14
15
  const { readExecutionSignals, summarizeSignalsBySurface } = require("./execution-signals");
@@ -69,30 +70,6 @@ function relativeTo(projectRoot, targetPath) {
69
70
  return path.relative(projectRoot, targetPath) || ".";
70
71
  }
71
72
 
72
- function collectRegisteredPenPaths(projectRoot, designRegistryPath) {
73
- const registryText = readTextIfExists(designRegistryPath);
74
- const matches = registryText.match(/\.da-vinci\/designs\/[^\s`]+\.pen/g) || [];
75
- const validPaths = [];
76
- const escapedPaths = [];
77
-
78
- for (const relativePath of [...new Set(matches)]) {
79
- const resolvedPath = path.resolve(projectRoot, relativePath);
80
- if (!isPathInside(projectRoot, resolvedPath)) {
81
- escapedPaths.push({
82
- relativePath,
83
- resolvedPath
84
- });
85
- continue;
86
- }
87
- validPaths.push(resolvedPath);
88
- }
89
-
90
- return {
91
- validPaths,
92
- escapedPaths
93
- };
94
- }
95
-
96
73
  function getNonEmptyChangeDirs(changesDir) {
97
74
  return listChildDirs(changesDir).filter((changeDir) => {
98
75
  const scan = listFilesRecursive(changeDir, CHANGE_SCAN_LIMITS);
@@ -368,7 +345,7 @@ function auditProject(projectPathInput, options = {}) {
368
345
  const {
369
346
  validPaths: registeredPenPaths,
370
347
  escapedPaths: escapedRegisteredPenPaths
371
- } = collectRegisteredPenPaths(projectRoot, designRegistryPath);
348
+ } = collectRegisteredPenPaths(projectRoot, readTextIfExists(designRegistryPath));
372
349
  for (const escapedPath of escapedRegisteredPenPaths) {
373
350
  const message =
374
351
  `Registered design source escapes project root and will be ignored: ${escapedPath.relativePath} ` +
package/lib/cli.js CHANGED
@@ -32,6 +32,11 @@ const {
32
32
  endPencilSession,
33
33
  getPencilSessionStatus
34
34
  } = require("./pencil-session");
35
+ const {
36
+ SAVE_STATUS,
37
+ saveCurrentDesign,
38
+ formatSaveCurrentDesignReport
39
+ } = require("./save-current-design");
35
40
  const {
36
41
  searchIconLibrary,
37
42
  formatIconSearchReport
@@ -74,6 +79,18 @@ const {
74
79
  const { diffSpec, formatDiffSpecReport } = require("./diff-spec");
75
80
  const { scaffoldFromBindings, formatScaffoldReport } = require("./scaffold");
76
81
  const { writeExecutionSignal } = require("./execution-signals");
82
+ const {
83
+ writeTaskExecutionEnvelope,
84
+ formatTaskExecutionReport
85
+ } = require("./task-execution");
86
+ const {
87
+ writeTaskReviewEnvelope,
88
+ formatTaskReviewReport
89
+ } = require("./task-review");
90
+ const {
91
+ runWorktreePreflight,
92
+ formatWorktreePreflightReport
93
+ } = require("./worktree-preflight");
77
94
  const { formatTuiHelp, launchTui } = require("../tui");
78
95
 
79
96
  const DEFAULT_MAX_PREFLIGHT_STDIN_BYTES = 1024 * 1024;
@@ -83,6 +100,7 @@ const OPTION_FLAGS_WITH_VALUES = new Set([
83
100
  "--home",
84
101
  "--platform",
85
102
  "--lang",
103
+ "--tui-width",
86
104
  "--project",
87
105
  "--mode",
88
106
  "--change",
@@ -95,6 +113,15 @@ const OPTION_FLAGS_WITH_VALUES = new Set([
95
113
  "--aliases",
96
114
  "--pencil-design",
97
115
  "--status",
116
+ "--stage",
117
+ "--summary",
118
+ "--task-group",
119
+ "--changed-files",
120
+ "--test-evidence",
121
+ "--concerns",
122
+ "--blockers",
123
+ "--issues",
124
+ "--reviewer",
98
125
  "--source",
99
126
  "--executed-reviewers",
100
127
  "--codex-bin",
@@ -113,6 +140,7 @@ const OPTION_FLAGS_WITH_VALUES = new Set([
113
140
  "--from",
114
141
  "--to",
115
142
  "--pen",
143
+ "--pencil-bin",
116
144
  "--nodes-file",
117
145
  "--variables-file",
118
146
  "--version",
@@ -123,6 +151,7 @@ const OPTION_FLAGS_WITH_VALUES = new Set([
123
151
  const HELP_OPTION_SPECS = [
124
152
  { flag: "--platform <value>", description: "codex, claude, gemini, or all" },
125
153
  { flag: "--lang <value>", description: "ui language for `da-vinci tui`: en or zh" },
154
+ { flag: "--tui-width <cols>", description: "fixed layout width (columns) for `da-vinci tui`" },
126
155
  { flag: "--home <path>", description: "override HOME for installation targets" },
127
156
  { flag: "--project <path>", description: "override project path for audit" },
128
157
  {
@@ -155,6 +184,16 @@ const HELP_OPTION_SPECS = [
155
184
  description: "icon-search family filter: all, material, rounded, outlined, sharp, lucide, feather, phosphor"
156
185
  },
157
186
  { flag: "--status <value>", description: "PASS, WARN, or BLOCK for supervisor-review" },
187
+ { flag: "--stage <value>", description: "task-review stage: spec or quality" },
188
+ { flag: "--summary <text>", description: "task-execution/task-review summary text" },
189
+ { flag: "--task-group <id>", description: "task group identifier for task-execution/task-review" },
190
+ { flag: "--changed-files <csv>", description: "comma-separated changed files for task-execution" },
191
+ { flag: "--test-evidence <csv>", description: "comma-separated test evidence commands for task-execution" },
192
+ { flag: "--concerns <csv>", description: "comma-separated concern text for task-execution" },
193
+ { flag: "--blockers <csv>", description: "comma-separated blocker text for task-execution" },
194
+ { flag: "--issues <csv>", description: "comma-separated issue text for task-review" },
195
+ { flag: "--reviewer <name>", description: "reviewer identifier for task-review" },
196
+ { flag: "--write-verification", description: "append task-review evidence into verification.md" },
158
197
  { flag: "--source <value>", description: "review source: skill, manual, inferred" },
159
198
  { flag: "--executed-reviewers <csv>", description: "reviewer skills that executed this review" },
160
199
  { flag: "--run-reviewers", description: "execute configured reviewer skills through codex exec" },
@@ -188,12 +227,15 @@ const HELP_OPTION_SPECS = [
188
227
  flag: "--strict",
189
228
  description: "enable strict failure mode for commands that support advisory defaults (for example icon-sync, lint-spec)"
190
229
  },
230
+ { flag: "--alt-screen", description: "enable alternate terminal screen buffer for `da-vinci tui`" },
231
+ { flag: "--no-alt-screen", description: "disable alternate terminal screen buffer for `da-vinci tui`" },
191
232
  {
192
233
  flag: "--continue-on-error",
193
234
  description: "print BLOCK/FAIL command results without throwing process errors"
194
235
  },
195
236
  { flag: "--json", description: "print structured JSON output when supported by the command" },
196
237
  { flag: "--pen <path>", description: "registered .pen path for sync checks" },
238
+ { flag: "--pencil-bin <path>", description: "override Pencil executable for interactive capture/verify flows" },
197
239
  {
198
240
  flag: "--from <path>",
199
241
  description: "source path for sync-pen-source, or baseline sidecars directory for diff-spec"
@@ -475,7 +517,7 @@ function printHelp() {
475
517
  " da-vinci install --platform codex,claude,gemini",
476
518
  " da-vinci uninstall --platform codex,claude,gemini",
477
519
  " da-vinci status",
478
- " da-vinci tui [--project <path>] [--change <id>] [--lang en|zh] [--strict] [--json] [--continue-on-error]",
520
+ " da-vinci tui [--project <path>] [--change <id>] [--lang en|zh] [--strict] [--json] [--continue-on-error] [--tui-width <cols>] [--alt-screen|--no-alt-screen]",
479
521
  " da-vinci workflow-status [--project <path>] [--change <id>] [--json]",
480
522
  " da-vinci next-step [--project <path>] [--change <id>] [--json]",
481
523
  " da-vinci lint-spec [--project <path>] [--change <id>] [--strict] [--json]",
@@ -487,6 +529,9 @@ function printHelp() {
487
529
  " da-vinci verify-implementation [--project <path>] [--change <id>] [--strict] [--json]",
488
530
  " da-vinci verify-structure [--project <path>] [--change <id>] [--strict] [--json]",
489
531
  " da-vinci verify-coverage [--project <path>] [--change <id>] [--strict] [--json]",
532
+ " da-vinci task-execution --project <path> --change <id> --task-group <id> --status <DONE|DONE_WITH_CONCERNS|NEEDS_CONTEXT|BLOCKED> --summary <text> [--changed-files <csv>] [--test-evidence <csv>] [--concerns <csv>] [--blockers <csv>] [--json]",
533
+ " da-vinci task-review --project <path> --change <id> --task-group <id> --stage <spec|quality> --status <PASS|WARN|BLOCK> --summary <text> [--issues <csv>] [--reviewer <name>] [--write-verification] [--json]",
534
+ " da-vinci worktree-preflight --project <path> [--change <id>] [--json]",
490
535
  " da-vinci diff-spec [--project <path>] [--change <id>] [--from <sidecars-dir>] [--json]",
491
536
  " da-vinci scaffold [--project <path>] [--change <id>] [--output <path>] [--json]",
492
537
  " da-vinci validate-assets",
@@ -502,6 +547,7 @@ function printHelp() {
502
547
  " da-vinci check-pen-baseline --pen <path> --baseline <path>[,<path>...] [--baseline <path>] [--prefer-source <path>]",
503
548
  " da-vinci sync-pen-source --from <path> --to <path>",
504
549
  " da-vinci snapshot-pen --input <path> --output <path>",
550
+ " da-vinci save-current-design --project <path> [--json] [--continue-on-error]",
505
551
  " da-vinci pencil-lock acquire --project <path>",
506
552
  " da-vinci pencil-lock release --project <path>",
507
553
  " da-vinci pencil-lock status",
@@ -945,6 +991,12 @@ async function runCli(argv) {
945
991
  const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
946
992
  const changeId = getOption(argv, "--change");
947
993
  const lang = getOption(argv, "--lang");
994
+ const tuiWidth = getOption(argv, "--tui-width");
995
+ const altScreen = argv.includes("--alt-screen")
996
+ ? true
997
+ : argv.includes("--no-alt-screen")
998
+ ? false
999
+ : undefined;
948
1000
  if (argv.includes("--help") || argv.includes("-h")) {
949
1001
  console.log(formatTuiHelp(lang));
950
1002
  return;
@@ -953,6 +1005,8 @@ async function runCli(argv) {
953
1005
  projectPath,
954
1006
  changeId,
955
1007
  lang,
1008
+ tuiWidth,
1009
+ altScreen,
956
1010
  strict: argv.includes("--strict"),
957
1011
  jsonOutput: argv.includes("--json"),
958
1012
  continueOnError
@@ -980,7 +1034,21 @@ async function runCli(argv) {
980
1034
  const result = deriveWorkflowStatus(projectPath, { changeId });
981
1035
 
982
1036
  if (argv.includes("--json")) {
983
- console.log(JSON.stringify(result.nextStep || null, null, 2));
1037
+ console.log(
1038
+ JSON.stringify(
1039
+ {
1040
+ stage: result.stage,
1041
+ checkpointState: result.checkpointState,
1042
+ nextStep: result.nextStep || null,
1043
+ discipline: result.discipline || null,
1044
+ executionProfile: result.executionProfile || null,
1045
+ worktreePreflight: result.worktreePreflight || null,
1046
+ verificationFreshness: result.verificationFreshness || null
1047
+ },
1048
+ null,
1049
+ 2
1050
+ )
1051
+ );
984
1052
  return;
985
1053
  }
986
1054
 
@@ -1140,6 +1208,63 @@ async function runCli(argv) {
1140
1208
  return;
1141
1209
  }
1142
1210
 
1211
+ if (command === "task-execution") {
1212
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
1213
+ const changeId = getOption(argv, "--change");
1214
+ const result = writeTaskExecutionEnvelope(projectPath, {
1215
+ changeId,
1216
+ taskGroupId: getOption(argv, "--task-group"),
1217
+ status: getOption(argv, "--status"),
1218
+ summary: getOption(argv, "--summary"),
1219
+ changedFiles: getCommaSeparatedOptionValues(argv, "--changed-files"),
1220
+ testEvidence: getCommaSeparatedOptionValues(argv, "--test-evidence"),
1221
+ concerns: getCommaSeparatedOptionValues(argv, "--concerns"),
1222
+ blockers: getCommaSeparatedOptionValues(argv, "--blockers")
1223
+ });
1224
+ const useJson = argv.includes("--json");
1225
+ const output = useJson ? JSON.stringify(result, null, 2) : formatTaskExecutionReport(result);
1226
+ if (emitOrThrowOnStatus(result.status, ["BLOCK"], output, continueOnError)) {
1227
+ return;
1228
+ }
1229
+ console.log(output);
1230
+ return;
1231
+ }
1232
+
1233
+ if (command === "task-review") {
1234
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
1235
+ const changeId = getOption(argv, "--change");
1236
+ const result = writeTaskReviewEnvelope(projectPath, {
1237
+ changeId,
1238
+ taskGroupId: getOption(argv, "--task-group"),
1239
+ stage: getOption(argv, "--stage"),
1240
+ status: getOption(argv, "--status"),
1241
+ summary: getOption(argv, "--summary"),
1242
+ issues: getCommaSeparatedOptionValues(argv, "--issues"),
1243
+ reviewer: getOption(argv, "--reviewer"),
1244
+ writeVerification: argv.includes("--write-verification")
1245
+ });
1246
+ const useJson = argv.includes("--json");
1247
+ const output = useJson ? JSON.stringify(result, null, 2) : formatTaskReviewReport(result);
1248
+ if (emitOrThrowOnStatus(result.status, ["BLOCK"], output, continueOnError)) {
1249
+ return;
1250
+ }
1251
+ console.log(output);
1252
+ return;
1253
+ }
1254
+
1255
+ if (command === "worktree-preflight") {
1256
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
1257
+ const changeId = getOption(argv, "--change");
1258
+ const result = runWorktreePreflight(projectPath);
1259
+ persistExecutionSignal(projectPath, changeId || "global", "worktree-preflight", result, false);
1260
+ if (argv.includes("--json")) {
1261
+ console.log(JSON.stringify(result, null, 2));
1262
+ return;
1263
+ }
1264
+ console.log(formatWorktreePreflightReport(result));
1265
+ return;
1266
+ }
1267
+
1143
1268
  if (command === "diff-spec") {
1144
1269
  const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
1145
1270
  const changeId = getOption(argv, "--change");
@@ -1425,6 +1550,35 @@ async function runCli(argv) {
1425
1550
  return;
1426
1551
  }
1427
1552
 
1553
+ if (command === "save-current-design") {
1554
+ const projectPath = getOption(argv, "--project") || positionalArgs[0] || process.cwd();
1555
+ const pencilBin = getOption(argv, "--pencil-bin");
1556
+ const result = await saveCurrentDesign({
1557
+ projectPath,
1558
+ homeDir,
1559
+ pencilBin,
1560
+ allowLocalBridge: true
1561
+ });
1562
+ const useJson = argv.includes("--json");
1563
+ const output = useJson
1564
+ ? JSON.stringify(result, null, 2)
1565
+ : formatSaveCurrentDesignReport(result);
1566
+
1567
+ if (
1568
+ emitOrThrowOnStatus(
1569
+ result.status,
1570
+ [SAVE_STATUS.BLOCKED, SAVE_STATUS.UNAVAILABLE],
1571
+ output,
1572
+ continueOnError
1573
+ )
1574
+ ) {
1575
+ return;
1576
+ }
1577
+
1578
+ console.log(output);
1579
+ return;
1580
+ }
1581
+
1428
1582
  if (command === "pencil-lock") {
1429
1583
  handlePencilLockCommand(argv, homeDir);
1430
1584
  return;
@@ -0,0 +1,146 @@
1
+ const path = require("path");
2
+ const { isPathInside } = require("./fs-safety");
3
+
4
+ const REGISTERED_PEN_PATTERN = /\.da-vinci\/designs\/[^\s`]+\.pen/g;
5
+
6
+ function normalizeProjectRoot(projectRoot) {
7
+ return path.resolve(projectRoot || process.cwd());
8
+ }
9
+
10
+ function collectRegisteredPenReferences(registryText) {
11
+ const matches = String(registryText || "").match(REGISTERED_PEN_PATTERN) || [];
12
+ const seen = new Set();
13
+ const unique = [];
14
+
15
+ for (const match of matches) {
16
+ const normalized = String(match || "").trim();
17
+ if (!normalized || seen.has(normalized)) {
18
+ continue;
19
+ }
20
+ seen.add(normalized);
21
+ unique.push(normalized);
22
+ }
23
+
24
+ return unique;
25
+ }
26
+
27
+ function normalizeBoundPenPath(projectRoot, penPath, options = {}) {
28
+ const root = normalizeProjectRoot(projectRoot);
29
+ const raw = typeof penPath === "string" ? penPath.trim() : "";
30
+
31
+ if (!raw) {
32
+ return {
33
+ ok: false,
34
+ code: options.missingCode || "missing_path",
35
+ input: raw,
36
+ resolvedPath: ""
37
+ };
38
+ }
39
+
40
+ if (options.rejectNew && raw.toLowerCase() === "new") {
41
+ return {
42
+ ok: false,
43
+ code: options.newCode || "active_editor_new",
44
+ input: raw,
45
+ resolvedPath: ""
46
+ };
47
+ }
48
+
49
+ const resolvedPath = path.normalize(path.isAbsolute(raw) ? raw : path.resolve(root, raw));
50
+ if (!isPathInside(root, resolvedPath)) {
51
+ return {
52
+ ok: false,
53
+ code: options.outsideCode || "outside_project_root",
54
+ input: raw,
55
+ resolvedPath
56
+ };
57
+ }
58
+
59
+ if (options.requirePenExtension !== false && !/\.pen$/i.test(resolvedPath)) {
60
+ return {
61
+ ok: false,
62
+ code: options.extensionCode || "not_pen_path",
63
+ input: raw,
64
+ resolvedPath
65
+ };
66
+ }
67
+
68
+ return {
69
+ ok: true,
70
+ code: "ok",
71
+ input: raw,
72
+ resolvedPath
73
+ };
74
+ }
75
+
76
+ function normalizeRegisteredPenPath(projectRoot, penPath) {
77
+ return normalizeBoundPenPath(projectRoot, penPath, {
78
+ missingCode: "registered_pen_missing",
79
+ outsideCode: "registered_pen_outside_project_root",
80
+ extensionCode: "registered_pen_not_pen_path"
81
+ });
82
+ }
83
+
84
+ function normalizeSessionPenPath(projectRoot, penPath) {
85
+ return normalizeBoundPenPath(projectRoot, penPath, {
86
+ missingCode: "session_pen_missing",
87
+ outsideCode: "session_pen_outside_project_root",
88
+ extensionCode: "session_pen_not_pen_path"
89
+ });
90
+ }
91
+
92
+ function normalizeActiveEditorPath(projectRoot, activeEditorPath) {
93
+ return normalizeBoundPenPath(projectRoot, activeEditorPath, {
94
+ rejectNew: true,
95
+ missingCode: "active_editor_missing",
96
+ newCode: "active_editor_new",
97
+ outsideCode: "active_editor_outside_project_root",
98
+ extensionCode: "active_editor_not_pen_path"
99
+ });
100
+ }
101
+
102
+ function collectRegisteredPenPaths(projectRoot, registryText) {
103
+ const root = normalizeProjectRoot(projectRoot);
104
+ const references = collectRegisteredPenReferences(registryText);
105
+ const validPaths = [];
106
+ const escapedPaths = [];
107
+ const seenResolved = new Set();
108
+
109
+ for (const relativePath of references) {
110
+ const normalized = normalizeRegisteredPenPath(root, relativePath);
111
+ if (normalized.ok) {
112
+ if (!seenResolved.has(normalized.resolvedPath)) {
113
+ seenResolved.add(normalized.resolvedPath);
114
+ validPaths.push(normalized.resolvedPath);
115
+ }
116
+ continue;
117
+ }
118
+
119
+ if (normalized.code === "registered_pen_outside_project_root") {
120
+ escapedPaths.push({
121
+ relativePath,
122
+ resolvedPath: normalized.resolvedPath
123
+ });
124
+ }
125
+ }
126
+
127
+ return {
128
+ validPaths,
129
+ escapedPaths
130
+ };
131
+ }
132
+
133
+ function resolvePreferredRegisteredPenPath(projectRoot, registryText) {
134
+ const resolved = collectRegisteredPenPaths(projectRoot, registryText);
135
+ return resolved.validPaths[0] || "";
136
+ }
137
+
138
+ module.exports = {
139
+ collectRegisteredPenReferences,
140
+ normalizeBoundPenPath,
141
+ normalizeRegisteredPenPath,
142
+ normalizeSessionPenPath,
143
+ normalizeActiveEditorPath,
144
+ collectRegisteredPenPaths,
145
+ resolvePreferredRegisteredPenPath
146
+ };