@proletariat/cli 0.3.36 → 0.3.40
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 +37 -2
- package/bin/dev.js +0 -0
- package/dist/commands/branch/where.js +6 -17
- package/dist/commands/epic/ticket.js +7 -24
- package/dist/commands/execution/config.js +4 -14
- package/dist/commands/execution/logs.js +6 -0
- package/dist/commands/execution/view.js +8 -0
- package/dist/commands/mcp-server.js +2 -1
- package/dist/commands/pmo/init.js +12 -40
- package/dist/commands/qa/index.d.ts +54 -0
- package/dist/commands/qa/index.js +762 -0
- package/dist/commands/repo/view.js +2 -8
- package/dist/commands/session/attach.js +4 -4
- package/dist/commands/session/health.js +4 -4
- package/dist/commands/session/list.js +1 -19
- package/dist/commands/session/peek.js +6 -6
- package/dist/commands/session/poke.js +2 -2
- package/dist/commands/ticket/epic.js +17 -43
- package/dist/commands/work/spawn-all.js +1 -1
- package/dist/commands/work/spawn.js +15 -4
- package/dist/commands/work/start.js +17 -9
- package/dist/commands/work/watch.js +1 -1
- package/dist/commands/workspace/prune.js +3 -3
- package/dist/hooks/init.js +10 -2
- package/dist/lib/agents/commands.d.ts +5 -0
- package/dist/lib/agents/commands.js +143 -97
- package/dist/lib/database/drizzle-schema.d.ts +465 -0
- package/dist/lib/database/drizzle-schema.js +53 -0
- package/dist/lib/database/index.d.ts +47 -1
- package/dist/lib/database/index.js +138 -20
- package/dist/lib/execution/runners.d.ts +34 -0
- package/dist/lib/execution/runners.js +134 -7
- package/dist/lib/execution/session-utils.d.ts +5 -0
- package/dist/lib/execution/session-utils.js +45 -3
- package/dist/lib/execution/spawner.js +15 -2
- package/dist/lib/execution/storage.d.ts +1 -1
- package/dist/lib/execution/storage.js +17 -2
- package/dist/lib/execution/types.d.ts +1 -0
- package/dist/lib/mcp/tools/index.d.ts +1 -0
- package/dist/lib/mcp/tools/index.js +1 -0
- package/dist/lib/mcp/tools/tmux.d.ts +16 -0
- package/dist/lib/mcp/tools/tmux.js +182 -0
- package/dist/lib/mcp/tools/work.js +52 -0
- package/dist/lib/pmo/schema.d.ts +1 -1
- package/dist/lib/pmo/schema.js +1 -0
- package/dist/lib/pmo/storage/base.js +207 -0
- package/dist/lib/pmo/storage/dependencies.d.ts +1 -0
- package/dist/lib/pmo/storage/dependencies.js +11 -3
- package/dist/lib/pmo/storage/epics.js +1 -1
- package/dist/lib/pmo/storage/helpers.d.ts +4 -4
- package/dist/lib/pmo/storage/helpers.js +36 -26
- package/dist/lib/pmo/storage/projects.d.ts +2 -0
- package/dist/lib/pmo/storage/projects.js +207 -119
- package/dist/lib/pmo/storage/specs.d.ts +2 -0
- package/dist/lib/pmo/storage/specs.js +274 -188
- package/dist/lib/pmo/storage/tickets.d.ts +2 -0
- package/dist/lib/pmo/storage/tickets.js +350 -290
- package/dist/lib/pmo/storage/views.d.ts +2 -0
- package/dist/lib/pmo/storage/views.js +183 -130
- package/dist/lib/prompt-json.d.ts +5 -0
- package/dist/lib/prompt-json.js +9 -0
- package/oclif.manifest.json +3922 -3819
- package/package.json +11 -6
- package/LICENSE +0 -190
|
@@ -5,7 +5,7 @@ import * as fs from 'node:fs';
|
|
|
5
5
|
import * as path from 'node:path';
|
|
6
6
|
import { execSync } from 'node:child_process';
|
|
7
7
|
import inquirer from 'inquirer';
|
|
8
|
-
import { getWorkspaceConfig, getWorkspaceAgents, getWorkspaceRepositories, getAgentWorktrees, addAgentsToDatabase, removeAgentsFromDatabase,
|
|
8
|
+
import { getWorkspaceConfig, getWorkspaceAgents, getWorkspaceRepositories, getAgentWorktrees, addAgentsToDatabase, removeAgentsFromDatabase, tryAddEphemeralAgentToDatabase, 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
11
|
import { getGitIdentity } from '../pr/index.js';
|
|
@@ -395,16 +395,21 @@ export async function removeAgentsFromWorkspace(workspaceInfo, agentNames) {
|
|
|
395
395
|
}
|
|
396
396
|
return { removed, failed };
|
|
397
397
|
}
|
|
398
|
+
/**
|
|
399
|
+
* Maximum number of retries when a name collision is detected at the DB level.
|
|
400
|
+
* Each retry generates a fresh name to avoid repeated collisions.
|
|
401
|
+
*/
|
|
402
|
+
const EPHEMERAL_CREATE_MAX_RETRIES = 5;
|
|
398
403
|
/**
|
|
399
404
|
* Create an ephemeral agent on-demand for a spawn operation.
|
|
400
405
|
* Creates worktree in agents/temp/{name}/
|
|
406
|
+
*
|
|
407
|
+
* Concurrency-safe: if the generated name collides at the DB level
|
|
408
|
+
* (e.g. a parallel process inserted the same name first), we clean up
|
|
409
|
+
* the on-disk artifacts, regenerate a name, and retry up to
|
|
410
|
+
* EPHEMERAL_CREATE_MAX_RETRIES times.
|
|
401
411
|
*/
|
|
402
412
|
export async function createEphemeralAgent(workspaceInfo, options) {
|
|
403
|
-
// Get existing agent names for uniqueness check
|
|
404
|
-
const existingNames = new Set([
|
|
405
|
-
...Array.from(getEphemeralAgentNames(workspaceInfo.path)),
|
|
406
|
-
...workspaceInfo.agents.map(a => a.name.toLowerCase())
|
|
407
|
-
]);
|
|
408
413
|
const log = options?.log;
|
|
409
414
|
// Get theme: use provided themeId, or fall back to workspace's active theme
|
|
410
415
|
let themeId = options?.themeId;
|
|
@@ -417,117 +422,158 @@ export async function createEphemeralAgent(workspaceInfo, options) {
|
|
|
417
422
|
// Extract base names currently in use by active agents
|
|
418
423
|
// This helps the generator prefer fresh base names
|
|
419
424
|
const inUseBaseNames = new Set(workspaceInfo.agents.map(agent => getAgentBaseName(agent).toLowerCase()));
|
|
420
|
-
// Create a conflict checker for external resources (tmux sessions, directories)
|
|
421
|
-
const checkExternalConflict = (candidateName) => {
|
|
422
|
-
// Check if a tmux session with this name already exists (could be from manual creation)
|
|
423
|
-
if (tmuxSessionExists(candidateName)) {
|
|
424
|
-
return { conflict: true, reason: `tmux session "${candidateName}" already exists` };
|
|
425
|
-
}
|
|
426
|
-
// Check if the directory already exists in agents/temp/
|
|
427
|
-
const candidateDir = path.join(tempAgentsBasePath, candidateName);
|
|
428
|
-
if (fs.existsSync(candidateDir)) {
|
|
429
|
-
return { conflict: true, reason: `directory "${candidateDir}" already exists` };
|
|
430
|
-
}
|
|
431
|
-
return { conflict: false };
|
|
432
|
-
};
|
|
433
|
-
// Log when conflicts are skipped during name generation
|
|
434
|
-
const onConflictSkipped = (name, reason) => {
|
|
435
|
-
log?.(`⚠️ Skipping name "${name}": ${reason}`);
|
|
436
|
-
};
|
|
437
|
-
// Generate unique ephemeral name using workspace theme
|
|
438
|
-
const nameOptions = {
|
|
439
|
-
themeId,
|
|
440
|
-
checkExternalConflict,
|
|
441
|
-
onConflictSkipped,
|
|
442
|
-
inUseBaseNames
|
|
443
|
-
};
|
|
444
|
-
const agentName = generateEphemeralAgentName(existingNames, nameOptions);
|
|
445
|
-
// Extract base name from the generated name (e.g., "bezos" from "bold-bezos" or "bold-bezos-2")
|
|
446
|
-
const baseName = extractBaseName(agentName);
|
|
447
|
-
// Create temp agents directory if it doesn't exist
|
|
448
|
-
if (!fs.existsSync(tempAgentsBasePath)) {
|
|
449
|
-
fs.mkdirSync(tempAgentsBasePath, { recursive: true });
|
|
450
|
-
}
|
|
451
|
-
const agentDir = path.join(tempAgentsBasePath, agentName);
|
|
452
|
-
// Create agent directory
|
|
453
|
-
if (!fs.existsSync(agentDir)) {
|
|
454
|
-
fs.mkdirSync(agentDir, { recursive: true });
|
|
455
|
-
}
|
|
456
|
-
// Create worktrees/clones for each repository
|
|
457
425
|
const reposPath = path.join(workspaceInfo.path, 'repos');
|
|
458
426
|
const mountMode = options?.mountMode || 'worktree';
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
427
|
+
for (let attempt = 0; attempt <= EPHEMERAL_CREATE_MAX_RETRIES; attempt++) {
|
|
428
|
+
// Re-read existing names on every attempt so we see names that were
|
|
429
|
+
// inserted by concurrent processes since our last try
|
|
430
|
+
const existingNames = new Set([
|
|
431
|
+
...Array.from(getEphemeralAgentNames(workspaceInfo.path)),
|
|
432
|
+
...workspaceInfo.agents.map(a => a.name.toLowerCase())
|
|
433
|
+
]);
|
|
434
|
+
// Create a conflict checker for external resources (tmux sessions, directories)
|
|
435
|
+
const checkExternalConflict = (candidateName) => {
|
|
436
|
+
if (tmuxSessionExists(candidateName)) {
|
|
437
|
+
return { conflict: true, reason: `tmux session "${candidateName}" already exists` };
|
|
438
|
+
}
|
|
439
|
+
const candidateDir = path.join(tempAgentsBasePath, candidateName);
|
|
440
|
+
if (fs.existsSync(candidateDir)) {
|
|
441
|
+
return { conflict: true, reason: `directory "${candidateDir}" already exists` };
|
|
442
|
+
}
|
|
443
|
+
return { conflict: false };
|
|
444
|
+
};
|
|
445
|
+
const onConflictSkipped = (name, reason) => {
|
|
446
|
+
log?.(`⚠️ Skipping name "${name}": ${reason}`);
|
|
447
|
+
};
|
|
448
|
+
const nameOptions = {
|
|
449
|
+
themeId,
|
|
450
|
+
checkExternalConflict,
|
|
451
|
+
onConflictSkipped,
|
|
452
|
+
inUseBaseNames
|
|
453
|
+
};
|
|
454
|
+
const agentName = generateEphemeralAgentName(existingNames, nameOptions);
|
|
455
|
+
const baseName = extractBaseName(agentName);
|
|
456
|
+
// Create temp agents directory if it doesn't exist
|
|
457
|
+
if (!fs.existsSync(tempAgentsBasePath)) {
|
|
458
|
+
fs.mkdirSync(tempAgentsBasePath, { recursive: true });
|
|
459
|
+
}
|
|
460
|
+
const agentDir = path.join(tempAgentsBasePath, agentName);
|
|
461
|
+
// Create agent directory
|
|
462
|
+
if (!fs.existsSync(agentDir)) {
|
|
463
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
464
|
+
}
|
|
465
|
+
// Create worktrees/clones for each repository
|
|
466
|
+
if (fs.existsSync(reposPath) && workspaceInfo.repositories.length > 0) {
|
|
467
|
+
for (const repo of workspaceInfo.repositories) {
|
|
468
|
+
const sourceRepoPath = path.join(reposPath, repo.name);
|
|
469
|
+
const targetPath = path.join(agentDir, repo.name);
|
|
470
|
+
if (fs.existsSync(sourceRepoPath) && !fs.existsSync(targetPath)) {
|
|
471
|
+
if (mountMode === 'clone') {
|
|
472
|
+
// CLONE MODE: Create independent git clone
|
|
473
|
+
try {
|
|
474
|
+
const remoteUrl = execSync('git remote get-url origin', {
|
|
475
|
+
cwd: sourceRepoPath,
|
|
476
|
+
encoding: 'utf-8',
|
|
477
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
478
|
+
}).trim();
|
|
479
|
+
if (remoteUrl) {
|
|
480
|
+
execSync(`git clone "${remoteUrl}" "${targetPath}"`, {
|
|
481
|
+
stdio: 'pipe'
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
if (!fs.existsSync(targetPath)) {
|
|
487
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
// WORKTREE MODE: Create git worktree
|
|
493
|
+
try {
|
|
494
|
+
execSync(`git worktree add --detach "${targetPath}"`, {
|
|
495
|
+
cwd: sourceRepoPath,
|
|
475
496
|
stdio: 'pipe'
|
|
476
497
|
});
|
|
477
498
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
fs.mkdirSync(targetPath, { recursive: true });
|
|
499
|
+
catch {
|
|
500
|
+
if (!fs.existsSync(targetPath)) {
|
|
501
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
502
|
+
}
|
|
483
503
|
}
|
|
484
504
|
}
|
|
485
505
|
}
|
|
486
|
-
|
|
487
|
-
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
// Create devcontainer config if not skipped (uses shared devcontainer generator)
|
|
509
|
+
if (!options?.skipDevcontainer) {
|
|
510
|
+
const devcontainerDir = path.join(agentDir, '.devcontainer');
|
|
511
|
+
if (!fs.existsSync(devcontainerDir)) {
|
|
512
|
+
const gitIdentity = getGitIdentity();
|
|
513
|
+
createDevcontainerConfig({
|
|
514
|
+
agentName,
|
|
515
|
+
agentDir,
|
|
516
|
+
repoWorktrees: mountMode === 'worktree' ? workspaceInfo.repositories.map(r => r.name) : undefined,
|
|
517
|
+
mountMode,
|
|
518
|
+
gitUserName: gitIdentity.name || undefined,
|
|
519
|
+
gitUserEmail: gitIdentity.email || undefined,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
// Attempt atomic DB insertion — returns null on name collision
|
|
524
|
+
const agent = tryAddEphemeralAgentToDatabase(workspaceInfo.path, agentName, baseName, options?.themeId, mountMode);
|
|
525
|
+
if (agent) {
|
|
526
|
+
return {
|
|
527
|
+
name: agentName,
|
|
528
|
+
baseName,
|
|
529
|
+
worktreePath: agentDir,
|
|
530
|
+
agent
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
// Name collision at DB level — clean up on-disk artifacts and retry
|
|
534
|
+
log?.(`⚠️ Name collision for "${agentName}" (attempt ${attempt + 1}/${EPHEMERAL_CREATE_MAX_RETRIES + 1}), retrying with a new name...`);
|
|
535
|
+
cleanupFailedEphemeralAgent(agentDir, workspaceInfo, mountMode);
|
|
536
|
+
}
|
|
537
|
+
// All retries exhausted
|
|
538
|
+
throw new Error(`Failed to create ephemeral agent after ${EPHEMERAL_CREATE_MAX_RETRIES + 1} attempts due to concurrent name collisions. ` +
|
|
539
|
+
`This can happen when many ephemeral agents are being created simultaneously. ` +
|
|
540
|
+
`Suggested remediation: wait a moment and retry, or specify a unique agent name with --agent.`);
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Clean up on-disk artifacts (directories, worktrees) for a failed ephemeral
|
|
544
|
+
* agent creation attempt. Used when a DB name collision forces a retry.
|
|
545
|
+
*/
|
|
546
|
+
function cleanupFailedEphemeralAgent(agentDir, workspaceInfo, mountMode) {
|
|
547
|
+
if (!fs.existsSync(agentDir))
|
|
548
|
+
return;
|
|
549
|
+
// Remove git worktrees first (if in worktree mode)
|
|
550
|
+
if (mountMode === 'worktree') {
|
|
551
|
+
const reposPath = path.join(workspaceInfo.path, 'repos');
|
|
552
|
+
if (fs.existsSync(reposPath)) {
|
|
553
|
+
for (const repo of workspaceInfo.repositories) {
|
|
554
|
+
const sourceRepoPath = path.join(reposPath, repo.name);
|
|
555
|
+
const worktreePath = path.join(agentDir, repo.name);
|
|
556
|
+
if (fs.existsSync(sourceRepoPath) && fs.existsSync(worktreePath)) {
|
|
488
557
|
try {
|
|
489
|
-
|
|
490
|
-
// Don't create a branch yet - that happens in work:start
|
|
491
|
-
// Use --detach to create without a branch reference
|
|
492
|
-
execSync(`git worktree add --detach "${targetPath}"`, {
|
|
558
|
+
execSync(`git worktree remove "${worktreePath}" --force`, {
|
|
493
559
|
cwd: sourceRepoPath,
|
|
494
560
|
stdio: 'pipe'
|
|
495
561
|
});
|
|
496
562
|
}
|
|
497
563
|
catch {
|
|
498
|
-
//
|
|
499
|
-
// The agent can still work without a worktree (e.g., for non-git projects)
|
|
500
|
-
if (!fs.existsSync(targetPath)) {
|
|
501
|
-
fs.mkdirSync(targetPath, { recursive: true });
|
|
502
|
-
}
|
|
564
|
+
// Ignore — rmSync below will clean up the directory
|
|
503
565
|
}
|
|
504
566
|
}
|
|
505
567
|
}
|
|
506
568
|
}
|
|
507
569
|
}
|
|
508
|
-
//
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
agentName,
|
|
515
|
-
agentDir,
|
|
516
|
-
repoWorktrees: mountMode === 'worktree' ? workspaceInfo.repositories.map(r => r.name) : undefined,
|
|
517
|
-
mountMode,
|
|
518
|
-
gitUserName: gitIdentity.name || undefined,
|
|
519
|
-
gitUserEmail: gitIdentity.email || undefined,
|
|
520
|
-
});
|
|
521
|
-
}
|
|
570
|
+
// Remove the agent directory
|
|
571
|
+
try {
|
|
572
|
+
fs.rmSync(agentDir, { recursive: true, force: true });
|
|
573
|
+
}
|
|
574
|
+
catch {
|
|
575
|
+
// Best-effort cleanup
|
|
522
576
|
}
|
|
523
|
-
// Add to database
|
|
524
|
-
const agent = addEphemeralAgentToDatabase(workspaceInfo.path, agentName, baseName, options?.themeId, mountMode);
|
|
525
|
-
return {
|
|
526
|
-
name: agentName,
|
|
527
|
-
baseName,
|
|
528
|
-
worktreePath: agentDir,
|
|
529
|
-
agent
|
|
530
|
-
};
|
|
531
577
|
}
|
|
532
578
|
/**
|
|
533
579
|
* Check if a tmux session exists for a given name
|