@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
@@ -7,87 +7,183 @@ import figlet from "figlet";
7
7
  import inquirer from "inquirer";
8
8
  import { createWorkflowStructure } from "../configurators/workflow.js";
9
9
  import { getInitToolChoices, resolveCliFlag, configurePlatform, getConfiguredPlatforms, getPlatformsWithPythonHooks, } from "../configurators/index.js";
10
+ import { getPythonCommandForPlatform } from "../configurators/shared.js";
10
11
  import { AI_TOOLS } from "../types/ai-tools.js";
11
12
  import { DIR_NAMES, FILE_NAMES, PATHS } from "../constants/paths.js";
12
13
  import { VERSION } from "../constants/version.js";
13
14
  import { agentsMdContent } from "../templates/markdown/index.js";
14
15
  import { setWriteMode, writeFile, } from "../utils/file-writer.js";
16
+ import { emptyTaskJson } from "../utils/task-json.js";
15
17
  import { detectProjectType, detectMonorepo, sanitizePkgName, } from "../utils/project-detector.js";
16
18
  import { initializeHashes } from "../utils/template-hash.js";
17
19
  import { fetchTemplateIndex, probeRegistryIndex, downloadTemplateById, downloadRegistryDirect, parseRegistrySource, TIMEOUTS, TEMPLATE_INDEX_URL, } from "../utils/template-fetcher.js";
18
20
  import { setupProxy, maskProxyUrl } from "../utils/proxy.js";
