myrlin-workbook 0.8.3 → 0.8.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myrlin-workbook",
3
- "version": "0.8.3",
3
+ "version": "0.8.4",
4
4
  "description": "Browser-based project manager for Claude Code sessions - session discovery, multi-terminal, cost tracking, docs, and kanban board",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -310,7 +310,7 @@ class Store extends EventEmitter {
310
310
 
311
311
  // ─── Session CRUD ────────────────────────────────────────
312
312
 
313
- createSession({ name, workspaceId, workingDir = '', topic = '', command = 'claude', resumeSessionId = null, tags = [] }) {
313
+ createSession({ name, workspaceId, workingDir = '', topic = '', command = 'claude', resumeSessionId = null, tags = [], initialPrompt = null, flags = [] }) {
314
314
  if (!this._state.workspaces[workspaceId]) return null;
315
315
  const id = crypto.randomUUID();
316
316
  const now = new Date().toISOString();
@@ -325,6 +325,8 @@ class Store extends EventEmitter {
325
325
  status: 'stopped', // 'running' | 'stopped' | 'error' | 'idle'
326
326
  pid: null,
327
327
  tags: Array.isArray(tags) ? tags : [],
328
+ initialPrompt: initialPrompt || null,
329
+ flags: Array.isArray(flags) ? flags : [],
328
330
  createdAt: now,
329
331
  lastActive: now,
330
332
  logs: [],
@@ -125,7 +125,7 @@ class PtySessionManager {
125
125
  * @param {boolean} [options.bypassPermissions=false] - If true, adds --dangerously-skip-permissions
126
126
  * @returns {PtySession} The PTY session object
127
127
  */
128
- spawnSession(sessionId, { command = 'claude', cwd, cols = 120, rows = 30, bypassPermissions = false, resumeSessionId = null, verbose = false, model = null, agentTeams = false, shell: requestedShell = null, newSession = false } = {}) {
128
+ spawnSession(sessionId, { command = 'claude', cwd, cols = 120, rows = 30, bypassPermissions = false, resumeSessionId = null, verbose = false, model = null, agentTeams = false, shell: requestedShell = null, newSession = false, initialPrompt = null, flags = [] } = {}) {
129
129
  // Return existing session if already alive
130
130
  const existing = this.sessions.get(sessionId);
131
131
  if (existing && existing.alive) {
@@ -154,11 +154,30 @@ class PtySessionManager {
154
154
  if (resumeSessionId) {
155
155
  fullCommand += ' --resume ' + resumeSessionId;
156
156
  } else if (cwd && !newSession) {
157
- // No explicit session to resume - use --continue to pick up most recent
158
- // conversation in this working directory. On a fresh dir with no history,
159
- // Claude will start a new conversation (same as bare `claude`).
160
- // Skip --continue when newSession is true (user explicitly wants a fresh session).
161
- fullCommand += ' --continue';
157
+ // Only add --continue when there is actually conversation history for this
158
+ // working directory. `claude --continue` exits with code 1 ("No conversation
159
+ // found to continue") on a fresh directory e.g. a brand-new git worktree.
160
+ // Scan ~/.claude/projects/ for any JSONL file whose encoded path matches cwd.
161
+ let hasHistory = false;
162
+ try {
163
+ const claudeDir = path.join(os.homedir(), '.claude', 'projects');
164
+ if (fs.existsSync(claudeDir)) {
165
+ const normalizedCwd = cwd.replace(/[/\\]/g, path.sep);
166
+ const match = fs.readdirSync(claudeDir).find(d => {
167
+ try {
168
+ return decodeURIComponent(d).replace(/[/\\]/g, path.sep) === normalizedCwd;
169
+ } catch (_) { return false; }
170
+ });
171
+ if (match) {
172
+ const projDir = path.join(claudeDir, match);
173
+ hasHistory = fs.readdirSync(projDir).some(f => f.endsWith('.jsonl'));
174
+ }
175
+ }
176
+ } catch (_) { /* filesystem error — fall through, don't add --continue */ }
177
+ if (hasHistory) {
178
+ fullCommand += ' --continue';
179
+ }
180
+ // If no history, run bare `claude` — starts a fresh session without error.
162
181
  }
163
182
  if (bypassPermissions) {
164
183
  fullCommand += ' --dangerously-skip-permissions';
@@ -169,6 +188,19 @@ class PtySessionManager {
169
188
  if (model) {
170
189
  fullCommand += ' --model ' + model;
171
190
  }
191
+ // Extra flags (e.g. from worktree task checkboxes), validated upstream
192
+ if (Array.isArray(flags)) {
193
+ for (const f of flags) {
194
+ if (f && /^[a-zA-Z0-9-]+$/.test(f)) {
195
+ fullCommand += ' --' + f;
196
+ }
197
+ }
198
+ }
199
+ // Initial prompt: appended as the last argument on first launch only
200
+ if (initialPrompt && typeof initialPrompt === 'string') {
201
+ const escaped = initialPrompt.replace(/'/g, "'\\''");
202
+ fullCommand += " '" + escaped + "'";
203
+ }
172
204
 
173
205
  // Validate cwd exists. If the provided path is invalid (e.g. an encoded
174
206
  // directory name like "-Users-jane-project"), resolve the real cwd from
@@ -479,6 +511,8 @@ class PtySessionManager {
479
511
  model: storeSession.model || null,
480
512
  agentTeams: storeSession.agentTeams || false,
481
513
  resumeSessionId: storeSession.resumeSessionId || null,
514
+ initialPrompt: storeSession.resumeSessionId ? null : (storeSession.initialPrompt || null),
515
+ flags: storeSession.resumeSessionId ? [] : (storeSession.flags || []),
482
516
  ...spawnOpts,
483
517
  });
484
518
  } else {
@@ -1239,9 +1239,9 @@
1239
1239
  <div class="form-group">
1240
1240
  <label class="form-label">Flags</label>
1241
1241
  <div class="form-checkboxes" id="new-task-flags">
1242
- <label class="form-checkbox"><input type="checkbox" value="--dangerously-skip-permissions" /> Skip Permissions</label>
1243
- <label class="form-checkbox"><input type="checkbox" value="--verbose" /> Verbose</label>
1244
- <label class="form-checkbox"><input type="checkbox" value="--agent-teams" id="new-task-agent-teams" /> Agent Teams</label>
1242
+ <label class="form-checkbox"><input type="checkbox" value="dangerously-skip-permissions" /> Skip Permissions</label>
1243
+ <label class="form-checkbox"><input type="checkbox" value="verbose" /> Verbose</label>
1244
+ <label class="form-checkbox"><input type="checkbox" value="agent-teams" id="new-task-agent-teams" /> Agent Teams</label>
1245
1245
  </div>
1246
1246
  </div>
1247
1247
  <details class="form-help-details" style="margin-top: 4px; margin-bottom: 8px;">
package/src/web/server.js CHANGED
@@ -3219,7 +3219,8 @@ app.post('/api/sessions/:id/spinoff-batch', requireAuth, async (req, res) => {
3219
3219
 
3220
3220
  // Create session
3221
3221
  const sessionName = (t.branch || t.title).replace(/^feat\//, '') + ' (spinoff)';
3222
- const newSession = store.createSession(workspaceId, {
3222
+ const newSession = store.createSession({
3223
+ workspaceId,
3223
3224
  name: sessionName,
3224
3225
  workingDir: worktreePath,
3225
3226
  command: 'claude',
@@ -4767,7 +4768,7 @@ app.get('/api/worktree-tasks', requireAuth, async (req, res) => {
4767
4768
  * Create a worktree task: creates git worktree, session, and task record.
4768
4769
  */
4769
4770
  app.post('/api/worktree-tasks', requireAuth, async (req, res) => {
4770
- const { workspaceId, repoDir, branch, description, baseBranch, featureId, model, tags, startNow } = req.body || {};
4771
+ const { workspaceId, repoDir, branch, description, baseBranch, featureId, model, tags, startNow, prompt, flags } = req.body || {};
4771
4772
  if (!workspaceId) return res.status(400).json({ error: 'workspaceId is required' });
4772
4773
  if (!repoDir) return res.status(400).json({ error: 'repoDir is required' });
4773
4774
  if (!branch) return res.status(400).json({ error: 'branch is required' });
@@ -4807,7 +4808,7 @@ app.post('/api/worktree-tasks', requireAuth, async (req, res) => {
4807
4808
  const root = await gitRepoRoot(repoDir);
4808
4809
  if (!root) return res.status(400).json({ error: 'Not a git repository' });
4809
4810
  const repoName = path.basename(root);
4810
- const worktreePath = path.join(path.dirname(root), `${repoName}-wt`, branch.replace(/\//g, '-'));
4811
+ let worktreePath = path.join(path.dirname(root), `${repoName}-wt`, branch.replace(/\//g, '-'));
4811
4812
 
4812
4813
  let branchExists = false;
4813
4814
  try {
@@ -4815,11 +4816,44 @@ app.post('/api/worktree-tasks', requireAuth, async (req, res) => {
4815
4816
  branchExists = true;
4816
4817
  } catch {}
4817
4818
 
4818
- const args = ['worktree', 'add'];
4819
- if (!branchExists) args.push('-b', branch);
4820
- args.push(worktreePath);
4821
- if (branchExists) args.push(branch);
4822
- await gitExec(args, root);
4819
+ // Check existing worktrees to avoid two fatal git errors:
4820
+ // 1. "already exists" — target path is already a registered worktree
4821
+ // 2. "already checked out" — the branch is checked out in a different worktree
4822
+ // Parse `git worktree list --porcelain` once and handle both cases.
4823
+ let skipWorktreeAdd = false;
4824
+ try {
4825
+ const listOut = await gitExec(['worktree', 'list', '--porcelain'], root);
4826
+
4827
+ // Case 1: exact path already registered — reuse it as-is
4828
+ if (listOut.includes(`worktree ${worktreePath}`)) {
4829
+ skipWorktreeAdd = true;
4830
+ }
4831
+
4832
+ // Case 2: branch already checked out in a *different* worktree path —
4833
+ // redirect worktreePath to that existing location so the rest of task
4834
+ // creation (session, record) still succeeds pointing at the right dir.
4835
+ if (!skipWorktreeAdd) {
4836
+ const branchRef = `refs/heads/${branch}`;
4837
+ const blocks = listOut.split('\n\n').filter(Boolean);
4838
+ for (const block of blocks) {
4839
+ const pathMatch = block.match(/^worktree (.+)$/m);
4840
+ const branchMatch = block.match(/^branch (.+)$/m);
4841
+ if (pathMatch && branchMatch && branchMatch[1].trim() === branchRef) {
4842
+ worktreePath = pathMatch[1].trim();
4843
+ skipWorktreeAdd = true;
4844
+ break;
4845
+ }
4846
+ }
4847
+ }
4848
+ } catch {}
4849
+
4850
+ if (!skipWorktreeAdd) {
4851
+ const args = ['worktree', 'add'];
4852
+ if (!branchExists) args.push('-b', branch);
4853
+ args.push(worktreePath);
4854
+ if (branchExists) args.push(branch);
4855
+ await gitExec(args, root);
4856
+ }
4823
4857
 
4824
4858
  // 1.5. Run init hooks (copy_files and init_script) if configured
4825
4859
  const initHooks = store.getWorktreeInitHooks();
@@ -4855,11 +4889,16 @@ app.post('/api/worktree-tasks', requireAuth, async (req, res) => {
4855
4889
 
4856
4890
  // 2. Create a session in this workspace pointing at the worktree
4857
4891
  const sessionName = branch.replace(/^feat\//, '') + ' (worktree task)';
4858
- const session = store.createSession(workspaceId, {
4892
+ const safePrompt = (typeof prompt === 'string' && prompt.trim()) ? prompt.trim() : null;
4893
+ const safeFlags = Array.isArray(flags) ? flags.filter(f => typeof f === 'string' && /^[a-zA-Z0-9-]+$/.test(f)) : [];
4894
+ const session = store.createSession({
4895
+ workspaceId,
4859
4896
  name: sessionName,
4860
4897
  workingDir: worktreePath,
4861
4898
  command: 'claude',
4862
4899
  model: model || undefined,
4900
+ initialPrompt: safePrompt,
4901
+ flags: safeFlags,
4863
4902
  });
4864
4903
  if (!session) {
4865
4904
  return res.status(500).json({ error: 'Failed to create session' });