@mindfoldhq/trellis 0.5.0-beta.8 → 0.5.0-rc.0

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 (210) hide show
  1. package/README.md +60 -95
  2. package/dist/cli/index.js +7 -0
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/commands/init.d.ts +13 -0
  5. package/dist/commands/init.d.ts.map +1 -1
  6. package/dist/commands/init.js +474 -210
  7. package/dist/commands/init.js.map +1 -1
  8. package/dist/commands/update.d.ts.map +1 -1
  9. package/dist/commands/update.js +295 -54
  10. package/dist/commands/update.js.map +1 -1
  11. package/dist/configurators/antigravity.d.ts.map +1 -1
  12. package/dist/configurators/antigravity.js +2 -8
  13. package/dist/configurators/antigravity.js.map +1 -1
  14. package/dist/configurators/claude.d.ts.map +1 -1
  15. package/dist/configurators/claude.js +4 -10
  16. package/dist/configurators/claude.js.map +1 -1
  17. package/dist/configurators/codebuddy.d.ts.map +1 -1
  18. package/dist/configurators/codebuddy.js +3 -3
  19. package/dist/configurators/codebuddy.js.map +1 -1
  20. package/dist/configurators/codex.d.ts.map +1 -1
  21. package/dist/configurators/codex.js +5 -13
  22. package/dist/configurators/codex.js.map +1 -1
  23. package/dist/configurators/copilot.d.ts.map +1 -1
  24. package/dist/configurators/copilot.js +5 -19
  25. package/dist/configurators/copilot.js.map +1 -1
  26. package/dist/configurators/cursor.d.ts.map +1 -1
  27. package/dist/configurators/cursor.js +3 -3
  28. package/dist/configurators/cursor.js.map +1 -1
  29. package/dist/configurators/droid.d.ts.map +1 -1
  30. package/dist/configurators/droid.js +3 -3
  31. package/dist/configurators/droid.js.map +1 -1
  32. package/dist/configurators/gemini.d.ts.map +1 -1
  33. package/dist/configurators/gemini.js +3 -5
  34. package/dist/configurators/gemini.js.map +1 -1
  35. package/dist/configurators/index.d.ts.map +1 -1
  36. package/dist/configurators/index.js +44 -55
  37. package/dist/configurators/index.js.map +1 -1
  38. package/dist/configurators/kilo.d.ts.map +1 -1
  39. package/dist/configurators/kilo.js +2 -8
  40. package/dist/configurators/kilo.js.map +1 -1
  41. package/dist/configurators/kiro.d.ts.map +1 -1
  42. package/dist/configurators/kiro.js +3 -3
  43. package/dist/configurators/kiro.js.map +1 -1
  44. package/dist/configurators/opencode.d.ts.map +1 -1
  45. package/dist/configurators/opencode.js +7 -4
  46. package/dist/configurators/opencode.js.map +1 -1
  47. package/dist/configurators/pi.d.ts +3 -0
  48. package/dist/configurators/pi.d.ts.map +1 -0
  49. package/dist/configurators/pi.js +44 -0
  50. package/dist/configurators/pi.js.map +1 -0
  51. package/dist/configurators/qoder.d.ts +7 -6
  52. package/dist/configurators/qoder.d.ts.map +1 -1
  53. package/dist/configurators/qoder.js +18 -12
  54. package/dist/configurators/qoder.js.map +1 -1
  55. package/dist/configurators/shared.d.ts +30 -6
  56. package/dist/configurators/shared.d.ts.map +1 -1
  57. package/dist/configurators/shared.js +65 -15
  58. package/dist/configurators/shared.js.map +1 -1
  59. package/dist/configurators/windsurf.d.ts.map +1 -1
  60. package/dist/configurators/windsurf.js +2 -8
  61. package/dist/configurators/windsurf.js.map +1 -1
  62. package/dist/constants/paths.d.ts +2 -0
  63. package/dist/constants/paths.d.ts.map +1 -1
  64. package/dist/constants/paths.js +2 -0
  65. package/dist/constants/paths.js.map +1 -1
  66. package/dist/migrations/manifests/0.5.0-beta.0.json +2 -0
  67. package/dist/migrations/manifests/0.5.0-beta.10.json +9 -0
  68. package/dist/migrations/manifests/0.5.0-beta.11.json +9 -0
  69. package/dist/migrations/manifests/0.5.0-beta.12.json +9 -0
  70. package/dist/migrations/manifests/0.5.0-beta.13.json +9 -0
  71. package/dist/migrations/manifests/0.5.0-beta.14.json +9 -0
  72. package/dist/migrations/manifests/0.5.0-beta.15.json +116 -0
  73. package/dist/migrations/manifests/0.5.0-beta.16.json +9 -0
  74. package/dist/migrations/manifests/0.5.0-beta.17.json +9 -0
  75. package/dist/migrations/manifests/0.5.0-beta.18.json +9 -0
  76. package/dist/migrations/manifests/0.5.0-beta.19.json +9 -0
  77. package/dist/migrations/manifests/0.5.0-beta.5.json +2 -0
  78. package/dist/migrations/manifests/0.5.0-beta.9.json +48 -0
  79. package/dist/migrations/manifests/0.5.0-rc.0.json +9 -0
  80. package/dist/templates/claude/agents/trellis-research.md +1 -1
  81. package/dist/templates/claude/settings.json +0 -4
  82. package/dist/templates/codebuddy/agents/trellis-research.md +1 -1
  83. package/dist/templates/codex/agents/trellis-research.toml +3 -2
  84. package/dist/templates/codex/hooks/session-start.py +126 -26
  85. package/dist/templates/codex/skills/finish-work/SKILL.md +41 -109
  86. package/dist/templates/codex/skills/start/SKILL.md +12 -9
  87. package/dist/templates/common/bundled-skills/trellis-meta/SKILL.md +73 -0
  88. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/add-project-local-conventions.md +83 -0
  89. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-agents.md +54 -0
  90. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-context-loading.md +81 -0
  91. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-hooks.md +57 -0
  92. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-skills-or-commands.md +78 -0
  93. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-spec-structure.md +83 -0
  94. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-task-lifecycle.md +90 -0
  95. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-workflow.md +64 -0
  96. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/overview.md +55 -0
  97. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/context-injection.md +68 -0
  98. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/generated-files.md +80 -0
  99. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/overview.md +51 -0
  100. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/spec-system.md +102 -0
  101. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/task-system.md +101 -0
  102. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/workflow.md +75 -0
  103. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/workspace-memory.md +71 -0
  104. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/agents.md +79 -0
  105. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/hooks-and-settings.md +69 -0
  106. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/overview.md +59 -0
  107. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/platform-map.md +74 -0
  108. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/skills-and-commands.md +83 -0
  109. package/dist/templates/common/commands/continue.md +9 -5
  110. package/dist/templates/common/commands/finish-work.md +34 -10
  111. package/dist/templates/common/index.d.ts +22 -2
  112. package/dist/templates/common/index.d.ts.map +1 -1
  113. package/dist/templates/common/index.js +53 -4
  114. package/dist/templates/common/index.js.map +1 -1
  115. package/dist/templates/common/skills/brainstorm.md +50 -4
  116. package/dist/templates/copilot/hooks/session-start.py +127 -30
  117. package/dist/templates/copilot/prompts/finish-work.prompt.md +44 -112
  118. package/dist/templates/copilot/prompts/start.prompt.md +12 -9
  119. package/dist/templates/cursor/agents/trellis-check.md +1 -1
  120. package/dist/templates/cursor/agents/trellis-implement.md +1 -1
  121. package/dist/templates/cursor/agents/trellis-research.md +2 -2
  122. package/dist/templates/cursor/hooks.json +7 -1
  123. package/dist/templates/droid/droids/trellis-research.md +1 -1
  124. package/dist/templates/extract.d.ts +6 -0
  125. package/dist/templates/extract.d.ts.map +1 -1
  126. package/dist/templates/extract.js +14 -0
  127. package/dist/templates/extract.js.map +1 -1
  128. package/dist/templates/gemini/agents/trellis-research.md +1 -1
  129. package/dist/templates/kiro/agents/trellis-research.json +1 -1
  130. package/dist/templates/markdown/agents.md +19 -12
  131. package/dist/templates/markdown/gitignore.txt +3 -0
  132. package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md.txt +24 -0
  133. package/dist/templates/opencode/agents/trellis-check.md +1 -1
  134. package/dist/templates/opencode/agents/trellis-implement.md +7 -4
  135. package/dist/templates/opencode/agents/trellis-research.md +2 -2
  136. package/dist/templates/opencode/lib/trellis-context.js +100 -13
  137. package/dist/templates/opencode/plugins/inject-subagent-context.js +70 -5
  138. package/dist/templates/opencode/plugins/inject-workflow-state.js +38 -44
  139. package/dist/templates/opencode/plugins/session-start.js +76 -31
  140. package/dist/templates/pi/agents/trellis-check.md +28 -0
  141. package/dist/templates/pi/agents/trellis-implement.md +33 -0
  142. package/dist/templates/pi/agents/trellis-research.md +25 -0
  143. package/dist/templates/pi/extensions/trellis/index.ts.txt +997 -0
  144. package/dist/templates/pi/index.d.ts +5 -0
  145. package/dist/templates/pi/index.d.ts.map +1 -0
  146. package/dist/templates/pi/index.js +12 -0
  147. package/dist/templates/pi/index.js.map +1 -0
  148. package/dist/templates/pi/settings.json +12 -0
  149. package/dist/templates/qoder/agents/trellis-research.md +1 -1
  150. package/dist/templates/shared-hooks/index.d.ts +31 -0
  151. package/dist/templates/shared-hooks/index.d.ts.map +1 -1
  152. package/dist/templates/shared-hooks/index.js +59 -0
  153. package/dist/templates/shared-hooks/index.js.map +1 -1
  154. package/dist/templates/shared-hooks/inject-shell-session-context.py +180 -0
  155. package/dist/templates/shared-hooks/inject-subagent-context.py +156 -27
  156. package/dist/templates/shared-hooks/inject-workflow-state.py +85 -92
  157. package/dist/templates/shared-hooks/session-start.py +232 -36
  158. package/dist/templates/trellis/config.yaml +6 -0
  159. package/dist/templates/trellis/gitignore.txt +3 -0
  160. package/dist/templates/trellis/index.d.ts +1 -1
  161. package/dist/templates/trellis/index.d.ts.map +1 -1
  162. package/dist/templates/trellis/index.js +2 -2
  163. package/dist/templates/trellis/index.js.map +1 -1
  164. package/dist/templates/trellis/scripts/common/__init__.py +8 -0
  165. package/dist/templates/trellis/scripts/common/active_task.py +593 -0
  166. package/dist/templates/trellis/scripts/common/cli_adapter.py +72 -14
  167. package/dist/templates/trellis/scripts/common/paths.py +61 -58
  168. package/dist/templates/trellis/scripts/common/session_context.py +12 -0
  169. package/dist/templates/trellis/scripts/common/task_context.py +27 -194
  170. package/dist/templates/trellis/scripts/common/task_store.py +102 -26
  171. package/dist/templates/trellis/scripts/common/tasks.py +4 -1
  172. package/dist/templates/trellis/scripts/common/types.py +0 -2
  173. package/dist/templates/trellis/scripts/common/workflow_phase.py +15 -3
  174. package/dist/templates/trellis/scripts/task.py +99 -34
  175. package/dist/templates/trellis/workflow.md +332 -64
  176. package/dist/types/ai-tools.d.ts +12 -3
  177. package/dist/types/ai-tools.d.ts.map +1 -1
  178. package/dist/types/ai-tools.js +29 -0
  179. package/dist/types/ai-tools.js.map +1 -1
  180. package/dist/utils/file-writer.d.ts.map +1 -1
  181. package/dist/utils/file-writer.js +7 -2
  182. package/dist/utils/file-writer.js.map +1 -1
  183. package/dist/utils/posix.d.ts +13 -0
  184. package/dist/utils/posix.d.ts.map +1 -0
  185. package/dist/utils/posix.js +15 -0
  186. package/dist/utils/posix.js.map +1 -0
  187. package/dist/utils/project-detector.d.ts +2 -0
  188. package/dist/utils/project-detector.d.ts.map +1 -1
  189. package/dist/utils/project-detector.js +120 -11
  190. package/dist/utils/project-detector.js.map +1 -1
  191. package/dist/utils/task-json.d.ts +46 -0
  192. package/dist/utils/task-json.d.ts.map +1 -0
  193. package/dist/utils/task-json.js +49 -0
  194. package/dist/utils/task-json.js.map +1 -0
  195. package/dist/utils/template-fetcher.d.ts +22 -6
  196. package/dist/utils/template-fetcher.d.ts.map +1 -1
  197. package/dist/utils/template-fetcher.js +405 -27
  198. package/dist/utils/template-fetcher.js.map +1 -1
  199. package/dist/utils/template-hash.d.ts +22 -3
  200. package/dist/utils/template-hash.d.ts.map +1 -1
  201. package/dist/utils/template-hash.js +99 -19
  202. package/dist/utils/template-hash.js.map +1 -1
  203. package/package.json +7 -7
  204. package/dist/templates/markdown/spec/backend/directory-structure.md +0 -292
  205. package/dist/templates/markdown/spec/backend/index.md +0 -40
  206. package/dist/templates/markdown/spec/backend/script-conventions.md +0 -742
  207. package/dist/templates/markdown/spec/guides/code-reuse-thinking-guide.md +0 -118
  208. package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md +0 -394
  209. package/dist/templates/shared-hooks/statusline.py +0 -218
  210. package/dist/templates/trellis/scripts/create_bootstrap.py +0 -298
