@proletariat/cli 0.3.26 → 0.3.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/bin/dev.js +7 -0
  2. package/bin/run.js +7 -0
  3. package/dist/commands/action/show.js +7 -1
  4. package/dist/commands/agent/shell.js +24 -10
  5. package/dist/commands/branch/list.js +14 -11
  6. package/dist/commands/branch/validate.js +10 -1
  7. package/dist/commands/claude.js +12 -40
  8. package/dist/commands/docker/clean.js +7 -9
  9. package/dist/commands/docker/index.js +5 -4
  10. package/dist/commands/docker/list.d.ts +1 -0
  11. package/dist/commands/docker/list.js +31 -17
  12. package/dist/commands/docker/status.d.ts +3 -1
  13. package/dist/commands/docker/status.js +28 -2
  14. package/dist/commands/docker/sync.js +7 -6
  15. package/dist/commands/epic/list.js +17 -2
  16. package/dist/commands/execution/list.js +25 -17
  17. package/dist/commands/pmo/init.js +22 -3
  18. package/dist/commands/repo/list.js +14 -8
  19. package/dist/commands/repo/view.js +2 -1
  20. package/dist/commands/roadmap/list.js +16 -1
  21. package/dist/commands/session/health.js +11 -10
  22. package/dist/commands/session/list.js +15 -8
  23. package/dist/commands/staff/list.d.ts +3 -1
  24. package/dist/commands/staff/list.js +15 -1
  25. package/dist/commands/theme/list.d.ts +3 -0
  26. package/dist/commands/theme/list.js +25 -0
  27. package/dist/commands/ticket/complete.js +4 -1
  28. package/dist/commands/ticket/create.d.ts +1 -0
  29. package/dist/commands/ticket/create.js +30 -0
  30. package/dist/commands/ticket/delete.js +3 -3
  31. package/dist/commands/ticket/edit.js +2 -2
  32. package/dist/commands/ticket/list.js +24 -5
  33. package/dist/commands/ticket/move.js +4 -1
  34. package/dist/commands/ticket/view.js +4 -2
  35. package/dist/commands/whoami.d.ts +3 -0
  36. package/dist/commands/whoami.js +22 -5
  37. package/dist/commands/work/complete.js +2 -2
  38. package/dist/commands/work/ready.js +2 -2
  39. package/dist/commands/work/revise.js +2 -2
  40. package/dist/commands/work/spawn.js +6 -21
  41. package/dist/commands/work/start.js +10 -25
  42. package/dist/commands/work/watch.js +57 -33
  43. package/dist/commands/workspace/prune.d.ts +3 -2
  44. package/dist/commands/workspace/prune.js +70 -10
  45. package/dist/lib/agents/commands.js +4 -0
  46. package/dist/lib/agents/index.js +12 -0
  47. package/dist/lib/execution/devcontainer.d.ts +4 -0
  48. package/dist/lib/execution/devcontainer.js +88 -3
  49. package/dist/lib/mcp/helpers.d.ts +15 -0
  50. package/dist/lib/mcp/helpers.js +15 -0
  51. package/dist/lib/mcp/tools/action.js +5 -5
  52. package/dist/lib/mcp/tools/board.js +7 -7
  53. package/dist/lib/mcp/tools/category.js +5 -5
  54. package/dist/lib/mcp/tools/cli-passthrough.js +30 -30
  55. package/dist/lib/mcp/tools/epic.js +8 -8
  56. package/dist/lib/mcp/tools/phase.js +7 -7
  57. package/dist/lib/mcp/tools/project.js +10 -10
  58. package/dist/lib/mcp/tools/roadmap.js +7 -7
  59. package/dist/lib/mcp/tools/spec.js +9 -9
  60. package/dist/lib/mcp/tools/status.js +6 -6
  61. package/dist/lib/mcp/tools/template.js +6 -6
  62. package/dist/lib/mcp/tools/ticket.js +19 -19
  63. package/dist/lib/mcp/tools/view.js +4 -4
  64. package/dist/lib/mcp/tools/work.js +6 -6
  65. package/dist/lib/mcp/tools/workflow.js +5 -5
  66. package/dist/lib/pmo/index.js +4 -0
  67. package/dist/lib/pmo/storage/base.js +49 -0
  68. package/dist/lib/pmo/types.d.ts +1 -1
  69. package/dist/lib/pmo/types.js +1 -0
  70. package/dist/lib/pr/index.d.ts +5 -0
  71. package/dist/lib/pr/index.js +69 -0
  72. package/dist/lib/repos/index.js +4 -0
  73. package/dist/lib/string-utils.d.ts +10 -0
  74. package/dist/lib/string-utils.js +16 -0
  75. package/oclif.manifest.json +3331 -3253
  76. package/package.json +3 -2
