@kynetic-ai/spec 0.1.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 (278) hide show
  1. package/README.md +263 -0
  2. package/dist/acp/client.d.ts +159 -0
  3. package/dist/acp/client.d.ts.map +1 -0
  4. package/dist/acp/client.js +255 -0
  5. package/dist/acp/client.js.map +1 -0
  6. package/dist/acp/framing.d.ts +119 -0
  7. package/dist/acp/framing.d.ts.map +1 -0
  8. package/dist/acp/framing.js +302 -0
  9. package/dist/acp/framing.js.map +1 -0
  10. package/dist/acp/index.d.ts +14 -0
  11. package/dist/acp/index.d.ts.map +1 -0
  12. package/dist/acp/index.js +13 -0
  13. package/dist/acp/index.js.map +1 -0
  14. package/dist/acp/types.d.ts +89 -0
  15. package/dist/acp/types.d.ts.map +1 -0
  16. package/dist/acp/types.js +99 -0
  17. package/dist/acp/types.js.map +1 -0
  18. package/dist/agents/adapters.d.ts +55 -0
  19. package/dist/agents/adapters.d.ts.map +1 -0
  20. package/dist/agents/adapters.js +84 -0
  21. package/dist/agents/adapters.js.map +1 -0
  22. package/dist/agents/index.d.ts +8 -0
  23. package/dist/agents/index.d.ts.map +1 -0
  24. package/dist/agents/index.js +10 -0
  25. package/dist/agents/index.js.map +1 -0
  26. package/dist/agents/spawner.d.ts +53 -0
  27. package/dist/agents/spawner.d.ts.map +1 -0
  28. package/dist/agents/spawner.js +83 -0
  29. package/dist/agents/spawner.js.map +1 -0
  30. package/dist/cli/batch.d.ts +82 -0
  31. package/dist/cli/batch.d.ts.map +1 -0
  32. package/dist/cli/batch.js +162 -0
  33. package/dist/cli/batch.js.map +1 -0
  34. package/dist/cli/commands/clone-for-testing.d.ts +6 -0
  35. package/dist/cli/commands/clone-for-testing.d.ts.map +1 -0
  36. package/dist/cli/commands/clone-for-testing.js +176 -0
  37. package/dist/cli/commands/clone-for-testing.js.map +1 -0
  38. package/dist/cli/commands/derive.d.ts +6 -0
  39. package/dist/cli/commands/derive.d.ts.map +1 -0
  40. package/dist/cli/commands/derive.js +450 -0
  41. package/dist/cli/commands/derive.js.map +1 -0
  42. package/dist/cli/commands/help.d.ts +6 -0
  43. package/dist/cli/commands/help.d.ts.map +1 -0
  44. package/dist/cli/commands/help.js +196 -0
  45. package/dist/cli/commands/help.js.map +1 -0
  46. package/dist/cli/commands/inbox.d.ts +6 -0
  47. package/dist/cli/commands/inbox.d.ts.map +1 -0
  48. package/dist/cli/commands/inbox.js +235 -0
  49. package/dist/cli/commands/inbox.js.map +1 -0
  50. package/dist/cli/commands/index.d.ts +20 -0
  51. package/dist/cli/commands/index.d.ts.map +1 -0
  52. package/dist/cli/commands/index.js +21 -0
  53. package/dist/cli/commands/index.js.map +1 -0
  54. package/dist/cli/commands/init.d.ts +6 -0
  55. package/dist/cli/commands/init.d.ts.map +1 -0
  56. package/dist/cli/commands/init.js +245 -0
  57. package/dist/cli/commands/init.js.map +1 -0
  58. package/dist/cli/commands/item.d.ts +6 -0
  59. package/dist/cli/commands/item.d.ts.map +1 -0
  60. package/dist/cli/commands/item.js +1311 -0
  61. package/dist/cli/commands/item.js.map +1 -0
  62. package/dist/cli/commands/link.d.ts +6 -0
  63. package/dist/cli/commands/link.d.ts.map +1 -0
  64. package/dist/cli/commands/link.js +288 -0
  65. package/dist/cli/commands/link.js.map +1 -0
  66. package/dist/cli/commands/log.d.ts +16 -0
  67. package/dist/cli/commands/log.d.ts.map +1 -0
  68. package/dist/cli/commands/log.js +291 -0
  69. package/dist/cli/commands/log.js.map +1 -0
  70. package/dist/cli/commands/meta.d.ts +15 -0
  71. package/dist/cli/commands/meta.d.ts.map +1 -0
  72. package/dist/cli/commands/meta.js +1378 -0
  73. package/dist/cli/commands/meta.js.map +1 -0
  74. package/dist/cli/commands/module.d.ts +6 -0
  75. package/dist/cli/commands/module.d.ts.map +1 -0
  76. package/dist/cli/commands/module.js +102 -0
  77. package/dist/cli/commands/module.js.map +1 -0
  78. package/dist/cli/commands/ralph.d.ts +9 -0
  79. package/dist/cli/commands/ralph.d.ts.map +1 -0
  80. package/dist/cli/commands/ralph.js +465 -0
  81. package/dist/cli/commands/ralph.js.map +1 -0
  82. package/dist/cli/commands/search.d.ts +6 -0
  83. package/dist/cli/commands/search.d.ts.map +1 -0
  84. package/dist/cli/commands/search.js +134 -0
  85. package/dist/cli/commands/search.js.map +1 -0
  86. package/dist/cli/commands/session.d.ts +164 -0
  87. package/dist/cli/commands/session.d.ts.map +1 -0
  88. package/dist/cli/commands/session.js +745 -0
  89. package/dist/cli/commands/session.js.map +1 -0
  90. package/dist/cli/commands/setup.d.ts +26 -0
  91. package/dist/cli/commands/setup.d.ts.map +1 -0
  92. package/dist/cli/commands/setup.js +586 -0
  93. package/dist/cli/commands/setup.js.map +1 -0
  94. package/dist/cli/commands/shadow.d.ts +6 -0
  95. package/dist/cli/commands/shadow.d.ts.map +1 -0
  96. package/dist/cli/commands/shadow.js +299 -0
  97. package/dist/cli/commands/shadow.js.map +1 -0
  98. package/dist/cli/commands/task.d.ts +6 -0
  99. package/dist/cli/commands/task.d.ts.map +1 -0
  100. package/dist/cli/commands/task.js +1514 -0
  101. package/dist/cli/commands/task.js.map +1 -0
  102. package/dist/cli/commands/tasks.d.ts +6 -0
  103. package/dist/cli/commands/tasks.d.ts.map +1 -0
  104. package/dist/cli/commands/tasks.js +347 -0
  105. package/dist/cli/commands/tasks.js.map +1 -0
  106. package/dist/cli/commands/trait.d.ts +10 -0
  107. package/dist/cli/commands/trait.d.ts.map +1 -0
  108. package/dist/cli/commands/trait.js +295 -0
  109. package/dist/cli/commands/trait.js.map +1 -0
  110. package/dist/cli/commands/validate.d.ts +6 -0
  111. package/dist/cli/commands/validate.d.ts.map +1 -0
  112. package/dist/cli/commands/validate.js +626 -0
  113. package/dist/cli/commands/validate.js.map +1 -0
  114. package/dist/cli/exit-codes.d.ts +62 -0
  115. package/dist/cli/exit-codes.d.ts.map +1 -0
  116. package/dist/cli/exit-codes.js +65 -0
  117. package/dist/cli/exit-codes.js.map +1 -0
  118. package/dist/cli/help/content.d.ts +35 -0
  119. package/dist/cli/help/content.d.ts.map +1 -0
  120. package/dist/cli/help/content.js +312 -0
  121. package/dist/cli/help/content.js.map +1 -0
  122. package/dist/cli/index.d.ts +5 -0
  123. package/dist/cli/index.d.ts.map +1 -0
  124. package/dist/cli/index.js +85 -0
  125. package/dist/cli/index.js.map +1 -0
  126. package/dist/cli/introspection.d.ts +87 -0
  127. package/dist/cli/introspection.d.ts.map +1 -0
  128. package/dist/cli/introspection.js +127 -0
  129. package/dist/cli/introspection.js.map +1 -0
  130. package/dist/cli/output.d.ts +56 -0
  131. package/dist/cli/output.d.ts.map +1 -0
  132. package/dist/cli/output.js +467 -0
  133. package/dist/cli/output.js.map +1 -0
  134. package/dist/cli/suggest.d.ts +16 -0
  135. package/dist/cli/suggest.d.ts.map +1 -0
  136. package/dist/cli/suggest.js +72 -0
  137. package/dist/cli/suggest.js.map +1 -0
  138. package/dist/index.d.ts +3 -0
  139. package/dist/index.d.ts.map +1 -0
  140. package/dist/index.js +5 -0
  141. package/dist/index.js.map +1 -0
  142. package/dist/parser/alignment.d.ts +113 -0
  143. package/dist/parser/alignment.d.ts.map +1 -0
  144. package/dist/parser/alignment.js +261 -0
  145. package/dist/parser/alignment.js.map +1 -0
  146. package/dist/parser/assess.d.ts +81 -0
  147. package/dist/parser/assess.d.ts.map +1 -0
  148. package/dist/parser/assess.js +197 -0
  149. package/dist/parser/assess.js.map +1 -0
  150. package/dist/parser/convention-validation.d.ts +48 -0
  151. package/dist/parser/convention-validation.d.ts.map +1 -0
  152. package/dist/parser/convention-validation.js +167 -0
  153. package/dist/parser/convention-validation.js.map +1 -0
  154. package/dist/parser/fix.d.ts +38 -0
  155. package/dist/parser/fix.d.ts.map +1 -0
  156. package/dist/parser/fix.js +185 -0
  157. package/dist/parser/fix.js.map +1 -0
  158. package/dist/parser/index.d.ts +12 -0
  159. package/dist/parser/index.d.ts.map +1 -0
  160. package/dist/parser/index.js +13 -0
  161. package/dist/parser/index.js.map +1 -0
  162. package/dist/parser/items.d.ts +138 -0
  163. package/dist/parser/items.d.ts.map +1 -0
  164. package/dist/parser/items.js +321 -0
  165. package/dist/parser/items.js.map +1 -0
  166. package/dist/parser/meta.d.ts +120 -0
  167. package/dist/parser/meta.d.ts.map +1 -0
  168. package/dist/parser/meta.js +441 -0
  169. package/dist/parser/meta.js.map +1 -0
  170. package/dist/parser/refs.d.ts +185 -0
  171. package/dist/parser/refs.d.ts.map +1 -0
  172. package/dist/parser/refs.js +404 -0
  173. package/dist/parser/refs.js.map +1 -0
  174. package/dist/parser/shadow.d.ts +253 -0
  175. package/dist/parser/shadow.d.ts.map +1 -0
  176. package/dist/parser/shadow.js +1053 -0
  177. package/dist/parser/shadow.js.map +1 -0
  178. package/dist/parser/traits.d.ts +72 -0
  179. package/dist/parser/traits.d.ts.map +1 -0
  180. package/dist/parser/traits.js +120 -0
  181. package/dist/parser/traits.js.map +1 -0
  182. package/dist/parser/validate.d.ts +89 -0
  183. package/dist/parser/validate.d.ts.map +1 -0
  184. package/dist/parser/validate.js +817 -0
  185. package/dist/parser/validate.js.map +1 -0
  186. package/dist/parser/yaml.d.ts +326 -0
  187. package/dist/parser/yaml.d.ts.map +1 -0
  188. package/dist/parser/yaml.js +1383 -0
  189. package/dist/parser/yaml.js.map +1 -0
  190. package/dist/ralph/cli-renderer.d.ts +20 -0
  191. package/dist/ralph/cli-renderer.d.ts.map +1 -0
  192. package/dist/ralph/cli-renderer.js +179 -0
  193. package/dist/ralph/cli-renderer.js.map +1 -0
  194. package/dist/ralph/events.d.ts +65 -0
  195. package/dist/ralph/events.d.ts.map +1 -0
  196. package/dist/ralph/events.js +397 -0
  197. package/dist/ralph/events.js.map +1 -0
  198. package/dist/ralph/index.d.ts +8 -0
  199. package/dist/ralph/index.d.ts.map +1 -0
  200. package/dist/ralph/index.js +10 -0
  201. package/dist/ralph/index.js.map +1 -0
  202. package/dist/schema/common.d.ts +46 -0
  203. package/dist/schema/common.d.ts.map +1 -0
  204. package/dist/schema/common.js +71 -0
  205. package/dist/schema/common.js.map +1 -0
  206. package/dist/schema/inbox.d.ts +90 -0
  207. package/dist/schema/inbox.d.ts.map +1 -0
  208. package/dist/schema/inbox.js +30 -0
  209. package/dist/schema/inbox.js.map +1 -0
  210. package/dist/schema/index.d.ts +6 -0
  211. package/dist/schema/index.d.ts.map +1 -0
  212. package/dist/schema/index.js +7 -0
  213. package/dist/schema/index.js.map +1 -0
  214. package/dist/schema/meta.d.ts +762 -0
  215. package/dist/schema/meta.d.ts.map +1 -0
  216. package/dist/schema/meta.js +144 -0
  217. package/dist/schema/meta.js.map +1 -0
  218. package/dist/schema/spec.d.ts +912 -0
  219. package/dist/schema/spec.d.ts.map +1 -0
  220. package/dist/schema/spec.js +104 -0
  221. package/dist/schema/spec.js.map +1 -0
  222. package/dist/schema/task.d.ts +664 -0
  223. package/dist/schema/task.d.ts.map +1 -0
  224. package/dist/schema/task.js +130 -0
  225. package/dist/schema/task.js.map +1 -0
  226. package/dist/sessions/index.d.ts +11 -0
  227. package/dist/sessions/index.d.ts.map +1 -0
  228. package/dist/sessions/index.js +13 -0
  229. package/dist/sessions/index.js.map +1 -0
  230. package/dist/sessions/store.d.ts +144 -0
  231. package/dist/sessions/store.d.ts.map +1 -0
  232. package/dist/sessions/store.js +325 -0
  233. package/dist/sessions/store.js.map +1 -0
  234. package/dist/sessions/types.d.ts +157 -0
  235. package/dist/sessions/types.d.ts.map +1 -0
  236. package/dist/sessions/types.js +90 -0
  237. package/dist/sessions/types.js.map +1 -0
  238. package/dist/strings/errors.d.ts +420 -0
  239. package/dist/strings/errors.d.ts.map +1 -0
  240. package/dist/strings/errors.js +282 -0
  241. package/dist/strings/errors.js.map +1 -0
  242. package/dist/strings/guidance.d.ts +65 -0
  243. package/dist/strings/guidance.d.ts.map +1 -0
  244. package/dist/strings/guidance.js +66 -0
  245. package/dist/strings/guidance.js.map +1 -0
  246. package/dist/strings/index.d.ts +12 -0
  247. package/dist/strings/index.d.ts.map +1 -0
  248. package/dist/strings/index.js +12 -0
  249. package/dist/strings/index.js.map +1 -0
  250. package/dist/strings/labels.d.ts +74 -0
  251. package/dist/strings/labels.d.ts.map +1 -0
  252. package/dist/strings/labels.js +75 -0
  253. package/dist/strings/labels.js.map +1 -0
  254. package/dist/strings/validation.d.ts +126 -0
  255. package/dist/strings/validation.d.ts.map +1 -0
  256. package/dist/strings/validation.js +135 -0
  257. package/dist/strings/validation.js.map +1 -0
  258. package/dist/utils/commit.d.ts +23 -0
  259. package/dist/utils/commit.d.ts.map +1 -0
  260. package/dist/utils/commit.js +67 -0
  261. package/dist/utils/commit.js.map +1 -0
  262. package/dist/utils/git.d.ts +57 -0
  263. package/dist/utils/git.d.ts.map +1 -0
  264. package/dist/utils/git.js +192 -0
  265. package/dist/utils/git.js.map +1 -0
  266. package/dist/utils/grep.d.ts +28 -0
  267. package/dist/utils/grep.d.ts.map +1 -0
  268. package/dist/utils/grep.js +86 -0
  269. package/dist/utils/grep.js.map +1 -0
  270. package/dist/utils/index.d.ts +8 -0
  271. package/dist/utils/index.d.ts.map +1 -0
  272. package/dist/utils/index.js +6 -0
  273. package/dist/utils/index.js.map +1 -0
  274. package/dist/utils/time.d.ts +18 -0
  275. package/dist/utils/time.d.ts.map +1 -0
  276. package/dist/utils/time.js +61 -0
  277. package/dist/utils/time.js.map +1 -0
  278. package/package.json +62 -0