@@ -2,17 +2,30 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import chalk from "chalk";
4
4
  import inquirer from "inquirer";
5
- import { PATHS, DIR_NAMES } from "../constants/paths.js";
5
+ import { DIR_NAMES, FILE_NAMES, PATHS } from "../constants/paths.js";
6
6
  import { VERSION, PACKAGE_NAME } from "../constants/version.js";
7
7
  import { getMigrationsForVersion, getAllMigrations, getMigrationMetadata, } from "../migrations/index.js";
8
8
  import { loadHashes, saveHashes, updateHashes, isTemplateModified, removeHash, renameHash, computeHash, } from "../utils/template-hash.js";
9
9
  import { compareVersions } from "../utils/compare-versions.js";
10
+ import { toPosix } from "../utils/posix.js";
10
11
  import { setupProxy } from "../utils/proxy.js";
12
+ import { emptyTaskJson } from "../utils/task-json.js";
11
13
  // Import templates for comparison
12
14
  import { getAllScripts,
13
15
  // Configuration
14
16
  configYamlTemplate, gitignoreTemplate, workflowMdTemplate, } from "../templates/trellis/index.js";
17
+ import { agentsMdContent } from "../templates/markdown/index.js";
15
18
  import { ALL_MANAGED_DIRS, getConfiguredPlatforms, collectPlatformTemplates, isManagedPath, isManagedRootDir, } from "../configurators/index.js";
