@tsuzuku/arise 1.0.0 → 1.0.1

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/README.md CHANGED
@@ -38,8 +38,12 @@ arise --branch feature/login
38
38
  # Explicitly specify a preset
39
39
  arise --branch feature/login --preset laravel
40
40
 
41
- # Specify base branch or custom workspace name
42
- arise --branch feature/login --source develop --focus agy
41
+ # Choose your AI CLI agent (e.g. Claude Code, Aider, Antigravity, or Copilot)
42
+ arise --branch feature/login --agent claude
43
+ arise --branch feature/login -a aider
44
+
45
+ # Specify base branch, custom workspace name, or focus pane
46
+ arise --branch feature/login --source develop --focus claude
43
47
  ```
44
48
 
45
49
  ### 2. Nuke / Clean Up a Worktree
@@ -82,7 +86,8 @@ Place a `.ariserc.json` in your repository root, worktrees base directory, or `~
82
86
  },
83
87
  "workspace": {
84
88
  "labelPrefix": "[API] ",
85
- "defaultFocus": "agy"
89
+ "agent": "claude",
90
+ "defaultFocus": "claude"
86
91
  },
87
92
  "scaffold": {
88
93
  "envSource": "/path/to/shared/.env",
package/arise.schema.json CHANGED
@@ -34,15 +34,29 @@
34
34
  },
35
35
  "workspace": {
36
36
  "type": "object",
37
- "description": "Herdr workspace naming and focus configuration",
37
+ "description": "Herdr workspace naming, agent, and focus configuration",
38
38
  "properties": {
39
39
  "labelPrefix": {
40
40
  "type": "string",
41
41
  "description": "Prefix added to Herdr workspace labels (e.g. '[BE] ')"
42
42
  },
43
+ "agent": {
44
+ "type": ["string", "object", "null"],
45
+ "description": "CLI AI agent to run in the workspace pane ('agy', 'claude', 'aider', 'copilot', 'none', or custom command object)",
46
+ "properties": {
47
+ "cmd": {
48
+ "type": ["string", "null"],
49
+ "description": "Command to execute the agent"
50
+ },
51
+ "title": {
52
+ "type": "string",
53
+ "description": "Display title for the agent pane in Herdr"
54
+ }
55
+ }
56
+ },
43
57
  "defaultFocus": {
44
58
  "type": "string",
45
- "description": "Default pane to focus upon creation ('agy', 'vim', 'logs', 'server', 'shell')"
59
+ "description": "Default pane to focus upon creation ('agent', 'agy', 'claude', 'vim', 'logs', 'server', 'shell')"
46
60
  }
47
61
  },
48
62
  "additionalProperties": false
@@ -83,6 +97,10 @@
83
97
  "focus": {
84
98
  "type": "boolean",
85
99
  "description": "Whether this pane should receive focus by default"
100
+ },
101
+ "isAgent": {
102
+ "type": "boolean",
103
+ "description": "Whether this pane is designated as the AI CLI agent pane"
86
104
  }
87
105
  },
88
106
  "additionalProperties": false
package/lib/cli.js CHANGED
@@ -15,7 +15,8 @@ Creation Arguments:
15
15
  --workspace, -w <name> (Optional) Custom Herdr workspace name.
16
16
  --source, -s, --base <src> (Optional) Base branch if creating a new branch. Defaults to preset default (e.g. 'develop' / 'prod').
17
17
  --preset, -p <preset> (Optional) Project preset ('node', 'laravel', 'generic', or custom). Auto-detected if omitted.
18
- --focus, -f <pane> (Optional) Pane to focus ('agy', 'vim', 'logs', 'server', 'shell'). Defaults to 'agy'.
18
+ --agent, -a <agent> (Optional) AI CLI agent to run in workspace ('agy', 'claude', 'aider', 'copilot', etc.).
19
+ --focus, -f <pane> (Optional) Pane to focus ('agent', 'agy', 'claude', 'vim', 'logs', 'server', 'shell'). Defaults to active agent or 'agy'.
19
20
 
20
21
  Nuke / Cleanup Arguments:
21
22
  --nuke, -n [<target>] Nuke the worktree: closes Herdr workspace, removes worktree directory,
