@pennixrv/trellis 0.7.0-beta.5 → 0.7.0-beta.50

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 (219) hide show
  1. package/dist/cli/index.d.ts.map +1 -1
  2. package/dist/cli/index.js +47 -3
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/commands/backup.d.ts +25 -0
  5. package/dist/commands/backup.d.ts.map +1 -0
  6. package/dist/commands/backup.js +152 -0
  7. package/dist/commands/backup.js.map +1 -0
  8. package/dist/commands/channel/adapters/claude.d.ts.map +1 -1
  9. package/dist/commands/channel/adapters/claude.js +2 -0
  10. package/dist/commands/channel/adapters/claude.js.map +1 -1
  11. package/dist/commands/channel/adapters/codex.d.ts +5 -1
  12. package/dist/commands/channel/adapters/codex.d.ts.map +1 -1
  13. package/dist/commands/channel/adapters/codex.js +19 -9
  14. package/dist/commands/channel/adapters/codex.js.map +1 -1
  15. package/dist/commands/channel/adapters/index.d.ts +3 -0
  16. package/dist/commands/channel/adapters/index.d.ts.map +1 -1
  17. package/dist/commands/channel/adapters/index.js +3 -3
  18. package/dist/commands/channel/adapters/index.js.map +1 -1
  19. package/dist/commands/channel/adapters/types.d.ts +2 -0
  20. package/dist/commands/channel/adapters/types.d.ts.map +1 -1
  21. package/dist/commands/channel/guard.d.ts.map +1 -1
  22. package/dist/commands/channel/guard.js +38 -29
  23. package/dist/commands/channel/guard.js.map +1 -1
  24. package/dist/commands/channel/index.d.ts.map +1 -1
  25. package/dist/commands/channel/index.js +52 -16
  26. package/dist/commands/channel/index.js.map +1 -1
  27. package/dist/commands/channel/kill.d.ts.map +1 -1
  28. package/dist/commands/channel/kill.js +37 -15
  29. package/dist/commands/channel/kill.js.map +1 -1
  30. package/dist/commands/channel/messages.d.ts.map +1 -1
  31. package/dist/commands/channel/messages.js +3 -2
  32. package/dist/commands/channel/messages.js.map +1 -1
  33. package/dist/commands/channel/process-identity.d.ts +3 -0
  34. package/dist/commands/channel/process-identity.d.ts.map +1 -0
  35. package/dist/commands/channel/process-identity.js +24 -0
  36. package/dist/commands/channel/process-identity.js.map +1 -0
  37. package/dist/commands/channel/profiles.d.ts +26 -0
  38. package/dist/commands/channel/profiles.d.ts.map +1 -0
  39. package/dist/commands/channel/profiles.js +186 -0
  40. package/dist/commands/channel/profiles.js.map +1 -0
  41. package/dist/commands/channel/rm.d.ts +0 -1
  42. package/dist/commands/channel/rm.d.ts.map +1 -1
  43. package/dist/commands/channel/rm.js +203 -96
  44. package/dist/commands/channel/rm.js.map +1 -1
  45. package/dist/commands/channel/spawn.d.ts +3 -0
  46. package/dist/commands/channel/spawn.d.ts.map +1 -1
  47. package/dist/commands/channel/spawn.js +43 -15
  48. package/dist/commands/channel/spawn.js.map +1 -1
  49. package/dist/commands/channel/supervisor/idle.d.ts +2 -0
  50. package/dist/commands/channel/supervisor/idle.d.ts.map +1 -1
  51. package/dist/commands/channel/supervisor/idle.js +15 -7
  52. package/dist/commands/channel/supervisor/idle.js.map +1 -1
  53. package/dist/commands/channel/supervisor/inbox.d.ts.map +1 -1
  54. package/dist/commands/channel/supervisor/inbox.js +48 -33
  55. package/dist/commands/channel/supervisor/inbox.js.map +1 -1
  56. package/dist/commands/channel/supervisor/stdout.d.ts.map +1 -1
  57. package/dist/commands/channel/supervisor/stdout.js +6 -3
  58. package/dist/commands/channel/supervisor/stdout.js.map +1 -1
  59. package/dist/commands/channel/supervisor/warning.d.ts.map +1 -1
  60. package/dist/commands/channel/supervisor/warning.js +2 -3
  61. package/dist/commands/channel/supervisor/warning.js.map +1 -1
  62. package/dist/commands/channel/supervisor.d.ts +12 -0
  63. package/dist/commands/channel/supervisor.d.ts.map +1 -1
  64. package/dist/commands/channel/supervisor.js +88 -16
  65. package/dist/commands/channel/supervisor.js.map +1 -1
  66. package/dist/commands/channel/wait.d.ts +3 -1
  67. package/dist/commands/channel/wait.d.ts.map +1 -1
  68. package/dist/commands/channel/wait.js +59 -14
  69. package/dist/commands/channel/wait.js.map +1 -1
  70. package/dist/commands/init.d.ts.map +1 -1
  71. package/dist/commands/init.js +23 -10
  72. package/dist/commands/init.js.map +1 -1
  73. package/dist/commands/update.d.ts.map +1 -1
  74. package/dist/commands/update.js +446 -333
  75. package/dist/commands/update.js.map +1 -1
  76. package/dist/commands/workflow.d.ts +1 -0
  77. package/dist/commands/workflow.d.ts.map +1 -1
  78. package/dist/commands/workflow.js +56 -1
  79. package/dist/commands/workflow.js.map +1 -1
  80. package/dist/configurators/codex.d.ts +6 -0
  81. package/dist/configurators/codex.d.ts.map +1 -1
  82. package/dist/configurators/codex.js +93 -0
  83. package/dist/configurators/codex.js.map +1 -1
  84. package/dist/configurators/shared.js +2 -2
  85. package/dist/configurators/shared.js.map +1 -1
  86. package/dist/constants/paths.d.ts +2 -0
  87. package/dist/constants/paths.d.ts.map +1 -1
  88. package/dist/constants/paths.js +2 -0
  89. package/dist/constants/paths.js.map +1 -1
  90. package/dist/migrations/manifests/0.7.0-beta.10.json +9 -0
  91. package/dist/migrations/manifests/0.7.0-beta.11.json +9 -0
  92. package/dist/migrations/manifests/0.7.0-beta.12.json +9 -0
  93. package/dist/migrations/manifests/0.7.0-beta.13.json +9 -0
  94. package/dist/migrations/manifests/0.7.0-beta.14.json +9 -0
  95. package/dist/migrations/manifests/0.7.0-beta.15.json +9 -0
  96. package/dist/migrations/manifests/0.7.0-beta.16.json +9 -0
  97. package/dist/migrations/manifests/0.7.0-beta.17.json +9 -0
  98. package/dist/migrations/manifests/0.7.0-beta.18.json +9 -0
  99. package/dist/migrations/manifests/0.7.0-beta.19.json +9 -0
  100. package/dist/migrations/manifests/0.7.0-beta.20.json +9 -0
  101. package/dist/migrations/manifests/0.7.0-beta.21.json +17 -0
  102. package/dist/migrations/manifests/0.7.0-beta.22.json +9 -0
  103. package/dist/migrations/manifests/0.7.0-beta.23.json +9 -0
  104. package/dist/migrations/manifests/0.7.0-beta.24.json +9 -0
  105. package/dist/migrations/manifests/0.7.0-beta.25.json +9 -0
  106. package/dist/migrations/manifests/0.7.0-beta.26.json +9 -0
  107. package/dist/migrations/manifests/0.7.0-beta.27.json +9 -0
  108. package/dist/migrations/manifests/0.7.0-beta.28.json +9 -0
  109. package/dist/migrations/manifests/0.7.0-beta.29.json +9 -0
  110. package/dist/migrations/manifests/0.7.0-beta.30.json +9 -0
  111. package/dist/migrations/manifests/0.7.0-beta.31.json +9 -0
  112. package/dist/migrations/manifests/0.7.0-beta.32.json +9 -0
  113. package/dist/migrations/manifests/0.7.0-beta.33.json +9 -0
  114. package/dist/migrations/manifests/0.7.0-beta.34.json +9 -0
  115. package/dist/migrations/manifests/0.7.0-beta.35.json +9 -0
  116. package/dist/migrations/manifests/0.7.0-beta.36.json +9 -0
  117. package/dist/migrations/manifests/0.7.0-beta.37.json +9 -0
  118. package/dist/migrations/manifests/0.7.0-beta.38.json +9 -0
  119. package/dist/migrations/manifests/0.7.0-beta.39.json +9 -0
  120. package/dist/migrations/manifests/0.7.0-beta.40.json +9 -0
  121. package/dist/migrations/manifests/0.7.0-beta.41.json +9 -0
  122. package/dist/migrations/manifests/0.7.0-beta.42.json +9 -0
  123. package/dist/migrations/manifests/0.7.0-beta.43.json +9 -0
  124. package/dist/migrations/manifests/0.7.0-beta.44.json +9 -0
  125. package/dist/migrations/manifests/0.7.0-beta.45.json +9 -0
  126. package/dist/migrations/manifests/0.7.0-beta.46.json +9 -0
  127. package/dist/migrations/manifests/0.7.0-beta.47.json +9 -0
  128. package/dist/migrations/manifests/0.7.0-beta.48.json +9 -0
  129. package/dist/migrations/manifests/0.7.0-beta.49.json +13 -0
  130. package/dist/migrations/manifests/0.7.0-beta.50.json +12 -0
  131. package/dist/migrations/manifests/0.7.0-beta.6.json +9 -0
  132. package/dist/migrations/manifests/0.7.0-beta.7.json +9 -0
  133. package/dist/migrations/manifests/0.7.0-beta.8.json +9 -0
  134. package/dist/migrations/manifests/0.7.0-beta.9.json +9 -0
  135. package/dist/templates/codex/hooks/session-start.py +21 -17
  136. package/dist/templates/common/bundled-skills/trellis-channel/SKILL.md +3 -1
  137. package/dist/templates/common/bundled-skills/trellis-channel/references/command-reference.md +24 -16
  138. package/dist/templates/common/bundled-skills/trellis-channel/references/forum.md +4 -12
  139. package/dist/templates/common/bundled-skills/trellis-channel/references/multi-target-dispatch.md +139 -0
  140. package/dist/templates/common/bundled-skills/trellis-channel/references/progress-debugging.md +9 -5
  141. package/dist/templates/common/bundled-skills/trellis-channel/references/subnode-work.md +84 -14
  142. package/dist/templates/common/bundled-skills/trellis-channel/references/workers.md +9 -11
  143. package/dist/templates/common/bundled-skills/trellis-channel/references/workflows.md +25 -14
  144. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-skills-or-commands.md +13 -11
  145. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-task-lifecycle.md +7 -2
  146. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/task-system.md +6 -1
  147. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/workflow.md +6 -4
  148. package/dist/templates/common/bundled-skills/trellis-session-insight/SKILL.md +42 -3
  149. package/dist/templates/common/bundled-skills/trellis-session-insight/references/cli-quick-reference.md +1 -1
  150. package/dist/templates/common/bundled-skills/trellis-session-insight/references/triggering-patterns.md +10 -2
  151. package/dist/templates/common/commands/continue.md +36 -8
  152. package/dist/templates/common/commands/finish-work.md +17 -2
  153. package/dist/templates/common/commands/start.md +8 -1
  154. package/dist/templates/common/skills/brainstorm.md +49 -22
  155. package/dist/templates/common/skills/break-loop.md +6 -10
  156. package/dist/templates/common/skills/check.md +34 -5
  157. package/dist/templates/copilot/hooks/session-start.py +19 -15
  158. package/dist/templates/copilot/prompts/brainstorm.prompt.md +27 -21
  159. package/dist/templates/markdown/spec/backend/database-guidelines.md.txt +22 -22
  160. package/dist/templates/markdown/spec/backend/directory-structure.md.txt +20 -20
  161. package/dist/templates/markdown/spec/backend/error-handling.md.txt +22 -22
  162. package/dist/templates/markdown/spec/backend/index.md.txt +19 -19
  163. package/dist/templates/markdown/spec/backend/logging-guidelines.md.txt +22 -22
  164. package/dist/templates/markdown/spec/backend/quality-guidelines.md.txt +22 -22
  165. package/dist/templates/markdown/spec/frontend/component-guidelines.md.txt +25 -25
  166. package/dist/templates/markdown/spec/frontend/directory-structure.md.txt +20 -20
  167. package/dist/templates/markdown/spec/frontend/hook-guidelines.md.txt +22 -22
  168. package/dist/templates/markdown/spec/frontend/index.md.txt +20 -20
  169. package/dist/templates/markdown/spec/frontend/quality-guidelines.md.txt +22 -22
  170. package/dist/templates/markdown/spec/frontend/state-management.md.txt +22 -22
  171. package/dist/templates/markdown/spec/frontend/type-safety.md.txt +22 -22
  172. package/dist/templates/markdown/spec/guides/code-reuse-thinking-guide.md.txt +80 -85
  173. package/dist/templates/markdown/spec/guides/cross-layer-thinking-guide.md.txt +164 -209
  174. package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md.txt +161 -185
  175. package/dist/templates/markdown/spec/guides/index.md.txt +51 -51
  176. package/dist/templates/markdown/workspace-index.md +44 -44
  177. package/dist/templates/opencode/lib/session-utils.js +2 -2
  178. package/dist/templates/shared-hooks/inject-shell-session-context.py +3 -6
  179. package/dist/templates/shared-hooks/inject-workflow-state.py +3 -2
  180. package/dist/templates/shared-hooks/session-start.py +23 -18
  181. package/dist/templates/trellis/agents/subnode-profiles.json +13 -0
  182. package/dist/templates/trellis/agents/subnode.md +11 -5
  183. package/dist/templates/trellis/config.yaml +2 -0
  184. package/dist/templates/trellis/index.d.ts +2 -1
  185. package/dist/templates/trellis/index.d.ts.map +1 -1
  186. package/dist/templates/trellis/index.js +4 -2
  187. package/dist/templates/trellis/index.js.map +1 -1
  188. package/dist/templates/trellis/scripts/add_session.py +35 -32
  189. package/dist/templates/trellis/scripts/common/active_task.py +8 -8
  190. package/dist/templates/trellis/scripts/common/developer.py +18 -18
  191. package/dist/templates/trellis/scripts/common/ownership_record.py +13 -8
  192. package/dist/templates/trellis/scripts/common/safe_commit.py +38 -9
  193. package/dist/templates/trellis/scripts/common/task_planning.py +137 -0
  194. package/dist/templates/trellis/scripts/common/task_store.py +46 -14
  195. package/dist/templates/trellis/scripts/common/workflow_phase.py +4 -3
  196. package/dist/templates/trellis/scripts/subnode_artifact.py +470 -21
  197. package/dist/templates/trellis/scripts/task.py +175 -2
  198. package/dist/templates/trellis/workflow.md +407 -388
  199. package/dist/utils/managed-paths.d.ts +2 -0
  200. package/dist/utils/managed-paths.d.ts.map +1 -1
  201. package/dist/utils/managed-paths.js +24 -2
  202. package/dist/utils/managed-paths.js.map +1 -1
  203. package/dist/utils/template-fetcher.d.ts +1 -0
  204. package/dist/utils/template-fetcher.d.ts.map +1 -1
  205. package/dist/utils/template-fetcher.js +2 -1
  206. package/dist/utils/template-fetcher.js.map +1 -1
  207. package/dist/utils/uninstall-scrubbers.d.ts.map +1 -1
  208. package/dist/utils/uninstall-scrubbers.js +5 -0
  209. package/dist/utils/uninstall-scrubbers.js.map +1 -1
  210. package/dist/utils/workflow-provenance.d.ts +24 -0
  211. package/dist/utils/workflow-provenance.d.ts.map +1 -0
  212. package/dist/utils/workflow-provenance.js +71 -0
  213. package/dist/utils/workflow-provenance.js.map +1 -0
  214. package/dist/utils/workflow-resolver.d.ts +9 -2
  215. package/dist/utils/workflow-resolver.d.ts.map +1 -1
  216. package/dist/utils/workflow-resolver.js +33 -7
  217. package/dist/utils/workflow-resolver.js.map +1 -1
  218. package/package.json +2 -2
  219. package/dist/templates/trellis/agents/subnode.env +0 -4
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import chalk from "chalk";
5
5
  import inquirer from "inquirer";