19
+ const CLAUDE_SETTINGS_PATH = ".claude/settings.json";
20
+ const TRELLIS_BLOCK_START = "<!-- TRELLIS:START -->";
21
+ const TRELLIS_BLOCK_END = "<!-- TRELLIS:END -->";
22
+ const LEGACY_UNTRACKED_AGENTS_MD_BLOCK_HASHES = new Set([
23
+ // v0.5.0-beta.17 and earlier wrote AGENTS.md but did not hash-track it.
24
+ // This hash is the pristine Trellis-managed block before the Subagents
25
+ // section was added, so old untouched projects can be updated without a
26
+ // false "modified by you" conflict.
27
+ "c1f511b1cfc1902f2147da159f09cc51f380b0c9e341cdb3ac5dea5233f3e307",
28
+ ]);
16
29
  // Paths that should never be touched (true user data)
17
30
  // spec/ is user-customized content created during init; update should never modify it
18
31
  const PROTECTED_PATHS = [
@@ -22,6 +35,160 @@ const PROTECTED_PATHS = [
22
35
  `${DIR_NAMES.WORKFLOW}/.developer`,
23
36
  `${DIR_NAMES.WORKFLOW}/.current-task`,
24
37
  ];
38
+ function getTrellisManagedBlock(content) {
39
+ const start = content.indexOf(TRELLIS_BLOCK_START);
40
+ if (start === -1) {
41
+ return null;
42
+ }
43
+ const end = content.indexOf(TRELLIS_BLOCK_END, start);
44
+ if (end === -1) {
45
+ return null;
46
+ }
47
+ return content.slice(start, end + TRELLIS_BLOCK_END.length);
48
+ }
49
+ function replaceTrellisManagedBlock(existingContent, templateContent) {
50
+ const existingStart = existingContent.indexOf(TRELLIS_BLOCK_START);
51
+ if (existingStart === -1) {
52
+ return null;
53
+ }
54
+ const existingEnd = existingContent.indexOf(TRELLIS_BLOCK_END, existingStart);
55
+ if (existingEnd === -1) {
56
+ return null;
57
+ }
58
+ const templateBlock = getTrellisManagedBlock(templateContent);
59
+ if (!templateBlock) {
60
+ return null;
61
+ }
62
+ return (existingContent.slice(0, existingStart) +
63
+ templateBlock +
64
+ existingContent.slice(existingEnd + TRELLIS_BLOCK_END.length));
65
+ }
66
+ /**
67
+ * Workflow-state breadcrumb tag-block replacement (used by buildWorkflowMdTemplate).
68
+ *
69
+ * Each `[workflow-state:STATUS]...[/workflow-state:STATUS]` block in workflow.md
70
+ * is the runtime source of truth for the per-turn breadcrumb that
71
+ * inject-workflow-state.py / .js read on every UserPromptSubmit. The blocks are
72
+ * managed by Trellis: when the CLI ships an updated breadcrumb body, every
73
+ * downstream user project must pick it up (otherwise the per-turn nudge stays
74
+ * silent on new mandatory steps — see Phase 3.4 commit drift / Phase 1.3 jsonl
75
+ * curation drift, the bugs that motivated workflow-state-contract.md).
76
+ *
77
+ * Replacement contract:
78
+ * - For every status block present in the *template* workflow.md, replace
79
+ * the user's same-named block with the template version.
80
+ * - If a block exists in the template but not in the user's file (either the
81
+ * user removed it, or they're upgrading from a pre-tag version), append
82
+ * the template block at the end of the file so the runtime hook can find it.
83
+ * - Everything outside the named blocks (the user's narrative customizations
84
+ * to the Phase Index, Skill Routing tables, etc.) is preserved verbatim.
85
+ */
86
+ const WORKFLOW_STATE_TAG_RE = /\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n([\s\S]*?)\n\s*\[\/workflow-state:\1\]/g;
87
+ function extractWorkflowStateBlocks(content) {
88
+ const blocks = new Map();
89
+ for (const match of content.matchAll(WORKFLOW_STATE_TAG_RE)) {
90
+ blocks.set(match[1], match[0]);
91
+ }
92
+ return blocks;
93
+ }
94
+ function replaceWorkflowStateBlock(content, status, newBlock) {
95
+ const re = new RegExp(`\\[workflow-state:${status}\\]\\s*\\n[\\s\\S]*?\\n\\s*\\[/workflow-state:${status}\\]`);
96
+ const match = content.match(re);
97
+ if (match?.index === undefined) {
98
+ return { content, replaced: false };
99
+ }
100
+ return {
101
+ content: content.slice(0, match.index) +
102
+ newBlock +
103
+ content.slice(match.index + match[0].length),
104
+ replaced: true,
105
+ };
106
+ }
107
+ function buildWorkflowMdTemplate(cwd) {
108
+ const fullPath = path.join(cwd, DIR_NAMES.WORKFLOW, "workflow.md");
109
+ if (!fs.existsSync(fullPath)) {
110
+ return workflowMdTemplate;
111
+ }
112
+ let existingContent;
113
+ try {
114
+ existingContent = fs.readFileSync(fullPath, "utf-8");
115
+ }
116
+ catch {
117
+ return workflowMdTemplate;
118
+ }
119
+ const templateBlocks = extractWorkflowStateBlocks(workflowMdTemplate);
120
+ if (templateBlocks.size === 0) {
121
+ // Template has no breadcrumb tags — fall back to full overwrite contract.
122
+ return workflowMdTemplate;
123
+ }
124
+ const existingBlocks = extractWorkflowStateBlocks(existingContent);
125
+ let merged = existingContent;
126
+ const appendQueue = [];
127
+ const customizedOverwritten = [];
128
+ for (const [status, block] of templateBlocks) {
129
+ const existing = existingBlocks.get(status);
130
+ if (existing && existing !== block) {
131
+ customizedOverwritten.push(status);
132
+ }
133
+ const result = replaceWorkflowStateBlock(merged, status, block);
134
+ if (result.replaced) {
135
+ merged = result.content;
136
+ }
137
+ else {
138
+ // User's file lacks this tag (older format, or user pruned it). Queue
139
+ // for append so the per-turn hook can find it; runtime degrades to
140
+ // generic "Refer to workflow.md" otherwise.
141
+ appendQueue.push(block);
142
+ }
143
+ }
144
+ if (appendQueue.length > 0) {
145
+ const trimmed = merged.replace(/\s+$/, "");
146
+ merged = `${trimmed}\n\n${appendQueue.join("\n\n")}\n`;
147
+ }
148
+ if (customizedOverwritten.length > 0) {
149
+ // Surface a one-line note so users who customized breadcrumb bodies know
150
+ // their edits were replaced. Same trade-off as the AGENTS.md
151
+ // TRELLIS:START/END managed-block: customizations inside the marked
152
+ // region are owned by the CLI; everything outside is preserved verbatim.
153
+ console.log(chalk.yellow(` Note: workflow.md [workflow-state:${customizedOverwritten.join(", ")}] block(s) ` +
154
+ `were updated to the latest CLI version. If you had customized breadcrumb wording, ` +
155
+ `re-apply your edits to the new bodies (content outside [workflow-state:*] tags is preserved).`));
156
+ }
157
+ return merged;
158
+ }
159
+ function buildAgentsMdTemplate(cwd) {
160
+ const fullPath = path.join(cwd, FILE_NAMES.AGENTS);
161
+ if (!fs.existsSync(fullPath)) {
162
+ return agentsMdContent;
163
+ }
164
+ const existingContent = fs.readFileSync(fullPath, "utf-8");
165
+ // Existing file already has TRELLIS:START/END markers — replace just the
166
+ // managed block, preserving everything outside it.
167
+ const replaced = replaceTrellisManagedBlock(existingContent, agentsMdContent);
168
+ if (replaced !== null) {
169
+ return replaced;
170
+ }
171
+ // Existing file has no managed-block markers (pre-0.5.0-beta.18 project, or
172
+ // user hand-wrote AGENTS.md without ever running through Trellis). Append
173
+ // the template's managed block at the end so user content is preserved
174
+ // instead of clobbered.
175
+ const templateBlock = getTrellisManagedBlock(agentsMdContent);
176
+ if (!templateBlock) {
177
+ return agentsMdContent;
178
+ }
179
+ const trimmed = existingContent.replace(/\s+$/, "");
180
+ return `${trimmed}\n\n${templateBlock}\n`;
181
+ }
182
+ function isKnownUntrackedTemplate(relativePath, existingContent) {
183
+ if (relativePath !== FILE_NAMES.AGENTS) {
184
+ return false;
185
+ }
186
+ const managedBlock = getTrellisManagedBlock(existingContent);
187
+ if (!managedBlock) {
188
+ return false;
189
+ }
190
+ return LEGACY_UNTRACKED_AGENTS_MD_BLOCK_HASHES.has(computeHash(managedBlock));
191
+ }
25
192
  /**
26
193
  * Check if a path is blocked by PROTECTED_PATHS
27
194
  */
@@ -227,6 +394,29 @@ function needsCodexUpgrade(cwd) {
227
394
  const hashes = loadHashes(cwd);
228
395
  return Object.keys(hashes).some((key) => key.startsWith(".agents/skills/"));
229
396
  }
397
+ function preserveExistingClaudeStatusLine(cwd, templates) {
398
+ const newSettingsContent = templates.get(CLAUDE_SETTINGS_PATH);
399
+ if (!newSettingsContent)
400
+ return;
401
+ const settingsPath = path.join(cwd, CLAUDE_SETTINGS_PATH);
402
+ if (!fs.existsSync(settingsPath))
403
+ return;
404
+ try {
405
+ const existingSettings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
406
+ if (!Object.prototype.hasOwnProperty.call(existingSettings, "statusLine")) {
407
+ return;
408
+ }
409
+ const newSettings = JSON.parse(newSettingsContent);
410
+ if (Object.prototype.hasOwnProperty.call(newSettings, "statusLine")) {
411
+ return;
412
+ }
413
+ newSettings.statusLine = existingSettings.statusLine;
414
+ templates.set(CLAUDE_SETTINGS_PATH, `${JSON.stringify(newSettings, null, 2)}\n`);
415
+ }
416
+ catch {
417
+ // Invalid local JSON is handled by the normal conflict path.
418
+ }
419
+ }
230
420
  function collectTemplateFiles(cwd, extraPlatforms,
231
421
  /**
232
422
  * Bypass `update.skip` when collecting templates. Enable this for breaking
@@ -255,11 +445,20 @@ bypassUpdateSkip = false) {
255
445
  // just user-facing documentation — `## Phase Index`, `## Phase 1/2/3` headings,
256
446
  // and `[workflow-state:STATUS]` tag blocks are parsed by get_context.py /
257
447
  // shared hooks, so scripts break silently when workflow.md drifts from the
258
- // CLI version. Users who customized their copy land in the normal
259
- // "Modified by you" confirm prompt at write time (not force-overwritten).
260
- files.set(`${DIR_NAMES.WORKFLOW}/workflow.md`, workflowMdTemplate);
448
+ // CLI version.
449
+ //
450
+ // Starting v0.5.0-rc.0, the breadcrumb tag blocks
451
+ // `[workflow-state:STATUS]...[/workflow-state:STATUS]` are managed via
452
+ // per-block replacement (similar to AGENTS.md's TRELLIS:START/END managed
453
+ // block) so user customizations to the rest of the file (Phase Index,
454
+ // Skill Routing tables, narrative customizations) are preserved while the
455
+ // runtime-critical breadcrumb bodies still pick up CLI updates. Outside
456
+ // the tag blocks the file falls through to the normal "Modified by you"
457
+ // confirm prompt at write time when the user has edited it.
458
+ files.set(`${DIR_NAMES.WORKFLOW}/workflow.md`, buildWorkflowMdTemplate(cwd));
261
459
  // workspace/index.md stays excluded — it's runtime-appended by add_session.py
262
460
  // (journal index) and has no script-parsed structure.
461
+ files.set(FILE_NAMES.AGENTS, buildAgentsMdTemplate(cwd));
263
462
  // Platform-specific templates (only for configured platforms)
264
463
  for (const platformId of platforms) {
265
464
  const platformFiles = collectPlatformTemplates(platformId);
@@ -269,6 +468,7 @@ bypassUpdateSkip = false) {
269
468
  }
270
469
  }
271
470
  }
471
+ preserveExistingClaudeStatusLine(cwd, files);
272
472
  // Apply update.skip from config.yaml (unless bypassed for breaking release)
273
473
  if (!bypassUpdateSkip) {
274
474
  const skipPaths = loadUpdateSkipPaths(cwd);
@@ -331,9 +531,11 @@ function analyzeChanges(cwd, hashes, templates) {
331
531
  // Content differs - check if user modified or template updated
332
532
  const storedHash = hashes[relativePath];
333
533
  const currentHash = computeHash(existingContent);
334
- if (storedHash && storedHash === currentHash) {
335
- // Hash matches stored hash - user didn't modify, template was updated
336
- // Safe to auto-update
534
+ if ((storedHash && storedHash === currentHash) ||
535
+ (!storedHash &&
536
+ isKnownUntrackedTemplate(relativePath, existingContent))) {
537
+ // Either the tracked hash matches, or this is a known pristine template
538
+ // from before the path was hash-tracked. Safe to auto-update.
337
539
  change.status = "changed";
338
540
  result.autoUpdateFiles.push(change);
339
541
  }
@@ -348,6 +550,15 @@ function analyzeChanges(cwd, hashes, templates) {
348
550
  }
349
551
  return result;
350
552
  }
553
+ function collectMissingAgentsMdHash(changes, hashes) {
554
+ const files = new Map();
555
+ for (const file of changes.unchangedFiles) {
556
+ if (file.relativePath === FILE_NAMES.AGENTS && !hashes[file.relativePath]) {
557
+ files.set(file.relativePath, file.newContent);
558
+ }
559
+ }
560
+ return files;
561
+ }
351
562
  /**
352
563
  * Print change summary
353
564
  */
@@ -481,11 +692,14 @@ function backupFile(cwd, backupDir, relativePath) {
481
692
  * Directories to backup as complete snapshot (derived from platform registry)
482
693
  */
483
694
  const BACKUP_DIRS = ALL_MANAGED_DIRS;
695
+ /** Root-level managed files to include in update backups. */
696
+ const BACKUP_FILES = [FILE_NAMES.AGENTS];
484
697
  /**
485
698
  * Patterns to exclude from backup (user data that shouldn't be backed up)
486
699
  */
487
700
  const BACKUP_EXCLUDE_PATTERNS = [
488
701
  ".backup-", // Previous backups
702
+ "/node_modules", // Installed dependencies; restore via package manager
489
703
  "/workspace/", // Developer workspace (user data)
490
704
  "/tasks/", // Task data (user data)
491
705
  "/spec/", // Spec files (user-customized content)
@@ -506,8 +720,16 @@ const BACKUP_EXCLUDE_PATTERNS = [
506
720
  * @internal Exported for testing only
507
721
  */
508
722
  export function shouldExcludeFromBackup(relativePath) {
723
+ // Normalize Windows backslashes to forward slashes so patterns like
724
+ // "/worktrees/" / "/tasks/" match regardless of host OS. Without this,
725
+ // Windows `path.relative` returns `.claude\worktrees\...` and none of
726
+ // the slash-prefixed exclude patterns trigger — which causes
727
+ // `collectAllFiles` to descend into platform worktrees (full nested
728
+ // project copies) and explode the scan. Same normalization pattern
729
+ // used by `isManagedPath` in configurators/index.ts.
730
+ const normalized = relativePath.replace(/\\/g, "/");
509
731
  for (const pattern of BACKUP_EXCLUDE_PATTERNS) {
510
- if (relativePath.includes(pattern)) {
732
+ if (normalized.includes(pattern)) {
511
733
  return true;
512
734
  }
513
735
  }
@@ -525,7 +747,7 @@ function createFullBackup(cwd) {
525
747
  const dirPath = path.join(cwd, dir);
526
748
  if (!fs.existsSync(dirPath))
527
749
  continue;
528
- const files = collectAllFiles(dirPath);
750
+ const files = collectAllFiles(dirPath, cwd);
529
751
  for (const fullPath of files) {
530
752
  const relativePath = path.relative(cwd, fullPath);
531
753
  // Skip excluded paths
@@ -539,6 +761,18 @@ function createFullBackup(cwd) {
539
761
  backupFile(cwd, backupDir, relativePath);
540
762
  }
541
763
  }
764
+ for (const relativePath of BACKUP_FILES) {
765
+ const fullPath = path.join(cwd, relativePath);
766
+ if (!fs.existsSync(fullPath))
767
+ continue;
768
+ if (shouldExcludeFromBackup(relativePath))
769
+ continue;
770
+ if (!hasFiles) {
771
+ fs.mkdirSync(backupDir, { recursive: true });
772
+ hasFiles = true;
773
+ }
774
+ backupFile(cwd, backupDir, relativePath);
775
+ }
542
776
  return hasFiles ? backupDir : null;
543
777
  }
544
778
  /**
@@ -577,18 +811,32 @@ async function getLatestNpmVersion() {
577
811
  /**
578
812
  * Recursively collect all files in a directory
579
813
  */
580
- function collectAllFiles(dirPath) {
814
+ function collectAllFiles(dirPath, cwd = process.cwd()) {
581
815
  if (!fs.existsSync(dirPath))
582
816
  return [];
583
817
  const files = [];
584
- const entries = fs.readdirSync(dirPath, { withFileTypes: true });
585
- for (const entry of entries) {
586
- const fullPath = path.join(dirPath, entry.name);
587
- if (entry.isDirectory()) {
588
- files.push(...collectAllFiles(fullPath));
589
- }
590
- else if (entry.isFile()) {
591
- files.push(fullPath);
818
+ const stack = [dirPath];
819
+ while (stack.length > 0) {
820
+ const currentDir = stack.pop();
821
+ if (!currentDir)
822
+ continue;
823
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
824
+ for (const entry of entries) {
825
+ const fullPath = path.join(currentDir, entry.name);
826
+ const relativePath = path.relative(cwd, fullPath);
827
+ // Never follow symlinks / Windows directory junctions — a junction
828
+ // pointing at an ancestor would loop the scan forever. Node's
829
+ // `isSymbolicLink()` returns true for NTFS junctions since v12.
830
+ if (entry.isSymbolicLink())
831
+ continue;
832
+ if (entry.isDirectory()) {
833
+ if (!shouldExcludeFromBackup(relativePath)) {
834
+ stack.push(fullPath);
835
+ }
836
+ }
837
+ else if (entry.isFile()) {
838
+ files.push(fullPath);
839
+ }
592
840
  }
593
841
  }
594
842
  return files;
@@ -603,11 +851,13 @@ function isDirectorySafeToReplace(cwd, dirRelativePath, hashes, templates) {
603
851
  const dirFullPath = path.join(cwd, dirRelativePath);
604
852
  if (!fs.existsSync(dirFullPath))
605
853
  return true;
606
- const files = collectAllFiles(dirFullPath);
854
+ const files = collectAllFiles(dirFullPath, cwd);
607
855
  if (files.length === 0)
608
856
  return true; // Empty directory is safe
609
857
  for (const fullPath of files) {
610
- const relativePath = path.relative(cwd, fullPath);
858
+ // POSIX-normalize: hashes/templates keys are persisted as POSIX, but
859
+ // `path.relative` returns OS-native separators (backslash on Windows).
860
+ const relativePath = toPosix(path.relative(cwd, fullPath));
611
861
  const storedHash = hashes[relativePath];
612
862
  const templateContent = templates.get(relativePath);
613
863
  // Check if file matches template content (handles untracked files)
@@ -1256,6 +1506,7 @@ export async function update(options) {
1256
1506
  }
1257
1507
  // Analyze changes (pass hashes for modification detection)
1258
1508
  const changes = analyzeChanges(cwd, hashes, templates);
1509
+ const missingAgentsMdHash = collectMissingAgentsMdHash(changes, hashes);
1259
1510
  // Print summary
1260
1511
  printChangeSummary(changes);
1261
1512
  // First-time hash tracking hint
@@ -1278,6 +1529,9 @@ export async function update(options) {
1278
1529
  changes.changedFiles.length === 0 &&
1279
1530
  !hasPendingMigrations &&
1280
1531
  !hasSafeDeletes) {
1532
+ if (!options.dryRun && missingAgentsMdHash.size > 0) {
1533
+ updateHashes(cwd, missingAgentsMdHash);
1534
+ }
1281
1535
  if (isSameVersion) {
1282
1536
  console.log(chalk.green("✓ Already up to date!"));
1283
1537
  }
@@ -1338,18 +1592,21 @@ export async function update(options) {
1338
1592
  console.log(chalk.gray("[Dry run] No changes made."));
1339
1593
  return;
1340
1594
  }
1341
- // Confirm
1342
- const { proceed } = await inquirer.prompt([
1343
- {
1344
- type: "confirm",
1345
- name: "proceed",
1346
- message: "Proceed?",
1347
- default: true,
1348
- },
1349
- ]);
1350
- if (!proceed) {
1351
- console.log(chalk.yellow("Update cancelled."));
1352
- return;
1595
+ // Batch-resolution flags are explicit consent for non-interactive runs.
1596
+ // Prompting here breaks CI and `node ... update --force --migrate` smoke tests.
1597
+ if (!options.force && !options.skipAll && !options.createNew) {
1598
+ const { proceed } = await inquirer.prompt([
1599
+ {
1600
+ type: "confirm",
1601
+ name: "proceed",
1602
+ message: "Proceed?",
1603
+ default: true,
1604
+ },
1605
+ ]);
1606
+ if (!proceed) {
1607
+ console.log(chalk.yellow("Update cancelled."));
1608
+ return;
1609
+ }
1353
1610
  }
1354
1611
  // Create complete backup of all managed platform/workflow directories
1355
1612
  const backupDir = createFullBackup(cwd);
@@ -1466,7 +1723,7 @@ export async function update(options) {
1466
1723
  // Update version file
1467
1724
  updateVersionFile(cwd);
1468
1725
  // Update template hashes for new, auto-updated, and overwritten files
1469
- const filesToHash = new Map();
1726
+ const filesToHash = new Map(missingAgentsMdHash);
1470
1727
  for (const file of changes.newFiles) {
1471
1728
  filesToHash.set(file.relativePath, file.newContent);
1472
1729
  }
@@ -1547,37 +1804,21 @@ export async function update(options) {
1547
1804
  currentDeveloper = nameMatch[1];
1548
1805
  }
1549
1806
  }
1550
- // Build task.json
1807
+ // Build task.json — canonical 24-field shape via shared factory.
1551
1808
  const taskTitle = `Migrate to v${cliVersion}`;
1552
1809
  const todayStr = today.toISOString().split("T")[0];
1553
- const taskJson = {
1810
+ const taskJson = emptyTaskJson({
1811
+ id: taskSlug,
1812
+ name: taskSlug,
1554
1813
  title: taskTitle,
1555
1814
  description: `Breaking change migration from v${projectVersion} to v${cliVersion}`,
1556
1815
  status: "planning",
1557
- dev_type: null,
1558
1816
  scope: "migration",
1559
1817
  priority: "P1",
1560
1818
  creator: "trellis-update",
1561
1819
  assignee: currentDeveloper,
1562
1820
  createdAt: todayStr,
1563
- completedAt: null,
1564
- branch: null,
1565
- base_branch: null,
1566
- worktree_path: null,
1567
- current_phase: 0,
1568
- next_action: [
1569
- { phase: 1, action: "review-guide" },
1570
- { phase: 2, action: "update-files" },
1571
- { phase: 3, action: "run-migrate" },
1572
- { phase: 4, action: "test" },
1573
- ],
1574
- commit: null,
1575
- pr_url: null,
1576
- subtasks: [],
1577
- children: [],
1578
- parent: null,
1579
- meta: {},
1580
- };
1821
+ });
1581
1822
  // Write task.json
1582
1823
  const taskJsonPath = path.join(taskDir, "task.json");
1583
1824
  fs.writeFileSync(taskJsonPath, JSON.stringify(taskJson, null, 2));