@@ -7,6 +7,7 @@ import { isValidAgentName, getSuggestedAgentNames, BUILTIN_THEMES, getThemePersi
7
7
  import { getWorkspaceRepositories, getActiveTheme } from '../database/index.js';
8
8
  import { styles } from '../styles.js';
9
9
  import { createDevcontainerConfig } from '../execution/devcontainer.js';
10
+ import { getGitIdentity } from '../pr/index.js';
10
11
  /**
11
12
  * Detect the current agent name from environment or directory structure.
12
13
  * Returns null if not running in an agent context.
@@ -93,6 +94,11 @@ function getRemoteUrl(repoPath) {
93
94
  export async function createAgentWorktrees(workspacePath, agents, hqPath, options) {
94
95
  const mountMode = options?.mountMode || 'worktree'; // Default to worktree for real-time file sync
95
96
  const modeLabel = mountMode === 'worktree' ? 'worktree' : 'clone';
97
+ // Detect git identity once for all agents (TKT-934)
98
+ const gitIdentity = getGitIdentity();
99
+ if (!gitIdentity.name && !gitIdentity.email) {
100
+ console.log(chalk.yellow('Warning: Could not detect git identity for devcontainer. Commits may use default identity.'));
101
+ }
96
102
  if (hqPath) {
97
103
  // HQ mode - create worktrees/clones for all repos in repos/ directory
98
104
  const reposDir = path.join(hqPath, 'repos');
@@ -227,6 +233,8 @@ export async function createAgentWorktrees(workspacePath, agents, hqPath, option
227
233
  agentDir,
228
234
  repoWorktrees: mountMode === 'worktree' && createdRepos.length > 0 ? createdRepos : undefined,
229
235
  mountMode,
236
+ gitUserName: gitIdentity.name || undefined,
237
+ gitUserEmail: gitIdentity.email || undefined,
230
238
  });
231
239
  }
232
240
  console.log(chalk.green(`✅ Agent ${agent} created with ${createdRepos.length} ${modeLabel}(s)`));
@@ -249,6 +257,8 @@ export async function createAgentWorktrees(workspacePath, agents, hqPath, option
249
257
  agentName: agent,
250
258
  agentDir,
251
259
  mountMode: mountMode,
260
+ gitUserName: gitIdentity.name || undefined,
261
+ gitUserEmail: gitIdentity.email || undefined,
252
262
  });
253
263
  }
254
264
  console.log(chalk.green(`✅ Placeholder agent ${agent} created`));
@@ -367,6 +377,8 @@ export async function createAgentWorktrees(workspacePath, agents, hqPath, option
367
377
  agentDir,
368
378
  repoWorktrees: mountMode === 'worktree' ? [repoName] : undefined, // Only pass repos for worktree mode
369
379
  mountMode,
380
+ gitUserName: gitIdentity.name || undefined,
381
+ gitUserEmail: gitIdentity.email || undefined,
370
382
  });
371
383
  }
372
384
  console.log(chalk.green(`✅ Agent ${agent} created with ${modeLabel}`));
@@ -17,6 +17,10 @@ export interface DevcontainerOptions {
17
17
  prltChannel?: string;
18
18
  /** Mount mode: 'worktree' needs parent repo mounts + git wrapper, 'clone' is self-contained */
19
19
  mountMode?: MountMode;
20
+ /** Git user.name for commit attribution (detected from gh/git config on host) */
21
+ gitUserName?: string;
22
+ /** Git user.email for commit attribution (detected from gh/git config on host) */
23
+ gitUserEmail?: string;
20
24
  }
