@guildai/cli 0.4.0 → 0.5.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
@@ -28,8 +28,12 @@ guild agent init --name my-agent
28
28
  ### 3. Develop and Save
29
29
 
30
30
  ```bash
31
- # Edit agent.ts, then save
32
- guild agent save --message "Initial implementation"
31
+ # Edit agent.ts, then commit and save
32
+ git add . && git commit -m "Initial implementation"
33
+ guild agent save
34
+
35
+ # Or stage+commit+push in one step
36
+ guild agent save -A --message "Initial implementation"
33
37
  ```
34
38
 
35
39
  ### 4. Test Your Agent
@@ -54,7 +58,8 @@ guild auth status # Check authentication status
54
58
 
55
59
  ```bash
56
60
  guild agent init --name my-agent # Initialize a new agent
57
- guild agent save --message "..." # Save changes
61
+ guild agent save # Push commits and create a version
62
+ guild agent save -A --message "..." # Stage+commit+push in one step
58
63
  guild agent versions # List versions
59
64
  guild agent publish # Publish latest draft version
60
65
  guild agent unpublish # Unpublish latest published version
@@ -5,7 +5,7 @@ import { handleAxiosError, ErrorCodes } from '../../lib/errors.js';
5
5
  import * as fs from 'fs/promises';
6
6
  import * as path from 'path';
7
7
  import { getAuthenticatedUrl } from '../../lib/auth.js';
8
- import { runGit, GitError, formatGitError } from '../../lib/git.js';
8
+ import { runGit, GitError, formatGitError, installPrePushHook } from '../../lib/git.js';
9
9
  import { createOutputWriter } from '../../lib/output.js';
10
10
  async function isDirectoryEmpty(dirPath) {
11
11
  try {
@@ -57,6 +57,8 @@ export function createAgentCloneCommand() {
57
57
  }
58
58
  await runGit(['clone', cloneUrl, targetDir]);
59
59
  output.progress(`✓ Cloned repository to ${targetDir}`);
60
+ await installPrePushHook(targetDir);
61
+ output.progress('✓ Installed pre-push hook');
60
62
  // Create guild.json if it doesn't exist
61
63
  const guildJsonPath = path.join(targetDir, 'guild.json');
62
64
  const guildJsonExists = await fs
@@ -80,8 +82,9 @@ export function createAgentCloneCommand() {
80
82
  output.progress('Next steps:');
81
83
  output.progress(` 1. cd ${targetDir}`);
82
84
  output.progress(' 2. Make your changes to the code');
83
- output.progress(` 3. Run 'guild agent save --message "your changes"'`);
84
- output.progress(` 4. Run 'guild agent test' to test your changes`);
85
+ output.progress(' 3. git add . && git commit -m "your changes"');
86
+ output.progress(" 4. Run 'guild agent save' to push and create a version");
87
+ output.progress(` 5. Run 'guild agent test' to test your changes`);
85
88
  }
86
89
  catch (error) {
87
90
  if (error instanceof GitError) {
@@ -8,7 +8,7 @@ import * as fs from 'fs/promises';
8
8
  import * as path from 'path';
9
9
  import * as readline from 'readline';
10
10
  import { getAuthenticatedUrl } from '../../lib/auth.js';
11
- import { runGit, GitError, formatGitError } from '../../lib/git.js';
11
+ import { runGit, GitError, formatGitError, installPrePushHook } from '../../lib/git.js';
12
12
  import { resolveOwnerId } from '../../lib/owner-helpers.js';
13
13
  const TEMPLATE_CHOICES = [
14
14
  {
@@ -311,6 +311,8 @@ export function createAgentInitCommand() {
311
311
  // .gitignore doesn't exist (backend should have created it), create it
312
312
  await fs.writeFile(gitignorePath, 'guild.json\n');
313
313
  }
314
+ // Install pre-push hook to block direct git push
315
+ await installPrePushHook(targetDir);
314
316
  // Complete progress tracking
315
317
  steps.complete('Agent initialized successfully');
316
318
  // Display next steps
@@ -318,13 +320,15 @@ export function createAgentInitCommand() {
318
320
  if (options.directory) {
319
321
  format.detail(`1. cd ${targetDir}`);
320
322
  format.detail('2. Edit agent.ts to implement your agent logic');
321
- format.detail(`3. Run 'guild agent save --message "Initial implementation"'`);
322
- format.detail(`4. Run 'guild agent test' to test your agent`);
323
+ format.detail('3. git add . && git commit -m "Initial implementation"');
324
+ format.detail("4. Run 'guild agent save' to push and create a version");
325
+ format.detail(`5. Run 'guild agent test' to test your agent`);
323
326
  }
324
327
  else {
325
328
  format.detail('1. Edit agent.ts to implement your agent logic');
326
- format.detail(`2. Run 'guild agent save --message "Initial implementation"'`);
327
- format.detail(`3. Run 'guild agent test' to test your agent`);
329
+ format.detail('2. git add . && git commit -m "Initial implementation"');
330
+ format.detail("3. Run 'guild agent save' to push and create a version");
331
+ format.detail(`4. Run 'guild agent test' to test your agent`);
328
332
  }
329
333
  }
330
334
  catch (error) {
@@ -13,7 +13,7 @@ export function createAgentOwnersCommand() {
13
13
  const client = new GuildAPIClient();
14
14
  const [me, orgs, globalConfig] = await Promise.all([
15
15
  client.get('/me'),
16
- client.get('/me/organizations'),
16
+ client.fetchAll('/me/organizations'),
17
17
  loadGlobalConfig(),
18
18
  ]);
19
19
  const defaultOwnerId = globalConfig?.default_owner;
@@ -24,7 +24,7 @@ export function createAgentOwnersCommand() {
24
24
  type: 'user',
25
25
  is_default: defaultOwnerId === me.id,
26
26
  },
27
- ...orgs.items.map((org) => ({
27
+ ...orgs.map((org) => ({
28
28
  id: org.id,
29
29
  name: org.name,
30
30
  type: 'organization',
@@ -12,8 +12,9 @@ import { pollUntilComplete } from '../../lib/polling.js';
12
12
  export function createAgentSaveCommand() {
13
13
  const cmd = new Command('save');
14
14
  cmd
15
- .description('Save agent code by committing and syncing with guildcore')
16
- .option('--message <text>', 'Commit message describing the changes')
15
+ .description('Push committed changes to Guild and create a version')
16
+ .option('-A, --all', 'Stage all changes and commit before pushing', false)
17
+ .option('--message <text>', 'Commit message (required with --all)')
17
18
  .option('--wait', 'Wait for validation to complete before returning', false)
18
19
  .option('--publish', 'Publish after validation passes (implies --wait)', false)
19
20
  .option('--json', 'Output JSON only (no progress messages)', false)
@@ -21,9 +22,9 @@ export function createAgentSaveCommand() {
21
22
  const cwd = process.cwd();
22
23
  let guildConfig = null;
23
24
  const output = createOutputWriter();
24
- // Check for --message
25
- if (!options.message) {
26
- output.error('Commit message is required', 'Provide a message describing your changes:\n guild agent save --message "Add new feature"');
25
+ // With --all, a commit message is required
26
+ if (options.all && !options.message) {
27
+ output.error('Commit message is required with --all', 'Provide a message describing your changes:\n guild agent save -A --message "Add new feature"');
27
28
  process.exit(1);
28
29
  }
29
30
  try {
@@ -39,45 +40,60 @@ export function createAgentSaveCommand() {
39
40
  }
40
41
  // Read guild.json
41
42
  guildConfig = JSON.parse(await fs.readFile(guildJsonPath, 'utf-8'));
42
- // Check for uncommitted changes
43
+ // Check for uncommitted changes and unpushed commits
43
44
  const { stdout: statusOutput } = await runGit(['status', '--porcelain'], {
44
45
  cwd,
45
46
  });
46
47
  const hasUncommittedChanges = statusOutput.trim().length > 0;
47
48
  let hasUnpushedCommits = false;
48
- if (!hasUncommittedChanges) {
49
- // Working tree is clean — check for unpushed commits from a
50
- // previous save that committed locally but failed to push.
49
+ // Check for unpushed commits
50
+ try {
51
+ const { stdout: branchForCheck } = await runGit(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd });
52
+ const { stdout: unpushed } = await runGit(['log', `origin/${branchForCheck.trim()}..HEAD`, '--oneline'], { cwd });
53
+ hasUnpushedCommits = unpushed.trim().length > 0;
54
+ }
55
+ catch {
56
+ // No remote tracking branch yet — if HEAD has commits, treat
57
+ // as unpushed so the push flow can create the remote branch.
51
58
  try {
52
- const { stdout: branchForCheck } = await runGit(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd });
53
- const { stdout: unpushed } = await runGit(['log', `origin/${branchForCheck.trim()}..HEAD`, '--oneline'], { cwd });
54
- hasUnpushedCommits = unpushed.trim().length > 0;
59
+ const { stdout: logOutput } = await runGit(['log', '--oneline', '-1'], {
60
+ cwd,
61
+ });
62
+ hasUnpushedCommits = logOutput.trim().length > 0;
55
63
  }
56
64
  catch {
57
- // No remote tracking branch yet — if HEAD has commits, treat
58
- // as unpushed so the push flow can create the remote branch.
59
- try {
60
- const { stdout: logOutput } = await runGit(['log', '--oneline', '-1'], {
61
- cwd,
62
- });
63
- hasUnpushedCommits = logOutput.trim().length > 0;
64
- }
65
- catch {
66
- hasUnpushedCommits = false;
67
- }
65
+ hasUnpushedCommits = false;
68
66
  }
69
- if (!hasUnpushedCommits) {
70
- output.error('No changes to commit', 'Working tree is clean. Make changes to your code before saving.');
71
- process.exit(1);
67
+ }
68
+ if (options.all) {
69
+ // --all: stage and commit before pushing
70
+ if (hasUncommittedChanges) {
71
+ await runGit(['add', '-A'], { cwd });
72
+ output.progress('✓ Staged changes');
73
+ await runGit(['commit', '-m', options.message], { cwd });
74
+ output.progress('✓ Committed locally');
75
+ hasUnpushedCommits = true;
72
76
  }
73
- output.progress('✓ Found unpushed commits, resuming push...');
74
77
  }
75
78
  else {
76
- // Stage and commit new changes
77
- await runGit(['add', '.'], { cwd });
78
- output.progress(' Staged changes');
79
- await runGit(['commit', '-m', options.message], { cwd });
80
- output.progress('✓ Committed locally');
79
+ // Default: push-only, no staging or committing
80
+ if (!hasUnpushedCommits && hasUncommittedChanges) {
81
+ output.error('Uncommitted changes', 'You have uncommitted changes. Either commit them first:\n git add . && git commit -m "your message"\n\nOr use --all to stage and commit automatically:\n guild agent save -A --message "your message"');
82
+ process.exit(1);
83
+ }
84
+ }
85
+ if (!hasUnpushedCommits) {
86
+ output.error('No changes to save', 'Working tree is clean and there are no unpushed commits.');
87
+ process.exit(1);
88
+ }
89
+ // Resolve version summary: use --message if provided, otherwise
90
+ // extract the latest commit message.
91
+ let versionMessage = options.message;
92
+ if (!versionMessage) {
93
+ const { stdout: commitMsg } = await runGit(['log', '-1', '--pretty=%s'], {
94
+ cwd,
95
+ });
96
+ versionMessage = commitMsg.trim();
81
97
  }
82
98
  // Get remote URL with auth credentials
83
99
  const { stdout: remoteUrl } = await runGit(['remote', 'get-url', 'origin'], {
@@ -94,8 +110,9 @@ export function createAgentSaveCommand() {
94
110
  });
95
111
  const currentBranch = branchName.trim();
96
112
  // Pull remote changes first (rebase to avoid merge commits)
113
+ const gitEnv = { GUILD_AGENT_SAVE: '1' };
97
114
  try {
98
- const { stdout: pullOutput } = await runGit(['pull', '--rebase', authenticatedUrl, currentBranch], { cwd });
115
+ const { stdout: pullOutput } = await runGit(['pull', '--rebase', authenticatedUrl, currentBranch], { cwd, env: gitEnv });
99
116
  if (pullOutput.includes('Updating') ||
100
117
  pullOutput.includes('Fast-forward')) {
101
118
  output.progress('✓ Synced with remote');
@@ -132,6 +149,7 @@ export function createAgentSaveCommand() {
132
149
  try {
133
150
  await runGit(['push', authenticatedUrl, `HEAD:${currentBranch}`], {
134
151
  cwd,
152
+ env: gitEnv,
135
153
  });
136
154
  break;
137
155
  }
@@ -180,7 +198,7 @@ export function createAgentSaveCommand() {
180
198
  const client = new GuildAPIClient();
181
199
  let version = await client.post(`/agents/${guildConfig.agent_id}/versions`, {
182
200
  commit_sha: commitSha,
183
- summary: options.message,
201
+ summary: versionMessage,
184
202
  version_type: 'COMMITTED',
185
203
  });
186
204
  output.progress(`✓ Created version (${version.id})`);
@@ -226,7 +244,7 @@ export function createAgentSaveCommand() {
226
244
  failureDetails = 'Could not fetch validation details.';
227
245
  }
228
246
  failureDetails +=
229
- '\n\nFix the issues, then save a new version:\n guild agent save --message "Fix validation errors"';
247
+ '\n\nFix the issues, commit, and save a new version:\n git add . && git commit -m "Fix validation errors"\n guild agent save';
230
248
  output.error('Validation failed', failureDetails);
231
249
  process.exit(1);
232
250
  }
@@ -56,8 +56,8 @@ async function resolveOwnerName(ownerId) {
56
56
  if (me.id === ownerId) {
57
57
  return me.name;
58
58
  }
59
- const orgs = await client.get('/me/organizations');
60
- const org = orgs.items.find((o) => o.id === ownerId);
59
+ const orgs = await client.fetchAll('/me/organizations');
60
+ const org = orgs.find((o) => o.id === ownerId);
61
61
  return org?.name;
62
62
  }
63
63
  catch (error) {
@@ -50,8 +50,8 @@ export function createTriggerCreateCommand() {
50
50
  }
51
51
  const client = new GuildAPIClient();
52
52
  // Find workspace_agent_id from agent identifier
53
- const workspaceAgentsResponse = await client.get(`/workspaces/${workspaceId}/workspace_agents`);
54
- const workspaceAgent = workspaceAgentsResponse.items.find((wa) => wa.agent.full_name === options.agent ||
53
+ const allWorkspaceAgents = await client.fetchAll(`/workspaces/${workspaceId}/workspace_agents`);
54
+ const workspaceAgent = allWorkspaceAgents.find((wa) => wa.agent.full_name === options.agent ||
55
55
  wa.agent.name === options.agent ||
56
56
  wa.agent.id === options.agent);
57
57
  if (!workspaceAgent) {
@@ -30,10 +30,10 @@ export function createWorkspaceAgentRemoveCommand() {
30
30
  }
31
31
  workspaceId = resolved.workspaceId;
32
32
  }
33
- // List workspace agents to find the one to remove
34
- const response = await client.get(`/workspaces/${workspaceId}/workspace_agents`);
33
+ // List all workspace agents to find the one to remove
34
+ const allWorkspaceAgents = await client.fetchAll(`/workspaces/${workspaceId}/workspace_agents`);
35
35
  // Find the agent by identifier (match full_name, name, agent id, or workspace_agent id)
36
- const workspaceAgent = response.items.find((wa) => wa.id === agentIdentifier ||
36
+ const workspaceAgent = allWorkspaceAgents.find((wa) => wa.id === agentIdentifier ||
37
37
  wa.agent.full_name === agentIdentifier ||
38
38
  wa.agent.name === agentIdentifier ||
39
39
  wa.agent.id === agentIdentifier);
@@ -50,5 +50,13 @@ export declare class GuildAPIClient {
50
50
  * Make a DELETE request
51
51
  */
52
52
  delete<T = unknown>(endpoint: string, config?: AxiosRequestConfig): Promise<T>;
53
+ /**
54
+ * Fetch all items from a paginated endpoint by paging through results.
55
+ * Use for display commands or ID-based lookups where you need the complete list.
56
+ *
57
+ * @param endpoint - API endpoint path (may include query params)
58
+ * @returns Flat array of all items across all pages
59
+ */
60
+ fetchAll<T>(endpoint: string): Promise<T[]>;
53
61
  }
54
62
  //# sourceMappingURL=api-client.d.ts.map
@@ -121,5 +121,27 @@ export class GuildAPIClient {
121
121
  async delete(endpoint, config) {
122
122
  return this.request('DELETE', endpoint, config);
123
123
  }
124
+ /**
125
+ * Fetch all items from a paginated endpoint by paging through results.
126
+ * Use for display commands or ID-based lookups where you need the complete list.
127
+ *
128
+ * @param endpoint - API endpoint path (may include query params)
129
+ * @returns Flat array of all items across all pages
130
+ */
131
+ async fetchAll(endpoint) {
132
+ const PAGE_SIZE = 100;
133
+ const allItems = [];
134
+ let offset = 0;
135
+ while (true) {
136
+ const separator = endpoint.includes('?') ? '&' : '?';
137
+ const url = `${endpoint}${separator}limit=${PAGE_SIZE}&offset=${offset}`;
138
+ const response = await this.get(url);
139
+ allItems.push(...response.items);
140
+ offset += response.pagination.limit;
141
+ if (!response.pagination.has_more)
142
+ break;
143
+ }
144
+ return allItems;
145
+ }
124
146
  }
125
147
  //# sourceMappingURL=api-client.js.map
@@ -170,7 +170,7 @@ export type AgentListResponse = PaginatedResponse<Agent>;
170
170
  export type WorkspaceListResponse = PaginatedResponse<Workspace>;
171
171
  export type VersionListResponse = PaginatedResponse<AgentVersion>;
172
172
  export type ContextListResponse = PaginatedResponse<Context>;
173
- export type WorkspaceAgentListResponse = CountedResponse<WorkspaceAgent>;
173
+ export type WorkspaceAgentListResponse = PaginatedResponse<WorkspaceAgent>;
174
174
  /**
175
175
  * Response from creating a new agent.
176
176
  */
package/dist/lib/git.d.ts CHANGED
@@ -6,6 +6,8 @@ export interface GitOptions {
6
6
  cwd?: string;
7
7
  /** Timeout in milliseconds (default: 30000 for most commands) */
8
8
  timeout?: number;
9
+ /** Extra environment variables to pass to the git subprocess */
10
+ env?: Record<string, string>;
9
11
  }
10
12
  /**
11
13
  * Result from a git command
@@ -57,4 +59,11 @@ export declare function runGit(args: string[], options?: GitOptions): Promise<Gi
57
59
  * Returns a user-friendly error message with the git output.
58
60
  */
59
61
  export declare function formatGitError(error: GitError): string;
62
+ /**
63
+ * Install a pre-push hook that blocks direct git push.
64
+ *
65
+ * The hook allows pushes only when the GUILD_AGENT_SAVE env var is set,
66
+ * which guild agent save sets automatically.
67
+ */
68
+ export declare function installPrePushHook(targetDir: string): Promise<void>;
60
69
  //# sourceMappingURL=git.d.ts.map
package/dist/lib/git.js CHANGED
@@ -6,6 +6,8 @@
6
6
  * by preventing interactive prompts and providing clear error messages.
7
7
  */
8
8
  import { execa } from 'execa';
9
+ import * as fs from 'fs/promises';
10
+ import * as path from 'path';
9
11
  import { debug } from './errors.js';
10
12
  /**
11
13
  * Error thrown when a git command fails
@@ -71,7 +73,7 @@ const NETWORK_COMMANDS = ['clone', 'push', 'pull', 'fetch'];
71
73
  * }
72
74
  */
73
75
  export async function runGit(args, options = {}) {
74
- const { cwd, timeout: customTimeout } = options;
76
+ const { cwd, timeout: customTimeout, env: extraEnv } = options;
75
77
  // Determine timeout based on command type
76
78
  const isNetworkCommand = args.length > 0 && NETWORK_COMMANDS.includes(args[0]);
77
79
  const timeout = customTimeout ?? (isNetworkCommand ? NETWORK_TIMEOUT : DEFAULT_TIMEOUT);
@@ -88,6 +90,7 @@ export async function runGit(args, options = {}) {
88
90
  GIT_TERMINAL_PROMPT: '0',
89
91
  // Prevent SSH from prompting for passwords/passphrases
90
92
  GIT_SSH_COMMAND: 'ssh -o BatchMode=yes',
93
+ ...extraEnv,
91
94
  },
92
95
  });
93
96
  }
@@ -149,4 +152,24 @@ export function formatGitError(error) {
149
152
  }
150
153
  return lines.join('\n');
151
154
  }
155
+ const PRE_PUSH_HOOK = `#!/bin/sh
156
+ if [ "$GUILD_AGENT_SAVE" != "1" ]; then
157
+ echo "Please use \\\`guild agent save\\\` to push."
158
+ exit 1
159
+ fi
160
+ exit 0
161
+ `;
162
+ /**
163
+ * Install a pre-push hook that blocks direct git push.
164
+ *
165
+ * The hook allows pushes only when the GUILD_AGENT_SAVE env var is set,
166
+ * which guild agent save sets automatically.
167
+ */
168
+ export async function installPrePushHook(targetDir) {
169
+ const hooksDir = path.join(targetDir, '.git', 'hooks');
170
+ await fs.mkdir(hooksDir, { recursive: true });
171
+ await fs.writeFile(path.join(hooksDir, 'pre-push'), PRE_PUSH_HOOK, {
172
+ mode: 0o755,
173
+ });
174
+ }
152
175
  //# sourceMappingURL=git.js.map
@@ -27,8 +27,8 @@ export async function resolveOwnerId(options) {
27
27
  if (me.id === ownerFlag) {
28
28
  return { id: me.id, name: me.name, type: 'user' };
29
29
  }
30
- const orgs = await client.get('/me/organizations');
31
- const org = orgs.items.find((o) => o.id === ownerFlag);
30
+ const orgs = await client.fetchAll('/me/organizations');
31
+ const org = orgs.find((o) => o.id === ownerFlag);
32
32
  if (org) {
33
33
  return { id: org.id, name: org.name, type: 'organization' };
34
34
  }
@@ -43,8 +43,8 @@ export async function resolveOwnerId(options) {
43
43
  if (me.id === envOwnerId) {
44
44
  return { id: me.id, name: me.name, type: 'user' };
45
45
  }
46
- const orgs = await client.get('/me/organizations');
47
- const org = orgs.items.find((o) => o.id === envOwnerId);
46
+ const orgs = await client.fetchAll('/me/organizations');
47
+ const org = orgs.find((o) => o.id === envOwnerId);
48
48
  if (org) {
49
49
  return { id: org.id, name: org.name, type: 'organization' };
50
50
  }
@@ -63,9 +63,9 @@ export async function resolveOwnerId(options) {
63
63
  }
64
64
  // Priority 4: Fetch user + orgs
65
65
  const me = await client.get('/me');
66
- const orgs = await client.get('/me/organizations');
66
+ const orgs = await client.fetchAll('/me/organizations');
67
67
  // No orgs → use current user
68
- if (orgs.items.length === 0) {
68
+ if (orgs.length === 0) {
69
69
  debug('No organizations found, using current user as owner');
70
70
  return { id: me.id, name: me.name, type: 'user' };
71
71
  }
@@ -80,7 +80,7 @@ export async function resolveOwnerId(options) {
80
80
  name: `${me.name} (personal)`,
81
81
  value: { id: me.id, name: me.name, type: 'user' },
82
82
  },
83
- ...orgs.items.map((org) => ({
83
+ ...orgs.map((org) => ({
84
84
  name: org.name,
85
85
  value: { id: org.id, name: org.name, type: 'organization' },
86
86
  })),
@@ -19,8 +19,12 @@ guild agent create my-agent --template LLM
19
19
  guild agent clone guildai/dev-assistant
20
20
  cd dev-assistant
21
21
 
22
- # Save changes (commits and syncs to Guild server)
23
- guild agent save --message "Description of changes"
22
+ # Save changes (pushes commits to Guild server)
23
+ git add . && git commit -m "Description of changes"
24
+ guild agent save
25
+
26
+ # Or stage+commit+push in one step
27
+ guild agent save -A --message "Description of changes"
24
28
 
25
29
  # Save and publish
26
30
  guild agent save --message "Description" --wait --publish
@@ -37,9 +41,8 @@ guild agent save --message "Description" --wait --publish
37
41
  ## NEVER Do These Things
38
42
 
39
43
  - ❌ Manually create `package.json`, `tsconfig.json`, or `guild.json`
40
- - ❌ Run `git push` directly (use `guild agent save`)
44
+ - ❌ Run `git push` directly (use `guild agent save` — a pre-push hook blocks direct pushes)
41
45
  - ❌ Run `git pull` directly (use `guild agent pull`)
42
- - ❌ Run `git commit` directly (use `guild agent save`)
43
46
  - ❌ Edit `guild.json` (it's generated and gitignored)
44
47
 
45
48
  ## Common Commands
@@ -93,15 +96,21 @@ guild agent grep "pattern" --published
93
96
 
94
97
  ### Saving Changes
95
98
 
99
+ Git owns the working tree, Guild owns the remote. Use normal git commands to stage and commit, then `guild agent save` to push and create a version.
100
+
96
101
  ```bash