19
- /**
20
- * Detect available Python command (python3 or python) and verify version >= 3.10
21
- */
22
- function getPythonCommand() {
23
- const MIN_MAJOR = 3;
24
- const MIN_MINOR = 10;
25
- function checkVersion(cmd) {
26
- try {
27
- const output = execSync(`${cmd} --version`, { stdio: "pipe" })
28
- .toString()
29
- .trim();
30
- const match = output.match(/Python (\d+)\.(\d+)/);
31
- if (!match)
32
- return false;
33
- const [, major, minor] = match.map(Number);
34
- return major > MIN_MAJOR || (major === MIN_MAJOR && minor >= MIN_MINOR);
35
- }
36
- catch {
37
- return false;
38
- }
39
- }
40
- if (checkVersion("python3"))
41
- return "python3";
42
- if (checkVersion("python"))
43
- return "python";
44
- // Check if Python exists but is too old
21
+ const MIN_PYTHON_MAJOR = 3;
22
+ const MIN_PYTHON_MINOR = 9;
23
+ const PYTHON_VERSION_RE = /Python (\d+)\.(\d+)/;
24
+ export function isSupportedPythonVersion(versionOutput) {
25
+ const match = versionOutput.match(PYTHON_VERSION_RE);
26
+ if (!match)
27
+ return false;
28
+ const major = Number(match[1]);
29
+ const minor = Number(match[2]);
30
+ return (major > MIN_PYTHON_MAJOR ||
31
+ (major === MIN_PYTHON_MAJOR && minor >= MIN_PYTHON_MINOR));
32
+ }
33
+ function detectPythonVersion(command) {
45
34
  try {
46
- const output = execSync("python3 --version", { stdio: "pipe" })
47
- .toString()
48
- .trim();
49
- console.warn(chalk.yellow(`⚠️ ${output} detected, but Trellis requires Python ≥ 3.10`));
35
+ return execSync(`${command} --version`, {
36
+ encoding: "utf-8",
37
+ stdio: "pipe",
38
+ }).trim();
50
39
  }
51
40
  catch {
52
- try {
53
- const output = execSync("python --version", { stdio: "pipe" })
54
- .toString()
55
- .trim();
56
- console.warn(chalk.yellow(`⚠️ ${output} detected, but Trellis requires Python ≥ 3.10`));
57
- }
58
- catch {
59
- // No Python at all
60
- }
41
+ return null;
42
+ }
43
+ }
44
+ export function requireSupportedPython(command) {
45
+ const versionOutput = detectPythonVersion(command);
46
+ if (!versionOutput) {
47
+ throw new Error(`Python command "${command}" not found. Trellis init requires Python ≥ 3.9.`);
61
48
  }
62
- return "python3";
49
+ if (!isSupportedPythonVersion(versionOutput)) {
50
+ throw new Error(`${versionOutput} detected via "${command}", but Trellis init requires Python ≥ 3.9.`);
51
+ }
52
+ return versionOutput;
53
+ }
54
+ function getOsDisplayName(platform = process.platform) {
55
+ switch (platform) {
56
+ case "win32":
57
+ return "Windows";
58
+ case "darwin":
59
+ return "macOS";
60
+ case "linux":
61
+ return "Linux";
62
+ default:
63
+ return platform;
64
+ }
65
+ }
66
+ function logPythonAdaptationNotice(command) {
67
+ const osName = getOsDisplayName();
68
+ console.log(chalk.blue(`📌 ${osName} detected: Trellis rendered Python commands as "${command}" in generated hooks, settings, and help text`));
63
69
  }
64
70
  // =============================================================================
65
71
  // Bootstrap Task Creation
66
72
  // =============================================================================
67
73
  const BOOTSTRAP_TASK_NAME = "00-bootstrap-guidelines";
68
- function getBootstrapPrdContent(projectType, packages) {
69
- const header = `# Bootstrap: Fill Project Development Guidelines
74
+ /**
75
+ * Slugify a developer name for safe use in task directory names.
76
+ *
77
+ * Unlike `sanitizePkgName` (which only strips npm @scope/ prefixes), this
78
+ * handles arbitrary developer input: spaces, Unicode letters, punctuation,
79
+ * path separators. Returns "user" fallback when input slugifies to empty.
80
+ *
81
+ * Exported for unit testing; not part of the public API.
82
+ */
83
+ export function slugifyDeveloperName(name) {
84
+ const slug = name
85
+ .toLowerCase()
86
+ .normalize("NFKD")
87
+ .replace(/[^\p{Letter}\p{Number}]+/gu, "-")
88
+ .replace(/^-+|-+$/g, "");
89
+ return slug || "user";
90
+ }
91
+ /**
92
+ * Write a task skeleton (task.json + prd.md).
93
+ *
94
+ * Idempotent: if the task dir already exists, returns true without touching
95
+ * anything. Shared by both creator bootstrap and joiner onboarding flows.
96
+ */
97
+ function writeTaskSkeleton(cwd, taskName, taskJson, prdContent) {
98
+ const taskDir = path.join(cwd, PATHS.TASKS, taskName);
99
+ if (fs.existsSync(taskDir))
100
+ return true; // idempotent
101
+ try {
102
+ fs.mkdirSync(taskDir, { recursive: true });
103
+ fs.writeFileSync(path.join(taskDir, FILE_NAMES.TASK_JSON), JSON.stringify(taskJson, null, 2), "utf-8");
104
+ fs.writeFileSync(path.join(taskDir, FILE_NAMES.PRD), prdContent, "utf-8");
105
+ return true;
106
+ }
107
+ catch {
108
+ return false;
109
+ }
110
+ }
111
+ /**
112
+ * Compute the bootstrap checklist items (previously stored as structured
113
+ * `subtasks: [{name, status}]` in task.json). Per task 04-21-task-schema-unify
114
+ * (D1), these live as markdown `- [ ]` items in prd.md instead, so task.json
115
+ * stays canonical with `subtasks: string[]` (child task dir names, same as
116
+ * task_store.py).
117
+ */
118
+ function getBootstrapChecklistItems(projectType, packages) {
119
+ if (packages && packages.length > 0) {
120
+ const items = packages.map((pkg) => `Fill guidelines for ${pkg.name}`);
121
+ items.push("Add code examples");
122
+ return items;
123
+ }
124
+ if (projectType === "frontend") {
125
+ return ["Fill frontend guidelines", "Add code examples"];
126
+ }
127
+ if (projectType === "backend") {
128
+ return ["Fill backend guidelines", "Add code examples"];
129
+ }
130
+ return [
131
+ "Fill backend guidelines",
132
+ "Fill frontend guidelines",
133
+ "Add code examples",
134
+ ];
135
+ }
136
+ function getBootstrapRelatedFiles(projectType, packages) {
137
+ if (packages && packages.length > 0) {
138
+ return packages.map((pkg) => `.trellis/spec/${sanitizePkgName(pkg.name)}/`);
139
+ }
140
+ if (projectType === "frontend") {
141
+ return [".trellis/spec/frontend/"];
142
+ }
143
+ if (projectType === "backend") {
144
+ return [".trellis/spec/backend/"];
145
+ }
146
+ return [".trellis/spec/backend/", ".trellis/spec/frontend/"];
147
+ }
148
+ function getBootstrapPrdContent(projectType, pythonCmd, packages) {
149
+ const checklistItems = getBootstrapChecklistItems(projectType, packages);
150
+ const checklistMarkdown = checklistItems
151
+ .map((item) => `- [ ] ${item}`)
152
+ .join("\n");
153
+ const header = `# Bootstrap Task: Fill Project Development Guidelines
70
154
 
71
- ## Purpose
155
+ **You (the AI) are running this task. The developer does not read this file.**
72
156
 
73
- Welcome to Trellis! This is your first task.
157
+ The developer just ran \`trellis init\` on this project for the first time.
158
+ \`.trellis/\` now exists with empty spec scaffolding, and this bootstrap task
159
+ exists under \`.trellis/tasks/\`. When they want to work on it, they should start
160
+ this task from a session that provides Trellis session identity.
74
161
 
75
- AI agents use \`.trellis/spec/\` to understand YOUR project's coding conventions.
76
- **Starting from scratch = AI writes generic code that doesn't match your project style.**
162
+ **Your job**: help them populate \`.trellis/spec/\` with the team's real
163
+ coding conventions. Every future AI session this project's
164
+ \`trellis-implement\` and \`trellis-check\` sub-agents — auto-loads spec files
165
+ listed in per-task jsonl manifests. Empty spec = sub-agents write generic
166
+ code. Real spec = sub-agents match the team's actual patterns.
77
167
 
78
- Filling these guidelines is a one-time setup that pays off for every future AI session.
168
+ Don't dump instructions. Open with a short greeting, figure out if the repo
169
+ has any existing convention docs (CLAUDE.md, .cursorrules, etc.), and drive
170
+ the rest conversationally.
79
171
 
80
172
  ---
81
173
 
82
- ## Your Task
174
+ ## Status (update the checkboxes as you complete each item)
83
175
 
84
- Fill in the guideline files based on your **existing codebase**.
176
+ ${checklistMarkdown}
177
+
178
+ ---
179
+
180
+ ## Spec files to populate
85
181
  `;
86
182
  const backendSection = `
87
183
 
88
- ### Backend Guidelines
184
+ ### Backend guidelines
89
185
 
90
- | File | What to Document |
186
+ | File | What to document |
91
187
  |------|------------------|
92
188
  | \`.trellis/spec/backend/directory-structure.md\` | Where different file types go (routes, services, utils) |
93
189
  | \`.trellis/spec/backend/database-guidelines.md\` | ORM, migrations, query patterns, naming conventions |
@@ -97,9 +193,9 @@ Fill in the guideline files based on your **existing codebase**.
97
193
  `;
98
194
  const frontendSection = `
99
195
 
100
- ### Frontend Guidelines
196
+ ### Frontend guidelines
101
197
 
102
- | File | What to Document |
198
+ | File | What to document |
103
199
  |------|------------------|
104
200
  | \`.trellis/spec/frontend/directory-structure.md\` | Component/page/hook organization |
105
201
  | \`.trellis/spec/frontend/component-guidelines.md\` | Component patterns, props conventions |
@@ -110,18 +206,20 @@ Fill in the guideline files based on your **existing codebase**.
110
206
  `;
111
207
  const footer = `
112
208
 
113
- ### Thinking Guides (Optional)
209
+ ### Thinking guides (already populated)
114
210
 
115
- The \`.trellis/spec/guides/\` directory contains thinking guides that are already
116
- filled with general best practices. You can customize them for your project if needed.
211
+ \`.trellis/spec/guides/\` contains general thinking guides pre-filled with
212
+ best practices. Customize only if something clearly doesn't fit this project.
117
213
 
118
214
  ---
119
215
 
120
- ## How to Fill Guidelines
216
+ ## How to fill the spec
121
217
 
122
- ### Step 0: Import from Existing Specs (Recommended)
218
+ ### Step 1: Import from existing convention files first (preferred)
123
219
 
124
- Many projects already have coding conventions documented. **Check these first** before writing from scratch:
220
+ Search the repo for existing convention docs. If any exist, read them and
221
+ extract the relevant rules into the matching \`.trellis/spec/\` files —
222
+ usually much faster than documenting from scratch.
125
223
 
126
224
  | File / Directory | Tool |
127
225
  |------|------|
@@ -138,50 +236,60 @@ Many projects already have coding conventions documented. **Check these first**
138
236
  | \`CONTRIBUTING.md\` | General project conventions |
139
237
  | \`.editorconfig\` | Editor formatting rules |
140
238
 
141
- If any of these exist, read them first and extract the relevant coding conventions into the corresponding \`.trellis/spec/\` files. This saves significant effort compared to writing everything from scratch.
239
+ ### Step 2: Analyze the codebase for anything not covered by existing docs
142
240
 
143
- ### Step 1: Analyze the Codebase
241
+ Scan real code to discover patterns. Before writing each spec file:
242
+ - Find 2-3 real examples of each pattern in the codebase.
243
+ - Reference real file paths (not hypothetical ones).
244
+ - Document anti-patterns the team clearly avoids.
144
245
 
145
- Ask AI to help discover patterns from actual code:
246
+ ### Step 3: Document reality, not ideals
146
247
 
147
- - "Read all existing config files (CLAUDE.md, .cursorrules, etc.) and extract coding conventions into .trellis/spec/"
148
- - "Analyze my codebase and document the patterns you see"
149
- - "Find error handling / component / API patterns and document them"
248
+ **Critical**: write what the code *actually does*, not what it should do.
249
+ Sub-agents match the spec, so aspirational patterns that don't exist in the
250
+ codebase will cause sub-agents to write code that looks out of place.
150
251
 
151
- ### Step 2: Document Reality, Not Ideals
252
+ If the team has known tech debt, document the current state — improvement
253
+ is a separate conversation, not a bootstrap concern.
152
254
 
153
- Write what your codebase **actually does**, not what you wish it did.
154
- AI needs to match existing patterns, not introduce new ones.
255
+ ---
155
256
 
156
- - **Look at existing code** - Find 2-3 examples of each pattern
157
- - **Include file paths** - Reference real files as examples
158
- - **List anti-patterns** - What does your team avoid?
257
+ ## Quick explainer of the runtime (share when they ask "why do we need spec at all")
159
258
 
160
- ---
259
+ - Every AI coding task spawns two sub-agents: \`trellis-implement\` (writes
260
+ code) and \`trellis-check\` (verifies quality).
261
+ - Each task has \`implement.jsonl\` / \`check.jsonl\` manifests listing which
262
+ spec files to load.
263
+ - The platform hook auto-injects those spec files + the task's \`prd.md\`
264
+ into every sub-agent prompt, so the sub-agent codes/reviews per team
265
+ conventions without anyone pasting them manually.
266
+ - Source of truth: \`.trellis/spec/\`. That's why filling it well now pays
267
+ off forever.
161
268
 
162
- ## Completion Checklist
269
+ ---
163
270
 
164
- - [ ] Guidelines filled for your project type
165
- - [ ] At least 2-3 real code examples in each guideline
166
- - [ ] Anti-patterns documented
271
+ ## Completion
167
272
 
168
- When done:
273
+ When the developer confirms the checklist items above are done with real
274
+ examples (not placeholders), guide them to run:
169
275
 
170
276
  \`\`\`bash
171
- python3 ./.trellis/scripts/task.py finish
172
- python3 ./.trellis/scripts/task.py archive 00-bootstrap-guidelines
277
+ ${pythonCmd} ./.trellis/scripts/task.py finish
278
+ ${pythonCmd} ./.trellis/scripts/task.py archive 00-bootstrap-guidelines
173
279
  \`\`\`
174
280
 
175
- ---
281
+ After archive, every new developer who joins this project will get a
282
+ \`00-join-<slug>\` onboarding task instead of this bootstrap task.
176
283
 
177
- ## Why This Matters
284
+ ---
178
285
 
179
- After completing this task:
286
+ ## Suggested opening line
180
287
 
181
- 1. AI will write code that matches your project style
182
- 2. Relevant \`/trellis:before-*-dev\` commands will inject real context
183
- 3. \`/trellis:check-*\` commands will validate against your actual standards
184
- 4. Future developers (human or AI) will onboard faster
288
+ "Welcome to Trellis! Your init just set me up to help you fill the project
289
+ spec a one-time setup so every future AI session follows the team's
290
+ conventions instead of writing generic code. Before we start, do you have
291
+ any existing convention docs (CLAUDE.md, .cursorrules, CONTRIBUTING.md,
292
+ etc.) I can pull from, or should I scan the codebase from scratch?"
185
293
  `;
186
294
  let content = header;
187
295
  if (packages && packages.length > 0) {
@@ -214,43 +322,15 @@ After completing this task:
214
322
  }
215
323
  function getBootstrapTaskJson(developer, projectType, packages) {
216
324
  const today = new Date().toISOString().split("T")[0];
217
- let subtasks;
218
- let relatedFiles;
219
- if (packages && packages.length > 0) {
220
- // Monorepo: subtask per package
221
- subtasks = packages.map((pkg) => ({
222
- name: `Fill guidelines for ${pkg.name}`,
223
- status: "pending",
224
- }));
225
- subtasks.push({ name: "Add code examples", status: "pending" });
226
- relatedFiles = packages.map((pkg) => `.trellis/spec/${sanitizePkgName(pkg.name)}/`);
227
- }
228
- else if (projectType === "frontend") {
229
- subtasks = [
230
- { name: "Fill frontend guidelines", status: "pending" },
231
- { name: "Add code examples", status: "pending" },
232
- ];
233
- relatedFiles = [".trellis/spec/frontend/"];
234
- }
235
- else if (projectType === "backend") {
236
- subtasks = [
237
- { name: "Fill backend guidelines", status: "pending" },
238
- { name: "Add code examples", status: "pending" },
239
- ];
240
- relatedFiles = [".trellis/spec/backend/"];
241
- }
242
- else {
243
- // fullstack
244
- subtasks = [
245
- { name: "Fill backend guidelines", status: "pending" },
246
- { name: "Fill frontend guidelines", status: "pending" },
247
- { name: "Add code examples", status: "pending" },
248
- ];
249
- relatedFiles = [".trellis/spec/backend/", ".trellis/spec/frontend/"];
250
- }
251
- return {
325
+ const relatedFiles = getBootstrapRelatedFiles(projectType, packages);
326
+ // Canonical 24-field shape via emptyTaskJson factory.
327
+ // Checklist items (previously stored as structured `subtasks`) are now
328
+ // rendered as `- [ ]` items in prd.md; task.json.subtasks is always
329
+ // string[] (child task dir names) per the canonical schema.
330
+ return emptyTaskJson({
252
331
  id: BOOTSTRAP_TASK_NAME,
253
- name: "Bootstrap Guidelines",
332
+ name: BOOTSTRAP_TASK_NAME,
333
+ title: "Bootstrap Guidelines",
254
334
  description: "Fill in project development guidelines for AI agents",
255
335
  status: "in_progress",
256
336
  dev_type: "docs",
@@ -258,49 +338,171 @@ function getBootstrapTaskJson(developer, projectType, packages) {
258
338
  creator: developer,
259
339
  assignee: developer,
260
340
  createdAt: today,
261
- completedAt: null,
262
- commit: null,
263
- subtasks,
264
- children: [],
265
- parent: null,
266
341
  relatedFiles,
267
342
  notes: `First-time setup task created by trellis init (${projectType} project)`,
268
- meta: {},
269
- };
343
+ });
270
344
  }
271
345
  /**
272
346
  * Create bootstrap task for first-time setup
273
347
  */
274
- function createBootstrapTask(cwd, developer, projectType, packages) {
275
- const taskDir = path.join(cwd, PATHS.TASKS, BOOTSTRAP_TASK_NAME);
276
- const taskRelativePath = `${PATHS.TASKS}/${BOOTSTRAP_TASK_NAME}`;
277
- // Check if already exists
278
- if (fs.existsSync(taskDir)) {
279
- return true; // Already exists, not an error
280
- }
281
- try {
282
- // Create task directory
283
- fs.mkdirSync(taskDir, { recursive: true });
284
- // Write task.json
285
- const taskJson = getBootstrapTaskJson(developer, projectType, packages);
286
- fs.writeFileSync(path.join(taskDir, FILE_NAMES.TASK_JSON), JSON.stringify(taskJson, null, 2), "utf-8");
287
- // Write prd.md
288
- const prdContent = getBootstrapPrdContent(projectType, packages);
289
- fs.writeFileSync(path.join(taskDir, FILE_NAMES.PRD), prdContent, "utf-8");
290
- // Set as current task
291
- const currentTaskFile = path.join(cwd, PATHS.CURRENT_TASK_FILE);
292
- fs.writeFileSync(currentTaskFile, taskRelativePath, "utf-8");
293
- return true;
294
- }
295
- catch {
296
- return false;
297
- }
348
+ function createBootstrapTask(cwd, developer, pythonCmd, projectType, packages) {
349
+ const taskJson = getBootstrapTaskJson(developer, projectType, packages);
350
+ const prdContent = getBootstrapPrdContent(projectType, pythonCmd, packages);
351
+ return writeTaskSkeleton(cwd, BOOTSTRAP_TASK_NAME, taskJson, prdContent);
352
+ }
353
+ // =============================================================================
354
+ // Joiner Onboarding Task Creation
355
+ // =============================================================================
356
+ /**
357
+ * task.json factory for joiner onboarding. Mirrors the bootstrap factory but
358
+ * uses dev_type "docs", higher priority "P1", and the developer-specific task
359
+ * name (so multiple joiners in the same checkout don't collide).
360
+ */
361
+ function getJoinerTaskJson(developer, taskName) {
362
+ const today = new Date().toISOString().split("T")[0];
363
+ return emptyTaskJson({
364
+ id: taskName,
365
+ name: taskName,
366
+ title: `Joining: Onboard to this Trellis project (${developer})`,
367
+ description: "Onboard a new developer to an existing Trellis project: learn the workflow, conventions, and find assigned work",
368
+ status: "in_progress",
369
+ dev_type: "docs",
370
+ priority: "P1",
371
+ creator: developer,
372
+ assignee: developer,
373
+ createdAt: today,
374
+ notes: "Generated by trellis init for a new developer joining an existing Trellis project",
375
+ });
376
+ }
377
+ /**
378
+ * PRD content for joiner onboarding. Kept concise (~80 lines) — deeper
379
+ * guidance lives in skills and docs.
380
+ */
381
+ function getJoinerPrdContent(developer, pythonCmd) {
382
+ const slug = slugifyDeveloperName(developer);
383
+ return `# Joiner Onboarding Task
384
+
385
+ **You (the AI) are running this task. The developer does not read this file.**
386
+
387
+ \`${developer}\` just ran \`trellis init\` on a fresh clone, saw "Developer
388
+ initialized", and will now start asking you questions in chat. This joiner task
389
+ exists under \`.trellis/tasks/\`; when they want to work on it, they should
390
+ start it from a session that provides Trellis session identity.
391
+
392
+ Your job is to orient them to Trellis. Don't dump all of this at them — open
393
+ with a short greeting, ask where they want to start, and fill in the rest as
394
+ they engage.
395
+
396
+ ---
397
+
398
+ ## Topics to cover (adapt order to their questions)
399
+
400
+ ### 1. What Trellis is + the workflow
401
+
402
+ Trellis is a workflow layer over Claude Code / Cursor / etc. that keeps AI
403
+ agents consistent with project-specific conventions instead of writing generic
404
+ code every session.
405
+
406
+ - **Three phases**: Plan (brainstorm → \`prd.md\`) → Execute (code + check) →
407
+ Finish (capture + wrap). Full reference: \`.trellis/workflow.md\`.
408
+ - **Task lifecycle**: planning → in_progress → done → archive, under
409
+ \`.trellis/tasks/\`.
410
+ - **Core slash commands**:
411
+ - \`/trellis:continue\` — resume the current session's active task
412
+ - \`/trellis:finish-work\` — wrap up a finished task
413
+ - \`/trellis:start\` — session boot from scratch (not needed here; the
414
+ SessionStart hook does its job automatically)
415
+
416
+ ### 2. Runtime mechanics (explain when they ask "how does it know what to do")
417
+
418
+ - **SessionStart hook** runs \`get_context.py\` and injects identity, git
419
+ status, session active task, active tasks, and workflow phase into the AI
420
+ conversation at every session start.
421
+ - **\`<workflow-state>\` tag** is auto-injected with every user message,
422
+ carrying the current task + phase hint.
423
+ - **\`/trellis:continue\`** loads the Phase Index, reads \`prd.md\` + recent
424
+ activity, and routes to the right skill (\`trellis-brainstorm\` for planning,
425
+ \`trellis-implement\` for coding, \`trellis-check\` for verification).
426
+ - **\`trellis-implement\` sub-agent** is spawned when code needs to be written.
427
+ The platform hook reads \`{TASK_DIR}/implement.jsonl\` and auto-injects those
428
+ spec files + \`prd.md\` into the sub-agent's prompt so it codes per project
429
+ conventions.
430
+ - **\`trellis-check\` sub-agent** follows the same pattern with \`check.jsonl\`
431
+ — reviews changes against specs, auto-fixes issues, runs lint/typecheck.
432
+
433
+ File layout (mention when they ask "where does what live"):
434
+ - \`.trellis/.runtime/sessions/<session>.json\` — session active-task state, gitignored
435
+ - \`.trellis/tasks/<task>/{implement,check}.jsonl\` — per-task context manifests
436
+ - \`.trellis/spec/\` — project-wide conventions (source of truth)
437
+ - \`.trellis/workspace/${developer}/journal-*.md\` — their session log,
438
+ rotated at ~2000 lines
439
+
440
+ ### 3. This project's actual conventions
441
+
442
+ - Summarize \`.trellis/spec/\` for them — what coding conventions this
443
+ specific team enforces.
444
+ - Point at the last 5 entries in \`.trellis/tasks/archive/\` as a rhythm
445
+ example of how people actually work here. **If archive is empty** (the
446
+ project just started), skip this — don't invent examples.
447
+ - Not your job in this onboarding to teach them the business code itself —
448
+ the README and their teammates handle that.
449
+
450
+ ### 4. Their assigned work
451
+
452
+ - Check if \`.trellis/workspace/${developer}/\` already exists — if yes, it's
453
+ their journal from another machine and worth mentioning.
454
+ - Run \`${pythonCmd} ./.trellis/scripts/task.py list --assignee ${developer}\` to
455
+ show tasks assigned to them. (Quote the name if it contains spaces.)
456
+ - Remind them that the "My Tasks" section appears in the SessionStart context
457
+ on every new session.
458
+
459
+ ---
460
+
461
+ ## Optional: walk through a small task end-to-end
462
+
463
+ If they want to practice before touching real work, offer to pick a tiny
464
+ P3 task or a typo fix and run the full cycle together: \`/trellis:continue\`
465
+ → you implement via sub-agents → \`/trellis:finish-work\`.
466
+
467
+ ---
468
+
469
+ ## Completion
470
+
471
+ When they feel oriented (or after you've covered the four topics with
472
+ reasonable back-and-forth), guide them to run:
473
+
474
+ \`\`\`bash
475
+ ${pythonCmd} ./.trellis/scripts/task.py finish
476
+ ${pythonCmd} ./.trellis/scripts/task.py archive 00-join-${slug}
477
+ \`\`\`
478
+
479
+ ---
480
+
481
+ ## Suggested opening line
482
+
483
+ "Welcome! Your \`trellis init\` set me up to onboard you to this project. I
484
+ can walk you through the workflow, show you the runtime mechanics under the
485
+ hood, summarize the team's spec, or jump to what you're already curious about
486
+ — which would you prefer?"
487
+ `;
488
+ }
489
+ /**
490
+ * Create joiner onboarding task for a new developer on an existing Trellis
491
+ * project. Task name is slugified to be filesystem-safe for arbitrary
492
+ * developer names (spaces, Unicode, punctuation).
493
+ */
494
+ function createJoinerOnboardingTask(cwd, developer, pythonCmd) {
495
+ const slug = slugifyDeveloperName(developer);
496
+ const taskName = `00-join-${slug}`;
497
+ const taskJson = getJoinerTaskJson(developer, taskName);
498
+ const prdContent = getJoinerPrdContent(developer, pythonCmd);
499
+ return writeTaskSkeleton(cwd, taskName, taskJson, prdContent);
298
500
  }
299
501
  /**
300
502
  * Handle re-init when .trellis/ already exists.
301
503
  * Returns true if handled (caller should return), false if user chose full re-init.
302
504
  */
303
- async function handleReinit(cwd, options, developerName) {
505
+ async function handleReinit(cwd, options, developerName, pythonCmd) {
304
506
  const TOOLS = getInitToolChoices();
305
507
  const configuredPlatforms = getConfiguredPlatforms(cwd);
306
508
  const configuredNames = [...configuredPlatforms]
@@ -396,8 +598,11 @@ async function handleReinit(cwd, options, developerName) {
396
598
  devName = await askInput("Your name: ");
397
599
  }
398
600
  }
601
+ // Capture pre-init state: if .developer did not exist before we ran
602
+ // init_developer.py, this checkout had no identity → treat as a new
603
+ // joiner onboarding onto an existing Trellis project.
604
+ const hadDeveloperFileBefore = fs.existsSync(path.join(cwd, DIR_NAMES.WORKFLOW, FILE_NAMES.DEVELOPER));
399
605
  try {
400
- const pythonCmd = getPythonCommand();
401
606
  const scriptPath = path.join(cwd, PATHS.SCRIPTS, "init_developer.py");
402
607
  execSync(`${pythonCmd} "${scriptPath}" "${devName}"`, {
403
608
  cwd,
@@ -407,7 +612,19 @@ async function handleReinit(cwd, options, developerName) {
407
612
  }
408
613
  catch {
409
614
  console.log(chalk.yellow("⚠ Could not initialize developer. Run manually:"));
410
- console.log(chalk.gray(` python3 .trellis/scripts/init_developer.py ${devName}`));
615
+ console.log(chalk.gray(` ${pythonCmd} .trellis/scripts/init_developer.py ${devName}`));
616
+ }
617
+ // Create joiner onboarding task for fresh checkouts (no prior .developer).
618
+ // Runs outside the init_developer try/catch so failures surface as warnings.
619
+ if (!hadDeveloperFileBefore) {
620
+ try {
621
+ if (!createJoinerOnboardingTask(cwd, devName, pythonCmd)) {
622
+ console.warn(chalk.yellow("⚠ Failed to create joiner onboarding task"));
623
+ }
624
+ }
625
+ catch (err) {
626
+ console.warn(chalk.yellow(`⚠ Joiner onboarding setup failed: ${err instanceof Error ? err.message : String(err)}`));
627
+ }
411
628
  }
412
629
  }
413
630
  return true;
@@ -438,6 +655,9 @@ function writeMonorepoConfig(cwd, packages) {
438
655
  if (pkg.isSubmodule) {
439
656
  lines.push(" type: submodule");
440
657
  }
658
+ else if (pkg.isGitRepo) {
659
+ lines.push(" git: true");
660
+ }
441
661
  }
442
662
  // Use first non-submodule package as default, fallback to first package
443
663
  const defaultPkg = packages.find((p) => !p.isSubmodule)?.name ?? packages[0]?.name;
@@ -449,6 +669,10 @@ function writeMonorepoConfig(cwd, packages) {
449
669
  export async function init(options) {
450
670
  const cwd = process.cwd();
451
671
  const isFirstInit = !fs.existsSync(path.join(cwd, DIR_NAMES.WORKFLOW));
672
+ // Captured here (before createWorkflowStructure + init_developer run) so
673
+ // the three-branch dispatch at the bottom can tell "fresh clone joiner"
674
+ // (.trellis/ exists, .developer missing) apart from "creator first init".
675
+ const hadDeveloperFileAtStart = fs.existsSync(path.join(cwd, DIR_NAMES.WORKFLOW, FILE_NAMES.DEVELOPER));
452
676
  // Generate ASCII art banner dynamically using FIGlet "Rebel" font
453
677
  const banner = figlet.textSync("Trellis", { font: "Rebel" });
454
678
  console.log(chalk.cyan(`\n${banner.trimEnd()}`));
@@ -468,6 +692,12 @@ export async function init(options) {
468
692
  writeMode = "skip";
469
693
  console.log(chalk.gray("Mode: Skip existing files\n"));
470
694
  }
695
+ else if (options.yes) {
696
+ // -y implies non-interactive: never prompt on conflicts. Default to skip
697
+ // (preserve user files) — explicit --force is required to overwrite.
698
+ writeMode = "skip";
699
+ console.log(chalk.gray("Mode: Non-interactive (skip existing files)\n"));
700
+ }
471
701
  setWriteMode(writeMode);
472
702
  // Detect developer name from git config or options
473
703
  let developerName = options.user;
@@ -489,11 +719,22 @@ export async function init(options) {
489
719
  if (developerName) {
490
720
  console.log(chalk.blue("👤 Developer:"), chalk.gray(developerName));
491
721
  }
722
+ const pythonCmd = getPythonCommandForPlatform();
723
+ requireSupportedPython(pythonCmd);
492
724
  // ==========================================================================
493
725
  // Re-init fast path: skip full flow when .trellis/ already exists
494
726
  // ==========================================================================
495
- if (!isFirstInit && !options.force && !options.skipExisting) {
496
- const reinitDone = await handleReinit(cwd, options, developerName);
727
+ // Aborted-init recovery (issue #204): if .trellis/ exists but tasks/ is
728
+ // empty, the previous init never reached bootstrap creation. Fall through
729
+ // to the full flow so the main-dispatch tasksEmpty fallback fires —
730
+ // handleReinit's joiner branch would otherwise mis-route the recovery.
731
+ const tasksDirEarly = path.join(cwd, PATHS.TASKS);
732
+ const tasksEmptyEarly = !fs.existsSync(tasksDirEarly) || fs.readdirSync(tasksDirEarly).length === 0;
733
+ if (!isFirstInit &&
734
+ !options.force &&
735
+ !options.skipExisting &&
736
+ !tasksEmptyEarly) {
737
+ const reinitDone = await handleReinit(cwd, options, developerName, pythonCmd);
497
738
  if (reinitDone)
498
739
  return;
499
740
  // reinitDone === false means user chose "full re-initialize" → fall through
@@ -540,7 +781,26 @@ export async function init(options) {
540
781
  // options.monorepo: true = --monorepo, false = --no-monorepo, undefined = auto
541
782
  const detected = detectMonorepo(cwd);
542
783
  if (options.monorepo === true && !detected) {
543
- console.log(chalk.red("Error: --monorepo specified but no monorepo configuration found."));
784
+ console.log(chalk.red("Error: --monorepo specified but no multi-package layout detected."));
785
+ console.log("");
786
+ console.log(chalk.gray("Checked:"));
787
+ console.log(chalk.gray(" ✗ pnpm-workspace.yaml"));
788
+ console.log(chalk.gray(" ✗ package.json workspaces"));
789
+ console.log(chalk.gray(" ✗ Cargo.toml [workspace]"));
790
+ console.log(chalk.gray(" ✗ go.work"));
791
+ console.log(chalk.gray(" ✗ pyproject.toml [tool.uv.workspace]"));
792
+ console.log(chalk.gray(" ✗ .gitmodules"));
793
+ console.log(chalk.gray(" ✗ sibling .git directories (need ≥ 2)"));
794
+ console.log("");
795
+ console.log("To configure manually, add to .trellis/config.yaml:");
796
+ console.log("");
797
+ console.log(chalk.cyan(" packages:"));
798
+ console.log(chalk.cyan(" frontend:"));
799
+ console.log(chalk.cyan(" path: ./frontend"));
800
+ console.log(chalk.cyan(" git: true # if it has its own .git"));
801
+ console.log(chalk.cyan(" backend:"));
802
+ console.log(chalk.cyan(" path: ./backend"));
803
+ console.log(chalk.cyan(" git: true"));
544
804
  return;
545
805
  }
546
806
  if (detected && detected.length > 0) {
@@ -552,11 +812,15 @@ export async function init(options) {
552
812
  // Show detected packages and ask
553
813
  console.log(chalk.blue("\n🔍 Detected monorepo packages:"));
554
814
  for (const pkg of detected) {
555
- const sub = pkg.isSubmodule ? chalk.gray(" (submodule)") : "";
815
+ const tag = pkg.isSubmodule
816
+ ? chalk.gray(" (submodule)")
817
+ : pkg.isGitRepo
818
+ ? chalk.gray(" (git repo)")
819
+ : "";
556
820
  console.log(chalk.gray(` - ${pkg.name}`) +
557
821
  chalk.gray(` (${pkg.path})`) +
558
822
  chalk.gray(` [${pkg.type}]`) +
559
- sub);
823
+ tag);
560
824
  }
561
825
  console.log("");
562
826
  const { useMonorepo } = await inquirer.prompt([
@@ -678,6 +942,7 @@ export async function init(options) {
678
942
  let selectedTemplate = null;
679
943
  // Pre-fetched templates list (used to pass selected SpecTemplate to downloadTemplateById)
680
944
  let fetchedTemplates = [];
945
+ let registryBackend;
681
946
  // Determine the index URL based on registry
682
947
  const indexUrl = registry
683
948
  ? `${registry.rawBaseUrl}/index.json`
@@ -702,10 +967,13 @@ export async function init(options) {
702
967
  process.stdout.write(chalk.gray(` Loading... 0s/${timeoutSec}s`));
703
968
  let templates;
704
969
  let registryProbeNotFound = false;
970
+ let registryProbeError;
705
971
  if (registry) {
706
- const probeResult = await probeRegistryIndex(indexUrl);
972
+ const probeResult = await probeRegistryIndex(indexUrl, registry);
707
973
  templates = probeResult.templates;
708
974
  registryProbeNotFound = probeResult.isNotFound;
975
+ registryProbeError = probeResult.error;
976
+ registryBackend = probeResult.backend;
709
977
  }
710
978
  else {
711
979
  templates = await fetchTemplateIndex(indexUrl);
@@ -720,7 +988,7 @@ export async function init(options) {
720
988
  }
721
989
  else if (templates.length === 0 && registry) {
722
990
  // Custom registry: transient error (not a 404) — abort, don't misclassify
723
- console.log(chalk.red(" Could not reach registry (network issue). Check your connection and try again."));
991
+ console.log(chalk.red(` ${registryProbeError?.message ?? "Could not reach registry. Check your connection and try again."}`));
724
992
  return;
725
993
  }
726
994
  else if (templates.length === 0) {
@@ -772,8 +1040,9 @@ export async function init(options) {
772
1040
  // Probe index.json to detect marketplace vs direct download
773
1041
  const customIndexUrl = `${registry.rawBaseUrl}/index.json`;
774
1042
  console.log(chalk.gray(` Checking for templates at ${registry.gigetSource}...`));
775
- const customProbe = await probeRegistryIndex(customIndexUrl);
1043
+ const customProbe = await probeRegistryIndex(customIndexUrl, registry);
776
1044
  const customTemplates = customProbe.templates;
1045
+ registryBackend = customProbe.backend;
777
1046
  if (customTemplates.length > 0) {
778
1047
  // Marketplace mode: show picker with custom templates
779
1048
  fetchedTemplates = customTemplates;
@@ -828,7 +1097,7 @@ export async function init(options) {
828
1097
  }
829
1098
  else {
830
1099
  // Transient error (not 404) — loop back, don't misclassify
831
- console.log(chalk.yellow(" Could not reach registry (network issue). Try again or enter a different source."));
1100
+ console.log(chalk.yellow(` ${customProbe.error?.message ?? "Could not reach registry. Try again or enter a different source."}`));
832
1101
  registry = undefined; // Reset so we don't fall through to direct download
833
1102
  }
834
1103
  }
@@ -874,7 +1143,8 @@ export async function init(options) {
874
1143
  // -y mode with --registry (no --template): probe index.json to detect mode
875
1144
  // Skip when monorepo mode already handled templates above
876
1145
  if (options.yes && registry && !selectedTemplate && !monorepoPackages) {
877
- const probeResult = await probeRegistryIndex(`${registry.rawBaseUrl}/index.json`);
1146
+ const probeResult = await probeRegistryIndex(`${registry.rawBaseUrl}/index.json`, registry);
1147
+ registryBackend = probeResult.backend;
878
1148
  if (probeResult.templates.length > 0) {
879
1149
  // Marketplace mode requires interactive selection — can't auto-select
880
1150
  console.log(chalk.red("Error: Registry is a marketplace with multiple templates. " +
@@ -883,7 +1153,7 @@ export async function init(options) {
883
1153
  }
884
1154
  if (!probeResult.isNotFound) {
885
1155
  // Transient error (not 404) — abort, don't misclassify as direct-download
886
- console.log(chalk.red("Error: Could not reach registry (network issue). Check your connection and try again."));
1156
+ console.log(chalk.red(`Error: ${probeResult.error?.message ?? "Could not reach registry. Check your connection and try again."}`));
887
1157
  return;
888
1158
  }
889
1159
  // isNotFound=true → no index.json, proceed with direct download (fetchedTemplates stays empty)
@@ -898,7 +1168,7 @@ export async function init(options) {
898
1168
  console.log(chalk.gray(" This may take a moment on slow connections."));
899
1169
  // Find pre-fetched SpecTemplate to avoid double-fetch
900
1170
  const prefetched = fetchedTemplates.find((t) => t.id === selectedTemplate);
901
- const result = await downloadTemplateById(cwd, selectedTemplate, templateStrategy, prefetched, registry);
1171
+ const result = await downloadTemplateById(cwd, selectedTemplate, templateStrategy, prefetched, registry, undefined, registryBackend);
902
1172
  if (result.success) {
903
1173
  if (result.skipped) {
904
1174
  console.log(chalk.gray(` ${result.message}`));
@@ -941,7 +1211,7 @@ export async function init(options) {
941
1211
  templateStrategy = actionAnswer.action;
942
1212
  }
943
1213
  }
944
- const result = await downloadRegistryDirect(cwd, registry, templateStrategy);
1214
+ const result = await downloadRegistryDirect(cwd, registry, templateStrategy, undefined, registryBackend);
945
1215
  if (result.success) {
946
1216
  if (result.skipped) {
947
1217
  console.log(chalk.gray(` ${result.message}`));
@@ -984,13 +1254,10 @@ export async function init(options) {
984
1254
  await configurePlatform(platformId, cwd);
985
1255
  }
986
1256
  }
987
- // Show Windows platform detection notice
988
- if (process.platform === "win32") {
989
- const pythonPlatforms = getPlatformsWithPythonHooks();
990
- const hasSelectedPythonPlatform = pythonPlatforms.some((id) => tools.includes(AI_TOOLS[id].cliFlag));
991
- if (hasSelectedPythonPlatform) {
992
- console.log(chalk.yellow('📌 Windows detected: Using "python" for hooks'));
993
- }
1257
+ const pythonPlatforms = getPlatformsWithPythonHooks();
1258
+ const hasSelectedPythonPlatform = pythonPlatforms.some((id) => tools.includes(AI_TOOLS[id].cliFlag));
1259
+ if (hasSelectedPythonPlatform) {
1260
+ logPythonAdaptationNotice(pythonCmd);
994
1261
  }
995
1262
  // Create root files (skip if exists)
996
1263
  await createRootFiles(cwd);
@@ -1002,23 +1269,46 @@ export async function init(options) {
1002
1269
  // Initialize developer identity (silent - no output)
1003
1270
  if (developerName) {
1004
1271
  try {
1005
- const pythonCmd = getPythonCommand();
1006
1272
  const scriptPath = path.join(cwd, PATHS.SCRIPTS, "init_developer.py");
1007
1273
  execSync(`${pythonCmd} "${scriptPath}" "${developerName}"`, {
1008
1274
  cwd,
1009
1275
  stdio: "pipe", // Silent
1010
1276
  });
1011
- // Create bootstrap task only on first init (not re-init for new platforms/devices)
1012
- if (isFirstInit) {
1013
- createBootstrapTask(cwd, developerName, projectType, monorepoPackages);
1014
- }
1015
1277
  }
1016
1278
  catch {
1017
1279
  // Silent failure - user can run init_developer.py manually
1018
1280
  }
1281
+ // Three-branch dispatch using flags captured at init() start (before
1282
+ // createWorkflowStructure/init_developer ran, so they reflect the disk
1283
+ // state of the user's checkout, not the state this init just produced):
1284
+ // isFirstInit=true → creator bootstrap (new project)
1285
+ // isFirstInit=false + no .developer file → joiner onboarding (fresh clone)
1286
+ // isFirstInit=false + .developer exists → same-dev re-init, no task
1287
+ //
1288
+ // Tasks-empty fallback (issue #204): if .trellis/ exists but tasks dir is
1289
+ // empty, the previous init aborted before creating the bootstrap task. Run
1290
+ // bootstrap creation regardless of isFirstInit. writeTaskSkeleton is
1291
+ // idempotent so repeated triggers are safe.
1292
+ //
1293
+ // Runs OUTSIDE the init_developer try/catch (which uses stdio: "pipe")
1294
+ // so joiner failures surface as warnings instead of being silently
1295
+ // swallowed.
1296
+ const tasksDir = path.join(cwd, PATHS.TASKS);
1297
+ const tasksEmpty = !fs.existsSync(tasksDir) || fs.readdirSync(tasksDir).length === 0;
1298
+ if (isFirstInit || tasksEmpty) {
1299
+ createBootstrapTask(cwd, developerName, pythonCmd, projectType, monorepoPackages);
1300
+ }
1301
+ else if (!hadDeveloperFileAtStart) {
1302
+ try {
1303
+ if (!createJoinerOnboardingTask(cwd, developerName, pythonCmd)) {
1304
+ console.warn(chalk.yellow("⚠ Failed to create joiner onboarding task"));
1305
+ }
1306
+ }
1307
+ catch (err) {
1308
+ console.warn(chalk.yellow(`⚠ Joiner onboarding setup failed: ${err instanceof Error ? err.message : String(err)}`));
1309
+ }
1310
+ }
1019
1311
  }
1020
- // Print "What We Solve" section
1021
- printWhatWeSolve();
1022
1312
  }
1023
1313
  /**
1024
1314
  * Simple readline-based input (no flickering like inquirer)
@@ -1036,37 +1326,11 @@ function askInput(prompt) {
1036
1326
  });
1037
1327
  }
1038
1328
  async function createRootFiles(cwd) {
1039
- const agentsPath = path.join(cwd, "AGENTS.md");
1329
+ const agentsPath = path.join(cwd, FILE_NAMES.AGENTS);
1040
1330
  // Write AGENTS.md from template
1041
1331
  const agentsWritten = await writeFile(agentsPath, agentsMdContent);
1042
1332
  if (agentsWritten) {
1043
1333
  console.log(chalk.blue("📄 Created AGENTS.md"));
1044
1334
  }
1045
1335
  }
1046
- /**
1047
- * Print "What We Solve" section showing Trellis value proposition
1048
- * Styled like a meme/rant to resonate with developer pain points
1049
- */
1050
- function printWhatWeSolve() {
1051
- console.log(chalk.gray("\nSound familiar? ") +
1052
- chalk.bold("You'll never say these again!!\n"));
1053
- // Pain point 1: Bug loop → Thinking Guides + Check Loop
1054
- console.log(chalk.gray("✗ ") + '"Fix A → break B → fix B → break A..."');
1055
- console.log(chalk.green(" ✓ ") +
1056
- chalk.white("Thinking Guides + Check Loop: Think first, verify after"));
1057
- // Pain point 2: Instructions ignored/forgotten → Sub-agents + per-agent spec injection
1058
- console.log(chalk.gray("✗ ") +
1059
- '"Wrote CLAUDE.md, AI ignored it. Reminded AI, it forgot 5 turns later."');
1060
- console.log(chalk.green(" ✓ ") +
1061
- chalk.white("Spec Injection: Rules enforced per task, not per chat"));
1062
- // Pain point 3: Missing connections → Cross-Layer Guide
1063
- console.log(chalk.gray("✗ ") + '"Code works but nothing connects..."');
1064
- console.log(chalk.green(" ✓ ") +
1065
- chalk.white("Cross-Layer Guide: Map data flow before coding"));
1066
- // Pain point 4: Code explosion → Plan Agent
1067
- console.log(chalk.gray("✗ ") + '"Asked for a button, got 9000 lines"');
1068
- console.log(chalk.green(" ✓ ") +
1069
- chalk.white("Plan Agent: Rejects and splits oversized tasks"));
1070
- console.log("");
1071
- }
1072
1336
  //# sourceMappingURL=init.js.map