@simonfestl/husky-cli 0.9.5 β†’ 0.9.7

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.
@@ -2,7 +2,7 @@
2
2
  * LLM Context Generator
3
3
  * Outputs markdown reference for LLM agents
4
4
  */
5
- const VERSION = "0.9.5";
5
+ const VERSION = "0.9.7";
6
6
  export function generateLLMContext() {
7
7
  return `# Husky CLI Reference (v${VERSION})
8
8
 
@@ -54,11 +54,15 @@ husky config test
54
54
  husky task list [--status <status>] # List tasks
55
55
  husky task get <id> # Get task details
56
56
  husky task create # Create new task
57
- husky task start <id> # Start working on task
57
+ husky task start <id> # Start task (auto-creates worktree!)
58
+ husky task start <id> --no-worktree # Start without worktree
58
59
  husky task done <id> [--pr <url>] # Mark task as done
59
60
  husky task update <id> # Update task fields
60
61
  husky task assign <id> <assignee> # Assign task
61
62
  husky task log <id> # View task activity log
63
+ husky task message <id> "msg" # Post status message (positional)
64
+ husky task message -m "msg" --id <id> # Post status message (flags)
65
+ husky task message -m "msg" # Uses HUSKY_TASK_ID env var
62
66
  \`\`\`
63
67
 
64
68
  ### Projects
@@ -125,8 +129,12 @@ husky changelog list # List changelogs
125
129
  \`\`\`bash
126
130
  husky worktree list # List worktrees
127
131
  husky worktree create <name> # Create isolated worktree
128
- husky worktree delete <name> # Delete worktree
129
- husky worktree switch <name> # Switch to worktree
132
+ husky worktree create <name> --task-id <id> # Create and register with task
133
+ husky worktree merge <name> # Merge back to base branch
134
+ husky worktree push <name> # Push branch to remote
135
+ husky worktree pr <name> -t "Title" # Create pull request
136
+ husky worktree remove <name> # Remove worktree
137
+ husky worktree sync-stats <name> --task-id <id> # Sync stats to dashboard
130
138
  \`\`\`
131
139
 
132
140
  ### VM Sessions (Cloud Agents)
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const serviceAccountCommand: Command;
@@ -0,0 +1,180 @@
1
+ import { Command } from "commander";
2
+ const API_URL = process.env.HUSKY_API_URL || "https://huskyv0-dashboard-474966775596.europe-west1.run.app";
3
+ const API_KEY = process.env.HUSKY_API_KEY || "";
4
+ async function fetchApi(path, options = {}) {
5
+ const response = await fetch(`${API_URL}${path}`, {
6
+ ...options,
7
+ headers: {
8
+ "Content-Type": "application/json",
9
+ "x-api-key": API_KEY,
10
+ ...options.headers,
11
+ },
12
+ });
13
+ if (!response.ok) {
14
+ const error = await response.json().catch(() => ({ error: "Unknown error" }));
15
+ throw new Error(error.error || `HTTP ${response.status}`);
16
+ }
17
+ return response.json();
18
+ }
19
+ function formatPersona(personaType) {
20
+ const icons = {
21
+ "support-agent": "🎧",
22
+ "research-agent": "πŸ”¬",
23
+ "accounting-agent": "πŸ“Š",
24
+ "marketing-agent": "πŸ“’",
25
+ "developer-agent": "πŸ’»",
26
+ "devops-agent": "πŸ”§",
27
+ };
28
+ return `${icons[personaType] || "πŸ‘€"} ${personaType}`;
29
+ }
30
+ function formatPermissionLevel(level) {
31
+ const colors = {
32
+ "read-only": "\x1b[90m", // gray
33
+ "developer": "\x1b[34m", // blue
34
+ "admin": "\x1b[31m", // red
35
+ };
36
+ const reset = "\x1b[0m";
37
+ return `${colors[level] || ""}${level}${reset}`;
38
+ }
39
+ export const serviceAccountCommand = new Command("sa")
40
+ .description("Manage Agent Service Accounts")
41
+ .addHelpText("after", `
42
+ Examples:
43
+ husky sa list List all service account bindings
44
+ husky sa get developer-agent Show binding for developer persona
45
+ husky sa sync Sync bindings from GCP config
46
+ husky sa verify Verify current VM's service account
47
+ `);
48
+ // List all bindings
49
+ serviceAccountCommand
50
+ .command("list")
51
+ .description("List all service account bindings")
52
+ .option("--json", "Output as JSON")
53
+ .action(async (options) => {
54
+ try {
55
+ const data = await fetchApi("/api/service-account-bindings");
56
+ const bindings = (data.bindings || []);
57
+ if (options.json) {
58
+ console.log(JSON.stringify(bindings, null, 2));
59
+ return;
60
+ }
61
+ if (bindings.length === 0) {
62
+ console.log("No service account bindings found.");
63
+ console.log("Run 'husky sa sync' to create bindings from GCP config.");
64
+ return;
65
+ }
66
+ console.log("\nπŸ“‹ Service Account Bindings\n");
67
+ console.log("━".repeat(80));
68
+ for (const binding of bindings) {
69
+ console.log(`${formatPersona(binding.personaType)}`);
70
+ console.log(` Email: ${binding.serviceAccountEmail}`);
71
+ console.log(` Permission: ${formatPermissionLevel(binding.permissionLevel)}`);
72
+ console.log(` Roles: ${binding.iamRoles.join(", ") || "none"}`);
73
+ if (binding.lastSyncedAt) {
74
+ console.log(` Last Synced: ${new Date(binding.lastSyncedAt).toLocaleString()}`);
75
+ }
76
+ console.log("━".repeat(80));
77
+ }
78
+ console.log(`\nTotal: ${bindings.length} bindings`);
79
+ }
80
+ catch (error) {
81
+ console.error("Error:", error instanceof Error ? error.message : error);
82
+ process.exit(1);
83
+ }
84
+ });
85
+ // Get specific binding
86
+ serviceAccountCommand
87
+ .command("get <persona>")
88
+ .description("Get service account binding for a persona")
89
+ .option("--json", "Output as JSON")
90
+ .action(async (persona, options) => {
91
+ try {
92
+ // First list all and filter by persona
93
+ const data = await fetchApi("/api/service-account-bindings");
94
+ const bindings = (data.bindings || []);
95
+ const binding = bindings.find((b) => b.personaType === persona);
96
+ if (!binding) {
97
+ console.log(`No binding found for persona: ${persona}`);
98
+ console.log("\nAvailable personas:");
99
+ bindings.forEach((b) => console.log(` - ${b.personaType}`));
100
+ return;
101
+ }
102
+ if (options.json) {
103
+ console.log(JSON.stringify(binding, null, 2));
104
+ return;
105
+ }
106
+ console.log(`\n${formatPersona(binding.personaType)}\n`);
107
+ console.log(`Email: ${binding.serviceAccountEmail}`);
108
+ console.log(`Permission: ${formatPermissionLevel(binding.permissionLevel)}`);
109
+ console.log(`Project: ${binding.projectId}`);
110
+ console.log(`Roles:`);
111
+ binding.iamRoles.forEach((role) => console.log(` - ${role}`));
112
+ if (binding.lastSyncedAt) {
113
+ console.log(`Last Synced: ${new Date(binding.lastSyncedAt).toLocaleString()}`);
114
+ }
115
+ console.log(`Created: ${new Date(binding.createdAt).toLocaleString()}`);
116
+ }
117
+ catch (error) {
118
+ console.error("Error:", error instanceof Error ? error.message : error);
119
+ process.exit(1);
120
+ }
121
+ });
122
+ // Sync from GCP
123
+ serviceAccountCommand
124
+ .command("sync")
125
+ .description("Sync service account bindings from GCP config")
126
+ .action(async () => {
127
+ try {
128
+ console.log("πŸ”„ Syncing service account bindings...\n");
129
+ const data = await fetchApi("/api/service-account-bindings/sync", {
130
+ method: "POST",
131
+ });
132
+ console.log(`βœ… Synced ${data.synced} bindings\n`);
133
+ for (const result of (data.results || [])) {
134
+ const icon = result.status === "synced" ? "βœ“" : "βœ—";
135
+ console.log(` ${icon} ${result.personaType}: ${result.email || result.status}`);
136
+ }
137
+ console.log("\nRun 'husky sa list' to see all bindings.");
138
+ }
139
+ catch (error) {
140
+ console.error("Error:", error instanceof Error ? error.message : error);
141
+ process.exit(1);
142
+ }
143
+ });
144
+ // Verify current VM's SA (for use on VMs)
145
+ serviceAccountCommand
146
+ .command("verify")
147
+ .description("Verify current VM's service account (run on VM)")
148
+ .action(async () => {
149
+ try {
150
+ // Check if running on GCP by querying metadata service
151
+ const metadataUrl = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email";
152
+ console.log("πŸ” Checking GCP metadata service...\n");
153
+ const response = await fetch(metadataUrl, {
154
+ headers: { "Metadata-Flavor": "Google" },
155
+ }).catch(() => null);
156
+ if (!response || !response.ok) {
157
+ console.log("❌ Not running on a GCP VM (metadata service unavailable)");
158
+ console.log("\nThis command is designed to run on GCP Compute Engine VMs.");
159
+ return;
160
+ }
161
+ const saEmail = await response.text();
162
+ console.log(`βœ… Running as service account: ${saEmail}`);
163
+ // Try to match to a persona
164
+ const personaMatch = saEmail.match(/husky-(\w+)@/);
165
+ if (personaMatch) {
166
+ const persona = personaMatch[1] + "-agent";
167
+ console.log(` Persona: ${formatPersona(persona)}`);
168
+ }
169
+ // Report to Husky if we have a task ID
170
+ const taskId = process.env.HUSKY_TASK_ID;
171
+ if (taskId) {
172
+ console.log(`\nπŸ“€ Reporting to Husky (task: ${taskId})...`);
173
+ // The VM startup script handles this, but we can also report here
174
+ }
175
+ }
176
+ catch (error) {
177
+ console.error("Error:", error instanceof Error ? error.message : error);
178
+ process.exit(1);
179
+ }
180
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const servicesCommand: Command;
@@ -0,0 +1,381 @@
1
+ import { Command } from "commander";
2
+ import { getConfig } from "./config.js";
3
+ // Helper: Ensure API is configured
4
+ function ensureConfig() {
5
+ const config = getConfig();
6
+ if (!config.apiUrl) {
7
+ console.error("Error: API URL not configured. Run: husky config set api-url <url>");
8
+ process.exit(1);
9
+ }
10
+ return { apiUrl: config.apiUrl, apiKey: config.apiKey };
11
+ }
12
+ // Helper: Make API request
13
+ async function fetchAPI(apiUrl, apiKey, path, options = {}) {
14
+ const response = await fetch(`${apiUrl}${path}`, {
15
+ ...options,
16
+ headers: {
17
+ "Content-Type": "application/json",
18
+ ...(apiKey ? { "x-api-key": apiKey } : {}),
19
+ ...options.headers,
20
+ },
21
+ });
22
+ if (!response.ok) {
23
+ const error = await response.text();
24
+ throw new Error(`API Error: ${response.status} - ${error}`);
25
+ }
26
+ return response.json();
27
+ }
28
+ export const servicesCommand = new Command("services")
29
+ .description("Manage Agent Services (Agentic Workflow Platform)");
30
+ // husky services list
31
+ servicesCommand
32
+ .command("list")
33
+ .description("List all agent services")
34
+ .option("--project <id>", "Filter by project ID")
35
+ .option("--json", "Output as JSON")
36
+ .action(async (options) => {
37
+ const config = ensureConfig();
38
+ try {
39
+ const params = new URLSearchParams();
40
+ if (options.project)
41
+ params.set("projectId", options.project);
42
+ const queryString = params.toString();
43
+ const path = queryString ? `/api/services?${queryString}` : "/api/services";
44
+ const services = await fetchAPI(config.apiUrl, config.apiKey, path);
45
+ if (options.json) {
46
+ console.log(JSON.stringify(services, null, 2));
47
+ return;
48
+ }
49
+ if (services.length === 0) {
50
+ console.log("No services found.");
51
+ return;
52
+ }
53
+ console.log("\n Agent Services");
54
+ console.log(" " + "─".repeat(80));
55
+ for (const s of services) {
56
+ const statusIcon = {
57
+ building: "πŸ”¨",
58
+ deployed: "πŸ“¦",
59
+ running: "βœ…",
60
+ failed: "❌",
61
+ disabled: "⏸️",
62
+ };
63
+ console.log(` ${statusIcon[s.status] || "❓"} ${s.name} (${s.type})`);
64
+ console.log(` ID: ${s.id}`);
65
+ console.log(` Status: ${s.status}`);
66
+ if (s.schedule)
67
+ console.log(` Schedule: ${s.schedule}`);
68
+ if (s.cloudRunUrl)
69
+ console.log(` URL: ${s.cloudRunUrl}`);
70
+ console.log(` Runs: ${s.runCount} (${s.successCount} success, ${s.failureCount} failed)`);
71
+ console.log("");
72
+ }
73
+ }
74
+ catch (error) {
75
+ console.error("Error:", error instanceof Error ? error.message : error);
76
+ process.exit(1);
77
+ }
78
+ });
79
+ // husky services get <id>
80
+ servicesCommand
81
+ .command("get <id>")
82
+ .description("Get service details")
83
+ .option("--json", "Output as JSON")
84
+ .action(async (id, options) => {
85
+ const config = ensureConfig();
86
+ try {
87
+ const service = await fetchAPI(config.apiUrl, config.apiKey, `/api/services/${id}`);
88
+ if (options.json) {
89
+ console.log(JSON.stringify(service, null, 2));
90
+ return;
91
+ }
92
+ console.log(`\n Service: ${service.name}`);
93
+ console.log(" " + "─".repeat(50));
94
+ console.log(` ID: ${service.id}`);
95
+ console.log(` Type: ${service.type}`);
96
+ console.log(` Status: ${service.status}`);
97
+ if (service.description)
98
+ console.log(` Description: ${service.description}`);
99
+ if (service.schedule)
100
+ console.log(` Schedule: ${service.schedule}`);
101
+ if (service.cloudRunUrl)
102
+ console.log(` URL: ${service.cloudRunUrl}`);
103
+ if (service.dockerImage)
104
+ console.log(` Image: ${service.dockerImage}`);
105
+ console.log(` Runs: ${service.runCount} total`);
106
+ console.log(` Success: ${service.successCount}`);
107
+ console.log(` Failures: ${service.failureCount}`);
108
+ if (service.avgDurationMs)
109
+ console.log(` Avg Duration: ${service.avgDurationMs}ms`);
110
+ if (service.lastError)
111
+ console.log(` Last Error: ${service.lastError}`);
112
+ console.log(` Created: ${new Date(service.createdAt).toLocaleString()}`);
113
+ console.log(` Updated: ${new Date(service.updatedAt).toLocaleString()}`);
114
+ console.log("");
115
+ }
116
+ catch (error) {
117
+ console.error("Error:", error instanceof Error ? error.message : error);
118
+ process.exit(1);
119
+ }
120
+ });
121
+ // husky services create <name>
122
+ servicesCommand
123
+ .command("create <name>")
124
+ .description("Create a new agent service")
125
+ .option("-t, --type <type>", "Service type (cloud-run, cloud-function, scheduled-job)", "cloud-run")
126
+ .option("-d, --description <desc>", "Service description")
127
+ .option("-s, --schedule <cron>", 'Cron schedule (e.g., "0 6 * * *")')
128
+ .option("--project <id>", "Link to project")
129
+ .option("--repo <url>", "Source repository URL")
130
+ .option("--path <path>", "Source path in repository")
131
+ .action(async (name, options) => {
132
+ const config = ensureConfig();
133
+ try {
134
+ const service = await fetchAPI(config.apiUrl, config.apiKey, "/api/services", {
135
+ method: "POST",
136
+ body: JSON.stringify({
137
+ name,
138
+ type: options.type,
139
+ description: options.description,
140
+ schedule: options.schedule,
141
+ projectId: options.project,
142
+ sourceRepo: options.repo,
143
+ sourcePath: options.path,
144
+ }),
145
+ });
146
+ console.log(`βœ“ Created service: ${service.name} (${service.id})`);
147
+ console.log(` Type: ${service.type}`);
148
+ console.log(` Status: ${service.status}`);
149
+ }
150
+ catch (error) {
151
+ console.error("Error:", error instanceof Error ? error.message : error);
152
+ process.exit(1);
153
+ }
154
+ });
155
+ // husky services update <id>
156
+ servicesCommand
157
+ .command("update <id>")
158
+ .description("Update a service")
159
+ .option("-d, --description <desc>", "Update description")
160
+ .option("-s, --schedule <cron>", "Update schedule")
161
+ .option("--status <status>", "Update status")
162
+ .option("--url <url>", "Set Cloud Run URL")
163
+ .option("--image <image>", "Set Docker image")
164
+ .action(async (id, options) => {
165
+ const config = ensureConfig();
166
+ const updates = {};
167
+ if (options.description)
168
+ updates.description = options.description;
169
+ if (options.schedule)
170
+ updates.schedule = options.schedule;
171
+ if (options.status)
172
+ updates.status = options.status;
173
+ if (options.url)
174
+ updates.cloudRunUrl = options.url;
175
+ if (options.image)
176
+ updates.dockerImage = options.image;
177
+ if (Object.keys(updates).length === 0) {
178
+ console.log("No updates specified. Use --help for available options.");
179
+ return;
180
+ }
181
+ try {
182
+ const service = await fetchAPI(config.apiUrl, config.apiKey, `/api/services/${id}`, {
183
+ method: "PATCH",
184
+ body: JSON.stringify(updates),
185
+ });
186
+ console.log(`βœ“ Updated service: ${service.name}`);
187
+ const changedFields = Object.keys(updates).join(", ");
188
+ console.log(` Changed: ${changedFields}`);
189
+ }
190
+ catch (error) {
191
+ console.error("Error:", error instanceof Error ? error.message : error);
192
+ process.exit(1);
193
+ }
194
+ });
195
+ // husky services delete <id>
196
+ servicesCommand
197
+ .command("delete <id>")
198
+ .description("Delete a service")
199
+ .option("-f, --force", "Skip confirmation")
200
+ .action(async (id, options) => {
201
+ const config = ensureConfig();
202
+ if (!options.force) {
203
+ console.log("Use --force to confirm deletion.");
204
+ return;
205
+ }
206
+ try {
207
+ await fetchAPI(config.apiUrl, config.apiKey, `/api/services/${id}`, { method: "DELETE" });
208
+ console.log(`βœ“ Deleted service: ${id}`);
209
+ }
210
+ catch (error) {
211
+ console.error("Error:", error instanceof Error ? error.message : error);
212
+ process.exit(1);
213
+ }
214
+ });
215
+ // husky services health
216
+ servicesCommand
217
+ .command("health")
218
+ .description("Check health of all services")
219
+ .option("--json", "Output as JSON")
220
+ .action(async (options) => {
221
+ const config = ensureConfig();
222
+ try {
223
+ const services = await fetchAPI(config.apiUrl, config.apiKey, "/api/services");
224
+ if (options.json) {
225
+ const healthData = services
226
+ .filter((s) => s.status !== "disabled")
227
+ .map((s) => ({
228
+ id: s.id,
229
+ name: s.name,
230
+ healthStatus: s.healthStatus || "unknown",
231
+ status: s.status,
232
+ lastError: s.lastError,
233
+ }));
234
+ console.log(JSON.stringify(healthData, null, 2));
235
+ return;
236
+ }
237
+ console.log("\n Service Health");
238
+ console.log(" " + "─".repeat(50));
239
+ let healthy = 0;
240
+ let unhealthy = 0;
241
+ let unknown = 0;
242
+ for (const s of services) {
243
+ if (s.status === "disabled")
244
+ continue;
245
+ const icon = s.healthStatus === "healthy"
246
+ ? "βœ…"
247
+ : s.healthStatus === "unhealthy"
248
+ ? "❌"
249
+ : "❓";
250
+ console.log(` ${icon} ${s.name}: ${s.healthStatus || "unknown"}`);
251
+ if (s.lastError && s.healthStatus === "unhealthy") {
252
+ console.log(` Last Error: ${s.lastError}`);
253
+ }
254
+ if (s.healthStatus === "healthy")
255
+ healthy++;
256
+ else if (s.healthStatus === "unhealthy")
257
+ unhealthy++;
258
+ else
259
+ unknown++;
260
+ }
261
+ console.log("");
262
+ console.log(` Summary: ${healthy} healthy, ${unhealthy} unhealthy, ${unknown} unknown`);
263
+ console.log("");
264
+ }
265
+ catch (error) {
266
+ console.error("Error:", error instanceof Error ? error.message : error);
267
+ process.exit(1);
268
+ }
269
+ });
270
+ // husky services run <id>
271
+ servicesCommand
272
+ .command("run <id>")
273
+ .description("Trigger a service run")
274
+ .option("--input <json>", "Input data as JSON")
275
+ .option("--json", "Output as JSON")
276
+ .action(async (id, options) => {
277
+ const config = ensureConfig();
278
+ let inputData = {};
279
+ if (options.input) {
280
+ try {
281
+ inputData = JSON.parse(options.input);
282
+ }
283
+ catch {
284
+ console.error("Error: --input must be valid JSON");
285
+ process.exit(1);
286
+ }
287
+ }
288
+ try {
289
+ const result = await fetchAPI(config.apiUrl, config.apiKey, `/api/services/${id}/run`, {
290
+ method: "POST",
291
+ body: JSON.stringify({ input: inputData }),
292
+ });
293
+ if (options.json) {
294
+ console.log(JSON.stringify(result, null, 2));
295
+ return;
296
+ }
297
+ console.log(`βœ“ Service run triggered: ${id}`);
298
+ if (result.runId)
299
+ console.log(` Run ID: ${result.runId}`);
300
+ if (result.status)
301
+ console.log(` Status: ${result.status}`);
302
+ }
303
+ catch (error) {
304
+ console.error("Error:", error instanceof Error ? error.message : error);
305
+ process.exit(1);
306
+ }
307
+ });
308
+ // husky services logs <id>
309
+ servicesCommand
310
+ .command("logs <id>")
311
+ .description("View service logs")
312
+ .option("-n, --lines <num>", "Number of log lines", "50")
313
+ .option("--json", "Output as JSON")
314
+ .action(async (id, options) => {
315
+ const config = ensureConfig();
316
+ try {
317
+ const logs = await fetchAPI(config.apiUrl, config.apiKey, `/api/services/${id}/logs?limit=${options.lines}`);
318
+ if (options.json) {
319
+ console.log(JSON.stringify(logs, null, 2));
320
+ return;
321
+ }
322
+ if (!logs.entries || logs.entries.length === 0) {
323
+ console.log(`No logs found for service ${id}`);
324
+ return;
325
+ }
326
+ console.log(`\n Logs for service: ${id}`);
327
+ console.log(" " + "─".repeat(70));
328
+ for (const entry of logs.entries) {
329
+ const timestamp = new Date(entry.timestamp).toLocaleString();
330
+ const level = entry.level?.toUpperCase() || "INFO";
331
+ console.log(` [${timestamp}] ${level}: ${entry.message}`);
332
+ }
333
+ console.log("");
334
+ }
335
+ catch (error) {
336
+ console.error("Error:", error instanceof Error ? error.message : error);
337
+ process.exit(1);
338
+ }
339
+ });
340
+ // husky services runs <id>
341
+ servicesCommand
342
+ .command("runs <id>")
343
+ .description("List recent runs for a service")
344
+ .option("-n, --limit <num>", "Number of runs to show", "10")
345
+ .option("--json", "Output as JSON")
346
+ .action(async (id, options) => {
347
+ const config = ensureConfig();
348
+ try {
349
+ const runs = await fetchAPI(config.apiUrl, config.apiKey, `/api/services/${id}/runs?limit=${options.limit}`);
350
+ if (options.json) {
351
+ console.log(JSON.stringify(runs, null, 2));
352
+ return;
353
+ }
354
+ if (!runs || runs.length === 0) {
355
+ console.log(`No runs found for service ${id}`);
356
+ return;
357
+ }
358
+ console.log(`\n Recent Runs for service: ${id}`);
359
+ console.log(" " + "─".repeat(70));
360
+ for (const run of runs) {
361
+ const statusIcon = run.status === "success"
362
+ ? "βœ…"
363
+ : run.status === "failed"
364
+ ? "❌"
365
+ : run.status === "running"
366
+ ? "β–Ά"
367
+ : "⏸️";
368
+ const timestamp = new Date(run.startedAt).toLocaleString();
369
+ const duration = run.durationMs ? `${run.durationMs}ms` : "β€”";
370
+ console.log(` ${statusIcon} ${run.id.slice(0, 12)} β”‚ ${timestamp} β”‚ ${duration} β”‚ ${run.status}`);
371
+ if (run.error) {
372
+ console.log(` Error: ${run.error}`);
373
+ }
374
+ }
375
+ console.log("");
376
+ }
377
+ catch (error) {
378
+ console.error("Error:", error instanceof Error ? error.message : error);
379
+ process.exit(1);
380
+ }
381
+ });
@@ -221,7 +221,8 @@ taskCommand
221
221
  taskCommand
222
222
  .command("start <id>")
223
223
  .description("Start working on a task")
224
- .action(async (id) => {
224
+ .option("--no-worktree", "Skip worktree creation")
225
+ .action(async (id, options) => {
225
226
  const config = getConfig();
226
227
  if (!config.apiUrl) {
227
228
  console.error("Error: API URL not configured.");
@@ -232,6 +233,15 @@ taskCommand
232
233
  const workerId = await ensureWorkerRegistered(config.apiUrl, config.apiKey || "");
233
234
  const sessionId = generateSessionId();
234
235
  await registerSession(config.apiUrl, config.apiKey || "", workerId, sessionId);
236
+ // Create worktree for isolation (unless --no-worktree)
237
+ let worktreeInfo = null;
238
+ if (options.worktree !== false) {
239
+ worktreeInfo = createWorktreeForTask(id);
240
+ if (worktreeInfo) {
241
+ console.log(`βœ“ Created worktree: ${worktreeInfo.path}`);
242
+ console.log(` Branch: ${worktreeInfo.branch}`);
243
+ }
244
+ }
235
245
  const res = await fetch(`${config.apiUrl}/api/tasks/${id}/start`, {
236
246
  method: "POST",
237
247
  headers: {
@@ -242,6 +252,11 @@ taskCommand
242
252
  agent: "claude-code",
243
253
  workerId,
244
254
  sessionId,
255
+ // Include worktree info if created
256
+ ...(worktreeInfo ? {
257
+ worktreePath: worktreeInfo.path,
258
+ worktreeBranch: worktreeInfo.branch,
259
+ } : {}),
245
260
  }),
246
261
  });
247
262
  if (!res.ok) {
@@ -251,6 +266,10 @@ taskCommand
251
266
  console.log(`βœ“ Started: ${task.title}`);
252
267
  console.log(` Worker: ${workerId}`);
253
268
  console.log(` Session: ${sessionId}`);
269
+ // Show hint to cd into worktree
270
+ if (worktreeInfo) {
271
+ console.log(`\nπŸ’‘ To work in isolation: cd ${worktreeInfo.path}`);
272
+ }
254
273
  }
255
274
  catch (error) {
256
275
  console.error("Error starting task:", error);
@@ -547,11 +566,23 @@ taskCommand
547
566
  });
548
567
  // husky task message <id> "message" - post status message to task
549
568
  taskCommand
550
- .command("message <id> <message>")
569
+ .command("message [id] [message]")
551
570
  .description("Post a status message to a task")
552
- .action(async (id, message) => {
571
+ .option("-m, --message <text>", "Message text (alternative to positional arg)")
572
+ .option("--id <taskId>", "Task ID (alternative to positional arg, or use HUSKY_TASK_ID)")
573
+ .action(async (idArg, messageArg, options) => {
553
574
  const config = ensureConfig();
554
- const taskId = id;
575
+ // Support both: `husky task message <id> <msg>` and `husky task message -m <msg> --id <id>`
576
+ const taskId = idArg || options.id || process.env.HUSKY_TASK_ID;
577
+ const message = messageArg || options.message;
578
+ if (!taskId) {
579
+ console.error("Error: Task ID required. Use positional arg, --id, or set HUSKY_TASK_ID");
580
+ process.exit(1);
581
+ }
582
+ if (!message) {
583
+ console.error("Error: Message required. Use positional arg or -m/--message");
584
+ process.exit(1);
585
+ }
555
586
  try {
556
587
  const res = await fetch(`${config.apiUrl}/api/tasks/${taskId}/status`, {
557
588
  method: "POST",