97
- # Save without publishing (creates draft)
98
- guild agent save --message "WIP: still testing"
102
+ # Commit with git, then push via Guild (creates draft)
103
+ git add . && git commit -m "WIP: still testing"
104
+ guild agent save
105
+
106
+ # Or stage+commit+push in one step
107
+ guild agent save -A --message "WIP: still testing"
99
108
 
100
109
  # Save and wait for validation
101
110
  guild agent save --message "Fix bug" --wait
102
111
 
103
112
  # Save, validate, and publish
104
- guild agent save --message "Release v1.0" --wait --publish
113
+ guild agent save -A --message "Release v1.0" --wait --publish
105
114
  ```
106
115
 
107
116
  ### Publishing
@@ -150,9 +159,9 @@ my-agent/
150
159
 
151
160
  ## Troubleshooting
152
161
 
153
- ### "No changes to commit"
162
+ ### "No changes to save"
154
163
 
155
- All changes are already committed. Make a code change first, then run `guild agent save` again.
164
+ Working tree is clean and there are no unpushed commits. Make a code change, commit it, then run `guild agent save` again.
156
165
 
157
166
  ### "guild.json not found"
158
167
 
@@ -103,9 +103,10 @@ Press `Ctrl+C` to exit.
103
103
  ### 4. Save and publish
104
104
 
105
105
  ```bash