@@ -49,6 +50,7 @@ function parseArgs(argv = process.argv.slice(2)) {
49
50
  workspaceName: null,
50
51
  source: null,
51
52
  presetName: null,
53
+ agent: null,
52
54
  focusTarget: null,
53
55
  installSkill: false,
54
56
  skillScope: 'global',
@@ -97,6 +99,11 @@ function parseArgs(argv = process.argv.slice(2)) {
97
99
  } else if (arg === '--preset' || arg === '-p') {
98
100
  flags.presetName = argv[i + 1];
99
101
  i++;
102
+ } else if (arg === '--agent' || arg === '-a') {
103
+ flags.agent = argv[i + 1];
104
+ i++;
105
+ } else if (arg.startsWith('--agent=')) {
106
+ flags.agent = arg.slice(8);
100
107
  } else if (arg === '--focus') {
101
108
  flags.focusTarget = argv[i + 1];
102
109
  i++;
package/lib/config.js CHANGED
@@ -91,18 +91,56 @@ function resolveConfiguration(flags = {}, cwd = process.cwd()) {
91
91
  protectedBranches: (fileConfig.repo && fileConfig.repo.protectedBranches) || (preset.repo && preset.repo.protectedBranches) || ['main', 'master', 'develop', 'prod', 'staging'],
92
92
  };
93
93
 
94
- // 3. Merge Workspace Settings
94
+ // 3. Resolve AI CLI Agent
95
+ const resolvedAgent = flags.agent
96
+ || process.env.ARISE_AGENT
97
+ || (fileConfig.workspace && fileConfig.workspace.agent)
98
+ || (preset.workspace && preset.workspace.agent)
99
+ || 'agy';
100
+
101
+ // 4. Merge Workspace Settings
102
+ const defaultFocus = (fileConfig.workspace && fileConfig.workspace.defaultFocus)
103
+ || (preset.workspace && preset.workspace.defaultFocus)
104
+ || 'agy';
105
+
95
106
  const workspaceConfig = {
96
107
  labelPrefix: (fileConfig.workspace && fileConfig.workspace.labelPrefix !== undefined)
97
108
  ? fileConfig.workspace.labelPrefix
98
109
  : (preset.workspace && preset.workspace.labelPrefix !== undefined ? preset.workspace.labelPrefix : ''),
99
- defaultFocus: (fileConfig.workspace && fileConfig.workspace.defaultFocus)
100
- || (preset.workspace && preset.workspace.defaultFocus)
101
- || 'agy',
110
+ agent: resolvedAgent,
111
+ defaultFocus,
102
112
  };
103
113
 
104
- // 4. Merge Layout
105
- const layout = fileConfig.layout || preset.layout || [];
114
+ // 5. Merge & Customize Layout for Active Agent
115
+ const baseLayout = fileConfig.layout || preset.layout || [];
116
+ const layout = baseLayout.map((pane) => {
117
+ const isAgentPane = pane.isAgent || pane.id === 'agy' || pane.id === 'agent' || pane.id === 'ai';
118
+ if (!isAgentPane) return { ...pane };
119
+
120
+ let agentCmd = pane.cmd;
121
+ let agentTitle = pane.title;
122
+
123
+ if (typeof resolvedAgent === 'string') {
124
+ const lower = resolvedAgent.toLowerCase().trim();
125
+ if (lower === 'none' || lower === 'false' || lower === 'null' || lower === 'disabled') {
126
+ agentCmd = null;
127
+ agentTitle = 'shell';
128
+ } else {
129
+ agentCmd = resolvedAgent;
130
+ agentTitle = resolvedAgent;
131
+ }
132
+ } else if (typeof resolvedAgent === 'object' && resolvedAgent !== null) {
133
+ agentCmd = resolvedAgent.cmd !== undefined ? resolvedAgent.cmd : (resolvedAgent.command || null);
134
+ agentTitle = resolvedAgent.title || resolvedAgent.cmd || pane.title;
135
+ }
136
+
137
+ return {
138
+ ...pane,
139
+ cmd: agentCmd,
140
+ title: agentTitle,
141
+ isAgent: true,
142
+ };
143
+ });
106
144
 
107
145
  // 5. Merge Scaffolding Settings
108
146
  const scaffoldConfig = {
package/lib/git.js CHANGED
@@ -72,17 +72,47 @@ function getWorktrees(options = {}) {
72
72
  }
73
73
  }
74
74
 
75
+ function fetchOrigin(options = {}) {
76
+ const gitCmd = getGitPrefix(options);
77
+ try {
78
+ const remotes = execSync(`${gitCmd} remote`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim().split(/\s+/);
79
+ if (!remotes.includes('origin')) return;
80
+
81
+ // In bare repos, remote.origin.fetch is often not set by default.
82
+ // Setting or fetching with the standard refspec ensures refs/remotes/origin/* are populated.
83
+ try {
84
+ const fetchRefspec = execSync(`${gitCmd} config --get remote.origin.fetch`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
85
+ if (!fetchRefspec) {
86
+ execSync(`${gitCmd} config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"`, { stdio: 'ignore' });
87
+ }
88
+ } catch (e) {
89
+ try {
90
+ execSync(`${gitCmd} config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"`, { stdio: 'ignore' });
91
+ } catch (err) {}
92
+ }
93
+
94
+ execSync(`${gitCmd} fetch origin`, { stdio: 'inherit' });
95
+ } catch (e) {
96
+ // Network or remote fetch error - continue gracefully with local refs
97
+ }
98
+ }
99
+
75
100
  function branchExistsLocally(branch, options = {}) {
76
101
  const gitCmd = getGitPrefix(options);
77
102
  try {
78
- execSync(`${gitCmd} rev-parse --verify "${branch}"`, { stdio: 'ignore' });
103
+ execSync(`${gitCmd} rev-parse --verify "refs/heads/${branch}"`, { stdio: 'ignore' });
79
104
  return true;
80
105
  } catch (e) {
81
106
  try {
82
- const list = execSync(`${gitCmd} branch --list "${branch}"`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
83
- return list !== '';
107
+ execSync(`${gitCmd} rev-parse --verify "${branch}"`, { stdio: 'ignore' });
108
+ return true;
84
109
  } catch (err) {
85
- return false;
110
+ try {
111
+ const list = execSync(`${gitCmd} branch --list "${branch}"`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
112
+ return list !== '';
113
+ } catch (err2) {
114
+ return false;
115
+ }
86
116
  }
87
117
  }
88
118
  }
@@ -90,16 +120,21 @@ function branchExistsLocally(branch, options = {}) {
90
120
  function branchExistsRemotely(branch, options = {}) {
91
121
  const gitCmd = getGitPrefix(options);
92
122
  try {
93
- execSync(`${gitCmd} rev-parse --verify "origin/${branch}"`, { stdio: 'ignore' });
123
+ execSync(`${gitCmd} rev-parse --verify "refs/remotes/origin/${branch}"`, { stdio: 'ignore' });
94
124
  return true;
95
125
  } catch (e) {
96
126
  try {
97
- const remoteList = execSync(`${gitCmd} branch -r --list "origin/${branch}"`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
98
- if (remoteList) return true;
99
- const lsRemote = execSync(`${gitCmd} ls-remote --heads origin "${branch}"`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
100
- return Boolean(lsRemote);
127
+ execSync(`${gitCmd} rev-parse --verify "origin/${branch}"`, { stdio: 'ignore' });
128
+ return true;
101
129
  } catch (err) {
102
- return false;
130
+ try {
131
+ const remoteList = execSync(`${gitCmd} branch -r --list "origin/${branch}"`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
132
+ if (remoteList) return true;
133
+ const lsRemote = execSync(`${gitCmd} ls-remote --heads origin "${branch}"`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
134
+ return Boolean(lsRemote);
135
+ } catch (err2) {
136
+ return false;
137
+ }
103
138
  }
104
139
  }
105
140
  }
@@ -108,6 +143,9 @@ function createWorktree({ worktreePath, branch, source = 'develop', repoDir, bar
108
143
  const options = { repoDir, bareRepo };
109
144
  const gitCmd = getGitPrefix(options);
110
145
 
146
+ console.log('Fetching latest from origin...');
147
+ fetchOrigin(options);
148
+
111
149
  const localExists = branchExistsLocally(branch, options);
112
150
  const remoteExists = branchExistsRemotely(branch, options);
113
151
 
@@ -116,16 +154,32 @@ function createWorktree({ worktreePath, branch, source = 'develop', repoDir, bar
116
154
  execSync(`${gitCmd} worktree add "${worktreePath}" "${branch}"`, { stdio: 'inherit' });
117
155
  } else if (remoteExists) {
118
156
  console.log(`Branch "${branch}" exists on remote. Checking out and adding worktree...`);
119
- execSync(`${gitCmd} worktree add "${worktreePath}" -b "${branch}" "origin/${branch}"`, { stdio: 'inherit' });
157
+ let startPoint = `origin/${branch}`;
158
+ try {
159
+ execSync(`${gitCmd} rev-parse --verify "${startPoint}"`, { stdio: 'ignore' });
160
+ } catch (e) {
161
+ startPoint = branch;
162
+ }
163
+ if (bareRepo) {
164
+ execSync(`${gitCmd} branch --track "${branch}" "${startPoint}"`, { stdio: 'inherit' });
165
+ execSync(`${gitCmd} worktree add "${worktreePath}" "${branch}"`, { stdio: 'inherit' });
166
+ } else {
167
+ execSync(`${gitCmd} worktree add "${worktreePath}" -b "${branch}" "${startPoint}"`, { stdio: 'inherit' });
168
+ }
120
169
  } else {
121
170
  // Creating a brand new branch off source
122
- console.log('Fetching latest from origin...');
171
+ let startPoint = null;
123
172
  try {
124
- execSync(`${gitCmd} fetch origin`, { stdio: 'inherit' });
125
- } catch (e) {}
126
-
127
- const remoteSourceExists = branchExistsRemotely(source, options);
128
- const startPoint = remoteSourceExists ? `origin/${source}` : source;
173
+ execSync(`${gitCmd} rev-parse --verify "origin/${source}"`, { stdio: 'ignore' });
174
+ startPoint = `origin/${source}`;
175
+ } catch (e) {
176
+ try {
177
+ execSync(`${gitCmd} rev-parse --verify "${source}"`, { stdio: 'ignore' });
178
+ startPoint = source;
179
+ } catch (err) {
180
+ throw new Error(`Cannot create branch "${branch}" off "${source}": Base branch or ref "${source}" does not exist locally or on remote.`);
181
+ }
182
+ }
129
183
 
130
184
  console.log(`Creating branch "${branch}" off "${startPoint}" and adding worktree...`);
131
185
  if (bareRepo) {
@@ -156,7 +210,7 @@ function syncPrimaryBranch({ branch, worktreePath, repoDir, bareRepo, protectedB
156
210
  console.warn(`==> Please commit, stash, or discard your changes if you want to sync with origin.`);
157
211
  } else {
158
212
  console.log(`==> Syncing ${branch} with origin/${branch}...`);
159
- execSync(`${gitCmd} fetch origin ${branch}`, { stdio: 'inherit' });
213
+ fetchOrigin(options);
160
214
  const resetCmd = bareRepo
161
215
  ? `${gitCmd} --work-tree="${worktreePath}" reset --hard origin/${branch}`
162
216
  : `git -C "${worktreePath}" reset --hard origin/${branch}`;
@@ -215,6 +269,7 @@ module.exports = {
215
269
  getGitPrefix,
216
270
  getRepoRootDir,
217
271
  getWorktrees,
272
+ fetchOrigin,
218
273
  branchExistsLocally,
219
274
  branchExistsRemotely,
220
275
  createWorktree,
package/lib/layout.js CHANGED
@@ -7,7 +7,7 @@ const DEFAULT_QUADRANT_LAYOUT = [
7
7
  { id: 'vim', title: 'vim', cmd: 'vim .', position: 'root' },
8
8
  { id: 'server', title: 'server', cmd: null, split: 'right', from: 'vim' },
9
9
  { id: 'shell', title: 'shell', cmd: null, split: 'down', from: 'vim' },
10
- { id: 'agy', title: 'agy', cmd: 'agy', split: 'down', from: 'server', focus: true },
10
+ { id: 'agy', title: 'agy', cmd: 'agy', split: 'down', from: 'server', focus: true, isAgent: true },
11
11
  ];
12
12
 
13
13
  /**
@@ -17,7 +17,7 @@ const DEFAULT_QUADRANT_LAYOUT = [
17
17
  * @param {Array} options.layout Array of pane definitions
18
18
  * @param {string} options.rootPaneId ID of the root pane created with workspace
19
19
  * @param {string} options.cwd Working directory for panes
20
- * @param {string} options.focusTarget Name or ID of pane to focus (e.g. 'agy', 'vim', 'logs', 'shell')
20
+ * @param {string} options.focusTarget Name or ID of pane to focus (e.g. 'agent', 'agy', 'claude', 'vim', 'logs', 'shell')
21
21
  */
22
22
  function renderLayout({ layout = DEFAULT_QUADRANT_LAYOUT, rootPaneId, cwd, focusTarget = 'agy' }) {
23
23
  const paneMap = new Map(); // id -> paneId
@@ -31,7 +31,10 @@ function renderLayout({ layout = DEFAULT_QUADRANT_LAYOUT, rootPaneId, cwd, focus
31
31
  if (!normalizedFocus) return false;
32
32
  const idMatch = paneDef.id && paneDef.id.toLowerCase() === normalizedFocus;
33
33
  const titleMatch = paneDef.title && paneDef.title.toLowerCase() === normalizedFocus;
34
- return idMatch || titleMatch;
34
+ const cmdMatch = paneDef.cmd && paneDef.cmd.toLowerCase().split(/\s+/)[0] === normalizedFocus;
35
+ const agentAlias = (normalizedFocus === 'agent' || normalizedFocus === 'agy' || normalizedFocus === 'ai') &&
36
+ (paneDef.id === 'agy' || paneDef.id === 'agent' || paneDef.isAgent);
37
+ return Boolean(idMatch || titleMatch || cmdMatch || agentAlias);
35
38
  }
36
39
 
37
40
  // 2. Iterate through pane definitions
@@ -108,19 +108,7 @@ async function executeNuke(flags, config, cwd = process.cwd()) {
108
108
  await config.hooks.onPreNuke(ctx);
109
109
  }
110
110
 
111
- // 1. Close Herdr Workspace if running
112
- const prefix = config.workspace.labelPrefix || '';
113
- const matchTargets = [
114
- targetSessionName,
115
- targetDirname,
116
- targetBranch,
117
- prefix ? `${prefix}${targetSessionName}` : null,
118
- prefix ? `${prefix}${targetDirname}` : null,
119
- ].filter(Boolean);
120
-
121
- herdr.closeWorkspacesMatching(matchTargets);
122
-
123
- // 2. Remove Worktree & Directory
111
+ // 1. Remove Worktree & Directory
124
112
  console.log(`\n==> [1/3] Removing git worktree and directory...`);
125
113
  const isRegisteredWorktree = worktrees.some(wt => path.resolve(wt.path) === path.resolve(targetWorktreePath));
126
114
 
@@ -148,27 +136,46 @@ async function executeNuke(flags, config, cwd = process.cwd()) {
148
136
  console.log(`Worktree directory is clean.`);
149
137
  }
150
138
 
151
- // 3. Delete Local Branch
139
+ // Helper for final cleanup steps (Herdr workspace close + Post-Nuke hook)
140
+ const finishCleanup = async () => {
141
+ // Post-Nuke Hook
142
+ if (typeof config.hooks.onPostNuke === 'function') {
143
+ await config.hooks.onPostNuke(ctx);
144
+ }
145
+
146
+ // Close Herdr Workspace if running (performed last so active pane is not terminated mid-cleanup)
147
+ const prefix = config.workspace.labelPrefix || '';
148
+ const matchTargets = [
149
+ targetSessionName,
150
+ targetDirname,
151
+ targetBranch,
152
+ prefix ? `${prefix}${targetSessionName}` : null,
153
+ prefix ? `${prefix}${targetDirname}` : null,
154
+ ].filter(Boolean);
155
+
156
+ herdr.closeWorkspacesMatching(matchTargets);
157
+
158
+ console.log(`\nCleanup complete!`);
159
+ };
160
+
161
+ // 2. Delete Local Branch
152
162
  if (flags.dirOnly) {
153
163
  console.log(`\n==> Skipping branch deletion (--dir-only / --keep-branch).`);
154
- if (typeof config.hooks.onPostNuke === 'function') await config.hooks.onPostNuke(ctx);
155
- console.log(`\nCleanup complete!`);
156
- process.exit(0);
164
+ await finishCleanup();
165
+ return;
157
166
  }
158
167
 
159
168
  const protectedBranches = config.repo.protectedBranches || ['main', 'master', 'develop', 'prod', 'staging', 'production'];
160
169
  if (targetBranch && protectedBranches.includes(targetBranch)) {
161
170
  console.warn(`\n==> WARNING: "${targetBranch}" is a protected branch. Skipping local and remote branch deletion.`);
162
- if (typeof config.hooks.onPostNuke === 'function') await config.hooks.onPostNuke(ctx);
163
- console.log(`\nCleanup complete!`);
164
- process.exit(0);
171
+ await finishCleanup();
172
+ return;
165
173
  }
166
174
 
167
175
  if (!targetBranch) {
168
176
  console.log(`\n==> No branch associated with "${target}". Skipping branch deletion.`);
169
- if (typeof config.hooks.onPostNuke === 'function') await config.hooks.onPostNuke(ctx);
170
- console.log(`\nCleanup complete!`);
171
- process.exit(0);
177
+ await finishCleanup();
178
+ return;
172
179
  }
173
180
 
174
181
  console.log(`\n==> [2/3] Deleting local branch "${targetBranch}"...`);
@@ -181,12 +188,11 @@ async function executeNuke(flags, config, cwd = process.cwd()) {
181
188
  console.log(`Local branch "${targetBranch}" does not exist or was already deleted.`);
182
189
  }
183
190
 
184
- // 4. Delete Remote Branch
191
+ // 3. Delete Remote Branch
185
192
  if (flags.keepRemote) {
186
193
  console.log(`\n==> Skipping remote branch deletion (--keep-remote / --local-only).`);
187
- if (typeof config.hooks.onPostNuke === 'function') await config.hooks.onPostNuke(ctx);
188
- console.log(`\nCleanup complete!`);
189
- process.exit(0);
194
+ await finishCleanup();
195
+ return;
190
196
  }
191
197
 
192
198
  console.log(`\n==> [3/3] Deleting remote branch "${targetBranch}" from origin...`);
@@ -198,12 +204,7 @@ async function executeNuke(flags, config, cwd = process.cwd()) {
198
204
  console.log(`Remote branch "${targetBranch}" does not exist on origin or was already deleted.`);
199
205
  }
200
206
 
201
- // Post-Nuke Hook
202
- if (typeof config.hooks.onPostNuke === 'function') {
203
- await config.hooks.onPostNuke(ctx);
204
- }
205
-
206
- console.log(`\nCleanup complete!`);
207
+ await finishCleanup();
207
208
  }
208
209
 
209
210
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "name": "@tsuzuku/arise",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
4
7
  "description": "Arise - Unified workplace-agnostic Git worktree and Herdr workspace orchestrator with pluggable project presets",
5
8
  "main": "index.js",
6
9
  "bin": {
@@ -21,7 +21,7 @@ module.exports = {
21
21
  layout: [
22
22
  { id: 'vim', title: 'vim', cmd: 'vim .', position: 'root' },
23
23
  { id: 'shell', title: 'shell', cmd: null, split: 'right', from: 'vim' },
24
- { id: 'agy', title: 'agy', cmd: 'agy', split: 'down', from: 'shell', focus: true },
24
+ { id: 'agy', title: 'agy', cmd: 'agy', split: 'down', from: 'shell', focus: true, isAgent: true },
25
25
  ],
26
26
 
27
27
  hooks: {
@@ -23,7 +23,7 @@ module.exports = {
23
23
  { id: 'vim', title: 'vim', cmd: 'vim .', position: 'root' },
24
24
  { id: 'logs', title: 'logs', cmd: 'tail -f storage/logs/laravel.log', split: 'right', from: 'vim' },
25
25
  { id: 'shell', title: 'shell', cmd: null, split: 'down', from: 'vim' },
26
- { id: 'agy', title: 'agy', cmd: 'agy', split: 'down', from: 'logs', focus: true },
26
+ { id: 'agy', title: 'agy', cmd: 'agy', split: 'down', from: 'logs', focus: true, isAgent: true },
27
27
  ],
28
28
 
29
29
  hooks: {
package/presets/node.js CHANGED
@@ -22,7 +22,7 @@ module.exports = {
22
22
  { id: 'vim', title: 'vim', cmd: 'vim .', position: 'root' },
23
23
  { id: 'server', title: 'npm server', cmd: 'npm run dev', split: 'right', from: 'vim' },
24
24
  { id: 'shell', title: 'shell', cmd: null, split: 'down', from: 'vim' },
25
- { id: 'agy', title: 'agy', cmd: 'agy', split: 'down', from: 'server', focus: true },
25
+ { id: 'agy', title: 'agy', cmd: 'agy', split: 'down', from: 'server', focus: true, isAgent: true },
26
26
  ],
27
27
 
28
28
  hooks: {
package/types.d.ts CHANGED
@@ -16,6 +16,7 @@ export interface CliFlags {
16
16
  workspaceName: string | null;
17
17
  source: string | null;
18
18
  presetName: string | null;
19
+ agent: string | null;
19
20
  focusTarget: string | null;
20
21
  installSkill: boolean;
21
22
  skillScope: 'global' | 'local' | null;
@@ -41,6 +42,8 @@ export interface PaneDefinition {
41
42
  split?: SplitDirection;
42
43
  /** Whether to focus this pane by default */
43
44
  focus?: boolean;
45
+ /** Whether this pane is designated as the AI CLI agent pane */
46
+ isAgent?: boolean;
44
47
  }
45
48
 
46
49
  export interface RepoConfig {
@@ -57,7 +60,9 @@ export interface RepoConfig {
57
60
  export interface WorkspaceConfig {
58
61
  /** Prefix added to Herdr workspace labels (e.g. '[BE] ') */
59
62
  labelPrefix?: string;
60
- /** Default pane to focus ('agy', 'vim', 'logs', 'server', 'shell') */
63
+ /** CLI AI agent to run in the workspace pane ('agy', 'claude', 'aider', 'copilot', 'none', etc.) */
64
+ agent?: string | { cmd: string; title?: string; [key: string]: any } | null;
65
+ /** Default pane to focus ('agent', 'agy', 'claude', 'vim', 'logs', 'server', 'shell') */
61
66
  defaultFocus?: string;
62
67
  }
63
68
 
@@ -34,15 +34,29 @@
34
34
  },
35
35
  "workspace": {
36
36
  "type": "object",
37
- "description": "Herdr workspace naming and focus configuration",
37
+ "description": "Herdr workspace naming, agent, and focus configuration",
38
38
  "properties": {
39
39
  "labelPrefix": {
40
40
  "type": "string",
41
41
  "description": "Prefix added to Herdr workspace labels (e.g. '[BE] ')"
42
42
  },
43
+ "agent": {
44
+ "type": ["string", "object", "null"],
45
+ "description": "CLI AI agent to run in the workspace pane ('agy', 'claude', 'aider', 'copilot', 'none', or custom command object)",
46
+ "properties": {
47
+ "cmd": {
48
+ "type": ["string", "null"],
49
+ "description": "Command to execute the agent"
50
+ },
51
+ "title": {
52
+ "type": "string",
53
+ "description": "Display title for the agent pane in Herdr"
54
+ }
55
+ }
56
+ },
43
57
  "defaultFocus": {
44
58
  "type": "string",
45
- "description": "Default pane to focus upon creation ('agy', 'vim', 'logs', 'server', 'shell')"
59
+ "description": "Default pane to focus upon creation ('agent', 'agy', 'claude', 'vim', 'logs', 'server', 'shell')"
46
60
  }
47
61
  },
48
62
  "additionalProperties": false
@@ -83,6 +97,10 @@
83
97
  "focus": {
84
98
  "type": "boolean",
85
99
  "description": "Whether this pane should receive focus by default"
100
+ },
101
+ "isAgent": {
102
+ "type": "boolean",
103
+ "description": "Whether this pane is designated as the AI CLI agent pane"
86
104
  }
87
105
  },
88
106
  "additionalProperties": false