@ktpartners/dgs-platform 2.8.0 → 3.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +41 -13
  3. package/agents/dgs-plan-checker.md +29 -3
  4. package/agents/dgs-planner.md +10 -0
  5. package/commands/dgs/abandon-quick.md +28 -0
  6. package/commands/dgs/add-tests.md +2 -2
  7. package/commands/dgs/audit-milestone.md +2 -2
  8. package/commands/dgs/capture-principle.md +11 -11
  9. package/commands/dgs/cleanup.md +2 -2
  10. package/commands/dgs/complete-milestone.md +11 -11
  11. package/commands/dgs/complete-quick.md +28 -0
  12. package/commands/dgs/create-milestone-job.md +2 -2
  13. package/commands/dgs/debug.md +3 -3
  14. package/commands/dgs/develop-idea.md +1 -1
  15. package/commands/dgs/fast.md +3 -1
  16. package/commands/dgs/health.md +1 -1
  17. package/commands/dgs/map-codebase.md +6 -6
  18. package/commands/dgs/new-milestone.md +5 -5
  19. package/commands/dgs/new-project.md +6 -6
  20. package/commands/dgs/plan-milestone-gaps.md +1 -1
  21. package/commands/dgs/progress.md +3 -3
  22. package/commands/dgs/quick-abandon.md +8 -0
  23. package/commands/dgs/quick-complete.md +8 -0
  24. package/commands/dgs/quick.md +10 -3
  25. package/commands/dgs/research-idea.md +2 -2
  26. package/commands/dgs/research-phase.md +3 -3
  27. package/commands/dgs/switch-project.md +1 -1
  28. package/commands/dgs/write-spec.md +3 -3
  29. package/deliver-great-systems/bin/dgs-tools.cjs +284 -30
  30. package/deliver-great-systems/bin/lib/commands.cjs +316 -31
  31. package/deliver-great-systems/bin/lib/commands.test.cjs +336 -0
  32. package/deliver-great-systems/bin/lib/config.cjs +39 -6
  33. package/deliver-great-systems/bin/lib/context.cjs +120 -0
  34. package/deliver-great-systems/bin/lib/core.cjs +28 -11
  35. package/deliver-great-systems/bin/lib/execution.cjs +49 -17
  36. package/deliver-great-systems/bin/lib/flat-migration.test.cjs +396 -0
  37. package/deliver-great-systems/bin/lib/ideas.cjs +206 -91
  38. package/deliver-great-systems/bin/lib/ideas.test.cjs +244 -1
  39. package/deliver-great-systems/bin/lib/init.cjs +306 -39
  40. package/deliver-great-systems/bin/lib/init.test.cjs +416 -6
  41. package/deliver-great-systems/bin/lib/jobs.cjs +124 -21
  42. package/deliver-great-systems/bin/lib/jobs.test.cjs +193 -74
  43. package/deliver-great-systems/bin/lib/migration.cjs +409 -1
  44. package/deliver-great-systems/bin/lib/migration.test.cjs +158 -1
  45. package/deliver-great-systems/bin/lib/milestone.cjs +54 -29
  46. package/deliver-great-systems/bin/lib/phase.cjs +128 -2
  47. package/deliver-great-systems/bin/lib/phase.test.cjs +420 -0
  48. package/deliver-great-systems/bin/lib/projects.cjs +28 -8
  49. package/deliver-great-systems/bin/lib/projects.test.cjs +86 -0
  50. package/deliver-great-systems/bin/lib/quick.cjs +584 -0
  51. package/deliver-great-systems/bin/lib/quick.test.cjs +596 -0
  52. package/deliver-great-systems/bin/lib/repos.cjs +25 -1
  53. package/deliver-great-systems/bin/lib/roadmap.cjs +34 -13
  54. package/deliver-great-systems/bin/lib/specs.cjs +3 -81
  55. package/deliver-great-systems/bin/lib/state-transition-gate.test.cjs +160 -0
  56. package/deliver-great-systems/bin/lib/state.cjs +142 -54
  57. package/deliver-great-systems/bin/lib/sync.cjs +75 -0
  58. package/deliver-great-systems/bin/lib/verify.cjs +80 -1
  59. package/deliver-great-systems/bin/lib/worktrees.cjs +764 -0
  60. package/deliver-great-systems/bin/lib/worktrees.test.cjs +887 -0
  61. package/deliver-great-systems/templates/claude-md.md +16 -0
  62. package/deliver-great-systems/workflows/abandon-quick.md +89 -0
  63. package/deliver-great-systems/workflows/add-idea.md +3 -3
  64. package/deliver-great-systems/workflows/add-tests.md +14 -0
  65. package/deliver-great-systems/workflows/add-todo.md +1 -0
  66. package/deliver-great-systems/workflows/approve-spec.md +25 -4
  67. package/deliver-great-systems/workflows/audit-phase.md +15 -5
  68. package/deliver-great-systems/workflows/cancel-job.md +1 -1
  69. package/deliver-great-systems/workflows/check-todos.md +2 -3
  70. package/deliver-great-systems/workflows/complete-milestone.md +197 -22
  71. package/deliver-great-systems/workflows/complete-quick.md +68 -0
  72. package/deliver-great-systems/workflows/consolidate-ideas.md +1 -1
  73. package/deliver-great-systems/workflows/create-milestone-job.md +4 -4
  74. package/deliver-great-systems/workflows/develop-idea.md +11 -11
  75. package/deliver-great-systems/workflows/diagnose-issues.md +14 -0
  76. package/deliver-great-systems/workflows/discuss-idea.md +1 -1
  77. package/deliver-great-systems/workflows/execute-phase.md +121 -32
  78. package/deliver-great-systems/workflows/execute-plan.md +12 -21
  79. package/deliver-great-systems/workflows/help.md +33 -29
  80. package/deliver-great-systems/workflows/init-product.md +2 -18
  81. package/deliver-great-systems/workflows/new-milestone.md +40 -24
  82. package/deliver-great-systems/workflows/new-project.md +22 -680
  83. package/deliver-great-systems/workflows/progress-all.md +133 -0
  84. package/deliver-great-systems/workflows/quick-abandon.md +89 -0
  85. package/deliver-great-systems/workflows/quick-complete.md +68 -0
  86. package/deliver-great-systems/workflows/quick.md +152 -23
  87. package/deliver-great-systems/workflows/refine-spec.md +1 -1
  88. package/deliver-great-systems/workflows/research-idea.md +8 -8
  89. package/deliver-great-systems/workflows/resume-project.md +2 -2
  90. package/deliver-great-systems/workflows/run-job.md +8 -8
  91. package/deliver-great-systems/workflows/validate-phase.md +39 -1
  92. package/deliver-great-systems/workflows/verify-work.md +14 -0
  93. package/deliver-great-systems/workflows/write-spec.md +2 -2
  94. package/package.json +1 -1