106
- guild agent save --message "First version" --wait --publish
106
+ guild agent save -A --message "First version" --wait --publish
107
107
  ```
108
108
 
109
+ - `-A` stages and commits all changes before pushing
109
110
  - `--wait` blocks until validation passes
110
111
  - `--publish` makes the agent available in the Guild catalog
111
112
 
@@ -288,16 +289,25 @@ guild agent chat "Hello" # Send a single message
288
289
 
289
290
  ### 3. Save your work
290
291
 
292
+ Commit with git, then push via Guild:
293
+
294
+ ```bash
295
+ git add . && git commit -m "Add Slack notifications"
296
+ guild agent save
297
+ ```
298
+
299
+ Or stage+commit+push in one step:
300
+
291
301
  ```bash
292
- guild agent save --message "Add Slack notifications"
302
+ guild agent save -A --message "Add Slack notifications"
293
303
  ```
294
304
 
295
- This commits your code and creates a new version in the Guild backend. Versions start as drafts.
305
+ This pushes your commits and creates a new version in the Guild backend. Versions start as drafts.
296
306
 
297
307
  ### 4. Publish
298
308
 
299
309
  ```bash
300
- guild agent save --message "Ready to ship" --wait --publish
310
+ guild agent save -A --message "Ready to ship" --wait --publish
301
311
  ```
302
312
 
303
313
  `--wait` blocks until validation passes. `--publish` makes the agent available in the catalog. You can also publish separately:
@@ -319,7 +329,7 @@ guild agent code # View source of latest version
319
329
  - Agent code lives in `agent.ts` (typically at the project root, but can be in a subdirectory like `src/`).
320
330
  - Don't add `@guildai/agents-sdk`, `zod`, or `@guildai-services/*` to `package.json`. The runtime provides them. Only add third-party packages you actually use.
321
331
  - Always call tools through `task.tools.<name>(args)`. Never access services directly.
322
- - Always use `guild agent save` to commit and `guild agent pull` to sync. Don't use raw git commands.
332
+ - Use `git add` and `git commit` to manage your working tree. Use `guild agent save` to push and create versions. Don't use `git push` directly (a pre-push hook blocks it).
323
333
  - Don't edit `guild.json` — it's managed by the CLI.
324
334
 
325
335
  ## Other Commands
@@ -396,13 +406,9 @@ guild agent get
396
406
  guild agent get <agent-id>
397
407
  ```
398
408
 
399
- ### "No changes to commit" after a failed save
400
-
401
- If a previous `guild agent save` committed locally but failed to push (e.g., network error), just run save again. It detects the unpushed commits and resumes:
409
+ ### "No changes to save"
402
410
 
403
- ```bash
404
- guild agent save --message "Retry"
405
- ```
411
+ Working tree is clean and there are no unpushed commits. Make a code change, commit it, then run `guild agent save` again. If a previous save committed locally but failed to push (e.g., network error), just run `guild agent save` again — it detects the unpushed commits and resumes.
406
412
 
407
413
  ### Validation failures
408
414
 
@@ -412,8 +418,9 @@ After saving, if validation fails:
412
418
  # Check the latest version for errors
413
419
  guild agent versions --limit 1
414
420
 
415
- # Fix the issue and save again
416
- guild agent save --message "Fix validation error" --wait
421
+ # Fix the issue, commit, and save again
422
+ git add . && git commit -m "Fix validation error"
423
+ guild agent save --wait
417
424
  ```
418
425
 
419
426
  ### Agent test not responding
@@ -421,7 +428,7 @@ guild agent save --message "Fix validation error" --wait
421
428
  If `guild agent test` hangs or produces no output:
422
429
 
423
430
  1. Check your agent code compiles: look for TypeScript errors in `agent.ts`
424
- 2. Make sure you've saved at least once: `guild agent save --message "initial"`
431
+ 2. Make sure you've saved at least once: `guild agent save -A --message "initial"`
425
432
  3. Try a single message instead: `guild agent chat "hello"`
426
433
 
427
434
  ## Next Steps
@@ -53,18 +53,24 @@ guild agent clone owner/agent-name
53
53
 
54
54
  ### Syncing and Saving
55
55
 
56
+ Git owns the working tree, Guild owns the remote. Use normal git commands to stage and commit, then `guild agent save` to push and create a version.
57
+
56
58
  ```bash
57
59
  # Pull remote changes (e.g., edits from other collaborators)
58
60
  guild agent pull
59
61
 
60
- # Save without publishing (creates draft)
61
- guild agent save --message "Description of changes"
62
+ # Commit with git, then push via Guild (creates draft)
63
+ git add . && git commit -m "Description of changes"
64
+ guild agent save
65
+
66
+ # Or stage+commit+push in one step
67
+ guild agent save -A --message "Description of changes"
62
68
 
63
69
  # Save and wait for validation
64
70
  guild agent save --message "Fix bug" --wait
65
71
 
66
72
  # Save, validate, and publish
67
- guild agent save --message "Release v1.0" --wait --publish
73
+ guild agent save -A --message "Release v1.0" --wait --publish
68
74
  ```
69
75
 
70
76
  ### Testing
@@ -87,10 +93,13 @@ guild agent chat "Hello, can you help me?"
87
93
  ### For Creating and Modifying
88
94
 
89
95
  - ✅ `guild agent create`, `guild agent init`, `guild agent clone`
90
- - ✅ `guild agent save --message "description"`
96
+ - ✅ `git add`, `git commit` (manage your own working tree)
97
+ - ✅ `guild agent save` (push commits and create a version)
98
+ - ✅ `guild agent save -A --message "desc"` (stage+commit+push in one step)
91
99
  - ✅ `guild agent pull` (sync remote changes into local directory)
92
100
  - ✅ `guild agent test`, `guild agent chat`
93
- - ❌ NEVER use `git commit`, `git push`, `gh repo` for agent operations
101
+ - ❌ NEVER use `git push` directly (a pre-push hook blocks this — use `guild agent save`)
102
+ - ❌ NEVER use `gh repo` for agent operations
94
103
  - ❌ NEVER manually create `package.json`, `tsconfig.json`, or `guild.json`
95
104
 
96
105
  ### For Investigating and Debugging
@@ -702,7 +711,8 @@ guild agent create <name> --template LLM # Create with specific templa
702
711
  guild agent init # Initialize local workspace
703
712
  guild agent init --fork <agent-id> # Fork existing agent
704
713
  guild agent pull # Pull remote changes
705
- guild agent save --message "description" # Save changes
714
+ guild agent save # Push commits and create a draft version
715
+ guild agent save -A --message "description" # Stage+commit+push in one step
706
716
  guild agent save --message "v1.0" --wait --publish # Save + validate + publish
707
717
  guild agent test # Interactive test
708
718
  guild agent test --ephemeral # Ephemeral test
@@ -731,9 +741,9 @@ GUILD_OWNER_ID=<id> guild agent init --name my-agent # Override owner for agent
731
741
 
732
742
  ## Troubleshooting
733
743
 
734
- ### "No changes to commit"
744
+ ### "No changes to save"
735
745
 
736
- All changes are already committed. Make a code change first, then run `guild agent save` again.
746
+ Working tree is clean and there are no unpushed commits. Make a code change, commit it, then run `guild agent save` again.
737
747
 
738
748
  ### "guild.json not found"
739
749
 
@@ -757,5 +767,5 @@ If `guild.json` is tracked in git (it shouldn't be):
757
767
  ```bash
758
768
  echo "guild.json" >> .gitignore
759
769
  git rm --cached guild.json
760
- guild agent save --message "fix: Add guild.json to gitignore"
770
+ guild agent save -A --message "fix: Add guild.json to gitignore"
761
771
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guildai/cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Guild.ai CLI - Build, test, and deploy AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",