21
25
  export interface DevcontainerJson {
22
26
  name: string;
@@ -106,6 +106,12 @@ export function generateDevcontainerJson(options, config) {
106
106
  PRLT_HOST_PATH: options.agentDir,
107
107
  // Mount mode - allows scripts to know if git wrapper is needed
108
108
  PRLT_MOUNT_MODE: mountMode,
109
+ // Git identity for commit attribution (detected from host's gh/git config)
110
+ ...(options.gitUserName ? { PRLT_GIT_USER_NAME: options.gitUserName } : {}),
111
+ ...(options.gitUserEmail ? { PRLT_GIT_USER_EMAIL: options.gitUserEmail } : {}),
112
+ // prlt channel info for version updates at container startup (TKT-954)
113
+ PRLT_REGISTRY: channel.registry,
114
+ ...(!useMount ? { PRLT_VERSION: channel.version || 'latest' } : {}),
109
115
  // /hq/.proletariat/bin contains prlt wrapper with ESM loader for native modules
110
116
  PATH: '/hq/.proletariat/bin:/home/node/.npm-global/bin:/usr/local/bin:/usr/bin:/bin',
111
117
  },
@@ -121,7 +127,7 @@ export function generateDevcontainerJson(options, config) {
121
127
  */
122
128
  export function generateDockerfile(options) {
123
129
  const timezone = options.timezone || 'America/Los_Angeles';
124
- return `FROM node:20
130
+ return `FROM node:22
125
131
 
126
132
  # Ensure we run as root for apt-get and system setup
127
133
  USER root
@@ -452,11 +458,90 @@ else
452
458
  echo "Warning: No GitHub token found, git push will require manual auth"
453
459
  fi
454
460
 
455
- # Check if prlt is already installed globally (via npm from GitHub Packages)
461
+ # Configure git user identity for commit attribution
462
+ # Uses env vars set by host (from gh/git config), with fallback detection
463
+ configure_git_identity() {
464
+ local git_name="\${PRLT_GIT_USER_NAME:-}"
465
+ local git_email="\${PRLT_GIT_USER_EMAIL:-}"
466
+
467
+ # Fallback: try gh api user if env vars are empty and gh is authenticated
468
+ if { [ -z "$git_name" ] || [ -z "$git_email" ]; } && command -v gh &> /dev/null && gh auth status &>/dev/null; then
469
+ if [ -z "$git_name" ]; then
470
+ git_name=$(gh api user -q '.name // .login' 2>/dev/null || true)
471
+ fi
472
+ if [ -z "$git_email" ]; then
473
+ git_email=$(gh api user -q '.email // empty' 2>/dev/null || true)
474
+ # Try emails API if public email is not set
475
+ if [ -z "$git_email" ]; then
476
+ git_email=$(gh api user/emails -q '[.[] | select(.primary)] | .[0].email' 2>/dev/null || true)
477
+ fi
478
+ fi
479
+ fi
480
+
481
+ # Fallback: try git config from mounted repos
482
+ if [ -z "$git_name" ] || [ -z "$git_email" ]; then
483
+ for repo_dir in /workspace/*/; do
484
+ if [ -d "$repo_dir/.git" ] || [ -f "$repo_dir/.git" ]; then
485
+ if [ -z "$git_name" ]; then
486
+ git_name=$(/usr/bin/git -C "$repo_dir" config user.name 2>/dev/null || true)
487
+ fi
488
+ if [ -z "$git_email" ]; then
489
+ git_email=$(/usr/bin/git -C "$repo_dir" config user.email 2>/dev/null || true)
490
+ fi
491
+ if [ -n "$git_name" ] && [ -n "$git_email" ]; then
492
+ break
493
+ fi
494
+ fi
495
+ done
496
+ fi
497
+
498
+ # Apply git config
499
+ if [ -n "$git_name" ]; then
500
+ /usr/bin/git config --global user.name "$git_name"
501
+ echo "Git user.name set to: $git_name"
502
+ fi
503
+ if [ -n "$git_email" ]; then
504
+ /usr/bin/git config --global user.email "$git_email"
505
+ echo "Git user.email set to: $git_email"
506
+ fi
507
+
508
+ # Warning if identity could not be determined
509
+ if [ -z "$git_name" ] && [ -z "$git_email" ]; then
510
+ echo "Warning: Could not determine git identity. Commits may use default identity."
511
+ echo " To fix: run 'gh auth login' or set git config user.name/user.email in your repo"
512
+ elif [ -z "$git_name" ]; then
513
+ echo "Warning: Could not determine git user.name"
514
+ elif [ -z "$git_email" ]; then
515
+ echo "Warning: Could not determine git user.email"
516
+ fi
517
+ }
518
+
519
+ configure_git_identity
520
+
521
+ # Check if prlt is already installed globally (via npm)
522
+ # TKT-954: Also check for updates - Docker layer caching may have installed an older version
456
523
  if command -v prlt &> /dev/null; then
457
524
  PRLT_PATH=$(which prlt)
458
525
  if [[ "$PRLT_PATH" == "/home/node/.npm-global/bin/prlt" ]]; then
459
- echo "prlt installed via npm, no setup needed"
526
+ DESIRED_VERSION="\${PRLT_VERSION:-latest}"
527
+ CURRENT_VERSION=$(prlt --version 2>/dev/null | grep -oE '[0-9]+\\.[0-9]+\\.[0-9]+' || echo "unknown")
528
+
529
+ # Determine the target version to compare against
530
+ if [ "$DESIRED_VERSION" = "latest" ] || [ "$DESIRED_VERSION" = "dev" ] || [ "$DESIRED_VERSION" = "next" ]; then
531
+ # For tags, check npm registry for the actual version number
532
+ TARGET_VERSION=$(npm view "@proletariat/cli@\${DESIRED_VERSION}" version 2>/dev/null || echo "unknown")
533
+ else
534
+ # For pinned versions (e.g., "1.2.3"), use directly
535
+ TARGET_VERSION="$DESIRED_VERSION"
536
+ fi
537
+
538
+ if [ "$TARGET_VERSION" != "unknown" ] && [ "$CURRENT_VERSION" != "$TARGET_VERSION" ]; then
539
+ echo "Updating prlt from v\${CURRENT_VERSION} to v\${TARGET_VERSION} (channel: \${DESIRED_VERSION})..."
540
+ npm install -g "@proletariat/cli@\${DESIRED_VERSION}" 2>&1
541
+ echo "prlt updated successfully"
542
+ else
543
+ echo "prlt v\${CURRENT_VERSION} is up to date"
544
+ fi
460
545
  exit 0
461
546
  fi
462
547
  fi
@@ -1,8 +1,23 @@
1
1
  /**
2
2
  * MCP Helper Functions
3
3
  */
4
+ import { z } from 'zod';
5
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
7
+ import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
8
+ import type { ServerRequest, ServerNotification } from '@modelcontextprotocol/sdk/types.js';
4
9
  import type { Ticket } from '../pmo/types.js';
5
10
  import type { McpToolResult } from './types.js';
11
+ /**
12
+ * Register an MCP tool with strict parameter validation.
13
+ *
14
+ * Uses z.object().strict() so that unknown/extra parameters are rejected
15
+ * with a clear error instead of being silently stripped.
16
+ * See: https://github.com/anthropics/proletariat/issues/366
17
+ */
18
+ export declare function strictTool<T extends Record<string, z.ZodType>>(server: McpServer, name: string, description: string, shape: T, handler: (params: {
19
+ [K in keyof T]: z.infer<T[K]>;
20
+ }, extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => CallToolResult | Promise<CallToolResult>): void;
6
21
  export declare function formatTicket(t: Ticket): {
7
22
  id: string;
8
23
  title: string;
@@ -1,6 +1,21 @@
1
1
  /**
2
2
  * MCP Helper Functions
3
3
  */
4
+ import { z } from 'zod';
5
+ /**
6
+ * Register an MCP tool with strict parameter validation.
7
+ *
8
+ * Uses z.object().strict() so that unknown/extra parameters are rejected
9
+ * with a clear error instead of being silently stripped.
10
+ * See: https://github.com/anthropics/proletariat/issues/366
11
+ */
12
+ export function strictTool(server, name, description, shape, handler) {
13
+ const strictSchema = z.object(shape).strict();
14
+ server.registerTool(name, {
15
+ description,
16
+ inputSchema: strictSchema,
17
+ }, handler);
18
+ }
4
19
  export function formatTicket(t) {
5
20
  return {
6
21
  id: t.id,
@@ -2,9 +2,9 @@
2
2
  * MCP Action Tools
3
3
  */
4
4
  import { z } from 'zod';
5
- import { errorResponse } from '../helpers.js';
5
+ import { errorResponse, strictTool } from '../helpers.js';
6
6
  export function registerActionTools(server, ctx) {
7
- server.tool('action_list', 'List work actions', { include_builtin: z.boolean().optional() }, async (params) => {
7
+ strictTool(server, 'action_list', 'List work actions', { include_builtin: z.boolean().optional() }, async (params) => {
8
8
  try {
9
9
  const actions = await ctx.storage.listActions({
10
10
  isBuiltin: params.include_builtin ? undefined : false,
@@ -29,7 +29,7 @@ export function registerActionTools(server, ctx) {
29
29
  return errorResponse(error);
30
30
  }
31
31
  });
32
- server.tool('action_show', 'Get action details', { id: z.string().describe('Action ID') }, async (params) => {
32
+ strictTool(server, 'action_show', 'Get action details', { id: z.string().describe('Action ID') }, async (params) => {
33
33
  try {
34
34
  const action = await ctx.storage.getAction(params.id);
35
35
  if (!action)
@@ -45,7 +45,7 @@ export function registerActionTools(server, ctx) {
45
45
  return errorResponse(error);
46
46
  }
47
47
  });
48
- server.tool('action_create', 'Create a work action', {
48
+ strictTool(server, 'action_create', 'Create a work action', {
49
49
  name: z.string().describe('Action name'),
50
50
  prompt: z.string().describe('Start prompt'),
51
51
  description: z.string().optional(),
@@ -71,7 +71,7 @@ export function registerActionTools(server, ctx) {
71
71
  return errorResponse(error);
72
72
  }
73
73
  });
74
- server.tool('action_delete', 'Delete an action', { id: z.string().describe('Action ID') }, async (params) => {
74
+ strictTool(server, 'action_delete', 'Delete an action', { id: z.string().describe('Action ID') }, async (params) => {
75
75
  try {
76
76
  await ctx.storage.deleteAction(params.id);
77
77
  return {
@@ -2,9 +2,9 @@
2
2
  * MCP Board Tools
3
3
  */
4
4
  import { z } from 'zod';
5
- import { errorResponse } from '../helpers.js';
5
+ import { errorResponse, strictTool } from '../helpers.js';
6
6
  export function registerBoardTools(server, ctx) {
7
- server.tool('board_show', 'Show the kanban board', { project: z.string().optional().describe('Project ID') }, async (params) => {
7
+ strictTool(server, 'board_show', 'Show the kanban board', { project: z.string().optional().describe('Project ID') }, async (params) => {
8
8
  try {
9
9
  let projectId = params.project;
10
10
  if (!projectId) {
@@ -43,7 +43,7 @@ export function registerBoardTools(server, ctx) {
43
43
  return errorResponse(error);
44
44
  }
45
45
  });
46
- server.tool('board_columns', 'Get column names for a project', { project: z.string().optional().describe('Project ID') }, async (params) => {
46
+ strictTool(server, 'board_columns', 'Get column names for a project', { project: z.string().optional().describe('Project ID') }, async (params) => {
47
47
  try {
48
48
  let projectId = params.project;
49
49
  if (!projectId) {
@@ -64,7 +64,7 @@ export function registerBoardTools(server, ctx) {
64
64
  return errorResponse(error);
65
65
  }
66
66
  });
67
- server.tool('board_create_column', 'Add a new column to the board', {
67
+ strictTool(server, 'board_create_column', 'Add a new column to the board', {
68
68
  project: z.string().describe('Project ID'),
69
69
  name: z.string().describe('Column name'),
70
70
  position: z.number().optional().describe('Position'),
@@ -82,7 +82,7 @@ export function registerBoardTools(server, ctx) {
82
82
  return errorResponse(error);
83
83
  }
84
84
  });
85
- server.tool('board_rename_column', 'Rename a column', {
85
+ strictTool(server, 'board_rename_column', 'Rename a column', {
86
86
  project: z.string().describe('Project ID'),
87
87
  column_id: z.string().describe('Column ID'),
88
88
  name: z.string().describe('New name'),
@@ -100,7 +100,7 @@ export function registerBoardTools(server, ctx) {
100
100
  return errorResponse(error);
101
101
  }
102
102
  });
103
- server.tool('board_move_column', 'Reorder a column', {
103
+ strictTool(server, 'board_move_column', 'Reorder a column', {
104
104
  project: z.string().describe('Project ID'),
105
105
  column_id: z.string().describe('Column ID'),
106
106
  position: z.number().describe('New position'),
@@ -118,7 +118,7 @@ export function registerBoardTools(server, ctx) {
118
118
  return errorResponse(error);
119
119
  }
120
120
  });
121
- server.tool('board_delete_column', 'Delete a column', {
121
+ strictTool(server, 'board_delete_column', 'Delete a column', {
122
122
  project: z.string().describe('Project ID'),
123
123
  column_id: z.string().describe('Column ID'),
124
124
  cascade: z.boolean().optional().describe('Delete tickets in column'),
@@ -2,9 +2,9 @@
2
2
  * MCP Category Tools
3
3
  */
4
4
  import { z } from 'zod';
5
- import { errorResponse } from '../helpers.js';
5
+ import { errorResponse, strictTool } from '../helpers.js';
6
6
  export function registerCategoryTools(server, ctx) {
7
- server.tool('category_list', 'List categories', { type: z.enum(['ticket', 'status']).optional() }, async (params) => {
7
+ strictTool(server, 'category_list', 'List categories', { type: z.enum(['ticket', 'status']).optional() }, async (params) => {
8
8
  try {
9
9
  const categories = await ctx.storage.listCategories({ type: params.type });
10
10
  return {
@@ -26,7 +26,7 @@ export function registerCategoryTools(server, ctx) {
26
26
  return errorResponse(error);
27
27
  }
28
28
  });
29
- server.tool('category_create', 'Create a category', {
29
+ strictTool(server, 'category_create', 'Create a category', {
30
30
  name: z.string().describe('Category name'),
31
31
  type: z.enum(['ticket', 'status']).describe('Category type'),
32
32
  description: z.string().optional(),
@@ -50,7 +50,7 @@ export function registerCategoryTools(server, ctx) {
50
50
  return errorResponse(error);
51
51
  }
52
52
  });
53
- server.tool('category_rename', 'Rename a category', {
53
+ strictTool(server, 'category_rename', 'Rename a category', {
54
54
  id: z.string().describe('Category ID'),
55
55
  name: z.string().describe('New name'),
56
56
  }, async (params) => {
@@ -67,7 +67,7 @@ export function registerCategoryTools(server, ctx) {
67
67
  return errorResponse(error);
68
68
  }
69
69
  });
70
- server.tool('category_delete', 'Delete a category', { id: z.string().describe('Category ID') }, async (params) => {
70
+ strictTool(server, 'category_delete', 'Delete a category', { id: z.string().describe('Category ID') }, async (params) => {
71
71
  try {
72
72
  await ctx.storage.deleteCategory(params.id);
73
73
  return {
@@ -5,9 +5,9 @@
5
5
  * They're wrapped for MCP but return formatted text output.
6
6
  */
7
7
  import { z } from 'zod';
8
- import { errorResponse, textResponse } from '../helpers.js';
8
+ import { errorResponse, textResponse, strictTool } from '../helpers.js';
9
9
  export function registerAgentTools(server, ctx) {
10
- server.tool('agent_list', 'List all agents', { type: z.enum(['staff', 'temp', 'all']).optional() }, async (params) => {
10
+ strictTool(server, 'agent_list', 'List all agents', { type: z.enum(['staff', 'temp', 'all']).optional() }, async (params) => {
11
11
  try {
12
12
  const typeFlag = params.type ? `--type ${params.type}` : '';
13
13
  const output = ctx.runCommand(`prlt agent list ${typeFlag} --json 2>/dev/null || prlt agent list ${typeFlag}`);
@@ -17,7 +17,7 @@ export function registerAgentTools(server, ctx) {
17
17
  return errorResponse(error);
18
18
  }
19
19
  });
20
- server.tool('agent_status', 'Check agent status', {}, async () => {
20
+ strictTool(server, 'agent_status', 'Check agent status', {}, async () => {
21
21
  try {
22
22
  const output = ctx.runCommand('prlt agent status --json 2>/dev/null || prlt agent status');
23
23
  return textResponse(output);
@@ -26,7 +26,7 @@ export function registerAgentTools(server, ctx) {
26
26
  return errorResponse(error);
27
27
  }
28
28
  });
29
- server.tool('agent_add', 'Add new agents', {
29
+ strictTool(server, 'agent_add', 'Add new agents', {
30
30
  names: z.array(z.string()).optional().describe('Agent names'),
31
31
  theme: z.string().optional().describe('Theme for names'),
32
32
  clone: z.boolean().optional().describe('Use git clone instead of worktree'),
@@ -42,7 +42,7 @@ export function registerAgentTools(server, ctx) {
42
42
  return errorResponse(error);
43
43
  }
44
44
  });
45
- server.tool('agent_remove', 'Remove an agent', { name: z.string().describe('Agent name') }, async (params) => {
45
+ strictTool(server, 'agent_remove', 'Remove an agent', { name: z.string().describe('Agent name') }, async (params) => {
46
46
  try {
47
47
  const output = ctx.runCommand(`prlt agent remove ${params.name} --json`);
48
48
  return textResponse(output);
@@ -53,7 +53,7 @@ export function registerAgentTools(server, ctx) {
53
53
  });
54
54
  }
55
55
  export function registerDockerTools(server, ctx) {
56
- server.tool('docker_status', 'Check Docker daemon status', {}, async () => {
56
+ strictTool(server, 'docker_status', 'Check Docker daemon status', {}, async () => {
57
57
  try {
58
58
  const output = ctx.runCommand('prlt docker status');
59
59
  return textResponse(output);
@@ -62,7 +62,7 @@ export function registerDockerTools(server, ctx) {
62
62
  return errorResponse(error);
63
63
  }
64
64
  });
65
- server.tool('docker_list', 'List Docker containers', {}, async () => {
65
+ strictTool(server, 'docker_list', 'List Docker containers', {}, async () => {
66
66
  try {
67
67
  const output = ctx.runCommand('prlt docker list');
68
68
  return textResponse(output);
@@ -71,7 +71,7 @@ export function registerDockerTools(server, ctx) {
71
71
  return errorResponse(error);
72
72
  }
73
73
  });
74
- server.tool('docker_start', 'Start Docker containers', { agent: z.string().optional().describe('Agent name') }, async (params) => {
74
+ strictTool(server, 'docker_start', 'Start Docker containers', { agent: z.string().optional().describe('Agent name') }, async (params) => {
75
75
  try {
76
76
  const agentArg = params.agent || '';
77
77
  const output = ctx.runCommand(`prlt docker start ${agentArg}`);
@@ -81,7 +81,7 @@ export function registerDockerTools(server, ctx) {
81
81
  return errorResponse(error);
82
82
  }
83
83
  });
84
- server.tool('docker_stop', 'Stop Docker containers', { agent: z.string().optional().describe('Agent name') }, async (params) => {
84
+ strictTool(server, 'docker_stop', 'Stop Docker containers', { agent: z.string().optional().describe('Agent name') }, async (params) => {
85
85
  try {
86
86
  const agentArg = params.agent || '';
87
87
  const output = ctx.runCommand(`prlt docker stop ${agentArg}`);
@@ -91,7 +91,7 @@ export function registerDockerTools(server, ctx) {
91
91
  return errorResponse(error);
92
92
  }
93
93
  });
94
- server.tool('docker_logs', 'Get container logs', {
94
+ strictTool(server, 'docker_logs', 'Get container logs', {
95
95
  agent: z.string().describe('Agent name'),
96
96
  lines: z.number().optional().describe('Number of lines'),
97
97
  }, async (params) => {
@@ -106,7 +106,7 @@ export function registerDockerTools(server, ctx) {
106
106
  });
107
107
  }
108
108
  export function registerRepoTools(server, ctx) {
109
- server.tool('repo_list', 'List repositories in workspace', {}, async () => {
109
+ strictTool(server, 'repo_list', 'List repositories in workspace', {}, async () => {
110
110
  try {
111
111
  const output = ctx.runCommand('prlt repo list');
112
112
  return textResponse(output);
@@ -115,7 +115,7 @@ export function registerRepoTools(server, ctx) {
115
115
  return errorResponse(error);
116
116
  }
117
117
  });
118
- server.tool('repo_add', 'Add a repository', {
118
+ strictTool(server, 'repo_add', 'Add a repository', {
119
119
  path: z.string().describe('Repository path or URL'),
120
120
  name: z.string().optional().describe('Name for the repo'),
121
121
  }, async (params) => {
@@ -128,7 +128,7 @@ export function registerRepoTools(server, ctx) {
128
128
  return errorResponse(error);
129
129
  }
130
130
  });
131
- server.tool('repo_remove', 'Remove a repository', { name: z.string().describe('Repository name') }, async (params) => {
131
+ strictTool(server, 'repo_remove', 'Remove a repository', { name: z.string().describe('Repository name') }, async (params) => {
132
132
  try {
133
133
  const output = ctx.runCommand(`prlt repo remove ${params.name}`);
134
134
  return textResponse(output);
@@ -139,7 +139,7 @@ export function registerRepoTools(server, ctx) {
139
139
  });
140
140
  }
141
141
  export function registerBranchTools(server, ctx) {
142
- server.tool('branch_list', 'List branches', {}, async () => {
142
+ strictTool(server, 'branch_list', 'List branches', {}, async () => {
143
143
  try {
144
144
  const output = ctx.runCommand('prlt branch list');
145
145
  return textResponse(output);
@@ -148,7 +148,7 @@ export function registerBranchTools(server, ctx) {
148
148
  return errorResponse(error);
149
149
  }
150
150
  });
151
- server.tool('branch_create', 'Create a branch for a ticket', {
151
+ strictTool(server, 'branch_create', 'Create a branch for a ticket', {
152
152
  ticket_id: z.string().describe('Ticket ID'),
153
153
  name: z.string().optional().describe('Branch name override'),
154
154
  }, async (params) => {
@@ -161,7 +161,7 @@ export function registerBranchTools(server, ctx) {
161
161
  return errorResponse(error);
162
162
  }
163
163
  });
164
- server.tool('branch_where', 'Show which ticket/branch you are on', {}, async () => {
164
+ strictTool(server, 'branch_where', 'Show which ticket/branch you are on', {}, async () => {
165
165
  try {
166
166
  const output = ctx.runCommand('prlt branch where');
167
167
  return textResponse(output);
@@ -172,7 +172,7 @@ export function registerBranchTools(server, ctx) {
172
172
  });
173
173
  }
174
174
  export function registerGitHubTools(server, ctx) {
175
- server.tool('gh_status', 'Check GitHub CLI status', {}, async () => {
175
+ strictTool(server, 'gh_status', 'Check GitHub CLI status', {}, async () => {
176
176
  try {
177
177
  const output = ctx.runCommand('prlt gh status');
178
178
  return textResponse(output);
@@ -181,7 +181,7 @@ export function registerGitHubTools(server, ctx) {
181
181
  return errorResponse(error);
182
182
  }
183
183
  });
184
- server.tool('gh_login', 'Login to GitHub', {}, async () => {
184
+ strictTool(server, 'gh_login', 'Login to GitHub', {}, async () => {
185
185
  try {
186
186
  const output = ctx.runCommand('prlt gh login');
187
187
  return textResponse(output);
@@ -190,7 +190,7 @@ export function registerGitHubTools(server, ctx) {
190
190
  return errorResponse(error);
191
191
  }
192
192
  });
193
- server.tool('pr_create', 'Create a pull request', {
193
+ strictTool(server, 'pr_create', 'Create a pull request', {
194
194
  ticket_id: z.string().optional().describe('Ticket ID'),
195
195
  title: z.string().optional().describe('PR title'),
196
196
  body: z.string().optional().describe('PR body'),
@@ -208,7 +208,7 @@ export function registerGitHubTools(server, ctx) {
208
208
  return errorResponse(error);
209
209
  }
210
210
  });
211
- server.tool('pr_list', 'List pull requests', {}, async () => {
211
+ strictTool(server, 'pr_list', 'List pull requests', {}, async () => {
212
212
  try {
213
213
  const output = ctx.runCommand('prlt pr list');
214
214
  return textResponse(output);
@@ -217,7 +217,7 @@ export function registerGitHubTools(server, ctx) {
217
217
  return errorResponse(error);
218
218
  }
219
219
  });
220
- server.tool('pr_status', 'Check PR status', {}, async () => {
220
+ strictTool(server, 'pr_status', 'Check PR status', {}, async () => {
221
221
  try {
222
222
  const output = ctx.runCommand('prlt pr status');
223
223
  return textResponse(output);
@@ -228,7 +228,7 @@ export function registerGitHubTools(server, ctx) {
228
228
  });
229
229
  }
230
230
  export function registerInitTools(server, ctx) {
231
- server.tool('init', 'Initialize an HQ workspace', {
231
+ strictTool(server, 'init', 'Initialize an HQ workspace', {
232
232
  name: z.string().describe('HQ name'),
233
233
  path: z.string().optional().describe('HQ path'),
234
234
  repos: z.array(z.string()).optional().describe('Repository paths'),
@@ -247,7 +247,7 @@ export function registerInitTools(server, ctx) {
247
247
  return errorResponse(error);
248
248
  }
249
249
  });
250
- server.tool('pmo_init', 'Initialize PMO in existing workspace', {
250
+ strictTool(server, 'pmo_init', 'Initialize PMO in existing workspace', {
251
251
  template: z.string().optional().describe('Board template'),
252
252
  name: z.string().optional().describe('Board name'),
253
253
  }, async (params) => {
@@ -263,7 +263,7 @@ export function registerInitTools(server, ctx) {
263
263
  });
264
264
  }
265
265
  export function registerUtilityTools(server, ctx) {
266
- server.tool('whoami', 'Show current context (agent, workspace)', {}, async () => {
266
+ strictTool(server, 'whoami', 'Show current context (agent, workspace)', {}, async () => {
267
267
  try {
268
268
  const output = ctx.runCommand('prlt whoami');
269
269
  return textResponse(output);
@@ -272,7 +272,7 @@ export function registerUtilityTools(server, ctx) {
272
272
  return errorResponse(error);
273
273
  }
274
274
  });
275
- server.tool('commit', 'Create a commit with ticket reference', {
275
+ strictTool(server, 'commit', 'Create a commit with ticket reference', {
276
276
  message: z.string().describe('Commit message'),
277
277
  ticket_id: z.string().optional().describe('Ticket ID'),
278
278
  }, async (params) => {
@@ -285,7 +285,7 @@ export function registerUtilityTools(server, ctx) {
285
285
  return errorResponse(error);
286
286
  }
287
287
  });
288
- server.tool('execution_list', 'List work executions', {}, async () => {
288
+ strictTool(server, 'execution_list', 'List work executions', {}, async () => {
289
289
  try {
290
290
  const output = ctx.runCommand('prlt execution list');
291
291
  return textResponse(output);
@@ -294,7 +294,7 @@ export function registerUtilityTools(server, ctx) {
294
294
  return errorResponse(error);
295
295
  }
296
296
  });
297
- server.tool('execution_view', 'View execution details', { id: z.string().describe('Execution ID') }, async (params) => {
297
+ strictTool(server, 'execution_view', 'View execution details', { id: z.string().describe('Execution ID') }, async (params) => {
298
298
  try {
299
299
  const output = ctx.runCommand(`prlt execution view ${params.id}`);
300
300
  return textResponse(output);
@@ -303,7 +303,7 @@ export function registerUtilityTools(server, ctx) {
303
303
  return errorResponse(error);
304
304
  }
305
305
  });
306
- server.tool('execution_logs', 'Get execution logs', { id: z.string().describe('Execution ID') }, async (params) => {
306
+ strictTool(server, 'execution_logs', 'Get execution logs', { id: z.string().describe('Execution ID') }, async (params) => {
307
307
  try {
308
308
  const output = ctx.runCommand(`prlt execution logs ${params.id}`);
309
309
  return textResponse(output);
@@ -312,7 +312,7 @@ export function registerUtilityTools(server, ctx) {
312
312
  return errorResponse(error);
313
313
  }
314
314
  });
315
- server.tool('session_list', 'List tmux sessions', {}, async () => {
315
+ strictTool(server, 'session_list', 'List tmux sessions', {}, async () => {
316
316
  try {
317
317
  const output = ctx.runCommand('prlt session list');
318
318
  return textResponse(output);
@@ -321,7 +321,7 @@ export function registerUtilityTools(server, ctx) {
321
321
  return errorResponse(error);
322
322
  }
323
323
  });
324
- server.tool('config_show', 'Show configuration', {}, async () => {
324
+ strictTool(server, 'config_show', 'Show configuration', {}, async () => {
325
325
  try {
326
326
  const output = ctx.runCommand('prlt config');
327
327
  return textResponse(output);