@@ -0,0 +1,887 @@
1
+ /**
2
+ * Tests for worktrees.cjs and resolveCodeContext() — git worktree lifecycle
3
+ *
4
+ * Uses real git repos in temp directories. All worktree commands call output()
5
+ * which exits the process, so tests invoke via subprocess (dgs-tools.cjs CLI).
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const { describe, it, beforeEach, afterEach } = require('node:test');
11
+ const assert = require('node:assert/strict');
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const { execSync } = require('child_process');
15
+ const { resetPaths, initPaths } = require('./paths.cjs');
16
+
17
+ const DGS_TOOLS = path.resolve(__dirname, '..', 'dgs-tools.cjs');
18
+
19
+ // ─── Test Helpers ────────────────────────────────────────────────────────────
20
+
21
+ const GIT_ENV = {
22
+ GIT_AUTHOR_NAME: 'Test',
23
+ GIT_AUTHOR_EMAIL: 'test@test.com',
24
+ GIT_COMMITTER_NAME: 'Test',
25
+ GIT_COMMITTER_EMAIL: 'test@test.com',
26
+ };
27
+
28
+ /**
29
+ * Create a minimal DGS environment with a real code repo for worktree tests.
30
+ *
31
+ * Layout:
32
+ * {tmpDir}/
33
+ * planning/ <- DGS planning root (git repo)
34
+ * config.json
35
+ * config.local.json
36
+ * REPOS.md
37
+ * PROJECTS.md
38
+ * projects/tp/STATE.md
39
+ * code-repo/ <- simulated code repo (git repo on main branch)
40
+ */
41
+ function createTestEnv(opts) {
42
+ opts = opts || {};
43
+ const tmpDir = fs.realpathSync(fs.mkdtempSync(path.join(require('os').tmpdir(), 'dgs-wt-')));
44
+ const planDir = path.join(tmpDir, 'planning');
45
+ const codeDir = path.join(tmpDir, 'code-repo');
46
+
47
+ // Create planning root (git repo)
48
+ fs.mkdirSync(planDir, { recursive: true });
49
+ execSync('git init -b main', { cwd: planDir, stdio: 'pipe', env: { ...process.env, ...GIT_ENV } });
50
+ execSync('git config user.email "test@test.com"', { cwd: planDir, stdio: 'pipe' });
51
+ execSync('git config user.name "Test"', { cwd: planDir, stdio: 'pipe' });
52
+
53
+ // Create code repo (git repo on main branch)
54
+ fs.mkdirSync(codeDir, { recursive: true });
55
+ execSync('git init -b main', { cwd: codeDir, stdio: 'pipe', env: { ...process.env, ...GIT_ENV } });
56
+ execSync('git config user.email "test@test.com"', { cwd: codeDir, stdio: 'pipe' });
57
+ execSync('git config user.name "Test"', { cwd: codeDir, stdio: 'pipe' });
58
+ fs.writeFileSync(path.join(codeDir, '.gitkeep'), '');
59
+ execSync('git add .', { cwd: codeDir, stdio: 'pipe' });
60
+ execSync('git commit -m "initial"', { cwd: codeDir, stdio: 'pipe', env: { ...process.env, ...GIT_ENV } });
61
+
62
+ // Setup field for REPOS.md
63
+ const setupCmd = opts.setup || '';
64
+ const setupCol = setupCmd ? ' | ' + setupCmd : '';
65
+ const setupHeader = setupCmd ? ' | Setup' : '';
66
+ const setupSep = setupCmd ? ' |-------' : '';
67
+
68
+ // DGS config files
69
+ fs.writeFileSync(path.join(planDir, 'config.json'), JSON.stringify({
70
+ git: { base_branch: 'main' },
71
+ }, null, 2));
72
+
73
+ fs.writeFileSync(path.join(planDir, 'config.local.json'), JSON.stringify({
74
+ current_project: 'tp',
75
+ }, null, 2));
76
+
77
+ // v2 markers
78
+ fs.writeFileSync(path.join(planDir, 'PROJECTS.md'), '# Projects\n');
79
+ fs.writeFileSync(path.join(planDir, 'REPOS.md'),
80
+ '# Repos\n\n' +
81
+ '| Name | Path | GitHub URL | Description' + setupHeader + ' |\n' +
82
+ '|------|------|------------|------------' + setupSep + ' |\n' +
83
+ '| code-repo | ' + path.relative(planDir, codeDir) + ' | | Test repo' + setupCol + ' |\n'
84
+ );
85
+
86
+ // Project structure
87
+ fs.mkdirSync(path.join(planDir, 'projects', 'tp'), { recursive: true });
88
+ fs.writeFileSync(path.join(planDir, 'projects', 'tp', 'STATE.md'), '# State\nStatus: planning\n');
89
+
90
+ // Commit planning files
91
+ execSync('git add .', { cwd: planDir, stdio: 'pipe' });
92
+ execSync('git commit -m "setup"', { cwd: planDir, stdio: 'pipe', env: { ...process.env, ...GIT_ENV } });
93
+
94
+ initPaths(planDir);
95
+
96
+ return {
97
+ tmpDir,
98
+ planDir,
99
+ codeDir,
100
+ cleanup: function() {
101
+ resetPaths();
102
+ // Also clean up any sibling worktree directories
103
+ try {
104
+ const parent = path.dirname(codeDir);
105
+ const entries = fs.readdirSync(parent);
106
+ for (const e of entries) {
107
+ if (e.startsWith('code-repo--')) {
108
+ fs.rmSync(path.join(parent, e), { recursive: true, force: true });
109
+ }
110
+ }
111
+ } catch { /* ignore */ }
112
+ fs.rmSync(tmpDir, { recursive: true, force: true });
113
+ },
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Run dgs-tools command and return parsed JSON output.
119
+ */
120
+ function runCmd(cwd, args) {
121
+ const result = execSync(
122
+ 'node ' + JSON.stringify(DGS_TOOLS) + ' ' + args,
123
+ { cwd, stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf-8', env: { ...process.env, ...GIT_ENV } }
124
+ );
125
+ return JSON.parse(result.trim());
126
+ }
127
+
128
+ /**
129
+ * Run dgs-tools command expecting an error.
130
+ */
131
+ function runCmdError(cwd, args) {
132
+ try {
133
+ execSync(
134
+ 'node ' + JSON.stringify(DGS_TOOLS) + ' ' + args,
135
+ { cwd, stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf-8', env: { ...process.env, ...GIT_ENV } }
136
+ );
137
+ return null; // No error
138
+ } catch (err) {
139
+ return err.stderr || err.message;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Read config.local.json from planning dir.
145
+ */
146
+ function readLocalConfig(planDir) {
147
+ return JSON.parse(fs.readFileSync(path.join(planDir, 'config.local.json'), 'utf-8'));
148
+ }
149
+
150
+ /**
151
+ * Write config.local.json to planning dir.
152
+ */
153
+ function writeLocalConfig(planDir, data) {
154
+ fs.writeFileSync(path.join(planDir, 'config.local.json'), JSON.stringify(data, null, 2) + '\n');
155
+ }
156
+
157
+ // ─── REPOS.md setup field parsing ─────────────────────────────────────────────
158
+
159
+ describe('REPOS.md setup field parsing', () => {
160
+ let env;
161
+ beforeEach(() => { env = createTestEnv({ setup: 'echo hello' }); });
162
+ afterEach(() => { env.cleanup(); });
163
+
164
+ it('parseReposMd returns setup field from 5th column', () => {
165
+ const { parseReposMd } = require('./repos.cjs');
166
+ const result = parseReposMd(env.planDir);
167
+ assert.ok(result);
168
+ assert.ok(result.repos.length > 0);
169
+ assert.equal(result.repos[0].setup, 'echo hello');
170
+ });
171
+ });
172
+
173
+ describe('REPOS.md without setup column', () => {
174
+ let env;
175
+ beforeEach(() => { env = createTestEnv(); });
176
+ afterEach(() => { env.cleanup(); });
177
+
178
+ it('parseReposMd returns empty string when Setup column missing', () => {
179
+ const { parseReposMd } = require('./repos.cjs');
180
+ const result = parseReposMd(env.planDir);
181
+ assert.ok(result);
182
+ assert.equal(result.repos[0].setup, '');
183
+ });
184
+ });
185
+
186
+ // ─── worktree creation ───────────────────────────────────────────────────────
187
+
188
+ describe('worktree creation', () => {
189
+ let env;
190
+ beforeEach(() => { env = createTestEnv(); });
191
+ afterEach(() => { env.cleanup(); });
192
+
193
+ it('creates milestone worktree at sibling path with correct branch', () => {
194
+ const result = runCmd(env.planDir, 'worktrees create test-ms --type milestone');
195
+ assert.ok(result.created);
196
+ assert.equal(result.slug, 'test-ms');
197
+ assert.equal(result.type, 'milestone');
198
+
199
+ // Verify directory exists
200
+ const wtPath = result.repos['code-repo'];
201
+ assert.ok(fs.existsSync(wtPath), 'Worktree directory should exist');
202
+ assert.ok(wtPath.includes('code-repo--tp-test-ms'), 'Path should use sibling naming');
203
+
204
+ // Verify branch
205
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', {
206
+ cwd: wtPath, encoding: 'utf-8', stdio: 'pipe',
207
+ }).trim();
208
+ assert.equal(branch, 'milestone/test-ms');
209
+
210
+ // Verify config state
211
+ const config = readLocalConfig(env.planDir);
212
+ assert.ok(config.projects.tp.worktrees['test-ms']);
213
+ assert.equal(config.projects.tp.worktrees['test-ms'].type, 'milestone');
214
+ });
215
+
216
+ it('creates quick worktree with quick branch prefix', () => {
217
+ const result = runCmd(env.planDir, 'worktrees create fix-bug --type quick');
218
+ assert.ok(result.created);
219
+ assert.equal(result.type, 'quick');
220
+
221
+ const wtPath = result.repos['code-repo'];
222
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', {
223
+ cwd: wtPath, encoding: 'utf-8', stdio: 'pipe',
224
+ }).trim();
225
+ assert.equal(branch, 'quick/fix-bug');
226
+ });
227
+
228
+ it('skips creation if worktree already exists', () => {
229
+ runCmd(env.planDir, 'worktrees create dup --type milestone');
230
+ // Second call should not error
231
+ const result = runCmd(env.planDir, 'worktrees create dup --type milestone');
232
+ assert.ok(result.created);
233
+ });
234
+
235
+ it('errors when main checkout is not on base_branch', () => {
236
+ // Switch code repo to a different branch
237
+ execSync('git checkout -b feature', { cwd: env.codeDir, stdio: 'pipe', env: { ...process.env, ...GIT_ENV } });
238
+ const err = runCmdError(env.planDir, 'worktrees create off-branch --type milestone');
239
+ assert.ok(err, 'Should error');
240
+ assert.ok(err.includes('not on main'), 'Error should mention base branch: ' + err);
241
+ });
242
+ });
243
+
244
+ describe('worktree creation with setup command', () => {
245
+ let env;
246
+ beforeEach(() => { env = createTestEnv({ setup: 'echo setup-ran > setup-marker.txt' }); });
247
+ afterEach(() => { env.cleanup(); });
248
+
249
+ it('runs setup command when REPOS.md has setup field', () => {
250
+ const result = runCmd(env.planDir, 'worktrees create with-setup --type milestone');
251
+ const wtPath = result.repos['code-repo'];
252
+ // The setup command creates a marker file -- check slug is appended
253
+ const markerPath = path.join(wtPath, 'setup-marker.txt');
254
+ assert.ok(fs.existsSync(markerPath), 'Setup marker should exist at ' + markerPath);
255
+ });
256
+ });
257
+
258
+ describe('worktree creation with failing setup', () => {
259
+ let env;
260
+ beforeEach(() => { env = createTestEnv({ setup: 'exit 1' }); });
261
+ afterEach(() => { env.cleanup(); });
262
+
263
+ it('keeps worktree on setup failure and sets setup_complete false', () => {
264
+ const result = runCmd(env.planDir, 'worktrees create fail-setup --type milestone');
265
+ const wtPath = result.repos['code-repo'];
266
+ assert.ok(fs.existsSync(wtPath), 'Worktree should still exist after setup failure');
267
+
268
+ const config = readLocalConfig(env.planDir);
269
+ assert.equal(config.projects.tp.worktrees['fail-setup'].setup_complete, false);
270
+ });
271
+ });
272
+
273
+ // ─── worktree removal ────────────────────────────────────────────────────────
274
+
275
+ describe('worktree removal', () => {
276
+ let env;
277
+ beforeEach(() => { env = createTestEnv(); });
278
+ afterEach(() => { env.cleanup(); });
279
+
280
+ it('removes worktree directory, git registration, and branch', () => {
281
+ const created = runCmd(env.planDir, 'worktrees create to-remove --type milestone');
282
+ const wtPath = created.repos['code-repo'];
283
+ assert.ok(fs.existsSync(wtPath));
284
+
285
+ const removed = runCmd(env.planDir, 'worktrees remove to-remove');
286
+ assert.ok(removed.removed);
287
+
288
+ // Directory gone
289
+ assert.ok(!fs.existsSync(wtPath), 'Worktree dir should be gone');
290
+
291
+ // Branch gone
292
+ const branches = execSync('git branch', { cwd: env.codeDir, encoding: 'utf-8', stdio: 'pipe' });
293
+ assert.ok(!branches.includes('milestone/to-remove'), 'Branch should be deleted');
294
+
295
+ // Config entry gone
296
+ const config = readLocalConfig(env.planDir);
297
+ assert.ok(!config.projects.tp.worktrees || !config.projects.tp.worktrees['to-remove']);
298
+ });
299
+
300
+ it('clears active_context if removed worktree was active', () => {
301
+ runCmd(env.planDir, 'worktrees create active-wt --type milestone');
302
+
303
+ // Set active_context
304
+ const config = readLocalConfig(env.planDir);
305
+ if (!config.execution) config.execution = {};
306
+ config.execution.active_context = 'active-wt';
307
+ writeLocalConfig(env.planDir, config);
308
+
309
+ runCmd(env.planDir, 'worktrees remove active-wt');
310
+
311
+ const after = readLocalConfig(env.planDir);
312
+ assert.equal(after.execution.active_context, null);
313
+ });
314
+
315
+ it('handles already-deleted worktree directory gracefully', () => {
316
+ const created = runCmd(env.planDir, 'worktrees create del-first --type milestone');
317
+ const wtPath = created.repos['code-repo'];
318
+
319
+ // Manually delete the directory
320
+ fs.rmSync(wtPath, { recursive: true, force: true });
321
+
322
+ // Remove should not error
323
+ const removed = runCmd(env.planDir, 'worktrees remove del-first');
324
+ assert.ok(removed.removed);
325
+ });
326
+ });
327
+
328
+ // ─── worktree list ───────────────────────────────────────────────────────────
329
+
330
+ describe('worktree list', () => {
331
+ let env;
332
+ beforeEach(() => { env = createTestEnv(); });
333
+ afterEach(() => { env.cleanup(); });
334
+
335
+ it('lists tracked worktrees with type, path, and status', () => {
336
+ runCmd(env.planDir, 'worktrees create list-test --type milestone');
337
+
338
+ const list = runCmd(env.planDir, 'worktrees list');
339
+ assert.ok(Array.isArray(list));
340
+ assert.ok(list.length >= 1);
341
+
342
+ const entry = list.find(e => e.slug === 'list-test');
343
+ assert.ok(entry);
344
+ assert.equal(entry.type, 'milestone');
345
+ assert.equal(entry.stale, false);
346
+ assert.ok(entry.repos['code-repo'].exists);
347
+ });
348
+
349
+ it('marks worktrees with missing directories as stale', () => {
350
+ const created = runCmd(env.planDir, 'worktrees create stale-test --type milestone');
351
+ const wtPath = created.repos['code-repo'];
352
+
353
+ // Manually delete directory
354
+ fs.rmSync(wtPath, { recursive: true, force: true });
355
+ // Also prune git worktree registry
356
+ execSync('git worktree prune', { cwd: env.codeDir, stdio: 'pipe' });
357
+
358
+ const list = runCmd(env.planDir, 'worktrees list');
359
+ const entry = list.find(e => e.slug === 'stale-test');
360
+ assert.ok(entry);
361
+ assert.equal(entry.stale, true);
362
+ assert.equal(entry.repos['code-repo'].exists, false);
363
+ });
364
+ });
365
+
366
+ // ─── worktree setup re-run ───────────────────────────────────────────────────
367
+
368
+ describe('worktree setup re-run', () => {
369
+ let env;
370
+ beforeEach(() => { env = createTestEnv({ setup: 'echo re-ran > setup-rerun.txt' }); });
371
+ afterEach(() => { env.cleanup(); });
372
+
373
+ it('re-runs setup command and updates setup_complete flag', () => {
374
+ // First create with a working setup
375
+ runCmd(env.planDir, 'worktrees create setup-rerun --type milestone');
376
+
377
+ // Run setup again
378
+ const result = runCmd(env.planDir, 'worktrees setup setup-rerun');
379
+ assert.ok(result.setup_complete);
380
+ assert.equal(result.results['code-repo'], 'success');
381
+ });
382
+ });
383
+
384
+ // ─── setup command $2 worktree path argument ─────────────────────────────────
385
+
386
+ describe('setup command receives worktree path as second argument', () => {
387
+ let env;
388
+ beforeEach(() => { env = createTestEnv({ setup: 'echo "$1 $2" > setup-args.txt' }); });
389
+ afterEach(() => { env.cleanup(); });
390
+
391
+ it('setup command receives worktree path as second argument', () => {
392
+ const result = runCmd(env.planDir, 'worktrees create path-arg --type milestone');
393
+ const wtPath = result.repos['code-repo'];
394
+ const argsFile = path.join(wtPath, 'setup-args.txt');
395
+ assert.ok(fs.existsSync(argsFile), 'setup-args.txt should exist at ' + argsFile);
396
+
397
+ const content = fs.readFileSync(argsFile, 'utf-8').trim();
398
+ // Content should contain slug and worktree path separated by space
399
+ assert.ok(content.startsWith('path-arg '), 'Should start with slug: ' + content);
400
+ assert.ok(content.includes(wtPath), 'Should contain worktree path: ' + content);
401
+ });
402
+
403
+ it('setup re-run passes worktree path as second argument', () => {
404
+ // First create worktree
405
+ runCmd(env.planDir, 'worktrees create rerun-path --type milestone');
406
+
407
+ // Update REPOS.md to use a different marker file for re-run
408
+ const reposPath = path.join(env.planDir, 'REPOS.md');
409
+ let reposMd = fs.readFileSync(reposPath, 'utf-8');
410
+ reposMd = reposMd.replace('echo "$1 $2" > setup-args.txt', 'echo "$1 $2" > setup-rerun-args.txt');
411
+ fs.writeFileSync(reposPath, reposMd);
412
+
413
+ // Re-run setup
414
+ const result = runCmd(env.planDir, 'worktrees setup rerun-path');
415
+ assert.ok(result.setup_complete);
416
+
417
+ const config = readLocalConfig(env.planDir);
418
+ const wtPath = config.projects.tp.worktrees['rerun-path'].repos['code-repo'];
419
+ const argsFile = path.join(wtPath, 'setup-rerun-args.txt');
420
+ assert.ok(fs.existsSync(argsFile), 'setup-rerun-args.txt should exist at ' + argsFile);
421
+
422
+ const content = fs.readFileSync(argsFile, 'utf-8').trim();
423
+ assert.ok(content.startsWith('rerun-path '), 'Should start with slug: ' + content);
424
+ assert.ok(content.includes(wtPath), 'Should contain worktree path: ' + content);
425
+ });
426
+ });
427
+
428
+ // ─── worktree prune ──────────────────────────────────────────────────────────
429
+
430
+ describe('worktree prune', () => {
431
+ let env;
432
+ beforeEach(() => { env = createTestEnv(); });
433
+ afterEach(() => { env.cleanup(); });
434
+
435
+ it('removes orphaned entries from config when directory is missing', () => {
436
+ const created = runCmd(env.planDir, 'worktrees create prune-test --type milestone');
437
+ const wtPath = created.repos['code-repo'];
438
+
439
+ // Manually delete
440
+ fs.rmSync(wtPath, { recursive: true, force: true });
441
+ execSync('git worktree prune', { cwd: env.codeDir, stdio: 'pipe' });
442
+
443
+ const result = runCmd(env.planDir, 'worktrees prune');
444
+ assert.ok(result.pruned.includes('prune-test'));
445
+ assert.ok(result.count >= 1);
446
+
447
+ // Config entry should be gone
448
+ const config = readLocalConfig(env.planDir);
449
+ assert.ok(!config.projects.tp.worktrees || !config.projects.tp.worktrees['prune-test']);
450
+ });
451
+
452
+ it('cleans up orphaned git branches', () => {
453
+ runCmd(env.planDir, 'worktrees create prune-branch --type milestone');
454
+ const created = readLocalConfig(env.planDir);
455
+ const wtPath = created.projects.tp.worktrees['prune-branch'].repos['code-repo'];
456
+
457
+ // Manually delete directory
458
+ fs.rmSync(wtPath, { recursive: true, force: true });
459
+ execSync('git worktree prune', { cwd: env.codeDir, stdio: 'pipe' });
460
+
461
+ runCmd(env.planDir, 'worktrees prune');
462
+
463
+ // Branch should be gone
464
+ const branches = execSync('git branch', { cwd: env.codeDir, encoding: 'utf-8', stdio: 'pipe' });
465
+ assert.ok(!branches.includes('milestone/prune-branch'), 'Branch should be deleted');
466
+ });
467
+
468
+ it('clears active_context if pruned worktree was active', () => {
469
+ runCmd(env.planDir, 'worktrees create prune-active --type milestone');
470
+
471
+ // Set active_context
472
+ const config = readLocalConfig(env.planDir);
473
+ if (!config.execution) config.execution = {};
474
+ config.execution.active_context = 'prune-active';
475
+ writeLocalConfig(env.planDir, config);
476
+
477
+ // Delete directory and prune
478
+ const wtPath = config.projects.tp.worktrees['prune-active'].repos['code-repo'];
479
+ fs.rmSync(wtPath, { recursive: true, force: true });
480
+ execSync('git worktree prune', { cwd: env.codeDir, stdio: 'pipe' });
481
+
482
+ runCmd(env.planDir, 'worktrees prune');
483
+
484
+ const after = readLocalConfig(env.planDir);
485
+ assert.equal(after.execution.active_context, null);
486
+ });
487
+ });
488
+
489
+ // ─── resolveCodeContext ──────────────────────────────────────────────────────
490
+
491
+ describe('resolveCodeContext', () => {
492
+ let env;
493
+ beforeEach(() => { env = createTestEnv(); });
494
+ afterEach(() => { env.cleanup(); });
495
+
496
+ it('returns main checkout when active_context is null', () => {
497
+ const config = readLocalConfig(env.planDir);
498
+ config.execution = { active_context: null };
499
+ writeLocalConfig(env.planDir, config);
500
+
501
+ resetPaths();
502
+ initPaths(env.planDir);
503
+ const { resolveCodeContext } = require('./context.cjs');
504
+ const result = resolveCodeContext(env.planDir, 'code-repo');
505
+ assert.equal(result.type, 'main');
506
+ });
507
+
508
+ it('returns main checkout when active_context is unset', () => {
509
+ // No execution key at all
510
+ const config = readLocalConfig(env.planDir);
511
+ delete config.execution;
512
+ writeLocalConfig(env.planDir, config);
513
+
514
+ resetPaths();
515
+ initPaths(env.planDir);
516
+ const { resolveCodeContext } = require('./context.cjs');
517
+ const result = resolveCodeContext(env.planDir, 'code-repo');
518
+ assert.equal(result.type, 'main');
519
+ });
520
+
521
+ it('returns milestone worktree directory when milestone context active', () => {
522
+ const created = runCmd(env.planDir, 'worktrees create ctx-ms --type milestone');
523
+ const wtPath = created.repos['code-repo'];
524
+
525
+ // Set active context
526
+ const config = readLocalConfig(env.planDir);
527
+ config.execution = { active_context: 'ctx-ms' };
528
+ writeLocalConfig(env.planDir, config);
529
+
530
+ resetPaths();
531
+ initPaths(env.planDir);
532
+ const { resolveCodeContext } = require('./context.cjs');
533
+ const result = resolveCodeContext(env.planDir, 'code-repo');
534
+ assert.equal(result.type, 'milestone');
535
+ assert.equal(result.directory, wtPath);
536
+ assert.equal(result.slug, 'ctx-ms');
537
+ });
538
+
539
+ it('returns quick worktree with mode when quick context active', () => {
540
+ const created = runCmd(env.planDir, 'worktrees create ctx-quick --type quick --mode debug');
541
+
542
+ // Set active context
543
+ const config = readLocalConfig(env.planDir);
544
+ config.execution = { active_context: 'ctx-quick' };
545
+ writeLocalConfig(env.planDir, config);
546
+
547
+ resetPaths();
548
+ initPaths(env.planDir);
549
+ const { resolveCodeContext } = require('./context.cjs');
550
+ const result = resolveCodeContext(env.planDir, 'code-repo');
551
+ assert.equal(result.type, 'quick');
552
+ assert.equal(result.mode, 'debug');
553
+ });
554
+
555
+ it('detects stale context and falls back to main when directory missing', () => {
556
+ const created = runCmd(env.planDir, 'worktrees create ctx-stale --type milestone');
557
+ const wtPath = created.repos['code-repo'];
558
+
559
+ // Set active context then delete directory
560
+ const config = readLocalConfig(env.planDir);
561
+ config.execution = { active_context: 'ctx-stale' };
562
+ writeLocalConfig(env.planDir, config);
563
+
564
+ fs.rmSync(wtPath, { recursive: true, force: true });
565
+ execSync('git worktree prune', { cwd: env.codeDir, stdio: 'pipe' });
566
+
567
+ resetPaths();
568
+ initPaths(env.planDir);
569
+ const { resolveCodeContext } = require('./context.cjs');
570
+ const result = resolveCodeContext(env.planDir, 'code-repo');
571
+ assert.equal(result.type, 'main');
572
+
573
+ // active_context should be cleared
574
+ const after = readLocalConfig(env.planDir);
575
+ assert.equal(after.execution.active_context, null);
576
+ });
577
+
578
+ it('detects stale context when config entry is missing', () => {
579
+ // Set active context to non-existent slug
580
+ const config = readLocalConfig(env.planDir);
581
+ config.execution = { active_context: 'nonexistent' };
582
+ writeLocalConfig(env.planDir, config);
583
+
584
+ resetPaths();
585
+ initPaths(env.planDir);
586
+ const { resolveCodeContext } = require('./context.cjs');
587
+ const result = resolveCodeContext(env.planDir, 'code-repo');
588
+ assert.equal(result.type, 'main');
589
+
590
+ // active_context should be cleared
591
+ const after = readLocalConfig(env.planDir);
592
+ assert.equal(after.execution.active_context, null);
593
+ });
594
+ });
595
+
596
+ // ─── rebaseAndMerge ──────────────────────────────────────────────────────────
597
+
598
+ /**
599
+ * Create test env with a bare remote for rebase/merge/push testing.
600
+ * Layout:
601
+ * {tmpDir}/
602
+ * planning/ <- DGS planning root
603
+ * remote.git/ <- bare remote
604
+ * code-repo/ <- clone of remote (main checkout)
605
+ */
606
+ function createRebaseTestEnv() {
607
+ const tmpDir = fs.realpathSync(fs.mkdtempSync(path.join(require('os').tmpdir(), 'dgs-rb-')));
608
+ const planDir = path.join(tmpDir, 'planning');
609
+ const remoteDir = path.join(tmpDir, 'remote.git');
610
+ const codeDir = path.join(tmpDir, 'code-repo');
611
+ const fullEnv = { ...process.env, ...GIT_ENV };
612
+
613
+ // Create a regular repo first, then clone it bare
614
+ const seedDir = path.join(tmpDir, '_seed');
615
+ fs.mkdirSync(seedDir, { recursive: true });
616
+ execSync('git init -b main', { cwd: seedDir, stdio: 'pipe', env: fullEnv });
617
+ execSync('git config user.email "test@test.com"', { cwd: seedDir, stdio: 'pipe' });
618
+ execSync('git config user.name "Test"', { cwd: seedDir, stdio: 'pipe' });
619
+ fs.writeFileSync(path.join(seedDir, 'file.txt'), 'initial');
620
+ execSync('git add . && git commit -m "initial"', { cwd: seedDir, stdio: 'pipe', env: fullEnv });
621
+
622
+ // Create bare remote from seed
623
+ execSync('git clone --bare "' + seedDir + '" remote.git', { cwd: tmpDir, stdio: 'pipe', env: fullEnv });
624
+ fs.rmSync(seedDir, { recursive: true, force: true });
625
+
626
+ // Clone to main checkout
627
+ execSync('git clone "' + remoteDir + '" code-repo', { cwd: tmpDir, stdio: 'pipe', env: fullEnv });
628
+ execSync('git config user.email "test@test.com"', { cwd: codeDir, stdio: 'pipe' });
629
+ execSync('git config user.name "Test"', { cwd: codeDir, stdio: 'pipe' });
630
+
631
+ // Create planning root
632
+ fs.mkdirSync(planDir, { recursive: true });
633
+ execSync('git init -b main', { cwd: planDir, stdio: 'pipe', env: fullEnv });
634
+ execSync('git config user.email "test@test.com"', { cwd: planDir, stdio: 'pipe' });
635
+ execSync('git config user.name "Test"', { cwd: planDir, stdio: 'pipe' });
636
+
637
+ fs.writeFileSync(path.join(planDir, 'config.json'), JSON.stringify({
638
+ git: { base_branch: 'main' },
639
+ }, null, 2));
640
+ fs.writeFileSync(path.join(planDir, 'config.local.json'), JSON.stringify({
641
+ current_project: 'tp',
642
+ }, null, 2));
643
+ fs.writeFileSync(path.join(planDir, 'PROJECTS.md'), '# Projects\n');
644
+ fs.writeFileSync(path.join(planDir, 'REPOS.md'),
645
+ '# Repos\n\n' +
646
+ '| Name | Path | GitHub URL | Description |\n' +
647
+ '|------|------|------------|-------------|\n' +
648
+ '| code-repo | ' + path.relative(planDir, codeDir) + ' | | Test repo |\n'
649
+ );
650
+ fs.mkdirSync(path.join(planDir, 'projects', 'tp'), { recursive: true });
651
+ fs.writeFileSync(path.join(planDir, 'projects', 'tp', 'STATE.md'), '# State\n');
652
+ execSync('git add . && git commit -m "setup"', { cwd: planDir, stdio: 'pipe', env: fullEnv });
653
+
654
+ initPaths(planDir);
655
+
656
+ return {
657
+ tmpDir, planDir, codeDir, remoteDir, fullEnv,
658
+ cleanup: function() {
659
+ resetPaths();
660
+ try {
661
+ const parent = path.dirname(codeDir);
662
+ const entries = fs.readdirSync(parent);
663
+ for (const e of entries) {
664
+ if (e.startsWith('code-repo--')) {
665
+ fs.rmSync(path.join(parent, e), { recursive: true, force: true });
666
+ }
667
+ }
668
+ } catch { /* ignore */ }
669
+ fs.rmSync(tmpDir, { recursive: true, force: true });
670
+ },
671
+ };
672
+ }
673
+
674
+ /**
675
+ * Create a worktree in a rebase test env and return state.
676
+ */
677
+ function createWorktreeForRebase(env, slug) {
678
+ slug = slug || 'test-ms';
679
+ const result = runCmd(env.planDir, 'worktrees create ' + slug + ' --type milestone');
680
+ const wtPath = result.repos['code-repo'];
681
+ return { slug, wtPath, branchName: 'milestone/' + slug };
682
+ }
683
+
684
+ describe('rebaseAndMerge', () => {
685
+ let env;
686
+ beforeEach(() => { env = createRebaseTestEnv(); });
687
+ afterEach(() => { env.cleanup(); });
688
+
689
+ it('rebases milestone branch onto base_branch and merges', () => {
690
+ const { slug, wtPath } = createWorktreeForRebase(env);
691
+
692
+ // Add a commit on milestone branch in worktree
693
+ fs.writeFileSync(path.join(wtPath, 'feature.txt'), 'milestone work');
694
+ execSync('git add . && git commit -m "milestone feature"', { cwd: wtPath, stdio: 'pipe', env: env.fullEnv });
695
+
696
+ const { rebaseAndMerge } = require('./worktrees.cjs');
697
+ const result = rebaseAndMerge(env.planDir, 'code-repo', slug, { push: false });
698
+
699
+ assert.ok(result.success, 'Should succeed: ' + (result.error || ''));
700
+ assert.ok(result.merged);
701
+ assert.ok(!result.conflicted);
702
+
703
+ // Verify milestone commit is on main
704
+ const log = execSync('git log --oneline', { cwd: env.codeDir, encoding: 'utf-8', stdio: 'pipe' });
705
+ assert.ok(log.includes('milestone feature'), 'Main should have milestone commit');
706
+ });
707
+
708
+ it('handles divergent branches (base has new commits)', () => {
709
+ const { slug, wtPath } = createWorktreeForRebase(env);
710
+
711
+ // Add commit on main (push to remote so pull works)
712
+ fs.writeFileSync(path.join(env.codeDir, 'main-change.txt'), 'main work');
713
+ execSync('git add . && git commit -m "main change"', { cwd: env.codeDir, stdio: 'pipe', env: env.fullEnv });
714
+ execSync('git push origin main', { cwd: env.codeDir, stdio: 'pipe', env: env.fullEnv });
715
+
716
+ // Add commit on milestone branch
717
+ fs.writeFileSync(path.join(wtPath, 'ms-change.txt'), 'milestone work');
718
+ execSync('git add . && git commit -m "milestone change"', { cwd: wtPath, stdio: 'pipe', env: env.fullEnv });
719
+
720
+ const { rebaseAndMerge } = require('./worktrees.cjs');
721
+ const result = rebaseAndMerge(env.planDir, 'code-repo', slug, { push: false });
722
+
723
+ assert.ok(result.success, 'Should succeed: ' + (result.error || ''));
724
+ assert.ok(result.merged);
725
+
726
+ // Both commits should be on main
727
+ const log = execSync('git log --oneline', { cwd: env.codeDir, encoding: 'utf-8', stdio: 'pipe' });
728
+ assert.ok(log.includes('main change'), 'Main should have main commit');
729
+ assert.ok(log.includes('milestone change'), 'Main should have milestone commit');
730
+ });
731
+
732
+ it('detects conflicts and aborts cleanly', () => {
733
+ const { slug, wtPath } = createWorktreeForRebase(env);
734
+
735
+ // Create conflict: same file different content on both branches
736
+ fs.writeFileSync(path.join(env.codeDir, 'shared.txt'), 'main version');
737
+ execSync('git add . && git commit -m "main shared"', { cwd: env.codeDir, stdio: 'pipe', env: env.fullEnv });
738
+ execSync('git push origin main', { cwd: env.codeDir, stdio: 'pipe', env: env.fullEnv });
739
+
740
+ fs.writeFileSync(path.join(wtPath, 'shared.txt'), 'milestone version');
741
+ execSync('git add . && git commit -m "milestone shared"', { cwd: wtPath, stdio: 'pipe', env: env.fullEnv });
742
+
743
+ const { rebaseAndMerge } = require('./worktrees.cjs');
744
+ const result = rebaseAndMerge(env.planDir, 'code-repo', slug, { push: false });
745
+
746
+ assert.ok(!result.success);
747
+ assert.ok(result.conflicted);
748
+ assert.ok(result.manualInstructions, 'Should have manual instructions');
749
+ assert.ok(result.manualInstructions.includes(wtPath), 'Instructions should include worktree path');
750
+
751
+ // Worktree should be in clean state (rebase aborted)
752
+ const status = execSync('git status --porcelain', { cwd: wtPath, encoding: 'utf-8', stdio: 'pipe' });
753
+ assert.equal(status.trim(), '', 'Worktree should be clean after abort');
754
+ });
755
+
756
+ it('skips rebase when already rebased (idempotent)', () => {
757
+ const { slug, wtPath, branchName } = createWorktreeForRebase(env);
758
+
759
+ // Add commit on milestone and manually rebase
760
+ fs.writeFileSync(path.join(wtPath, 'feature.txt'), 'work');
761
+ execSync('git add . && git commit -m "feature"', { cwd: wtPath, stdio: 'pipe', env: env.fullEnv });
762
+ execSync('git rebase main', { cwd: wtPath, stdio: 'pipe', env: env.fullEnv });
763
+
764
+ const { rebaseAndMerge } = require('./worktrees.cjs');
765
+ const result = rebaseAndMerge(env.planDir, 'code-repo', slug, { push: false });
766
+
767
+ assert.ok(result.success, 'Should succeed: ' + (result.error || ''));
768
+ assert.ok(result.merged);
769
+ });
770
+
771
+ it('pushes to remote after merge', () => {
772
+ const { slug, wtPath } = createWorktreeForRebase(env);
773
+
774
+ fs.writeFileSync(path.join(wtPath, 'pushed.txt'), 'push test');
775
+ execSync('git add . && git commit -m "to push"', { cwd: wtPath, stdio: 'pipe', env: env.fullEnv });
776
+
777
+ const { rebaseAndMerge } = require('./worktrees.cjs');
778
+ const result = rebaseAndMerge(env.planDir, 'code-repo', slug, { push: true });
779
+
780
+ assert.ok(result.success, 'Should succeed: ' + (result.error || ''));
781
+ assert.ok(result.pushed, 'Should have pushed');
782
+
783
+ // Verify remote has the commit
784
+ const remoteLog = execSync('git log --oneline', { cwd: env.remoteDir, encoding: 'utf-8', stdio: 'pipe' });
785
+ assert.ok(remoteLog.includes('to push'), 'Remote should have pushed commit');
786
+ });
787
+
788
+ it('returns error when worktree slug not found', () => {
789
+ const { rebaseAndMerge } = require('./worktrees.cjs');
790
+ const result = rebaseAndMerge(env.planDir, 'code-repo', 'nonexistent', { push: false });
791
+
792
+ assert.ok(!result.success);
793
+ assert.ok(result.error.includes('nonexistent'));
794
+ });
795
+ });
796
+
797
+ // ─── checkWorktreeHealth ─────────────────────────────────────────────────────
798
+
799
+ describe('checkWorktreeHealth', () => {
800
+ let env;
801
+ beforeEach(() => { env = createRebaseTestEnv(); });
802
+ afterEach(() => { env.cleanup(); });
803
+
804
+ it('reports healthy when branch correct and no uncommitted changes', () => {
805
+ createWorktreeForRebase(env);
806
+
807
+ const { checkWorktreeHealth } = require('./worktrees.cjs');
808
+ const result = checkWorktreeHealth(env.planDir, 'test-ms');
809
+
810
+ assert.ok(result.healthy);
811
+ assert.equal(result.issues.length, 0);
812
+ });
813
+
814
+ it('detects wrong branch', () => {
815
+ const { wtPath } = createWorktreeForRebase(env);
816
+
817
+ // Switch worktree to a different branch
818
+ execSync('git checkout -b wrong-branch', { cwd: wtPath, stdio: 'pipe', env: env.fullEnv });
819
+
820
+ const { checkWorktreeHealth } = require('./worktrees.cjs');
821
+ const result = checkWorktreeHealth(env.planDir, 'test-ms');
822
+
823
+ assert.ok(!result.healthy);
824
+ assert.ok(result.issues.length > 0);
825
+ assert.ok(result.issues[0].includes('expected branch'));
826
+ });
827
+
828
+ it('detects uncommitted changes', () => {
829
+ const { wtPath } = createWorktreeForRebase(env);
830
+
831
+ // Create uncommitted change
832
+ fs.writeFileSync(path.join(wtPath, 'dirty.txt'), 'uncommitted');
833
+
834
+ const { checkWorktreeHealth } = require('./worktrees.cjs');
835
+ const result = checkWorktreeHealth(env.planDir, 'test-ms');
836
+
837
+ assert.ok(!result.healthy);
838
+ assert.ok(result.issues.some(i => i.includes('uncommitted')));
839
+ });
840
+
841
+ it('detects missing worktree directory', () => {
842
+ const { wtPath } = createWorktreeForRebase(env);
843
+
844
+ // Delete the worktree directory
845
+ fs.rmSync(wtPath, { recursive: true, force: true });
846
+
847
+ const { checkWorktreeHealth } = require('./worktrees.cjs');
848
+ const result = checkWorktreeHealth(env.planDir, 'test-ms');
849
+
850
+ assert.ok(!result.healthy);
851
+ assert.ok(result.issues.some(i => i.includes('missing')));
852
+ });
853
+ });
854
+
855
+ // ─── main checkout invariant ─────────────────────────────────────────────────
856
+
857
+ describe('main checkout invariant', () => {
858
+ let env;
859
+ beforeEach(() => { env = createTestEnv(); });
860
+ afterEach(() => { env.cleanup(); });
861
+
862
+ it('main checkout remains on base_branch after worktree create and remove', () => {
863
+ // Verify main is on main before
864
+ let branch = execSync('git rev-parse --abbrev-ref HEAD', {
865
+ cwd: env.codeDir, encoding: 'utf-8', stdio: 'pipe',
866
+ }).trim();
867
+ assert.equal(branch, 'main');
868
+
869
+ // Create worktree
870
+ runCmd(env.planDir, 'worktrees create inv-test --type milestone');
871
+
872
+ // Verify main still on main
873
+ branch = execSync('git rev-parse --abbrev-ref HEAD', {
874
+ cwd: env.codeDir, encoding: 'utf-8', stdio: 'pipe',
875
+ }).trim();
876
+ assert.equal(branch, 'main');
877
+
878
+ // Remove worktree
879
+ runCmd(env.planDir, 'worktrees remove inv-test');
880
+
881
+ // Verify main still on main
882
+ branch = execSync('git rev-parse --abbrev-ref HEAD', {
883
+ cwd: env.codeDir, encoding: 'utf-8', stdio: 'pipe',
884
+ }).trim();
885
+ assert.equal(branch, 'main');
886
+ });
887
+ });