@proletariat/cli 0.3.25 → 0.3.27

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 (123) hide show
  1. package/dist/commands/action/index.js +2 -2
  2. package/dist/commands/action/show.js +7 -1
  3. package/dist/commands/agent/auth.js +1 -1
  4. package/dist/commands/agent/cleanup.js +6 -6
  5. package/dist/commands/agent/discover.js +1 -1
  6. package/dist/commands/agent/remove.js +4 -4
  7. package/dist/commands/autocomplete/setup.d.ts +2 -2
  8. package/dist/commands/autocomplete/setup.js +5 -5
  9. package/dist/commands/branch/create.js +31 -30
  10. package/dist/commands/branch/list.js +14 -11
  11. package/dist/commands/branch/validate.js +10 -1
  12. package/dist/commands/category/create.js +4 -5
  13. package/dist/commands/category/delete.js +2 -3
  14. package/dist/commands/category/rename.js +2 -3
  15. package/dist/commands/claude.d.ts +2 -8
  16. package/dist/commands/claude.js +26 -26
  17. package/dist/commands/commit.d.ts +2 -8
  18. package/dist/commands/commit.js +4 -26
  19. package/dist/commands/config/index.d.ts +2 -10
  20. package/dist/commands/config/index.js +8 -34
  21. package/dist/commands/docker/clean.js +7 -9
  22. package/dist/commands/docker/index.d.ts +2 -2
  23. package/dist/commands/docker/index.js +13 -12
  24. package/dist/commands/docker/list.d.ts +1 -0
  25. package/dist/commands/docker/list.js +31 -17
  26. package/dist/commands/docker/status.d.ts +3 -1
  27. package/dist/commands/docker/status.js +28 -2
  28. package/dist/commands/docker/sync.js +7 -6
  29. package/dist/commands/epic/delete.js +4 -5
  30. package/dist/commands/epic/list.js +17 -2
  31. package/dist/commands/execution/list.js +25 -17
  32. package/dist/commands/feedback/submit.d.ts +2 -2
  33. package/dist/commands/feedback/submit.js +9 -9
  34. package/dist/commands/link/index.js +2 -2
  35. package/dist/commands/pmo/init.d.ts +2 -2
  36. package/dist/commands/pmo/init.js +29 -10
  37. package/dist/commands/project/spec.js +6 -6
  38. package/dist/commands/repo/list.js +14 -8
  39. package/dist/commands/repo/view.js +2 -1
  40. package/dist/commands/roadmap/list.js +16 -1
  41. package/dist/commands/session/health.d.ts +29 -0
  42. package/dist/commands/session/health.js +496 -0
  43. package/dist/commands/session/index.js +4 -0
  44. package/dist/commands/session/list.js +15 -8
  45. package/dist/commands/spec/edit.js +2 -3
  46. package/dist/commands/staff/add.d.ts +2 -2
  47. package/dist/commands/staff/add.js +15 -14
  48. package/dist/commands/staff/index.js +2 -2
  49. package/dist/commands/staff/list.d.ts +3 -1
  50. package/dist/commands/staff/list.js +15 -1
  51. package/dist/commands/staff/remove.js +4 -4
  52. package/dist/commands/status/index.js +6 -7
  53. package/dist/commands/template/apply.js +10 -11
  54. package/dist/commands/template/create.js +18 -17
  55. package/dist/commands/template/index.d.ts +2 -2
  56. package/dist/commands/template/index.js +6 -6
  57. package/dist/commands/template/save.js +8 -7
  58. package/dist/commands/template/update.js +6 -7
  59. package/dist/commands/terminal/title.d.ts +2 -26
  60. package/dist/commands/terminal/title.js +4 -33
  61. package/dist/commands/theme/index.d.ts +2 -2
  62. package/dist/commands/theme/index.js +19 -18
  63. package/dist/commands/theme/list.d.ts +3 -0
  64. package/dist/commands/theme/list.js +25 -0
  65. package/dist/commands/theme/set.d.ts +2 -2
  66. package/dist/commands/theme/set.js +5 -5
  67. package/dist/commands/ticket/complete.js +4 -1
  68. package/dist/commands/ticket/create.d.ts +1 -0
  69. package/dist/commands/ticket/create.js +64 -16
  70. package/dist/commands/ticket/delete.js +18 -16
  71. package/dist/commands/ticket/edit.js +22 -14
  72. package/dist/commands/ticket/epic.js +12 -10
  73. package/dist/commands/ticket/list.js +24 -5
  74. package/dist/commands/ticket/move.js +4 -1
  75. package/dist/commands/ticket/project.js +11 -9
  76. package/dist/commands/ticket/reassign.js +23 -19
  77. package/dist/commands/ticket/spec.js +7 -5
  78. package/dist/commands/ticket/update.js +55 -53
  79. package/dist/commands/ticket/view.js +4 -2
  80. package/dist/commands/whoami.d.ts +3 -0
  81. package/dist/commands/whoami.js +22 -4
  82. package/dist/commands/work/complete.js +2 -2
  83. package/dist/commands/work/ready.js +9 -9
  84. package/dist/commands/work/revise.js +15 -13
  85. package/dist/commands/work/spawn.js +154 -57
  86. package/dist/commands/work/start.d.ts +1 -0
  87. package/dist/commands/work/start.js +299 -177
  88. package/dist/commands/workspace/prune.d.ts +3 -2
  89. package/dist/commands/workspace/prune.js +70 -10
  90. package/dist/hooks/init.js +4 -0
  91. package/dist/lib/agents/commands.js +4 -0
  92. package/dist/lib/agents/index.js +12 -0
  93. package/dist/lib/execution/devcontainer.d.ts +4 -0
  94. package/dist/lib/execution/devcontainer.js +63 -0
  95. package/dist/lib/mcp/helpers.d.ts +15 -0
  96. package/dist/lib/mcp/helpers.js +15 -0
  97. package/dist/lib/mcp/tools/action.js +5 -5
  98. package/dist/lib/mcp/tools/board.js +7 -7
  99. package/dist/lib/mcp/tools/category.js +5 -5
  100. package/dist/lib/mcp/tools/cli-passthrough.js +30 -30
  101. package/dist/lib/mcp/tools/epic.js +8 -8
  102. package/dist/lib/mcp/tools/phase.js +7 -7
  103. package/dist/lib/mcp/tools/project.js +10 -10
  104. package/dist/lib/mcp/tools/roadmap.js +7 -7
  105. package/dist/lib/mcp/tools/spec.js +9 -9
  106. package/dist/lib/mcp/tools/status.js +6 -6
  107. package/dist/lib/mcp/tools/template.js +6 -6
  108. package/dist/lib/mcp/tools/ticket.js +19 -19
  109. package/dist/lib/mcp/tools/view.js +4 -4
  110. package/dist/lib/mcp/tools/work.js +6 -6
  111. package/dist/lib/mcp/tools/workflow.js +5 -5
  112. package/dist/lib/pmo/index.js +4 -0
  113. package/dist/lib/pmo/storage/base.js +49 -0
  114. package/dist/lib/pr/index.d.ts +9 -0
  115. package/dist/lib/pr/index.js +101 -14
  116. package/dist/lib/prompt-command.d.ts +3 -0
  117. package/dist/lib/prompt-json.d.ts +72 -1
  118. package/dist/lib/prompt-json.js +46 -0
  119. package/dist/lib/repos/index.js +4 -0
  120. package/dist/lib/string-utils.d.ts +10 -0
  121. package/dist/lib/string-utils.js +16 -0
  122. package/oclif.manifest.json +594 -449
  123. package/package.json +3 -2