@@ -0,0 +1,1053 @@
1
+ /**
2
+ * Shadow branch utilities for transparent spec/task state tracking.
3
+ *
4
+ * Shadow branch concept:
5
+ * - Orphan branch (kspec-meta) stores kspec state
6
+ * - .kspec/ directory is a git worktree pointing to shadow branch
7
+ * - Main branch gitignores .kspec/
8
+ * - All kspec read/write operations target .kspec/
9
+ * - Changes auto-commit to shadow branch
10
+ */
11
+ import * as fs from 'node:fs/promises';
12
+ import * as path from 'node:path';
13
+ import { execSync, exec } from 'node:child_process';
14
+ import { promisify } from 'node:util';
15
+ const execAsync = promisify(exec);
16
+ // Import getVerboseMode for checking CLI --debug-shadow flag
17
+ // We use a getter function to avoid issues with circular dependencies
18
+ let getVerboseModeFunc = null;
19
+ export function setVerboseModeGetter(getter) {
20
+ getVerboseModeFunc = getter;
21
+ }
22
+ /**
23
+ * Error types for shadow branch issues
24
+ */
25
+ export class ShadowError extends Error {
26
+ code;
27
+ suggestion;
28
+ constructor(message, code, suggestion) {
29
+ super(message);
30
+ this.code = code;
31
+ this.suggestion = suggestion;
32
+ this.name = 'ShadowError';
33
+ }
34
+ }
35
+ /**
36
+ * Default shadow branch name
37
+ */
38
+ export const SHADOW_BRANCH_NAME = 'kspec-meta';
39
+ /**
40
+ * Default shadow worktree directory
41
+ */
42
+ export const SHADOW_WORKTREE_DIR = '.kspec';
43
+ /**
44
+ * Check if debug mode is enabled.
45
+ * Debug mode can be enabled via:
46
+ * - KSPEC_DEBUG=1 environment variable
47
+ * - Verbose flag (passed from CLI --debug-shadow option)
48
+ *
49
+ * When enabled, shadow branch operations output detailed information.
50
+ */
51
+ export function isDebugMode(verboseFlag) {
52
+ if (process.env.KSPEC_DEBUG === '1') {
53
+ return true;
54
+ }
55
+ if (verboseFlag === true) {
56
+ return true;
57
+ }
58
+ // Check CLI --debug-shadow flag via getter
59
+ if (getVerboseModeFunc?.()) {
60
+ return true;
61
+ }
62
+ return false;
63
+ }
64
+ /**
65
+ * Check if we're in a git repository
66
+ */
67
+ export async function isGitRepo(dir) {
68
+ try {
69
+ execSync('git rev-parse --git-dir', {
70
+ cwd: dir,
71
+ stdio: ['pipe', 'pipe', 'pipe'],
72
+ encoding: 'utf-8',
73
+ });
74
+ return true;
75
+ }
76
+ catch {
77
+ return false;
78
+ }
79
+ }
80
+ /**
81
+ * Get the git root directory
82
+ */
83
+ export function getGitRoot(dir) {
84
+ try {
85
+ const result = execSync('git rev-parse --show-toplevel', {
86
+ cwd: dir,
87
+ stdio: ['pipe', 'pipe', 'pipe'],
88
+ encoding: 'utf-8',
89
+ }).trim();
90
+ return result;
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ }
96
+ /**
97
+ * Check if a branch exists
98
+ */
99
+ export async function branchExists(dir, branchName) {
100
+ try {
101
+ execSync(`git show-ref --verify --quiet refs/heads/${branchName}`, {
102
+ cwd: dir,
103
+ stdio: ['pipe', 'pipe', 'pipe'],
104
+ });
105
+ return true;
106
+ }
107
+ catch {
108
+ return false;
109
+ }
110
+ }
111
+ /**
112
+ * Check if a directory is a valid git worktree
113
+ */
114
+ export async function isValidWorktree(worktreeDir) {
115
+ try {
116
+ // Check if .git file exists (worktrees have a .git file, not directory)
117
+ const gitPath = path.join(worktreeDir, '.git');
118
+ const stat = await fs.stat(gitPath);
119
+ if (stat.isFile()) {
120
+ // Read the .git file to verify it points to a worktree
121
+ const content = await fs.readFile(gitPath, 'utf-8');
122
+ return content.trim().startsWith('gitdir:');
123
+ }
124
+ return false;
125
+ }
126
+ catch {
127
+ return false;
128
+ }
129
+ }
130
+ /**
131
+ * Detect if running from inside the shadow worktree directory.
132
+ * Returns the main project root if detected, null otherwise.
133
+ *
134
+ * Detection logic:
135
+ * 1. Check if .git is a file (worktrees have .git files, not directories)
136
+ * 2. Read the gitdir reference from the .git file
137
+ * 3. Check if it points to a worktree for .kspec (pattern: <project>/.git/worktrees/-kspec)
138
+ */
139
+ export async function detectRunningFromShadowWorktree(cwd) {
140
+ try {
141
+ const gitPath = path.join(cwd, '.git');
142
+ const stat = await fs.stat(gitPath);
143
+ // Worktrees have a .git file, not directory
144
+ if (!stat.isFile()) {
145
+ return null;
146
+ }
147
+ const content = await fs.readFile(gitPath, 'utf-8');
148
+ const match = content.trim().match(/^gitdir:\s*(.+)$/);
149
+ if (!match) {
150
+ return null;
151
+ }
152
+ const gitdir = match[1];
153
+ // Check if this is a shadow worktree (pattern: <project>/.git/worktrees/-kspec)
154
+ if (gitdir.includes('.git/worktrees/')) {
155
+ const worktreesMatch = gitdir.match(/^(.+)\/\.git\/worktrees\//);
156
+ if (worktreesMatch) {
157
+ const mainProjectRoot = worktreesMatch[1];
158
+ const cwdBase = path.basename(cwd);
159
+ const worktreeName = path.basename(gitdir);
160
+ if (cwdBase === SHADOW_WORKTREE_DIR || worktreeName.includes('kspec')) {
161
+ return mainProjectRoot;
162
+ }
163
+ }
164
+ }
165
+ return null;
166
+ }
167
+ catch {
168
+ return null;
169
+ }
170
+ }
171
+ /**
172
+ * Detect shadow branch configuration from a directory.
173
+ * Returns shadow config if .kspec/ exists and is valid.
174
+ */
175
+ export async function detectShadow(startDir) {
176
+ const gitRoot = getGitRoot(startDir);
177
+ if (!gitRoot) {
178
+ return null;
179
+ }
180
+ const worktreeDir = path.join(gitRoot, SHADOW_WORKTREE_DIR);
181
+ try {
182
+ await fs.access(worktreeDir);
183
+ // Verify it's a valid worktree
184
+ if (await isValidWorktree(worktreeDir)) {
185
+ return {
186
+ enabled: true,
187
+ worktreeDir,
188
+ branchName: SHADOW_BRANCH_NAME,
189
+ projectRoot: gitRoot,
190
+ };
191
+ }
192
+ // Directory exists but not a valid worktree
193
+ return null;
194
+ }
195
+ catch {
196
+ // .kspec/ doesn't exist
197
+ return null;
198
+ }
199
+ }
200
+ /**
201
+ * Get detailed shadow branch status
202
+ */
203
+ export async function getShadowStatus(projectRoot) {
204
+ const worktreeDir = path.join(projectRoot, SHADOW_WORKTREE_DIR);
205
+ const status = {
206
+ exists: false,
207
+ healthy: false,
208
+ branchExists: false,
209
+ worktreeExists: false,
210
+ worktreeLinked: false,
211
+ };
212
+ // Check if we're in a git repo
213
+ if (!(await isGitRepo(projectRoot))) {
214
+ status.error = 'Not a git repository';
215
+ return status;
216
+ }
217
+ // Check if branch exists
218
+ status.branchExists = await branchExists(projectRoot, SHADOW_BRANCH_NAME);
219
+ // Check if worktree directory exists
220
+ try {
221
+ await fs.access(worktreeDir);
222
+ status.worktreeExists = true;
223
+ }
224
+ catch {
225
+ status.worktreeExists = false;
226
+ }
227
+ // Check if worktree is properly linked
228
+ if (status.worktreeExists) {
229
+ status.worktreeLinked = await isValidWorktree(worktreeDir);
230
+ }
231
+ // Determine overall status
232
+ status.exists = status.branchExists || status.worktreeExists;
233
+ status.healthy = status.branchExists && status.worktreeExists && status.worktreeLinked;
234
+ if (!status.healthy && status.exists) {
235
+ if (!status.branchExists) {
236
+ status.error = 'Shadow branch missing but worktree exists';
237
+ }
238
+ else if (!status.worktreeExists) {
239
+ status.error = 'Shadow branch exists but worktree missing';
240
+ }
241
+ else if (!status.worktreeLinked) {
242
+ status.error = 'Worktree exists but not properly linked';
243
+ }
244
+ }
245
+ return status;
246
+ }
247
+ /**
248
+ * Create an appropriate ShadowError based on status
249
+ */
250
+ export function createShadowError(status) {
251
+ if (!status.branchExists && !status.worktreeExists) {
252
+ return new ShadowError('Shadow branch not initialized', 'NOT_INITIALIZED', 'Run `kspec init` to create shadow branch and worktree.');
253
+ }
254
+ if (status.branchExists && !status.worktreeExists) {
255
+ return new ShadowError('.kspec/ directory missing', 'DIRECTORY_MISSING', 'Run `kspec shadow repair` to recreate the worktree.');
256
+ }
257
+ if (status.worktreeExists && !status.worktreeLinked) {
258
+ return new ShadowError('Worktree disconnected from git', 'WORKTREE_DISCONNECTED', 'Run `kspec shadow repair` to fix the worktree link.');
259
+ }
260
+ return new ShadowError(status.error || 'Unknown shadow branch error', 'GIT_ERROR', 'Check git status and try `kspec shadow repair`.');
261
+ }
262
+ /**
263
+ * Auto-commit changes to shadow branch.
264
+ * Called after write operations when shadow is enabled.
265
+ *
266
+ * @param worktreeDir Path to .kspec/ directory
267
+ * @param message Commit message
268
+ * @param verbose Enable debug output (defaults to KSPEC_DEBUG env var)
269
+ * @returns true if commit succeeded, false if nothing to commit
270
+ */
271
+ export async function shadowAutoCommit(worktreeDir, message, verbose) {
272
+ const debug = isDebugMode(verbose);
273
+ try {
274
+ if (debug) {
275
+ console.error(`[DEBUG] Shadow auto-commit: git add -A (cwd: ${worktreeDir})`);
276
+ }
277
+ // Stage all changes
278
+ execSync('git add -A', {
279
+ cwd: worktreeDir,
280
+ stdio: ['pipe', 'pipe', 'pipe'],
281
+ });
282
+ // Check if there are staged changes
283
+ try {
284
+ if (debug) {
285
+ console.error(`[DEBUG] Shadow auto-commit: git diff --cached --quiet`);
286
+ }
287
+ execSync('git diff --cached --quiet', {
288
+ cwd: worktreeDir,
289
+ stdio: ['pipe', 'pipe', 'pipe'],
290
+ });
291
+ // No error = no changes
292
+ if (debug) {
293
+ console.error(`[DEBUG] Shadow auto-commit: No changes to commit`);
294
+ }
295
+ return false;
296
+ }
297
+ catch {
298
+ // Error = there are changes, proceed with commit
299
+ }
300
+ if (debug) {
301
+ console.error(`[DEBUG] Shadow auto-commit: git commit -m "${message}"`);
302
+ }
303
+ // Commit with message
304
+ // Set KSPEC_SHADOW_COMMIT=1 to signal authorized commit to git hooks
305
+ execSync(`git commit -m "${message.replace(/"/g, '\\"')}"`, {
306
+ cwd: worktreeDir,
307
+ stdio: ['pipe', 'pipe', 'pipe'],
308
+ env: { ...process.env, KSPEC_SHADOW_COMMIT: '1' },
309
+ });
310
+ if (debug) {
311
+ console.error(`[DEBUG] Shadow auto-commit: Success`);
312
+ }
313
+ return true;
314
+ }
315
+ catch (error) {
316
+ // AC: Only log error if debug mode enabled
317
+ if (debug) {
318
+ console.error('Shadow auto-commit failed:', error);
319
+ }
320
+ return false;
321
+ }
322
+ }
323
+ /**
324
+ * Generate commit message for a kspec operation.
325
+ */
326
+ export function generateCommitMessage(operation, ref, detail) {
327
+ const parts = [];
328
+ switch (operation) {
329
+ case 'task-start':
330
+ parts.push(`Start @${ref}`);
331
+ break;
332
+ case 'task-complete':
333
+ parts.push(`Complete @${ref}`);
334
+ if (detail)
335
+ parts.push(`: ${detail}`);
336
+ break;
337
+ case 'task-note':
338
+ parts.push(`Note on @${ref}`);
339
+ break;
340
+ case 'task-add':
341
+ parts.push(`Add task: ${detail || ref}`);
342
+ break;
343
+ case 'inbox-add':
344
+ parts.push(`Inbox: ${detail?.slice(0, 50)}${(detail?.length || 0) > 50 ? '...' : ''}`);
345
+ break;
346
+ case 'inbox-promote':
347
+ parts.push(`Promote to @${ref}`);
348
+ break;
349
+ case 'item-add':
350
+ parts.push(`Add @${ref}`);
351
+ break;
352
+ case 'item-set':
353
+ parts.push(`Update @${ref}`);
354
+ break;
355
+ case 'item-delete':
356
+ parts.push(`Delete @${ref}`);
357
+ break;
358
+ case 'derive':
359
+ parts.push(`Derive from @${ref}`);
360
+ break;
361
+ default:
362
+ parts.push(operation);
363
+ if (ref)
364
+ parts.push(` @${ref}`);
365
+ }
366
+ return parts.join('');
367
+ }
368
+ /**
369
+ * Resolve a path relative to shadow worktree if enabled.
370
+ * Falls back to original path if shadow is not enabled.
371
+ */
372
+ export function resolveShadowPath(originalPath, shadowConfig, projectRoot) {
373
+ if (!shadowConfig?.enabled) {
374
+ return originalPath;
375
+ }
376
+ // If the path is within the project root, rewrite to shadow worktree
377
+ const relativePath = path.relative(projectRoot, originalPath);
378
+ // Skip if path is outside project or already in .kspec
379
+ if (relativePath.startsWith('..') || relativePath.startsWith(SHADOW_WORKTREE_DIR)) {
380
+ return originalPath;
381
+ }
382
+ // Handle spec/ -> .kspec/ mapping
383
+ if (relativePath.startsWith('spec/') || relativePath.startsWith('spec\\')) {
384
+ const specRelative = relativePath.slice(5); // Remove 'spec/'
385
+ return path.join(shadowConfig.worktreeDir, specRelative);
386
+ }
387
+ // For task/inbox files at root, move to .kspec
388
+ if (relativePath.endsWith('.tasks.yaml') || relativePath.endsWith('.inbox.yaml')) {
389
+ return path.join(shadowConfig.worktreeDir, relativePath);
390
+ }
391
+ return originalPath;
392
+ }
393
+ /**
394
+ * Commit changes to shadow branch if enabled.
395
+ * This is the primary interface for CLI commands to trigger auto-commit.
396
+ *
397
+ * @param shadowConfig Shadow configuration (from KspecContext.shadow)
398
+ * @param operation Operation type (e.g., 'task-start', 'task-complete')
399
+ * @param ref Reference slug or ULID (optional)
400
+ * @param detail Additional detail for commit message (optional)
401
+ * @param verbose Enable debug output (defaults to KSPEC_DEBUG env var)
402
+ * @returns true if committed, false if shadow not enabled or nothing to commit
403
+ */
404
+ export async function commitIfShadow(shadowConfig, operation, ref, detail, verbose) {
405
+ if (!shadowConfig?.enabled) {
406
+ return false;
407
+ }
408
+ const message = generateCommitMessage(operation, ref, detail);
409
+ const committed = await shadowAutoCommit(shadowConfig.worktreeDir, message, verbose);
410
+ // AC-1: Fire-and-forget push after each commit
411
+ if (committed) {
412
+ shadowPushAsync(shadowConfig.worktreeDir, verbose);
413
+ }
414
+ return committed;
415
+ }
416
+ /**
417
+ * Check if shadow is required but not available, and throw appropriate error.
418
+ * Use this at the start of commands that require shadow mode.
419
+ *
420
+ * @param shadowConfig Shadow configuration from context
421
+ * @param projectRoot Project root for status check
422
+ * @throws ShadowError if shadow is not properly configured
423
+ */
424
+ export async function requireShadow(shadowConfig, projectRoot) {
425
+ if (shadowConfig?.enabled) {
426
+ return; // Shadow is available
427
+ }
428
+ const status = await getShadowStatus(projectRoot);
429
+ throw createShadowError(status);
430
+ }
431
+ /**
432
+ * Format a ShadowError for display in CLI.
433
+ * Returns a user-friendly message with suggestion.
434
+ */
435
+ export function formatShadowError(error) {
436
+ return `${error.message}\n\nSuggestion: ${error.suggestion}`;
437
+ }
438
+ /**
439
+ * Check if a remote exists (default: origin)
440
+ */
441
+ export async function hasRemote(projectRoot, remoteName = 'origin') {
442
+ try {
443
+ const { stdout } = await execAsync(`git remote get-url ${remoteName}`, {
444
+ cwd: projectRoot,
445
+ });
446
+ return stdout.trim().length > 0;
447
+ }
448
+ catch {
449
+ return false;
450
+ }
451
+ }
452
+ /**
453
+ * Check if a branch exists on a remote
454
+ */
455
+ export async function remoteBranchExists(projectRoot, branchName, remoteName = 'origin') {
456
+ try {
457
+ execSync(`git show-ref --verify --quiet refs/remotes/${remoteName}/${branchName}`, {
458
+ cwd: projectRoot,
459
+ stdio: ['pipe', 'pipe', 'pipe'],
460
+ });
461
+ return true;
462
+ }
463
+ catch {
464
+ return false;
465
+ }
466
+ }
467
+ /**
468
+ * Fetch from remote to ensure refs are up to date.
469
+ * Returns true if fetch succeeded, false otherwise.
470
+ */
471
+ export async function fetchRemote(projectRoot, remoteName = 'origin') {
472
+ try {
473
+ await execAsync(`git fetch ${remoteName}`, {
474
+ cwd: projectRoot,
475
+ });
476
+ return true;
477
+ }
478
+ catch {
479
+ return false;
480
+ }
481
+ }
482
+ /**
483
+ * Push shadow branch to remote with tracking.
484
+ * Returns true if push succeeded, false otherwise.
485
+ */
486
+ export async function pushShadowBranch(worktreeDir, remoteName = 'origin') {
487
+ try {
488
+ await execAsync(`git push -u ${remoteName} ${SHADOW_BRANCH_NAME}`, {
489
+ cwd: worktreeDir,
490
+ });
491
+ return true;
492
+ }
493
+ catch {
494
+ return false;
495
+ }
496
+ }
497
+ /**
498
+ * Check if shadow branch has remote tracking configured.
499
+ * AC-4: Used to determine whether sync should be attempted.
500
+ */
501
+ export async function hasRemoteTracking(worktreeDir) {
502
+ try {
503
+ const { stdout } = await execAsync(`git config branch.${SHADOW_BRANCH_NAME}.remote`, { cwd: worktreeDir });
504
+ return stdout.trim().length > 0;
505
+ }
506
+ catch {
507
+ return false;
508
+ }
509
+ }
510
+ /**
511
+ * Ensure shadow branch has remote tracking configured.
512
+ * AC-8: If shadow has no tracking but main branch has origin remote,
513
+ * automatically configure tracking to origin/kspec-meta.
514
+ *
515
+ * @param worktreeDir Path to .kspec/ worktree
516
+ * @param projectRoot Git repository root
517
+ * @returns true if tracking is now configured (was already or just set up)
518
+ */
519
+ export async function ensureRemoteTracking(worktreeDir, projectRoot) {
520
+ // Check if already has tracking
521
+ if (await hasRemoteTracking(worktreeDir)) {
522
+ return true;
523
+ }
524
+ // Check if main branch has origin remote
525
+ if (!(await hasRemote(projectRoot))) {
526
+ return false;
527
+ }
528
+ // Set up tracking for shadow branch to origin/kspec-meta
529
+ try {
530
+ await execAsync(`git config branch.${SHADOW_BRANCH_NAME}.remote origin`, { cwd: worktreeDir });
531
+ await execAsync(`git config branch.${SHADOW_BRANCH_NAME}.merge refs/heads/${SHADOW_BRANCH_NAME}`, { cwd: worktreeDir });
532
+ return true;
533
+ }
534
+ catch {
535
+ return false;
536
+ }
537
+ }
538
+ /**
539
+ * Fire-and-forget push to remote.
540
+ * AC-1: Called after each auto-commit when tracking is configured.
541
+ * AC-8: Automatically sets up tracking if main branch has remote.
542
+ * Silently ignores errors - the local commit succeeded regardless.
543
+ *
544
+ * @param worktreeDir Path to .kspec/ worktree
545
+ * @param verbose Enable debug output
546
+ */
547
+ export async function shadowPushAsync(worktreeDir, verbose) {
548
+ const debug = isDebugMode(verbose);
549
+ // AC-8: Auto-configure tracking if main has remote but shadow doesn't
550
+ const projectRoot = path.dirname(worktreeDir);
551
+ await ensureRemoteTracking(worktreeDir, projectRoot);
552
+ // Check if tracking is configured before attempting push
553
+ if (!(await hasRemoteTracking(worktreeDir))) {
554
+ if (debug) {
555
+ console.error('[DEBUG] Shadow push: No remote tracking configured, skipping');
556
+ }
557
+ return; // AC-4: silently skip if no tracking
558
+ }
559
+ try {
560
+ if (debug) {
561
+ console.error(`[DEBUG] Shadow push: git push (cwd: ${worktreeDir})`);
562
+ }
563
+ // Don't await - fire and forget
564
+ execAsync('git push', { cwd: worktreeDir }).catch((err) => {
565
+ if (debug) {
566
+ console.error('[DEBUG] Shadow push failed:', err);
567
+ }
568
+ // Silently ignore push failures - local state is correct
569
+ });
570
+ }
571
+ catch (err) {
572
+ if (debug) {
573
+ console.error('[DEBUG] Shadow push error:', err);
574
+ }
575
+ }
576
+ }
577
+ /**
578
+ * Pull remote changes to shadow branch.
579
+ * AC-2: Called at session start to sync before operations.
580
+ * AC-6: Uses --ff-only first, falls back to --rebase.
581
+ * AC-3: On conflict, returns failure with suggestion.
582
+ * AC-8: Automatically sets up tracking if main branch has remote.
583
+ */
584
+ export async function shadowPull(worktreeDir) {
585
+ const result = {
586
+ success: false,
587
+ pulled: false,
588
+ pushed: false,
589
+ hadConflict: false,
590
+ };
591
+ // AC-8: Auto-configure tracking if main has remote but shadow doesn't
592
+ const projectRoot = path.dirname(worktreeDir);
593
+ await ensureRemoteTracking(worktreeDir, projectRoot);
594
+ // AC-4: Skip if no remote tracking
595
+ if (!(await hasRemoteTracking(worktreeDir))) {
596
+ result.success = true;
597
+ return result;
598
+ }
599
+ // Check if remote branch exists before attempting pull
600
+ // Fetch first to ensure refs are up to date
601
+ await fetchRemote(projectRoot);
602
+ const remoteHasBranch = await remoteBranchExists(projectRoot, SHADOW_BRANCH_NAME);
603
+ if (!remoteHasBranch) {
604
+ // Remote branch doesn't exist yet - nothing to pull, but success
605
+ result.success = true;
606
+ return result;
607
+ }
608
+ try {
609
+ // Try fast-forward only first (cleanest)
610
+ await execAsync('git pull --ff-only', { cwd: worktreeDir });
611
+ result.success = true;
612
+ result.pulled = true;
613
+ return result;
614
+ }
615
+ catch {
616
+ // Fast-forward failed, try rebase
617
+ }
618
+ try {
619
+ // AC-6: Fall back to rebase
620
+ await execAsync('git pull --rebase', { cwd: worktreeDir });
621
+ result.success = true;
622
+ result.pulled = true;
623
+ return result;
624
+ }
625
+ catch {
626
+ // Rebase failed - likely conflict
627
+ }
628
+ // AC-3: Conflict detected - abort rebase and report
629
+ try {
630
+ await execAsync('git rebase --abort', { cwd: worktreeDir });
631
+ }
632
+ catch {
633
+ // May not be in rebase state, ignore
634
+ }
635
+ result.hadConflict = true;
636
+ result.error = 'Sync conflict detected. Run `kspec shadow resolve` to fix.';
637
+ return result;
638
+ }
639
+ /**
640
+ * Full sync operation: pull then push.
641
+ * Used by session start and explicit sync commands.
642
+ */
643
+ export async function shadowSync(worktreeDir) {
644
+ // First pull
645
+ const pullResult = await shadowPull(worktreeDir);
646
+ if (!pullResult.success) {
647
+ return pullResult;
648
+ }
649
+ // Then push (only if tracking configured, checked inside)
650
+ if (await hasRemoteTracking(worktreeDir)) {
651
+ try {
652
+ await execAsync('git push', { cwd: worktreeDir });
653
+ pullResult.pushed = true;
654
+ }
655
+ catch {
656
+ // Push failed - not a critical error, local state is correct
657
+ // Could be permissions, network, etc.
658
+ }
659
+ }
660
+ return pullResult;
661
+ }
662
+ /**
663
+ * Check if .gitignore has uncommitted changes
664
+ */
665
+ async function hasUncommittedGitignore(projectRoot) {
666
+ try {
667
+ // Check both staged and unstaged changes to .gitignore
668
+ const { stdout } = await execAsync('git status --porcelain .gitignore', {
669
+ cwd: projectRoot,
670
+ });
671
+ return stdout.trim().length > 0;
672
+ }
673
+ catch {
674
+ return false;
675
+ }
676
+ }
677
+ /**
678
+ * Commit only .gitignore with a message
679
+ */
680
+ async function commitGitignore(projectRoot) {
681
+ await execAsync('git add .gitignore', { cwd: projectRoot });
682
+ await execAsync('git commit -m "chore: add .kspec/ to .gitignore for shadow branch"', {
683
+ cwd: projectRoot,
684
+ });
685
+ }
686
+ /**
687
+ * Add .kspec/ to .gitignore if not already present.
688
+ * Fails if .gitignore has uncommitted changes.
689
+ * Commits the change after adding.
690
+ */
691
+ async function ensureGitignore(projectRoot) {
692
+ const gitignorePath = path.join(projectRoot, '.gitignore');
693
+ const entry = `${SHADOW_WORKTREE_DIR}/`;
694
+ // Fail fast if .gitignore has uncommitted changes
695
+ if (await hasUncommittedGitignore(projectRoot)) {
696
+ throw new ShadowError('.gitignore has uncommitted changes', 'GIT_ERROR', 'Commit or stash your .gitignore changes before running kspec init.');
697
+ }
698
+ try {
699
+ let content = '';
700
+ try {
701
+ content = await fs.readFile(gitignorePath, 'utf-8');
702
+ }
703
+ catch {
704
+ // File doesn't exist, will create
705
+ }
706
+ // Check if already present (handle various formats)
707
+ const lines = content.split('\n');
708
+ const patterns = [
709
+ SHADOW_WORKTREE_DIR,
710
+ `${SHADOW_WORKTREE_DIR}/`,
711
+ `/${SHADOW_WORKTREE_DIR}`,
712
+ `/${SHADOW_WORKTREE_DIR}/`,
713
+ ];
714
+ for (const line of lines) {
715
+ const trimmed = line.trim();
716
+ if (patterns.includes(trimmed)) {
717
+ return false; // Already present
718
+ }
719
+ }
720
+ // Add to gitignore
721
+ const newContent = content.endsWith('\n') || content === ''
722
+ ? `${content}${entry}\n`
723
+ : `${content}\n${entry}\n`;
724
+ await fs.writeFile(gitignorePath, newContent, 'utf-8');
725
+ // Commit the change
726
+ await commitGitignore(projectRoot);
727
+ return true;
728
+ }
729
+ catch (error) {
730
+ if (error instanceof ShadowError) {
731
+ throw error;
732
+ }
733
+ throw new ShadowError(`Failed to update .gitignore: ${error}`, 'GIT_ERROR', 'Check file permissions for .gitignore');
734
+ }
735
+ }
736
+ /**
737
+ * Generate initial manifest content for shadow branch
738
+ */
739
+ function generateShadowManifest(projectName) {
740
+ return `# ${projectName} - Kynetic Spec
741
+ # Generated by kspec init
742
+
743
+ kynetic: "1.0"
744
+
745
+ project:
746
+ name: "${projectName}"
747
+ version: "0.1.0"
748
+ status: draft
749
+ description: |
750
+ Add your project description here.
751
+
752
+ # Module includes
753
+ includes:
754
+ - modules/main.yaml
755
+
756
+ # Configuration
757
+ config:
758
+ validation:
759
+ strict_refs: true
760
+ require_acceptance: false
761
+ `;
762
+ }
763
+ /**
764
+ * Generate initial module content
765
+ */
766
+ function generateShadowModule(projectName) {
767
+ return `# ${projectName} - Main Module
768
+ # Add your spec items here
769
+
770
+ items: []
771
+ `;
772
+ }
773
+ /**
774
+ * Generate initial tasks file
775
+ */
776
+ function generateShadowTasks(projectName) {
777
+ return `# ${projectName} - Tasks
778
+ # Track implementation work here
779
+
780
+ tasks: []
781
+ `;
782
+ }
783
+ /**
784
+ * Generate initial inbox file
785
+ */
786
+ function generateShadowInbox() {
787
+ return `# Inbox - Quick Capture
788
+ # Ideas and notes that haven't been triaged yet
789
+
790
+ items: []
791
+ `;
792
+ }
793
+ /**
794
+ * Install pre-commit hook to protect kspec-meta branch.
795
+ * Hook prevents direct commits to shadow branch unless KSPEC_SHADOW_COMMIT=1.
796
+ *
797
+ * Note: Git worktrees use hooks from the main .git/hooks directory (via commondir),
798
+ * not from .git/worktrees/-kspec/hooks. So we install to main hooks directory.
799
+ *
800
+ * @param projectRoot Git repository root
801
+ * @returns true if hook was installed, false if already exists
802
+ */
803
+ async function installShadowHook(projectRoot) {
804
+ const hooksDir = path.join(projectRoot, '.git', 'hooks');
805
+ const hookPath = path.join(hooksDir, 'pre-commit');
806
+ const sourceHookPath = path.join(projectRoot, 'hooks', 'pre-commit');
807
+ try {
808
+ // Check if source hook exists
809
+ try {
810
+ await fs.access(sourceHookPath);
811
+ }
812
+ catch {
813
+ // Source hook doesn't exist - skip installation
814
+ return false;
815
+ }
816
+ // Check if hook already exists
817
+ try {
818
+ await fs.access(hookPath);
819
+ // Hook exists - don't overwrite (user may have custom hooks)
820
+ return false;
821
+ }
822
+ catch {
823
+ // Hook doesn't exist - install it
824
+ }
825
+ // Copy hook from source
826
+ const hookContent = await fs.readFile(sourceHookPath, 'utf-8');
827
+ await fs.writeFile(hookPath, hookContent, { mode: 0o755 });
828
+ return true;
829
+ }
830
+ catch (error) {
831
+ // Silently fail - hook installation is optional
832
+ return false;
833
+ }
834
+ }
835
+ /**
836
+ * Convert project name to slug (kebab-case)
837
+ */
838
+ function toSlug(projectName) {
839
+ return projectName
840
+ .toLowerCase()
841
+ .replace(/[^a-z0-9]+/g, '-')
842
+ .replace(/^-|-$/g, '');
843
+ }
844
+ /**
845
+ * Initialize shadow branch and worktree.
846
+ * Creates orphan branch, worktree, updates gitignore, and creates initial structure.
847
+ *
848
+ * @param projectRoot Git repository root
849
+ * @param options Initialization options
850
+ * @returns Result indicating what was created
851
+ */
852
+ export async function initializeShadow(projectRoot, options = {}) {
853
+ const result = {
854
+ success: false,
855
+ branchCreated: false,
856
+ worktreeCreated: false,
857
+ gitignoreUpdated: false,
858
+ initialCommit: false,
859
+ alreadyExists: false,
860
+ createdFromRemote: false,
861
+ pushedToRemote: false,
862
+ };
863
+ // Check if we're in a git repo
864
+ if (!(await isGitRepo(projectRoot))) {
865
+ result.error = 'Not a git repository';
866
+ return result;
867
+ }
868
+ const worktreeDir = path.join(projectRoot, SHADOW_WORKTREE_DIR);
869
+ // Check current status
870
+ const status = await getShadowStatus(projectRoot);
871
+ // Handle existing shadow branch
872
+ if (status.healthy && !options.force) {
873
+ result.alreadyExists = true;
874
+ result.success = true;
875
+ return result;
876
+ }
877
+ // Derive project name if not provided
878
+ const projectName = options.projectName || path.basename(projectRoot)
879
+ .replace(/[-_]/g, ' ')
880
+ .replace(/\b\w/g, (c) => c.toUpperCase());
881
+ const slug = toSlug(projectName);
882
+ // Check for remote shadow branch (AC-4: fetch to ensure refs are up to date)
883
+ const remoteExists = await hasRemote(projectRoot);
884
+ let remoteHasShadow = false;
885
+ if (remoteExists) {
886
+ await fetchRemote(projectRoot); // Best effort, ignore failures
887
+ remoteHasShadow = await remoteBranchExists(projectRoot, SHADOW_BRANCH_NAME);
888
+ }
889
+ try {
890
+ // Step 1: Update .gitignore first (before creating .kspec/)
891
+ result.gitignoreUpdated = await ensureGitignore(projectRoot);
892
+ // Step 2: Create worktree with orphan branch (or attach to existing branch)
893
+ if (!status.worktreeExists || !status.worktreeLinked) {
894
+ // Remove existing directory if present but not linked
895
+ if (status.worktreeExists && !status.worktreeLinked) {
896
+ await fs.rm(worktreeDir, { recursive: true, force: true });
897
+ }
898
+ // Remove stale worktree reference if any
899
+ try {
900
+ await execAsync(`git worktree remove ${SHADOW_WORKTREE_DIR} --force`, {
901
+ cwd: projectRoot,
902
+ });
903
+ }
904
+ catch {
905
+ // Ignore - worktree may not exist in git's list
906
+ }
907
+ if (remoteHasShadow) {
908
+ // AC-1: Remote has shadow branch - create worktree from it with tracking
909
+ await execAsync(`git worktree add ${SHADOW_WORKTREE_DIR} ${SHADOW_BRANCH_NAME}`, { cwd: projectRoot });
910
+ // Set up tracking for the branch
911
+ await execAsync(`git branch --set-upstream-to=origin/${SHADOW_BRANCH_NAME} ${SHADOW_BRANCH_NAME}`, { cwd: projectRoot });
912
+ result.createdFromRemote = true;
913
+ }
914
+ else if (!status.branchExists) {
915
+ // AC-2/AC-3: No remote branch or no remote - create orphan branch
916
+ await execAsync(`git worktree add --orphan -b ${SHADOW_BRANCH_NAME} ${SHADOW_WORKTREE_DIR}`, { cwd: projectRoot });
917
+ result.branchCreated = true;
918
+ }
919
+ else {
920
+ // Attach to existing local branch
921
+ await execAsync(`git worktree add ${SHADOW_WORKTREE_DIR} ${SHADOW_BRANCH_NAME}`, { cwd: projectRoot });
922
+ }
923
+ result.worktreeCreated = true;
924
+ }
925
+ // Step 3: Create initial structure if empty (only for new branches, not remote)
926
+ const manifestPath = path.join(worktreeDir, `${slug}.yaml`);
927
+ const modulesDir = path.join(worktreeDir, 'modules');
928
+ const moduleFilePath = path.join(modulesDir, 'main.yaml');
929
+ const tasksPath = path.join(worktreeDir, `${slug}.tasks.yaml`);
930
+ const inboxPath = path.join(worktreeDir, `${slug}.inbox.yaml`);
931
+ let filesCreated = false;
932
+ // Only create files if manifest doesn't exist (remote branches will have files)
933
+ try {
934
+ // Look for any .yaml manifest file (project name may differ)
935
+ const files = await fs.readdir(worktreeDir);
936
+ const hasManifest = files.some(f => f.endsWith('.yaml') && !f.includes('.tasks.') && !f.includes('.inbox.'));
937
+ if (!hasManifest) {
938
+ throw new Error('No manifest found');
939
+ }
940
+ }
941
+ catch {
942
+ // Manifest doesn't exist, create initial structure
943
+ await fs.mkdir(modulesDir, { recursive: true });
944
+ await fs.writeFile(manifestPath, generateShadowManifest(projectName), 'utf-8');
945
+ await fs.writeFile(moduleFilePath, generateShadowModule(projectName), 'utf-8');
946
+ await fs.writeFile(tasksPath, generateShadowTasks(projectName), 'utf-8');
947
+ await fs.writeFile(inboxPath, generateShadowInbox(), 'utf-8');
948
+ filesCreated = true;
949
+ }
950
+ // Step 4: Initial commit if files were created
951
+ if (filesCreated) {
952
+ result.initialCommit = await shadowAutoCommit(worktreeDir, `Initialize ${projectName} spec`);
953
+ }
954
+ // Step 5: AC-2: Push new branch to remote to establish tracking
955
+ if (result.branchCreated && remoteExists && !remoteHasShadow) {
956
+ result.pushedToRemote = await pushShadowBranch(worktreeDir);
957
+ }
958
+ // Step 6: Install pre-commit hook to protect shadow branch
959
+ await installShadowHook(projectRoot);
960
+ result.success = true;
961
+ return result;
962
+ }
963
+ catch (error) {
964
+ result.error = error instanceof Error ? error.message : String(error);
965
+ return result;
966
+ }
967
+ }
968
+ /**
969
+ * Repair a broken shadow branch setup.
970
+ * Handles cases where worktree is disconnected or directory is missing.
971
+ *
972
+ * @param projectRoot Git repository root
973
+ * @returns Result indicating what was repaired
974
+ */
975
+ export async function repairShadow(projectRoot) {
976
+ const status = await getShadowStatus(projectRoot);
977
+ if (status.healthy) {
978
+ return {
979
+ success: true,
980
+ branchCreated: false,
981
+ worktreeCreated: false,
982
+ gitignoreUpdated: false,
983
+ initialCommit: false,
984
+ alreadyExists: true,
985
+ createdFromRemote: false,
986
+ pushedToRemote: false,
987
+ };
988
+ }
989
+ if (!status.branchExists) {
990
+ // Can't repair without a branch - need full init
991
+ return {
992
+ success: false,
993
+ branchCreated: false,
994
+ worktreeCreated: false,
995
+ gitignoreUpdated: false,
996
+ initialCommit: false,
997
+ alreadyExists: false,
998
+ createdFromRemote: false,
999
+ pushedToRemote: false,
1000
+ error: 'Shadow branch does not exist. Run `kspec init` instead.',
1001
+ };
1002
+ }
1003
+ // Branch exists but worktree is broken - repair it
1004
+ const worktreeDir = path.join(projectRoot, SHADOW_WORKTREE_DIR);
1005
+ try {
1006
+ // Remove stale worktree reference
1007
+ try {
1008
+ await execAsync(`git worktree remove ${SHADOW_WORKTREE_DIR} --force`, {
1009
+ cwd: projectRoot,
1010
+ });
1011
+ }
1012
+ catch {
1013
+ // Ignore - worktree may not be in git's list
1014
+ }
1015
+ // Remove directory if exists (handles corrupted .git file case)
1016
+ await fs.rm(worktreeDir, { recursive: true, force: true });
1017
+ // Prune stale worktree references (cleans up orphaned entries)
1018
+ try {
1019
+ await execAsync('git worktree prune', { cwd: projectRoot });
1020
+ }
1021
+ catch {
1022
+ // Ignore - prune is best-effort
1023
+ }
1024
+ // Recreate worktree
1025
+ await execAsync(`git worktree add ${SHADOW_WORKTREE_DIR} ${SHADOW_BRANCH_NAME}`, { cwd: projectRoot });
1026
+ // Install pre-commit hook
1027
+ await installShadowHook(projectRoot);
1028
+ return {
1029
+ success: true,
1030
+ branchCreated: false,
1031
+ worktreeCreated: true,
1032
+ gitignoreUpdated: false,
1033
+ initialCommit: false,
1034
+ alreadyExists: false,
1035
+ createdFromRemote: false,
1036
+ pushedToRemote: false,
1037
+ };
1038
+ }
1039
+ catch (error) {
1040
+ return {
1041
+ success: false,
1042
+ branchCreated: false,
1043
+ worktreeCreated: false,
1044
+ gitignoreUpdated: false,
1045
+ initialCommit: false,
1046
+ alreadyExists: false,
1047
+ createdFromRemote: false,
1048
+ pushedToRemote: false,
1049
+ error: error instanceof Error ? error.message : String(error),
1050
+ };
1051
+ }
1052
+ }
1053
+ //# sourceMappingURL=shadow.js.map