6
6
  import { DIR_NAMES, FILE_NAMES, PATHS } from "../constants/paths.js";
7
+ import { BACKUP_COMPLETE_MARKER, BACKUP_PRUNE_LOCK } from "./backup.js";
7
8
  import { VERSION, PACKAGE_NAME } from "../constants/version.js";
8
9
  import { getMigrationsForVersion, getAllMigrations, getMigrationMetadata, getConfigSectionsAddedBetween, } from "../migrations/index.js";
9
10
  import { loadHashes, saveHashes, updateHashes, isTemplateModified, removeHash, renameHash, computeHash, shouldExcludeFromHash, } from "../utils/template-hash.js";
@@ -20,14 +21,16 @@ import { COPILOT_INSTRUCTIONS_BLOCK_END, COPILOT_INSTRUCTIONS_BLOCK_START, COPIL
20
21
  import { ALL_MANAGED_DIRS, getConfiguredPlatforms, collectPlatformTemplates, } from "../configurators/index.js";
21
22
  import { replacePythonCommandLiterals } from "../configurators/shared.js";
22
23
  import { CLAUDE_STATUSLINE_PATH, isClaudeStatuslineManaged, } from "../configurators/claude.js";
23
- import { preserveCodexAgentModelKeys } from "../configurators/codex.js";
24
+ import { preserveCodexAgentModelKeys, preserveCodexProjectConfig, } from "../configurators/codex.js";
24
25
  import { printZcodeSetupHint } from "../configurators/zcode.js";
25
26
  import { ensureGitattributes } from "../configurators/workflow.js";
26
27
  import { getStatuslineHook } from "../templates/claude/index.js";
27
28
  import { pruneOrphanManifestKeys } from "../utils/manifest-prune.js";
28
29
  import { fetchRegistrySpecTemplates, collectDirectoryFiles, removeDirectory, parseRegistrySource, probeRegistryIndex, downloadTemplateById, } from "../utils/template-fetcher.js";
29
30
  import { loadSpecRegistryConfig } from "../utils/registry-config.js";
30
- import { cleanupEmptyDirs, getManagedMarkdownBlock, mergeManagedMarkdownBlock, TRELLIS_BLOCK_END, TRELLIS_BLOCK_START, } from "../utils/managed-paths.js";
31
+ import { cleanupEmptyDirs, getManagedMarkdownBlock, mergeManagedMarkdownBlock, reportAgentsOverride, TRELLIS_BLOCK_END, TRELLIS_BLOCK_START, } from "../utils/managed-paths.js";
32
+ import { loadWorkflowProvenance, writeWorkflowProvenance, workflowProvenanceMismatches, } from "../utils/workflow-provenance.js";
33
+ import { NATIVE_WORKFLOW_ID, resolveWorkflowTemplate, } from "../utils/workflow-resolver.js";
31
34
  export { cleanupEmptyDirs, TRELLIS_BLOCK_END, TRELLIS_BLOCK_START, } from "../utils/managed-paths.js";
32
35
  const CLAUDE_SETTINGS_PATH = ".claude/settings.json";
33
36
  const LEGACY_UNTRACKED_AGENTS_MD_BLOCK_HASHES = new Set([
@@ -37,73 +40,6 @@ const LEGACY_UNTRACKED_AGENTS_MD_BLOCK_HASHES = new Set([
37
40
  // false "modified by you" conflict.
38
41
  "c1f511b1cfc1902f2147da159f09cc51f380b0c9e341cdb3ac5dea5233f3e307",
39
42
  ]);
40
- const CODEX_WORKFLOW_STATE_HOOK_PATH = ".codex/hooks/inject-workflow-state.py";
41
- const LEGACY_PENNIX_CODEX_DISPATCH_SIGNATURES = [
42
- "def _resolve_codex_dispatch_mode(config: dict, repo_root: Path | None = None) -> str:",
43
- "from common.config import get_workflow_dispatch_mode",
44
- "return get_workflow_dispatch_mode(root, config=config)",
45
- ];
46
- const LEGACY_PENNIX_ACTIVE_TASK_GUARD = " active = _resolve_active_task(root, input_data)\n" +
47
- " if not active.task_path:\n" +
48
- " return None";
49
- const PENNIX_AMBIGUITY_PROJECTION = " active = _resolve_active_task(root, input_data)\n" +
50
- ' if active.source_type == "unbound_ambiguous":\n' +
51
- ' candidates = ", ".join(active.candidate_paths)\n' +
52
- ' return candidates, "unbound_ambiguous", active.source\n' +
53
- " if not active.task_path:\n" +
54
- " return None";
55
- const LEGACY_PENNIX_BREADCRUMB_HEADER = ' header = f"Status: {status}" if task_id is None else f"Task: {task_id} ({status})"';
56
- const PENNIX_AMBIGUITY_BREADCRUMB_HEADER = ' if status == "unbound_ambiguous":\n' +
57
- ' header = f"Status: {status}\\nCandidates: {task_id}"\n' +
58
- " else:\n" +
59
- ' header = f"Status: {status}" if task_id is None else f"Task: {task_id} ({status})"';
60
- /**
61
- * Preserve the historical Pennix Codex hook while adding the one projection
62
- * it predates. This is intentionally stricter than hash tracking: the Hook
63
- * carried a Pennix-specific dispatch resolver, so pristine installations were
64
- * correctly classified as locally modified and never received issue #180.
65
- */
66
- function migrateLegacyPennixCodexWorkflowHook(relativePath, existingContent) {
67
- if (relativePath !== CODEX_WORKFLOW_STATE_HOOK_PATH ||
68
- !LEGACY_PENNIX_CODEX_DISPATCH_SIGNATURES.every((signature) => existingContent.includes(signature))) {
69
- return null;
70
- }
71
- const hasProjection = existingContent.includes(PENNIX_AMBIGUITY_PROJECTION);
72
- const hasHeader = existingContent.includes(PENNIX_AMBIGUITY_BREADCRUMB_HEADER);
73
- if (hasProjection && hasHeader) {
74
- return { content: existingContent, needsUpdate: false };
75
- }
76
- if ((!hasProjection &&
77
- !existingContent.includes(LEGACY_PENNIX_ACTIVE_TASK_GUARD)) ||
78
- (!hasHeader && !existingContent.includes(LEGACY_PENNIX_BREADCRUMB_HEADER))) {
79
- return null;
80
- }
81
- return {
82
- content: existingContent
83
- .replace(LEGACY_PENNIX_ACTIVE_TASK_GUARD, PENNIX_AMBIGUITY_PROJECTION)
84
- .replace(LEGACY_PENNIX_BREADCRUMB_HEADER, PENNIX_AMBIGUITY_BREADCRUMB_HEADER),
85
- needsUpdate: !hasProjection || !hasHeader,
86
- };
87
- }
88
- /** Keep recognized Pennix Hook customizations in the desired template map. */
89
- function preserveLegacyPennixCodexWorkflowHook(cwd, templates) {
90
- if (!templates.has(CODEX_WORKFLOW_STATE_HOOK_PATH)) {
91
- return;
92
- }
93
- const hookPath = path.join(cwd, CODEX_WORKFLOW_STATE_HOOK_PATH);
94
- if (!fs.existsSync(hookPath)) {
95
- return;
96
- }
97
- try {
98
- const migration = migrateLegacyPennixCodexWorkflowHook(CODEX_WORKFLOW_STATE_HOOK_PATH, fs.readFileSync(hookPath, "utf-8"));
99
- if (migration) {
100
- templates.set(CODEX_WORKFLOW_STATE_HOOK_PATH, migration.content);
101
- }
102
- }
103
- catch {
104
- // The normal conflict path reports unreadable or concurrently changed files.
105
- }
106
- }
107
43
  // Paths that should never be touched (true user data)
108
44
  // spec/ is user-customized content created during init; update should never modify it
109
45
  const PROTECTED_PATHS = [
@@ -127,6 +63,31 @@ function buildManagedBlockTemplate(cwd, relativePath, templateContent, startMark
127
63
  function buildAgentsMdTemplate(cwd) {
128
64
  return buildManagedBlockTemplate(cwd, FILE_NAMES.AGENTS, agentsMdContent, TRELLIS_BLOCK_START, TRELLIS_BLOCK_END);
129
65
  }
66
+ function countOccurrences(content, needle) {
67
+ return content.split(needle).length - 1;
68
+ }
69
+ /**
70
+ * A generated Markdown file can contain a user-owned document around one
71
+ * Trellis-managed block. If only that block changed, the whole file is still
72
+ * an update owned by Trellis and must not enter the user-conflict path.
73
+ */
74
+ function isManagedMarkdownBlockOnlyChange(relativePath, existingContent, newContent) {
75
+ if (relativePath !== FILE_NAMES.AGENTS)
76
+ return false;
77
+ if (countOccurrences(existingContent, TRELLIS_BLOCK_START) !== 1 ||
78
+ countOccurrences(existingContent, TRELLIS_BLOCK_END) !== 1 ||
79
+ countOccurrences(newContent, TRELLIS_BLOCK_START) !== 1 ||
80
+ countOccurrences(newContent, TRELLIS_BLOCK_END) !== 1) {
81
+ return false;
82
+ }
83
+ const existingBlock = getTrellisManagedBlock(existingContent);
84
+ const newBlock = getTrellisManagedBlock(newContent);
85
+ return (existingBlock !== null &&
86
+ newBlock !== null &&
87
+ existingBlock !== newBlock &&
88
+ existingContent.replace(existingBlock, "") ===
89
+ newContent.replace(newBlock, ""));
90
+ }
130
91
  function buildCopilotInstructionsTemplate(cwd) {
131
92
  return buildManagedBlockTemplate(cwd, COPILOT_INSTRUCTIONS_PATH, getCopilotInstructions(), COPILOT_INSTRUCTIONS_BLOCK_START, COPILOT_INSTRUCTIONS_BLOCK_END);
132
93
  }
@@ -134,6 +95,9 @@ function isKnownUntrackedTemplate(relativePath, existingContent) {
134
95
  if (relativePath !== FILE_NAMES.AGENTS) {
135
96
  return false;
136
97
  }
98
+ if (countOccurrences(existingContent, TRELLIS_BLOCK_START) !== 1 ||
99
+ countOccurrences(existingContent, TRELLIS_BLOCK_END) !== 1)
100
+ return false;
137
101
  const managedBlock = getTrellisManagedBlock(existingContent);
138
102
  if (!managedBlock) {
139
103
  return false;
@@ -513,6 +477,58 @@ function preserveExistingRegistryConfig(cwd, template) {
513
477
  ` source: ${registry.source}\n` +
514
478
  (registry.template ? ` template: ${registry.template}\n` : ""));
515
479
  }
480
+ /**
481
+ * Preserve top-level YAML sections not declared by the Trellis template.
482
+ * Project extensions are identified by structure, not by Pennix-specific key
483
+ * names; `registry` remains handled by its existing normalized path.
484
+ */
485
+ function preserveExistingConfigExtensions(cwd, template) {
486
+ const configPath = path.join(cwd, DIR_NAMES.WORKFLOW, "config.yaml");
487
+ if (!fs.existsSync(configPath)) {
488
+ return template;
489
+ }
490
+ const existing = fs.readFileSync(configPath, "utf-8");
491
+ const sectionPattern = /^([A-Za-z_][A-Za-z0-9_-]*):(?:\s|$)/;
492
+ const sections = (content) => {
493
+ const lines = content.split(/\r?\n/);
494
+ const starts = [];
495
+ for (let index = 0; index < lines.length; index += 1) {
496
+ const match = lines[index]?.match(sectionPattern);
497
+ if (match?.[1])
498
+ starts.push({ key: match[1], index });
499
+ }
500
+ return new Map(starts.map(({ key, index }, position) => [
501
+ key,
502
+ {
503
+ content: lines
504
+ .slice(index, starts[position + 1]?.index ?? lines.length)
505
+ .join("\n"),
506
+ index,
507
+ },
508
+ ]));
509
+ };
510
+ const templateSections = sections(template);
511
+ const templateKeys = new Set(templateSections.keys());
512
+ const existingSections = sections(existing);
513
+ const templateLines = new Set(template.split(/\r?\n/));
514
+ const extensions = [...existingSections].filter(([key]) => key !== "registry" && !templateKeys.has(key));
515
+ const withRegistry = preserveExistingRegistryConfig(cwd, template);
516
+ if (extensions.length === 0)
517
+ return withRegistry;
518
+ // Keep project comments that introduce an extension, but do not duplicate
519
+ // the template's trailing documentation when the extension was appended
520
+ // directly after a generated section. The line-based parser intentionally
521
+ // stays limited to the generated top-level YAML shape.
522
+ const extensionContent = extensions.map(([, section]) => {
523
+ const preceding = existing
524
+ .split(/\r?\n/)
525
+ .slice(Math.max(0, section.index - 32), section.index);
526
+ const projectComments = preceding.filter((line) => line.trimStart().startsWith("#") && !templateLines.has(line));
527
+ const leading = projectComments.length > 0 ? `${projectComments.join("\n")}\n` : "";
528
+ return `${leading}${section.content.trimEnd()}`;
529
+ });
530
+ return `${withRegistry.trimEnd()}\n${extensionContent.join("\n\n")}\n`;
531
+ }
516
532
  async function collectRegistrySpecTemplates(cwd) {
517
533
  const config = loadSpecRegistryConfig(cwd);
518
534
  if (!config)
@@ -589,16 +605,15 @@ bypassUpdateSkip = false) {
589
605
  files.set(`${PATHS.AGENTS}/${agentFile}`, content);
590
606
  }
591
607
  // Configuration
592
- files.set(`${DIR_NAMES.WORKFLOW}/config.yaml`, preserveExistingRegistryConfig(cwd, configYamlTemplate));
608
+ files.set(`${DIR_NAMES.WORKFLOW}/config.yaml`, preserveExistingConfigExtensions(cwd, configYamlTemplate));
593
609
  files.set(`${DIR_NAMES.WORKFLOW}/.gitignore`, gitignoreTemplate);
594
- // workflow.md is included here because it is runtime-parsed by
595
- // get_context.py and shared hooks. Keep it on the normal template update
596
- // path: if the installed file still matches the tracked hash, update the
597
- // whole file. If the user edited it, the standard modified-file prompt /
598
- // --force behavior applies. Partial tag-block merging is unsafe because
599
- // platform routing markers outside [workflow-state:*] blocks are also
600
- // script-consumed.
601
- files.set(`${DIR_NAMES.WORKFLOW}/workflow.md`, workflowMdTemplate);
610
+ // workflow.md is runtime-parsed by get_context.py and shared hooks. Keep it
611
+ // on the normal whole-file update path, but resolve the project's selected
612
+ // workflow instead of silently substituting the bundled native workflow.
613
+ const selectedWorkflow = await resolveWorkflowUpdateTemplate(cwd, hashes);
614
+ if (selectedWorkflow !== null) {
615
+ files.set(`${DIR_NAMES.WORKFLOW}/workflow.md`, selectedWorkflow);
616
+ }
602
617
  // workspace/index.md stays excluded — it's runtime-appended by add_session.py
603
618
  // (journal index) and has no script-parsed structure.
604
619
  files.set(FILE_NAMES.AGENTS, buildAgentsMdTemplate(cwd));
@@ -621,7 +636,7 @@ bypassUpdateSkip = false) {
621
636
  // as a modified-file conflict by the hash comparison below.
622
637
  if (platforms.has("codex")) {
623
638
  preserveCodexAgentModelKeys(cwd, files);
624
- preserveLegacyPennixCodexWorkflowHook(cwd, files);
639
+ preserveCodexProjectConfig(cwd, files);
625
640
  }
626
641
  preserveExistingClaudeStatusLine(cwd, files);
627
642
  if (platforms.has("claude-code") && isClaudeStatuslineManaged(cwd, hashes)) {
@@ -648,6 +663,55 @@ bypassUpdateSkip = false) {
648
663
  }
649
664
  return files;
650
665
  }
666
+ /**
667
+ * Resolve the workflow selected by this project for generic updates.
668
+ *
669
+ * Provenance is the durable source of truth for marketplace workflows. Older
670
+ * projects without provenance retain their historical behavior: hash-tracked
671
+ * workflow.md uses the bundled template, while an untracked custom workflow
672
+ * remains outside the generic update set.
673
+ */
674
+ async function resolveWorkflowUpdateTemplate(cwd, hashes) {
675
+ const provenance = loadWorkflowProvenance(cwd);
676
+ if (!provenance) {
677
+ return hashes[PATHS.WORKFLOW_GUIDE_FILE] ? workflowMdTemplate : null;
678
+ }
679
+ const template = await resolveWorkflowTemplate(provenance.workflow_id, {
680
+ source: provenance.source_kind === "marketplace"
681
+ ? provenance.registry
682
+ : undefined,
683
+ });
684
+ const nativeVersionTransition = template.id === NATIVE_WORKFLOW_ID &&
685
+ template.source === "bundled" &&
686
+ provenance.ref !== template.ref &&
687
+ compareVersions(provenance.ref, template.ref) !== null;
688
+ const mismatches = workflowProvenanceMismatches(provenance, template).filter((mismatch) => !(nativeVersionTransition &&
689
+ (mismatch === "ref" ||
690
+ (mismatch === "template integrity" &&
691
+ hashes[PATHS.WORKFLOW_GUIDE_FILE] === provenance.content_sha256))));
692
+ if (mismatches.length > 0) {
693
+ throw new Error(`Workflow provenance verification failed: ${mismatches.join(", ")}.`);
694
+ }
695
+ return template.id === NATIVE_WORKFLOW_ID
696
+ ? workflowMdTemplate
697
+ : template.content;
698
+ }
699
+ function refreshNativeWorkflowProvenance(cwd) {
700
+ const record = loadWorkflowProvenance(cwd);
701
+ if (record?.source_kind !== "bundled" ||
702
+ record.workflow_id !== NATIVE_WORKFLOW_ID)
703
+ return;
704
+ const workflowPath = path.join(cwd, PATHS.WORKFLOW_GUIDE_FILE);
705
+ if (fs.existsSync(workflowPath) &&
706
+ fs.readFileSync(workflowPath, "utf-8") ===
707
+ replacePythonCommandLiterals(workflowMdTemplate)) {
708
+ writeWorkflowProvenance(cwd, {
709
+ ...record,
710
+ ref: VERSION,
711
+ content_sha256: computeHash(workflowMdTemplate),
712
+ });
713
+ }
714
+ }
651
715
  /**
652
716
  * Analyze changes between current files and templates
653
717
  *
@@ -687,11 +751,15 @@ function analyzeChanges(cwd, hashes, templates) {
687
751
  }
688
752
  else {
689
753
  const existingContent = fs.readFileSync(fullPath, "utf-8");
690
- const legacyPennixHook = migrateLegacyPennixCodexWorkflowHook(relativePath, existingContent);
691
- if (legacyPennixHook?.needsUpdate &&
692
- newContent === legacyPennixHook.content) {
754
+ const markers = relativePath === FILE_NAMES.AGENTS
755
+ ? [TRELLIS_BLOCK_START, TRELLIS_BLOCK_END]
756
+ : relativePath === COPILOT_INSTRUCTIONS_PATH
757
+ ? [COPILOT_INSTRUCTIONS_BLOCK_START, COPILOT_INSTRUCTIONS_BLOCK_END]
758
+ : null;
759
+ if (markers &&
760
+ mergeManagedMarkdownBlock(existingContent, newContent, markers[0], markers[1]) === null) {
693
761
  change.status = "changed";
694
- result.autoUpdateFiles.push(change);
762
+ result.changedFiles.push(change);
695
763
  continue;
696
764
  }
697
765
  if (existingContent === newContent) {
@@ -713,6 +781,12 @@ function analyzeChanges(cwd, hashes, templates) {
713
781
  change.status = "changed";
714
782
  result.autoUpdateFiles.push(change);
715
783
  }
784
+ else if (isManagedMarkdownBlockOnlyChange(relativePath, existingContent, newContent)) {
785
+ // The project-owned prose around AGENTS.md is unchanged; only the
786
+ // explicitly managed Trellis block moved to a new template version.
787
+ change.status = "changed";
788
+ result.autoUpdateFiles.push(change);
789
+ }
716
790
  else {
717
791
  // Hash differs (or no stored hash) - user modified the file
718
792
  // Needs confirmation
@@ -955,6 +1029,7 @@ export function shouldExcludeFromBackup(relativePath) {
955
1029
  */
956
1030
  function createFullBackup(cwd) {
957
1031
  const backupDir = createBackupDirPath(cwd);
1032
+ const stagingDir = `${backupDir}.incomplete`;
958
1033
  let hasFiles = false;
959
1034
  for (const dir of BACKUP_DIRS) {
960
1035
  const dirPath = path.join(cwd, dir);
@@ -968,10 +1043,10 @@ function createFullBackup(cwd) {
968
1043
  continue;
969
1044
  // Create backup
970
1045
  if (!hasFiles) {
971
- fs.mkdirSync(backupDir, { recursive: true });
1046
+ fs.mkdirSync(stagingDir, { recursive: true });
972
1047
  hasFiles = true;
973
1048
  }
974
- backupFile(cwd, backupDir, relativePath);
1049
+ backupFile(cwd, stagingDir, relativePath);
975
1050
  }
976
1051
  }
977
1052
  for (const relativePath of BACKUP_FILES) {
@@ -981,10 +1056,14 @@ function createFullBackup(cwd) {
981
1056
  if (shouldExcludeFromBackup(relativePath))
982
1057
  continue;
983
1058
  if (!hasFiles) {
984
- fs.mkdirSync(backupDir, { recursive: true });
1059
+ fs.mkdirSync(stagingDir, { recursive: true });
985
1060
  hasFiles = true;
986
1061
  }
987
- backupFile(cwd, backupDir, relativePath);
1062
+ backupFile(cwd, stagingDir, relativePath);
1063
+ }
1064
+ if (hasFiles) {
1065
+ fs.writeFileSync(path.join(stagingDir, BACKUP_COMPLETE_MARKER), VERSION, "utf8");
1066
+ fs.renameSync(stagingDir, backupDir);
988
1067
  }
989
1068
  return hasFiles ? backupDir : null;
990
1069
  }
@@ -1224,7 +1303,9 @@ export function classifyMigrations(migrations, cwd, hashes, templates) {
1224
1303
  const newExists = fs.existsSync(newPath);
1225
1304
  if (newExists) {
1226
1305
  // Target exists - check if it only contains unmodified template files
1227
- if (isDirectorySafeToReplace(cwd, item.to, hashes, templates)) {
1306
+ if (isDirectorySafeToReplace(cwd, item.to, hashes, templates) &&
1307
+ (!dirMatchesCurrentTemplates(cwd, item.to, templates) ||
1308
+ isDirectorySafeToReplace(cwd, item.from, hashes, templates))) {
1228
1309
  // Safe to delete target and rename source
1229
1310
  result.auto.push(item);
1230
1311
  }
@@ -1451,6 +1532,11 @@ export async function executeMigrations(classified, cwd, options, templates) {
1451
1532
  // flavored bytes, so just drop the now-redundant source instead (#447).
1452
1533
  if (fs.existsSync(newPath) &&
1453
1534
  dirMatchesCurrentTemplates(cwd, item.to, templates)) {
1535
+ if (!isDirectorySafeToReplace(cwd, item.from, loadHashes(cwd), templates)) {
1536
+ result.skipped++;
1537
+ console.log(chalk.yellow(" ○ Preserved modified migration source: " + item.from));
1538
+ continue;
1539
+ }
1454
1540
  removeDirectoryRecursive(oldPath);
1455
1541
  const hashes = loadHashes(cwd);
1456
1542
  const updatedHashes = {};
@@ -1633,6 +1719,7 @@ export async function update(options) {
1633
1719
  }
1634
1720
  console.log(chalk.cyan("\nTrellis Update"));
1635
1721
  console.log(chalk.cyan("══════════════\n"));
1722
+ reportAgentsOverride(cwd);
1636
1723
  // Set up proxy before any network calls (npm version check)
1637
1724
  setupProxy();
1638
1725
  // Get versions
@@ -1865,6 +1952,8 @@ export async function update(options) {
1865
1952
  }
1866
1953
  if (zcodeConfigured)
1867
1954
  printZcodeSetupHint();
1955
+ if (!options.dryRun)
1956
+ refreshNativeWorkflowProvenance(cwd);
1868
1957
  return;
1869
1958
  }
1870
1959
  // Show what this operation will do
@@ -1928,277 +2017,301 @@ export async function update(options) {
1928
2017
  return;
1929
2018
  }
1930
2019
  }
1931
- // Create complete backup of all managed platform/workflow directories
1932
- const backupDir = createFullBackup(cwd);
1933
- if (backupDir) {
1934
- console.log(chalk.gray(`\nBackup created: ${path.relative(cwd, backupDir)}/`));
1935
- }
1936
- // Execute migrations if --migrate flag is set
1937
- if (options.migrate && classifiedMigrations) {
1938
- const migrationResult = await executeMigrations(classifiedMigrations, cwd, {
1939
- force: options.force,
1940
- skipAll: options.skipAll,
1941
- }, templates);
1942
- printMigrationResult(migrationResult);
1943
- // Hardcoded: Rename traces-*.md to journal-*.md in workspace directories
1944
- // Why hardcoded: The migration system only supports fixed path renames, not pattern-based.
1945
- // traces-*.md files are in .trellis/workspace/{developer}/ with variable developer names
1946
- // and variable file numbers (traces-1.md, traces-2.md, etc.), so we can't enumerate them
1947
- // in the migration manifest. This is a one-time migration for the 0.2.0 naming redesign.
1948
- const workspaceDir = path.join(cwd, PATHS.WORKSPACE);
1949
- const { renamed: journalRenamed, skipped: journalSkipped } = renameTracesToJournal(workspaceDir);
1950
- if (journalRenamed > 0) {
1951
- console.log(chalk.cyan(`Renamed ${journalRenamed} traces file(s) to journal`));
1952
- }
1953
- for (const oldPath of journalSkipped) {
1954
- console.warn(chalk.yellow(`Kept ${path.relative(cwd, oldPath)}: its journal target already exists`));
1955
- }
1956
- }
1957
- // Execute safe-file-delete (after backup, before template writes)
1958
- let safeDeleted = 0;
1959
- if (hasSafeDeletes) {
1960
- safeDeleted = executeSafeFileDeletes(safeFileDeletes, cwd);
1961
- if (safeDeleted > 0) {
1962
- console.log(chalk.cyan(`\nCleaned up ${safeDeleted} deprecated command file(s)`));
2020
+ // Keep the rollback snapshot protected until all update writes finish.
2021
+ const backupLockPath = path.join(cwd, DIR_NAMES.WORKFLOW, BACKUP_PRUNE_LOCK);
2022
+ const backupLock = fs.openSync(backupLockPath, "wx");
2023
+ try {
2024
+ // Create complete backup of all managed platform/workflow directories
2025
+ const backupDir = createFullBackup(cwd);
2026
+ if (backupDir) {
2027
+ console.log(chalk.gray(`\nBackup created: ${path.relative(cwd, backupDir)}/`));
2028
+ }
2029
+ // Execute migrations if --migrate flag is set
2030
+ if (options.migrate && classifiedMigrations) {
2031
+ const migrationResult = await executeMigrations(classifiedMigrations, cwd, {
2032
+ force: options.force,
2033
+ skipAll: options.skipAll,
2034
+ }, templates);
2035
+ printMigrationResult(migrationResult);
2036
+ // Hardcoded: Rename traces-*.md to journal-*.md in workspace directories
2037
+ // Why hardcoded: The migration system only supports fixed path renames, not pattern-based.
2038
+ // traces-*.md files are in .trellis/workspace/{developer}/ with variable developer names
2039
+ // and variable file numbers (traces-1.md, traces-2.md, etc.), so we can't enumerate them
2040
+ // in the migration manifest. This is a one-time migration for the 0.2.0 naming redesign.
2041
+ const workspaceDir = path.join(cwd, PATHS.WORKSPACE);
2042
+ const { renamed: journalRenamed, skipped: journalSkipped } = renameTracesToJournal(workspaceDir);
2043
+ if (journalRenamed > 0) {
2044
+ console.log(chalk.cyan(`Renamed ${journalRenamed} traces file(s) to journal`));
2045
+ }
2046
+ for (const oldPath of journalSkipped) {
2047
+ console.warn(chalk.yellow(`Kept ${path.relative(cwd, oldPath)}: its journal target already exists`));
2048
+ }
1963
2049
  }
1964
- }
1965
- // Track results
1966
- let added = 0;
1967
- let autoUpdated = 0;
1968
- let updated = 0;
1969
- let skipped = 0;
1970
- let createdNew = 0;
1971
- // Add new files
1972
- if (changes.newFiles.length > 0) {
1973
- console.log(chalk.blue("\nAdding new files..."));
1974
- for (const file of changes.newFiles) {
1975
- const dir = path.dirname(file.path);
1976
- fs.mkdirSync(dir, { recursive: true });
1977
- fs.writeFileSync(file.path, file.newContent);
1978
- // Make scripts executable
1979
- if (file.relativePath.endsWith(".sh") ||
1980
- file.relativePath.endsWith(".py")) {
1981
- fs.chmodSync(file.path, "755");
2050
+ // Execute safe-file-delete (after backup, before template writes)
2051
+ let safeDeleted = 0;
2052
+ if (hasSafeDeletes) {
2053
+ safeDeleted = executeSafeFileDeletes(safeFileDeletes, cwd);
2054
+ if (safeDeleted > 0) {
2055
+ console.log(chalk.cyan(`\nCleaned up ${safeDeleted} deprecated command file(s)`));
1982
2056
  }
1983
- console.log(chalk.green(` + ${file.relativePath}`));
1984
- added++;
1985
2057
  }
1986
- }
1987
- // Auto-update files (template updated, user didn't modify)
1988
- if (changes.autoUpdateFiles.length > 0) {
1989
- console.log(chalk.blue("\nAuto-updating template files..."));
1990
- for (const file of changes.autoUpdateFiles) {
1991
- fs.writeFileSync(file.path, file.newContent);
1992
- // Make scripts executable
1993
- if (file.relativePath.endsWith(".sh") ||
1994
- file.relativePath.endsWith(".py")) {
1995
- fs.chmodSync(file.path, "755");
2058
+ // Track results
2059
+ let added = 0;
2060
+ let autoUpdated = 0;
2061
+ let updated = 0;
2062
+ let skipped = 0;
2063
+ let createdNew = 0;
2064
+ // Add new files
2065
+ if (changes.newFiles.length > 0) {
2066
+ console.log(chalk.blue("\nAdding new files..."));
2067
+ for (const file of changes.newFiles) {
2068
+ const dir = path.dirname(file.path);
2069
+ fs.mkdirSync(dir, { recursive: true });
2070
+ fs.writeFileSync(file.path, file.newContent);
2071
+ // Make scripts executable
2072
+ if (file.relativePath.endsWith(".sh") ||
2073
+ file.relativePath.endsWith(".py")) {
2074
+ fs.chmodSync(file.path, "755");
2075
+ }
2076
+ console.log(chalk.green(` + ${file.relativePath}`));
2077
+ added++;
1996
2078
  }
1997
- console.log(chalk.cyan(` ↑ ${file.relativePath}`));
1998
- autoUpdated++;
1999
2079
  }
2000
- }
2001
- // Handle changed files
2002
- if (changes.changedFiles.length > 0) {
2003
- console.log(chalk.blue("\n--- Resolving conflicts ---\n"));
2004
- const applyToAll = { action: null };
2005
- for (const file of changes.changedFiles) {
2006
- const action = await promptConflictResolution(file, options, applyToAll);
2007
- if (action === "overwrite") {
2080
+ // Auto-update files (template updated, user didn't modify)
2081
+ if (changes.autoUpdateFiles.length > 0) {
2082
+ console.log(chalk.blue("\nAuto-updating template files..."));
2083
+ for (const file of changes.autoUpdateFiles) {
2008
2084
  fs.writeFileSync(file.path, file.newContent);
2085
+ // Make scripts executable
2009
2086
  if (file.relativePath.endsWith(".sh") ||
2010
2087
  file.relativePath.endsWith(".py")) {
2011
2088
  fs.chmodSync(file.path, "755");
2012
2089
  }
2013
- console.log(chalk.yellow(` ✓ Overwritten: ${file.relativePath}`));
2014
- updated++;
2090
+ console.log(chalk.cyan(` ↑ ${file.relativePath}`));
2091
+ autoUpdated++;
2015
2092
  }
2016
- else if (action === "create-new") {
2017
- const newPath = file.path + ".new";
2018
- fs.writeFileSync(newPath, file.newContent);
2019
- console.log(chalk.blue(` ✓ Created: ${file.relativePath}.new`));
2020
- createdNew++;
2093
+ }
2094
+ // Handle changed files
2095
+ if (changes.changedFiles.length > 0) {
2096
+ console.log(chalk.blue("\n--- Resolving conflicts ---\n"));
2097
+ const applyToAll = { action: null };
2098
+ for (const file of changes.changedFiles) {
2099
+ const action = await promptConflictResolution(file, options, applyToAll);
2100
+ if (action === "overwrite") {
2101
+ const markers = file.relativePath === FILE_NAMES.AGENTS
2102
+ ? [TRELLIS_BLOCK_START, TRELLIS_BLOCK_END]
2103
+ : file.relativePath === COPILOT_INSTRUCTIONS_PATH
2104
+ ? [
2105
+ COPILOT_INSTRUCTIONS_BLOCK_START,
2106
+ COPILOT_INSTRUCTIONS_BLOCK_END,
2107
+ ]
2108
+ : null;
2109
+ if (markers &&
2110
+ mergeManagedMarkdownBlock(fs.readFileSync(file.path, "utf-8"), file.newContent, markers[0], markers[1]) === null) {
2111
+ throw new Error("Refusing ambiguous managed Markdown markers in " +
2112
+ file.relativePath +
2113
+ "; use --create-new or repair the markers.");
2114
+ }
2115
+ fs.writeFileSync(file.path, file.newContent);
2116
+ if (file.relativePath.endsWith(".sh") ||
2117
+ file.relativePath.endsWith(".py")) {
2118
+ fs.chmodSync(file.path, "755");
2119
+ }
2120
+ console.log(chalk.yellow(` ✓ Overwritten: ${file.relativePath}`));
2121
+ updated++;
2122
+ }
2123
+ else if (action === "create-new") {
2124
+ const newPath = file.path + ".new";
2125
+ fs.writeFileSync(newPath, file.newContent);
2126
+ console.log(chalk.blue(` ✓ Created: ${file.relativePath}.new`));
2127
+ createdNew++;
2128
+ }
2129
+ else {
2130
+ console.log(chalk.gray(` ○ Skipped: ${file.relativePath}`));
2131
+ skipped++;
2132
+ }
2021
2133
  }
2022
- else {
2023
- console.log(chalk.gray(` ○ Skipped: ${file.relativePath}`));
2024
- skipped++;
2134
+ }
2135
+ // Append additive config.yaml sections introduced between versions.
2136
+ // Sentinel-gated, so users keep their customizations and re-running update
2137
+ // on already-migrated files is a no-op. Skipped on unknown / downgrade.
2138
+ let configSectionsAppended = 0;
2139
+ if (cliVsProject > 0 && projectVersion !== "unknown") {
2140
+ const sectionEntries = getConfigSectionsAddedBetween(projectVersion, cliVersion);
2141
+ if (sectionEntries.length > 0) {
2142
+ const { appended } = applyConfigSectionsAdded(sectionEntries, cwd, templates);
2143
+ configSectionsAppended = appended;
2025
2144
  }
2026
2145
  }
2027
- }
2028
- // Append additive config.yaml sections introduced between versions.
2029
- // Sentinel-gated, so users keep their customizations and re-running update
2030
- // on already-migrated files is a no-op. Skipped on unknown / downgrade.
2031
- let configSectionsAppended = 0;
2032
- if (cliVsProject > 0 && projectVersion !== "unknown") {
2033
- const sectionEntries = getConfigSectionsAddedBetween(projectVersion, cliVersion);
2034
- if (sectionEntries.length > 0) {
2035
- const { appended } = applyConfigSectionsAdded(sectionEntries, cwd, templates);
2036
- configSectionsAppended = appended;
2037
- }
2038
- }
2039
- // Update version file
2040
- updateVersionFile(cwd);
2041
- // Update template hashes for new, auto-updated, and overwritten files
2042
- const filesToHash = new Map(unchangedFileHashRepairs);
2043
- for (const file of changes.newFiles) {
2044
- filesToHash.set(file.relativePath, file.newContent);
2045
- }
2046
- // Auto-updated files always get new hash
2047
- for (const file of changes.autoUpdateFiles) {
2048
- filesToHash.set(file.relativePath, file.newContent);
2049
- }
2050
- // Only hash overwritten files (not skipped or .new copies)
2051
- for (const file of changes.changedFiles) {
2052
- const fullPath = path.join(cwd, file.relativePath);
2053
- if (fs.existsSync(fullPath)) {
2054
- const content = fs.readFileSync(fullPath, "utf-8");
2055
- if (content === file.newContent) {
2056
- filesToHash.set(file.relativePath, file.newContent);
2146
+ // Update version file
2147
+ updateVersionFile(cwd);
2148
+ refreshNativeWorkflowProvenance(cwd);
2149
+ // Update template hashes for new, auto-updated, and overwritten files
2150
+ const filesToHash = new Map(unchangedFileHashRepairs);
2151
+ for (const file of changes.newFiles) {
2152
+ filesToHash.set(file.relativePath, file.newContent);
2153
+ }
2154
+ // Auto-updated files always get new hash
2155
+ for (const file of changes.autoUpdateFiles) {
2156
+ filesToHash.set(file.relativePath, file.newContent);
2157
+ }
2158
+ // Only hash overwritten files (not skipped or .new copies)
2159
+ for (const file of changes.changedFiles) {
2160
+ const fullPath = path.join(cwd, file.relativePath);
2161
+ if (fs.existsSync(fullPath)) {
2162
+ const content = fs.readFileSync(fullPath, "utf-8");
2163
+ if (content === file.newContent) {
2164
+ filesToHash.set(file.relativePath, file.newContent);
2165
+ }
2057
2166
  }
2058
2167
  }
2059
- }
2060
- if (filesToHash.size > 0) {
2061
- updateHashes(cwd, filesToHash);
2062
- }
2063
- // Print summary
2064
- console.log(chalk.cyan("\n--- Summary ---\n"));
2065
- if (added > 0) {
2066
- console.log(` Added: ${added} file(s)`);
2067
- }
2068
- if (autoUpdated > 0) {
2069
- console.log(` Auto-updated: ${autoUpdated} file(s)`);
2070
- }
2071
- if (updated > 0) {
2072
- console.log(` Updated: ${updated} file(s)`);
2073
- }
2074
- if (skipped > 0) {
2075
- console.log(` Skipped: ${skipped} file(s)`);
2076
- }
2077
- if (createdNew > 0) {
2078
- console.log(` Created .new copies: ${createdNew} file(s)`);
2079
- }
2080
- if (safeDeleted > 0) {
2081
- console.log(` Cleaned up: ${safeDeleted} deprecated file(s)`);
2082
- }
2083
- if (configSectionsAppended > 0) {
2084
- console.log(` Config sections added: ${configSectionsAppended}`);
2085
- }
2086
- if (backupDir) {
2087
- console.log(` Backup: ${path.relative(cwd, backupDir)}/`);
2088
- }
2089
- const actionWord = isDowngrade ? "Downgrade" : "Update";
2090
- console.log(chalk.green(`\n✅ ${actionWord} complete! (${projectVersion} → ${cliVersion})`));
2091
- if (createdNew > 0) {
2092
- console.log(chalk.gray("\nTip: Review .new files and merge changes manually if needed."));
2093
- }
2094
- // Create migration task if there are breaking changes with migration guides
2095
- if (cliVsProject > 0 && projectVersion !== "unknown") {
2096
- const metadata = getMigrationMetadata(projectVersion, cliVersion);
2097
- if (metadata.breaking && metadata.migrationGuides.length > 0) {
2098
- // Create task directory
2099
- const today = new Date();
2100
- const monthDay = `${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`;
2101
- const taskSlug = `migrate-to-${cliVersion}`;
2102
- const taskDirName = `${monthDay}-${taskSlug}`;
2103
- const tasksDir = path.join(cwd, DIR_NAMES.WORKFLOW, DIR_NAMES.TASKS);
2104
- const taskDir = path.join(tasksDir, taskDirName);
2105
- // Check if task already exists
2106
- if (!fs.existsSync(taskDir)) {
2107
- fs.mkdirSync(taskDir, { recursive: true });
2108
- // Get current developer for assignee.
2109
- // `.developer` is a key=value file (written by init_developer.py):
2110
- // name=<developer-name>
2111
- // initialized_at=<iso8601>
2112
- // Reading it raw and .trim()-ing embeds the entire file contents
2113
- // (including the `name=` prefix and the `initialized_at` line) into
2114
- // the assignee field, producing bogus assignees like
2115
- // "name=suyuan\ninitialized_at=2026-04-07T23:41:21.978312" that
2116
- // later break session-start task rendering.
2117
- const developerFile = path.join(cwd, DIR_NAMES.WORKFLOW, ".developer");
2118
- let currentDeveloper = "unknown";
2119
- if (fs.existsSync(developerFile)) {
2120
- const raw = fs.readFileSync(developerFile, "utf-8");
2121
- const nameMatch = raw.match(/^\s*name\s*=\s*(.+?)\s*$/m);
2122
- if (nameMatch) {
2123
- currentDeveloper = nameMatch[1];
2168
+ if (filesToHash.size > 0) {
2169
+ updateHashes(cwd, filesToHash);
2170
+ }
2171
+ // Print summary
2172
+ console.log(chalk.cyan("\n--- Summary ---\n"));
2173
+ if (added > 0) {
2174
+ console.log(` Added: ${added} file(s)`);
2175
+ }
2176
+ if (autoUpdated > 0) {
2177
+ console.log(` Auto-updated: ${autoUpdated} file(s)`);
2178
+ }
2179
+ if (updated > 0) {
2180
+ console.log(` Updated: ${updated} file(s)`);
2181
+ }
2182
+ if (skipped > 0) {
2183
+ console.log(` Skipped: ${skipped} file(s)`);
2184
+ }
2185
+ if (createdNew > 0) {
2186
+ console.log(` Created .new copies: ${createdNew} file(s)`);
2187
+ }
2188
+ if (safeDeleted > 0) {
2189
+ console.log(` Cleaned up: ${safeDeleted} deprecated file(s)`);
2190
+ }
2191
+ if (configSectionsAppended > 0) {
2192
+ console.log(` Config sections added: ${configSectionsAppended}`);
2193
+ }
2194
+ if (backupDir) {
2195
+ console.log(` Backup: ${path.relative(cwd, backupDir)}/`);
2196
+ }
2197
+ const actionWord = isDowngrade ? "Downgrade" : "Update";
2198
+ console.log(chalk.green(`\n✅ ${actionWord} complete! (${projectVersion} → ${cliVersion})`));
2199
+ if (createdNew > 0) {
2200
+ console.log(chalk.gray("\nTip: Review .new files and merge changes manually if needed."));
2201
+ }
2202
+ // Create migration task if there are breaking changes with migration guides
2203
+ if (cliVsProject > 0 && projectVersion !== "unknown") {
2204
+ const metadata = getMigrationMetadata(projectVersion, cliVersion);
2205
+ if (metadata.breaking && metadata.migrationGuides.length > 0) {
2206
+ // Create task directory
2207
+ const today = new Date();
2208
+ const monthDay = `${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`;
2209
+ const taskSlug = `migrate-to-${cliVersion}`;
2210
+ const taskDirName = `${monthDay}-${taskSlug}`;
2211
+ const tasksDir = path.join(cwd, DIR_NAMES.WORKFLOW, DIR_NAMES.TASKS);
2212
+ const taskDir = path.join(tasksDir, taskDirName);
2213
+ // Check if task already exists
2214
+ if (!fs.existsSync(taskDir)) {
2215
+ fs.mkdirSync(taskDir, { recursive: true });
2216
+ // Get current developer for assignee.
2217
+ // `.developer` is a key=value file (written by init_developer.py):
2218
+ // name=<developer-name>
2219
+ // initialized_at=<iso8601>
2220
+ // Reading it raw and .trim()-ing embeds the entire file contents
2221
+ // (including the `name=` prefix and the `initialized_at` line) into
2222
+ // the assignee field, producing bogus assignees like
2223
+ // "name=suyuan\ninitialized_at=2026-04-07T23:41:21.978312" that
2224
+ // later break session-start task rendering.
2225
+ const developerFile = path.join(cwd, DIR_NAMES.WORKFLOW, ".developer");
2226
+ let currentDeveloper = "unknown";
2227
+ if (fs.existsSync(developerFile)) {
2228
+ const raw = fs.readFileSync(developerFile, "utf-8");
2229
+ const nameMatch = raw.match(/^\s*name\s*=\s*(.+?)\s*$/m);
2230
+ if (nameMatch) {
2231
+ currentDeveloper = nameMatch[1];
2232
+ }
2124
2233
  }
2125
- }
2126
- // Build task.json — canonical 24-field shape via shared factory.
2127
- const taskTitle = `Migrate to v${cliVersion}`;
2128
- const todayStr = today.toISOString().split("T")[0];
2129
- const taskJson = emptyTaskJson({
2130
- id: taskSlug,
2131
- name: taskSlug,
2132
- title: taskTitle,
2133
- description: `Breaking change migration from v${projectVersion} to v${cliVersion}`,
2134
- status: "planning",
2135
- scope: "migration",
2136
- priority: "P1",
2137
- creator: "trellis-update",
2138
- assignee: currentDeveloper,
2139
- createdAt: todayStr,
2140
- });
2141
- // Write task.json
2142
- const taskJsonPath = path.join(taskDir, "task.json");
2143
- fs.writeFileSync(taskJsonPath, JSON.stringify(taskJson, null, 2));
2144
- // Build PRD content
2145
- let prdContent = `# Migration Task: Upgrade to v${cliVersion}\n\n`;
2146
- prdContent += `**Created**: ${todayStr}\n`;
2147
- prdContent += `**From Version**: ${projectVersion}\n`;
2148
- prdContent += `**To Version**: ${cliVersion}\n`;
2149
- prdContent += `**Assignee**: ${currentDeveloper}\n\n`;
2150
- prdContent += `## Status\n\n- [ ] Review migration guide\n- [ ] Update custom files\n- [ ] Run \`trellis update --migrate\`\n- [ ] Test workflows\n\n`;
2151
- for (const { version, guide, aiInstructions, } of metadata.migrationGuides) {
2152
- prdContent += `---\n\n## v${version} Migration Guide\n\n`;
2153
- prdContent += guide;
2154
- prdContent += "\n\n";
2155
- if (aiInstructions) {
2156
- prdContent += `### AI Assistant Instructions\n\n`;
2157
- prdContent += `When helping with this migration:\n\n`;
2158
- prdContent += aiInstructions;
2234
+ // Build task.json — canonical 24-field shape via shared factory.
2235
+ const taskTitle = `Migrate to v${cliVersion}`;
2236
+ const todayStr = today.toISOString().split("T")[0];
2237
+ const taskJson = emptyTaskJson({
2238
+ id: taskSlug,
2239
+ name: taskSlug,
2240
+ title: taskTitle,
2241
+ description: `Breaking change migration from v${projectVersion} to v${cliVersion}`,
2242
+ status: "planning",
2243
+ scope: "migration",
2244
+ priority: "P1",
2245
+ creator: "trellis-update",
2246
+ assignee: currentDeveloper,
2247
+ createdAt: todayStr,
2248
+ });
2249
+ // Write task.json
2250
+ const taskJsonPath = path.join(taskDir, "task.json");
2251
+ fs.writeFileSync(taskJsonPath, JSON.stringify(taskJson, null, 2));
2252
+ // Build PRD content
2253
+ let prdContent = `# Migration Task: Upgrade to v${cliVersion}\n\n`;
2254
+ prdContent += `**Created**: ${todayStr}\n`;
2255
+ prdContent += `**From Version**: ${projectVersion}\n`;
2256
+ prdContent += `**To Version**: ${cliVersion}\n`;
2257
+ prdContent += `**Assignee**: ${currentDeveloper}\n\n`;
2258
+ prdContent += `## Status\n\n- [ ] Review migration guide\n- [ ] Update custom files\n- [ ] Run \`trellis update --migrate\`\n- [ ] Test workflows\n\n`;
2259
+ for (const { version, guide, aiInstructions, } of metadata.migrationGuides) {
2260
+ prdContent += `---\n\n## v${version} Migration Guide\n\n`;
2261
+ prdContent += guide;
2159
2262
  prdContent += "\n\n";
2263
+ if (aiInstructions) {
2264
+ prdContent += `### AI Assistant Instructions\n\n`;
2265
+ prdContent += `When helping with this migration:\n\n`;
2266
+ prdContent += aiInstructions;
2267
+ prdContent += "\n\n";
2268
+ }
2160
2269
  }
2270
+ // Write PRD
2271
+ const prdPath = path.join(taskDir, "prd.md");
2272
+ fs.writeFileSync(prdPath, prdContent);
2273
+ console.log("");
2274
+ console.log(chalk.bgCyan.black.bold(" 📋 MIGRATION TASK CREATED "));
2275
+ console.log(chalk.cyan(`A task has been created to help you complete the migration:`));
2276
+ console.log(chalk.white(` ${DIR_NAMES.WORKFLOW}/${DIR_NAMES.TASKS}/${taskDirName}/`));
2277
+ console.log("");
2278
+ console.log(chalk.gray("Use AI to help: Ask Claude/Cursor to read the task and fix your custom files."));
2161
2279
  }
2162
- // Write PRD
2163
- const prdPath = path.join(taskDir, "prd.md");
2164
- fs.writeFileSync(prdPath, prdContent);
2165
- console.log("");
2166
- console.log(chalk.bgCyan.black.bold(" 📋 MIGRATION TASK CREATED "));
2167
- console.log(chalk.cyan(`A task has been created to help you complete the migration:`));
2168
- console.log(chalk.white(` ${DIR_NAMES.WORKFLOW}/${DIR_NAMES.TASKS}/${taskDirName}/`));
2169
- console.log("");
2170
- console.log(chalk.gray("Use AI to help: Ask Claude/Cursor to read the task and fix your custom files."));
2171
2280
  }
2172
2281
  }
2173
- }
2174
- if (zcodeConfigured)
2175
- printZcodeSetupHint();
2176
- // Display breaking change warnings at the very end (so they don't scroll off screen)
2177
- if (cliVsProject > 0 && projectVersion !== "unknown") {
2178
- const finalMetadata = getMigrationMetadata(projectVersion, cliVersion);
2179
- if (finalMetadata.breaking || finalMetadata.changelog.length > 0) {
2180
- console.log("");
2181
- console.log(chalk.cyan("═".repeat(60)));
2182
- if (finalMetadata.breaking) {
2183
- console.log(chalk.bgRed.white.bold(" ⚠️ BREAKING CHANGES ") +
2184
- chalk.red.bold(" This update contains breaking changes!"));
2282
+ if (zcodeConfigured)
2283
+ printZcodeSetupHint();
2284
+ // Display breaking change warnings at the very end (so they don't scroll off screen)
2285
+ if (cliVsProject > 0 && projectVersion !== "unknown") {
2286
+ const finalMetadata = getMigrationMetadata(projectVersion, cliVersion);
2287
+ if (finalMetadata.breaking || finalMetadata.changelog.length > 0) {
2185
2288
  console.log("");
2186
- }
2187
- if (finalMetadata.changelog.length > 0) {
2188
- console.log(chalk.cyan.bold("📋 What's Changed:"));
2189
- for (const entry of finalMetadata.changelog) {
2190
- console.log(chalk.white(` ${entry}`));
2289
+ console.log(chalk.cyan("═".repeat(60)));
2290
+ if (finalMetadata.breaking) {
2291
+ console.log(chalk.bgRed.white.bold(" ⚠️ BREAKING CHANGES ") +
2292
+ chalk.red.bold(" This update contains breaking changes!"));
2293
+ console.log("");
2191
2294
  }
2192
- console.log("");
2193
- }
2194
- if (finalMetadata.recommendMigrate && !options.migrate) {
2195
- console.log(chalk.bgGreen.black.bold(" 💡 RECOMMENDED ") +
2196
- chalk.green.bold(" Run with --migrate to complete the migration"));
2197
- console.log(chalk.gray(" This will remove legacy files and apply all changes."));
2198
- console.log("");
2295
+ if (finalMetadata.changelog.length > 0) {
2296
+ console.log(chalk.cyan.bold("📋 What's Changed:"));
2297
+ for (const entry of finalMetadata.changelog) {
2298
+ console.log(chalk.white(` ${entry}`));
2299
+ }
2300
+ console.log("");
2301
+ }
2302
+ if (finalMetadata.recommendMigrate && !options.migrate) {
2303
+ console.log(chalk.bgGreen.black.bold(" 💡 RECOMMENDED ") +
2304
+ chalk.green.bold(" Run with --migrate to complete the migration"));
2305
+ console.log(chalk.gray(" This will remove legacy files and apply all changes."));
2306
+ console.log("");
2307
+ }
2308
+ console.log(chalk.cyan("═".repeat(60)));
2199
2309
  }
2200
- console.log(chalk.cyan("═".repeat(60)));
2201
2310
  }
2202
2311
  }
2312
+ finally {
2313
+ fs.closeSync(backupLock);
2314
+ fs.rmSync(backupLockPath, { force: true });
2315
+ }
2203
2316
  }
2204
2317
  //# sourceMappingURL=update.js.map