@@ -1,9 +1,10 @@
1
- import { Command } from '@oclif/core';
2
- export default class WorkspacePrune extends Command {
1
+ import { PromptCommand } from '../../lib/prompt-command.js';
2
+ export default class WorkspacePrune extends PromptCommand {
3
3
  static description: string;
4
4
  static examples: string[];
5
5
  static flags: {
6
6
  'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
+ force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
8
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
9
  };
9
10
  run(): Promise<void>;
@@ -1,14 +1,17 @@
1
- import { Command, Flags } from '@oclif/core';
1
+ import { Flags } from '@oclif/core';
2
2
  import * as fs from 'node:fs';
3
3
  import * as path from 'node:path';
4
+ import { PromptCommand } from '../../lib/prompt-command.js';
4
5
  import { styles } from '../../lib/styles.js';
5
6
  import { getRegisteredHeadquarters, unregisterHeadquarters, } from '../../lib/machine-config.js';
6
7
  import { getWorkspaceAgents, removeAgentsFromDatabase, getDatabasePath, } from '../../lib/database/index.js';
7
- export default class WorkspacePrune extends Command {
8
+ import { outputConfirmationNeededAsJson, createMetadata, } from '../../lib/prompt-json.js';
9
+ export default class WorkspacePrune extends PromptCommand {
8
10
  static description = 'Remove stale workspace entries and agents with deleted worktrees';
9
11
  static examples = [
10
12
  '<%= config.bin %> <%= command.id %> --dry-run',
11
13
  '<%= config.bin %> <%= command.id %>',
14
+ '<%= config.bin %> <%= command.id %> --force',
12
15
  ];
13
16
  static flags = {
14
17
  'dry-run': Flags.boolean({
@@ -16,6 +19,11 @@ export default class WorkspacePrune extends Command {
16
19
  description: 'Show what would be removed without removing',
17
20
  default: false,
18
21
  }),
22
+ force: Flags.boolean({
23
+ char: 'f',
24
+ description: 'Skip confirmation prompt and prune immediately',
25
+ default: false,
26
+ }),
19
27
  json: Flags.boolean({
20
28
  description: 'Output as JSON',
21
29
  default: false,
@@ -23,6 +31,10 @@ export default class WorkspacePrune extends Command {
23
31
  };
24
32
  async run() {
25
33
  const { flags } = await this.parse(WorkspacePrune);
34
+ // In non-TTY mode without --json (CI, scripts, piped), default to dry-run unless --force is set.
35
+ // In --json mode, we use confirmation_needed output instead of auto-dry-run so agents can review and confirm.
36
+ const isNonTTY = !process.stdout.isTTY;
37
+ const effectiveDryRun = flags['dry-run'] || (!flags.json && isNonTTY && !flags.force);
26
38
  // Find stale entries
27
39
  const staleWorkspaces = this.findStaleWorkspaces();
28
40
  const staleAgents = this.findStaleAgents();
@@ -30,7 +42,7 @@ export default class WorkspacePrune extends Command {
30
42
  // JSON output
31
43
  if (flags.json) {
32
44
  const output = {
33
- dryRun: flags['dry-run'],
45
+ dryRun: effectiveDryRun,
34
46
  staleWorkspaces: staleWorkspaces.map(w => ({
35
47
  name: w.name,
36
48
  path: w.path,
@@ -40,13 +52,22 @@ export default class WorkspacePrune extends Command {
40
52
  agentName: a.agentName,
41
53
  expectedPath: a.expectedPath,
42
54
  })),
43
- totalRemoved: flags['dry-run'] ? 0 : totalStale,
55
+ totalRemoved: effectiveDryRun ? 0 : totalStale,
44
56
  totalFound: totalStale,
45
57
  };
46
- this.log(JSON.stringify(output, null, 2));
47
- if (!flags['dry-run'] && totalStale > 0) {
58
+ if (!effectiveDryRun && totalStale > 0 && !flags.force) {
59
+ // In JSON mode without --force, output confirmation needed
60
+ outputConfirmationNeededAsJson({
61
+ staleWorkspaces: staleWorkspaces.map(w => ({ name: w.name, path: w.path })),
62
+ staleAgents: staleAgents.map(a => ({ workspaceName: a.workspaceName, agentName: a.agentName })),
63
+ totalFound: totalStale,
64
+ }, 'prlt workspace prune --force --json', `Found ${totalStale} stale entries. Run with --force to remove them.`, createMetadata('workspace prune', flags));
65
+ return;
66
+ }
67
+ if (!effectiveDryRun && totalStale > 0) {
48
68
  this.performPrune(staleWorkspaces, staleAgents);
49
69
  }
70
+ this.log(JSON.stringify(output, null, 2));
50
71
  return;
51
72
  }
52
73
  // Human-readable output
@@ -84,7 +105,7 @@ export default class WorkspacePrune extends Command {
84
105
  }
85
106
  // Summary
86
107
  this.log('');
87
- if (flags['dry-run']) {
108
+ if (effectiveDryRun) {
88
109
  this.log(styles.warning(`[DRY RUN] Would remove:`));
89
110
  if (staleWorkspaces.length > 0) {
90
111
  this.log(styles.muted(` • ${staleWorkspaces.length} workspace registration(s)`));
@@ -93,10 +114,15 @@ export default class WorkspacePrune extends Command {
93
114
  this.log(styles.muted(` • ${staleAgents.length} agent record(s)`));
94
115
  }
95
116
  this.log('');
96
- this.log(styles.muted('Run without --dry-run to remove these entries.'));
117
+ if (isNonTTY) {
118
+ this.log(styles.muted('Non-TTY environment detected. Run with --force to remove these entries.'));
119
+ }
120
+ else {
121
+ this.log(styles.muted('Run without --dry-run to remove these entries.'));
122
+ }
97
123
  }
98
- else {
99
- // Perform the actual prune
124
+ else if (flags.force) {
125
+ // --force: skip confirmation
100
126
  this.performPrune(staleWorkspaces, staleAgents);
101
127
  this.log(styles.success('Pruned:'));
102
128
  if (staleWorkspaces.length > 0) {
@@ -106,6 +132,40 @@ export default class WorkspacePrune extends Command {
106
132
  this.log(styles.muted(` • ${staleAgents.length} agent record(s)`));
107
133
  }
108
134
  }
135
+ else {
136
+ // Interactive confirmation
137
+ const summary = [];
138
+ if (staleWorkspaces.length > 0) {
139
+ summary.push(`${staleWorkspaces.length} workspace registration(s)`);
140
+ }
141
+ if (staleAgents.length > 0) {
142
+ summary.push(`${staleAgents.length} agent record(s)`);
143
+ }
144
+ const choices = [
145
+ { name: 'Yes', value: true },
146
+ { name: 'No', value: false },
147
+ ];
148
+ const message = `Remove ${summary.join(' and ')}?`;
149
+ const { confirmed } = await this.prompt([{
150
+ type: 'list',
151
+ name: 'confirmed',
152
+ message,
153
+ choices,
154
+ }], { flags: flags, commandName: 'workspace prune' });
155
+ if (confirmed) {
156
+ this.performPrune(staleWorkspaces, staleAgents);
157
+ this.log(styles.success('\nPruned:'));
158
+ if (staleWorkspaces.length > 0) {
159
+ this.log(styles.muted(` • ${staleWorkspaces.length} workspace registration(s)`));
160
+ }
161
+ if (staleAgents.length > 0) {
162
+ this.log(styles.muted(` • ${staleAgents.length} agent record(s)`));
163
+ }
164
+ }
165
+ else {
166
+ this.log(styles.muted('\nPrune cancelled.'));
167
+ }
168
+ }
109
169
  this.log('');
110
170
  }
111
171
  findStaleWorkspaces() {
@@ -13,6 +13,10 @@ const hook = async function ({ id, config }) {
13
13
  if (id === 'init') {
14
14
  return;
15
15
  }
16
+ // Skip when --help flag is present - help should always be available
17
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
18
+ return;
19
+ }
16
20
  // Skip for help-related commands/flags
17
21
  // When user runs just `prlt` with no args, id is undefined
18
22
  if (!id || id === 'help') {
@@ -8,6 +8,7 @@ import inquirer from 'inquirer';
8
8
  import { getWorkspaceConfig, getWorkspaceAgents, getWorkspaceRepositories, getAgentWorktrees, addAgentsToDatabase, removeAgentsFromDatabase, addEphemeralAgentToDatabase, getEphemeralAgentNames, getActiveTheme, markAgentCleaned, discoverAgentsOnDisk } from '../database/index.js';
9
9
  import { isValidAgentName, getSuggestedAgentNames, generateEphemeralAgentName, getThemePersistentDir, getThemeEphemeralDir, extractBaseName, getAgentBaseName, } from '../themes.js';
10
10
  import { createDevcontainerConfig } from '../execution/devcontainer.js';
11
+ import { getGitIdentity } from '../pr/index.js';
11
12
  import { getPMOContext } from '../pmo/index.js';
12
13
  /**
13
14
  * Format a list of agents for display in error messages.
@@ -508,11 +509,14 @@ export async function createEphemeralAgent(workspaceInfo, options) {
508
509
  if (!options?.skipDevcontainer) {
509
510
  const devcontainerDir = path.join(agentDir, '.devcontainer');
510
511
  if (!fs.existsSync(devcontainerDir)) {
512
+ const gitIdentity = getGitIdentity();
511
513
  createDevcontainerConfig({
512
514
  agentName,
513
515
  agentDir,
514
516
  repoWorktrees: mountMode === 'worktree' ? workspaceInfo.repositories.map(r => r.name) : undefined,
515
517
  mountMode,
518
+ gitUserName: gitIdentity.name || undefined,
519
+ gitUserEmail: gitIdentity.email || undefined,
516
520
  });
517
521
  }
518
522
  }
@@ -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,9 @@ 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 } : {}),
109
112
  // /hq/.proletariat/bin contains prlt wrapper with ESM loader for native modules
110
113
  PATH: '/hq/.proletariat/bin:/home/node/.npm-global/bin:/usr/local/bin:/usr/bin:/bin',
111
114
  },
@@ -452,6 +455,66 @@ else
452
455
  echo "Warning: No GitHub token found, git push will require manual auth"
453
456
  fi
454
457
 
458
+ # Configure git user identity for commit attribution
459
+ # Uses env vars set by host (from gh/git config), with fallback detection
460
+ configure_git_identity() {
461
+ local git_name="\${PRLT_GIT_USER_NAME:-}"
462
+ local git_email="\${PRLT_GIT_USER_EMAIL:-}"
463
+
464
+ # Fallback: try gh api user if env vars are empty and gh is authenticated
465
+ if { [ -z "$git_name" ] || [ -z "$git_email" ]; } && command -v gh &> /dev/null && gh auth status &>/dev/null; then
466
+ if [ -z "$git_name" ]; then
467
+ git_name=$(gh api user -q '.name // .login' 2>/dev/null || true)
468
+ fi
469
+ if [ -z "$git_email" ]; then
470
+ git_email=$(gh api user -q '.email // empty' 2>/dev/null || true)
471
+ # Try emails API if public email is not set
472
+ if [ -z "$git_email" ]; then
473
+ git_email=$(gh api user/emails -q '[.[] | select(.primary)] | .[0].email' 2>/dev/null || true)
474
+ fi
475
+ fi
476
+ fi
477
+
478
+ # Fallback: try git config from mounted repos
479
+ if [ -z "$git_name" ] || [ -z "$git_email" ]; then
480
+ for repo_dir in /workspace/*/; do
481
+ if [ -d "$repo_dir/.git" ] || [ -f "$repo_dir/.git" ]; then
482
+ if [ -z "$git_name" ]; then
483
+ git_name=$(/usr/bin/git -C "$repo_dir" config user.name 2>/dev/null || true)
484
+ fi
485
+ if [ -z "$git_email" ]; then
486
+ git_email=$(/usr/bin/git -C "$repo_dir" config user.email 2>/dev/null || true)
487
+ fi
488
+ if [ -n "$git_name" ] && [ -n "$git_email" ]; then
489
+ break
490
+ fi
491
+ fi
492
+ done
493
+ fi
494
+
495
+ # Apply git config
496
+ if [ -n "$git_name" ]; then
497
+ /usr/bin/git config --global user.name "$git_name"
498
+ echo "Git user.name set to: $git_name"
499
+ fi
500
+ if [ -n "$git_email" ]; then
501
+ /usr/bin/git config --global user.email "$git_email"
502
+ echo "Git user.email set to: $git_email"
503
+ fi
504
+
505
+ # Warning if identity could not be determined
506
+ if [ -z "$git_name" ] && [ -z "$git_email" ]; then
507
+ echo "Warning: Could not determine git identity. Commits may use default identity."
508
+ echo " To fix: run 'gh auth login' or set git config user.name/user.email in your repo"
509
+ elif [ -z "$git_name" ]; then
510
+ echo "Warning: Could not determine git user.name"
511
+ elif [ -z "$git_email" ]; then
512
+ echo "Warning: Could not determine git user.email"
513
+ fi
514
+ }
515
+
516
+ configure_git_identity
517
+
455
518
  # Check if prlt is already installed globally (via npm from GitHub Packages)
456
519
  if command -v prlt &> /dev/null; then
457
520
  PRLT_PATH=$(which prlt)
@@ -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 {