@proletariat/cli 0.3.8 → 0.3.9
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/LICENSE +21 -0
- package/bin/dev.js +0 -0
- package/dist/commands/branch/where.d.ts +21 -0
- package/dist/commands/branch/where.js +213 -0
- package/dist/commands/pmo/init.js +23 -5
- package/dist/commands/project/create.js +9 -10
- package/dist/commands/whoami.d.ts +1 -0
- package/dist/commands/whoami.js +36 -6
- package/dist/commands/work/start.js +11 -0
- package/dist/lib/database/index.d.ts +6 -0
- package/dist/lib/database/index.js +38 -0
- package/dist/lib/execution/devcontainer.js +3 -0
- package/dist/lib/execution/runners.js +3 -2
- package/dist/lib/execution/spawner.d.ts +3 -1
- package/dist/lib/execution/spawner.js +9 -2
- package/dist/lib/execution/storage.d.ts +14 -0
- package/dist/lib/execution/storage.js +88 -0
- package/dist/lib/pmo/index.d.ts +7 -21
- package/dist/lib/pmo/index.js +22 -101
- package/dist/lib/pmo/storage/base.d.ts +2 -2
- package/dist/lib/pmo/storage/base.js +14 -89
- package/dist/lib/pmo/templates-builtin.d.ts +66 -0
- package/dist/lib/pmo/templates-builtin.js +192 -0
- package/dist/lib/themes.js +4 -4
- package/oclif.manifest.json +3378 -3331
- package/package.json +4 -6
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Chris McDermut
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/bin/dev.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { PMOCommand } from '../../lib/pmo/index.js';
|
|
2
|
+
export default class BranchWhere extends PMOCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
|
+
project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
};
|
|
9
|
+
static args: {
|
|
10
|
+
search: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
11
|
+
};
|
|
12
|
+
protected getPMOOptions(): {
|
|
13
|
+
promptIfMultiple: boolean;
|
|
14
|
+
};
|
|
15
|
+
execute(): Promise<void>;
|
|
16
|
+
private getGitWorktrees;
|
|
17
|
+
private findMatchingWorktrees;
|
|
18
|
+
private findDatabaseWorktrees;
|
|
19
|
+
private getWorkspacePath;
|
|
20
|
+
private mergeResults;
|
|
21
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
|
|
5
|
+
import { styles } from '../../lib/styles.js';
|
|
6
|
+
import { isGitRepo, isTicketId } from '../../lib/branch/index.js';
|
|
7
|
+
import { openWorkspaceDatabase } from '../../lib/database/index.js';
|
|
8
|
+
export default class BranchWhere extends PMOCommand {
|
|
9
|
+
static description = 'Find which directory a branch is checked out in';
|
|
10
|
+
static examples = [
|
|
11
|
+
'<%= config.bin %> <%= command.id %> TKT-468',
|
|
12
|
+
'<%= config.bin %> <%= command.id %> feat/chris/add-auth',
|
|
13
|
+
'<%= config.bin %> <%= command.id %> TKT-468 --json',
|
|
14
|
+
];
|
|
15
|
+
static flags = {
|
|
16
|
+
...pmoBaseFlags,
|
|
17
|
+
json: Flags.boolean({
|
|
18
|
+
description: 'Output in JSON format',
|
|
19
|
+
default: false,
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
static args = {
|
|
23
|
+
search: Args.string({
|
|
24
|
+
description: 'Branch name or ticket ID to search for',
|
|
25
|
+
required: true,
|
|
26
|
+
}),
|
|
27
|
+
};
|
|
28
|
+
getPMOOptions() {
|
|
29
|
+
return { promptIfMultiple: false };
|
|
30
|
+
}
|
|
31
|
+
async execute() {
|
|
32
|
+
const { args, flags } = await this.parse(BranchWhere);
|
|
33
|
+
const search = args.search;
|
|
34
|
+
// Check if in git repo
|
|
35
|
+
if (!isGitRepo()) {
|
|
36
|
+
this.error('Not in a git repository.');
|
|
37
|
+
}
|
|
38
|
+
// Get all worktrees from git
|
|
39
|
+
const worktrees = this.getGitWorktrees();
|
|
40
|
+
// Search for matching branches
|
|
41
|
+
const matches = this.findMatchingWorktrees(worktrees, search);
|
|
42
|
+
// Also check the database for agent worktrees
|
|
43
|
+
const dbMatches = this.findDatabaseWorktrees(search);
|
|
44
|
+
// Combine results, preferring git worktree info
|
|
45
|
+
const allMatches = this.mergeResults(matches, dbMatches);
|
|
46
|
+
if (allMatches.length === 0) {
|
|
47
|
+
if (flags.json) {
|
|
48
|
+
this.log(JSON.stringify({ found: false, search, matches: [] }, null, 2));
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
this.log(styles.muted(`\nNo worktree found for "${search}"\n`));
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (flags.json) {
|
|
56
|
+
this.log(JSON.stringify({
|
|
57
|
+
found: true,
|
|
58
|
+
search,
|
|
59
|
+
matches: allMatches.map(m => ({
|
|
60
|
+
path: m.path,
|
|
61
|
+
branch: m.branch,
|
|
62
|
+
commit: m.commit,
|
|
63
|
+
})),
|
|
64
|
+
}, null, 2));
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
this.log('');
|
|
68
|
+
if (allMatches.length === 1) {
|
|
69
|
+
const match = allMatches[0];
|
|
70
|
+
this.log(styles.header(`Branch: ${match.branch}`));
|
|
71
|
+
this.log(`Path: ${styles.success(match.path)}`);
|
|
72
|
+
if (match.commit) {
|
|
73
|
+
this.log(`Commit: ${styles.muted(match.commit)}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
this.log(styles.header(`Found ${allMatches.length} matching worktrees:`));
|
|
78
|
+
this.log('');
|
|
79
|
+
for (const match of allMatches) {
|
|
80
|
+
this.log(` ${styles.success(match.branch)}`);
|
|
81
|
+
this.log(` ${match.path}`);
|
|
82
|
+
if (match.commit) {
|
|
83
|
+
this.log(` ${styles.muted(`commit: ${match.commit}`)}`);
|
|
84
|
+
}
|
|
85
|
+
this.log('');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
this.log('');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
getGitWorktrees() {
|
|
92
|
+
try {
|
|
93
|
+
const output = execSync('git worktree list --porcelain', {
|
|
94
|
+
encoding: 'utf-8',
|
|
95
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
96
|
+
});
|
|
97
|
+
const worktrees = [];
|
|
98
|
+
let current = {};
|
|
99
|
+
for (const line of output.split('\n')) {
|
|
100
|
+
if (line.startsWith('worktree ')) {
|
|
101
|
+
if (current.path) {
|
|
102
|
+
worktrees.push(current);
|
|
103
|
+
}
|
|
104
|
+
current = { path: line.substring(9) };
|
|
105
|
+
}
|
|
106
|
+
else if (line.startsWith('HEAD ')) {
|
|
107
|
+
current.commit = line.substring(5);
|
|
108
|
+
}
|
|
109
|
+
else if (line.startsWith('branch refs/heads/')) {
|
|
110
|
+
current.branch = line.substring(18);
|
|
111
|
+
}
|
|
112
|
+
else if (line === 'bare') {
|
|
113
|
+
current.bare = true;
|
|
114
|
+
}
|
|
115
|
+
else if (line === 'detached') {
|
|
116
|
+
current.detached = true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Don't forget the last entry
|
|
120
|
+
if (current.path) {
|
|
121
|
+
worktrees.push(current);
|
|
122
|
+
}
|
|
123
|
+
return worktrees.filter(w => w.branch && !w.bare);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
findMatchingWorktrees(worktrees, search) {
|
|
130
|
+
const searchLower = search.toLowerCase();
|
|
131
|
+
const isTicket = isTicketId(search);
|
|
132
|
+
return worktrees.filter(w => {
|
|
133
|
+
if (!w.branch)
|
|
134
|
+
return false;
|
|
135
|
+
const branchLower = w.branch.toLowerCase();
|
|
136
|
+
// Exact match
|
|
137
|
+
if (branchLower === searchLower)
|
|
138
|
+
return true;
|
|
139
|
+
// If searching by ticket ID, match branches that start with it
|
|
140
|
+
if (isTicket && branchLower.startsWith(searchLower + '/')) {
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
// Partial match: branch contains the search term
|
|
144
|
+
if (branchLower.includes(searchLower))
|
|
145
|
+
return true;
|
|
146
|
+
return false;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
findDatabaseWorktrees(search) {
|
|
150
|
+
try {
|
|
151
|
+
const workspacePath = this.getWorkspacePath();
|
|
152
|
+
if (!workspacePath)
|
|
153
|
+
return [];
|
|
154
|
+
const db = openWorkspaceDatabase(workspacePath);
|
|
155
|
+
const searchLower = search.toLowerCase();
|
|
156
|
+
const isTicket = isTicketId(search);
|
|
157
|
+
// Query for matching branches
|
|
158
|
+
let query;
|
|
159
|
+
let params;
|
|
160
|
+
if (isTicket) {
|
|
161
|
+
// Match branches starting with ticket ID
|
|
162
|
+
query = 'SELECT * FROM agent_worktrees WHERE LOWER(branch) LIKE ?';
|
|
163
|
+
params = [`${searchLower}/%`];
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
// Match branches containing the search term
|
|
167
|
+
query = 'SELECT * FROM agent_worktrees WHERE LOWER(branch) LIKE ?';
|
|
168
|
+
params = [`%${searchLower}%`];
|
|
169
|
+
}
|
|
170
|
+
const rows = db.prepare(query).all(...params);
|
|
171
|
+
db.close();
|
|
172
|
+
return rows.map(row => ({
|
|
173
|
+
path: path.join(workspacePath, row.worktree_path),
|
|
174
|
+
branch: row.branch,
|
|
175
|
+
commit: row.last_commit_hash || '',
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
getWorkspacePath() {
|
|
183
|
+
// Try to find workspace by looking for .proletariat directory
|
|
184
|
+
let current = process.cwd();
|
|
185
|
+
const root = path.parse(current).root;
|
|
186
|
+
while (current !== root) {
|
|
187
|
+
try {
|
|
188
|
+
const dbPath = path.join(current, '.proletariat', 'workspace.db');
|
|
189
|
+
const fs = require('node:fs');
|
|
190
|
+
if (fs.existsSync(dbPath)) {
|
|
191
|
+
return current;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// Continue searching
|
|
196
|
+
}
|
|
197
|
+
current = path.dirname(current);
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
mergeResults(gitWorktrees, dbWorktrees) {
|
|
202
|
+
// Use git worktrees as primary source, add any db-only entries
|
|
203
|
+
const seen = new Set(gitWorktrees.map(w => w.branch));
|
|
204
|
+
const merged = [...gitWorktrees];
|
|
205
|
+
for (const dbw of dbWorktrees) {
|
|
206
|
+
if (!seen.has(dbw.branch)) {
|
|
207
|
+
merged.push(dbw);
|
|
208
|
+
seen.add(dbw.branch);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return merged;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -5,16 +5,18 @@ import { execSync } from 'node:child_process';
|
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
import inquirer from 'inquirer';
|
|
7
7
|
import Database from 'better-sqlite3';
|
|
8
|
-
import { SQLiteStorage, getColumnsForTemplate, createPMO, promptForPMOLocation, promptForBoardTemplate, promptForBoardName, promptForCustomColumns, determinePMOPath, } from '../../lib/pmo/index.js';
|
|
8
|
+
import { SQLiteStorage, getColumnsForTemplate, createPMO, promptForPMOLocation, promptForBoardTemplate, promptForBoardName, promptForCustomColumns, determinePMOPath, getPickerTemplates, } from '../../lib/pmo/index.js';
|
|
9
9
|
import { styles } from '../../lib/styles.js';
|
|
10
10
|
import { isGHInstalled, isGHAuthenticated, getGHUsername, isGHTokenInEnv } from '../../lib/pr/index.js';
|
|
11
11
|
import { shouldOutputJson, outputPromptAsJson, createMetadata, buildPromptConfig, } from '../../lib/prompt-json.js';
|
|
12
|
+
// Build template options dynamically from shared definitions (picker templates + custom)
|
|
13
|
+
const PICKER_TEMPLATE_IDS = [...getPickerTemplates().map(t => t.id), 'custom'];
|
|
12
14
|
export default class PMOInit extends Command {
|
|
13
15
|
static description = 'Initialize PMO (Project Management Org) in current directory or HQ';
|
|
14
16
|
static examples = [
|
|
15
17
|
'<%= config.bin %> <%= command.id %>',
|
|
16
|
-
'<%= config.bin %> <%= command.id %> --location repo:proletariat --template
|
|
17
|
-
'<%= config.bin %> <%= command.id %> --location separate --template
|
|
18
|
+
'<%= config.bin %> <%= command.id %> --location repo:proletariat --template 5-tool',
|
|
19
|
+
'<%= config.bin %> <%= command.id %> --location separate --template linear',
|
|
18
20
|
];
|
|
19
21
|
static flags = {
|
|
20
22
|
location: Flags.string({
|
|
@@ -24,7 +26,7 @@ export default class PMOInit extends Command {
|
|
|
24
26
|
template: Flags.string({
|
|
25
27
|
char: 't',
|
|
26
28
|
description: 'Board template',
|
|
27
|
-
options:
|
|
29
|
+
options: PICKER_TEMPLATE_IDS,
|
|
28
30
|
}),
|
|
29
31
|
name: Flags.string({
|
|
30
32
|
char: 'n',
|
|
@@ -93,12 +95,28 @@ export default class PMOInit extends Command {
|
|
|
93
95
|
location = await promptForPMOLocation(hqRoot);
|
|
94
96
|
}
|
|
95
97
|
// Get board template using shared prompt (or from flag)
|
|
98
|
+
// If DB exists, query it for templates (source of truth)
|
|
96
99
|
let template;
|
|
97
100
|
if (flags.template) {
|
|
98
101
|
template = flags.template;
|
|
99
102
|
}
|
|
100
103
|
else {
|
|
101
|
-
|
|
104
|
+
let storage;
|
|
105
|
+
if (hqRoot) {
|
|
106
|
+
const dbPath = path.join(hqRoot, '.proletariat', 'workspace.db');
|
|
107
|
+
if (fs.existsSync(dbPath)) {
|
|
108
|
+
try {
|
|
109
|
+
storage = new SQLiteStorage(dbPath);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// Ignore - will fall back to builtin templates
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
template = await promptForBoardTemplate(storage);
|
|
117
|
+
if (storage) {
|
|
118
|
+
await storage.close();
|
|
119
|
+
}
|
|
102
120
|
}
|
|
103
121
|
// Get columns for template
|
|
104
122
|
let columns = getColumnsForTemplate(template);
|
|
@@ -2,10 +2,12 @@ import { Flags, Args } from '@oclif/core';
|
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import inquirer from 'inquirer';
|
|
5
|
-
import { createBoardContent, createSpecFolders, PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
|
|
5
|
+
import { createBoardContent, createSpecFolders, PMOCommand, pmoBaseFlags, BUILTIN_TEMPLATES } from '../../lib/pmo/index.js';
|
|
6
6
|
import { styles } from '../../lib/styles.js';
|
|
7
7
|
import { slugify } from '../../lib/pmo/utils.js';
|
|
8
8
|
import { shouldOutputJson, outputPromptAsJson, createMetadata, buildFormPromptConfig, } from '../../lib/prompt-json.js';
|
|
9
|
+
// Build template options dynamically from shared definitions
|
|
10
|
+
const TEMPLATE_IDS = BUILTIN_TEMPLATES.map(t => t.id);
|
|
9
11
|
export default class ProjectCreate extends PMOCommand {
|
|
10
12
|
static description = 'Create a new project in the PMO';
|
|
11
13
|
static examples = [
|
|
@@ -35,7 +37,7 @@ export default class ProjectCreate extends PMOCommand {
|
|
|
35
37
|
template: Flags.string({
|
|
36
38
|
char: 't',
|
|
37
39
|
description: 'Workflow template',
|
|
38
|
-
options:
|
|
40
|
+
options: TEMPLATE_IDS,
|
|
39
41
|
default: 'kanban',
|
|
40
42
|
}),
|
|
41
43
|
interactive: Flags.boolean({
|
|
@@ -58,14 +60,11 @@ export default class ProjectCreate extends PMOCommand {
|
|
|
58
60
|
// Get project data first (before storage so prompts work)
|
|
59
61
|
let projectData;
|
|
60
62
|
if (flags.interactive || (!args.name && !flags.name)) {
|
|
61
|
-
// Build choices
|
|
62
|
-
const templateChoices =
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
{ name: '5-Tool Founder - Ship, Grow, Support, Strategy, BizOps → In Progress → Review → Done', value: '5-tool-founder' },
|
|
67
|
-
{ name: 'GTM - Ideation → Planning → In Development → Ready to Launch → Launched', value: 'gtm' },
|
|
68
|
-
];
|
|
63
|
+
// Build choices dynamically from shared template definitions
|
|
64
|
+
const templateChoices = BUILTIN_TEMPLATES.map(t => ({
|
|
65
|
+
name: `${t.name} - ${t.statuses.map(s => s.name).join(' → ')}`,
|
|
66
|
+
value: t.id,
|
|
67
|
+
}));
|
|
69
68
|
// Define fields once - single source of truth for both JSON and interactive modes
|
|
70
69
|
const fields = [
|
|
71
70
|
{ type: 'input', name: 'name', message: 'Project name:', default: flags.name },
|
package/dist/commands/whoami.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
3
4
|
import { execSync } from 'node:child_process';
|
|
4
5
|
import { colors } from '../lib/colors.js';
|
|
6
|
+
import { getAgentByPath } from '../lib/database/index.js';
|
|
5
7
|
export default class Whoami extends Command {
|
|
6
8
|
static description = 'Show current agent/environment context';
|
|
7
9
|
static examples = [
|
|
@@ -43,6 +45,11 @@ export default class Whoami extends Command {
|
|
|
43
45
|
if (pmoPath) {
|
|
44
46
|
this.log(` PMO path: ${colors.textMuted(pmoPath)}`);
|
|
45
47
|
}
|
|
48
|
+
// Show host path if available (set in devcontainer for agent identity)
|
|
49
|
+
const hostPath = process.env.PRLT_HOST_PATH;
|
|
50
|
+
if (hostPath) {
|
|
51
|
+
this.log(` Host path: ${colors.textMuted(hostPath)}`);
|
|
52
|
+
}
|
|
46
53
|
this.log('');
|
|
47
54
|
}
|
|
48
55
|
detectAgentName() {
|
|
@@ -50,18 +57,30 @@ export default class Whoami extends Command {
|
|
|
50
57
|
if (process.env.PRLT_AGENT_NAME) {
|
|
51
58
|
return process.env.PRLT_AGENT_NAME;
|
|
52
59
|
}
|
|
53
|
-
// Try to detect from directory structure
|
|
54
|
-
// Pattern: /workspace/proletariat-{agentName} or agents/staff/{agentName}
|
|
55
60
|
const cwd = process.cwd();
|
|
61
|
+
// Try database lookup (most reliable on host)
|
|
62
|
+
const workspacePath = this.findWorkspaceRoot(cwd);
|
|
63
|
+
if (workspacePath) {
|
|
64
|
+
try {
|
|
65
|
+
const agent = getAgentByPath(workspacePath, cwd);
|
|
66
|
+
if (agent) {
|
|
67
|
+
return agent.name;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// DB lookup failed, fall back to other methods
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Fallback: detect from directory structure
|
|
56
75
|
// Devcontainer pattern: /workspace/proletariat-{agent}
|
|
57
76
|
const workspaceMatch = cwd.match(/\/workspace\/[^/]+-(\w+)/);
|
|
58
77
|
if (workspaceMatch) {
|
|
59
78
|
return workspaceMatch[1];
|
|
60
79
|
}
|
|
61
|
-
// Host pattern: agents/staff/{agent}
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
return
|
|
80
|
+
// Host pattern: agents/staff/{agent} or agents/temp/{agent}
|
|
81
|
+
const agentDirMatch = cwd.match(/agents\/(?:staff|temp)\/([\w-]+)/);
|
|
82
|
+
if (agentDirMatch) {
|
|
83
|
+
return agentDirMatch[1];
|
|
65
84
|
}
|
|
66
85
|
// Try git branch pattern: agent-{name}
|
|
67
86
|
try {
|
|
@@ -76,6 +95,17 @@ export default class Whoami extends Command {
|
|
|
76
95
|
}
|
|
77
96
|
return null;
|
|
78
97
|
}
|
|
98
|
+
findWorkspaceRoot(startDir) {
|
|
99
|
+
let currentDir = startDir;
|
|
100
|
+
while (currentDir !== '/') {
|
|
101
|
+
const dbPath = path.join(currentDir, '.proletariat', 'workspace.db');
|
|
102
|
+
if (fs.existsSync(dbPath)) {
|
|
103
|
+
return currentDir;
|
|
104
|
+
}
|
|
105
|
+
currentDir = path.dirname(currentDir);
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
79
109
|
detectRepoName() {
|
|
80
110
|
// Try to get repo name from directory or git remote
|
|
81
111
|
try {
|
|
@@ -318,6 +318,12 @@ export default class WorkStart extends PMOCommand {
|
|
|
318
318
|
// Get staff agents that exist on disk (warns about missing directories)
|
|
319
319
|
const activeStaffAgents = getActiveStaffAgents(workspaceInfo, (msg) => this.log(msg));
|
|
320
320
|
if (activeStaffAgents.length > 0) {
|
|
321
|
+
// Clean up stale executions before checking availability (TKT-604)
|
|
322
|
+
// This fixes agents appearing as "busy" when their sessions have terminated
|
|
323
|
+
const cleanedUp = executionStorage.cleanupStaleExecutions();
|
|
324
|
+
if (cleanedUp > 0) {
|
|
325
|
+
this.log(styles.muted(` Cleaned up ${cleanedUp} stale execution(s)`));
|
|
326
|
+
}
|
|
321
327
|
// Get list of busy agents (already running something)
|
|
322
328
|
const busyAgentNames = new Set();
|
|
323
329
|
for (const agent of activeStaffAgents) {
|
|
@@ -1156,6 +1162,11 @@ export default class WorkStart extends PMOCommand {
|
|
|
1156
1162
|
this.log('');
|
|
1157
1163
|
// Get staff agents that exist on disk (warns about missing directories)
|
|
1158
1164
|
const activeStaffAgents = getActiveStaffAgents(workspaceInfo, (msg) => this.log(msg));
|
|
1165
|
+
// Clean up stale executions before checking availability (TKT-604)
|
|
1166
|
+
const cleanedUp = executionStorage.cleanupStaleExecutions();
|
|
1167
|
+
if (cleanedUp > 0) {
|
|
1168
|
+
this.log(styles.muted(` Cleaned up ${cleanedUp} stale execution(s)`));
|
|
1169
|
+
}
|
|
1159
1170
|
const busyAgentNames = new Set();
|
|
1160
1171
|
for (const agent of activeStaffAgents) {
|
|
1161
1172
|
const runningExecutions = executionStorage.getAgentRunningExecutions(agent.name);
|
|
@@ -109,6 +109,12 @@ export declare function removeEphemeralAgent(workspacePath: string, agentName: s
|
|
|
109
109
|
* Get all agents in workspace
|
|
110
110
|
*/
|
|
111
111
|
export declare function getWorkspaceAgents(workspacePath: string, includeCleanedUp?: boolean): Agent[];
|
|
112
|
+
/**
|
|
113
|
+
* Get an agent by directory path.
|
|
114
|
+
* Looks up agent where the given absolute path is inside the agent's worktree.
|
|
115
|
+
* Returns null if no matching agent found.
|
|
116
|
+
*/
|
|
117
|
+
export declare function getAgentByPath(workspacePath: string, absolutePath: string): Agent | null;
|
|
112
118
|
/**
|
|
113
119
|
* Mark an agent as cleaned up (keeps the record for history)
|
|
114
120
|
*/
|
|
@@ -379,6 +379,44 @@ export function getWorkspaceAgents(workspacePath, includeCleanedUp = false) {
|
|
|
379
379
|
cleaned_at: row.cleaned_at,
|
|
380
380
|
}));
|
|
381
381
|
}
|
|
382
|
+
/**
|
|
383
|
+
* Get an agent by directory path.
|
|
384
|
+
* Looks up agent where the given absolute path is inside the agent's worktree.
|
|
385
|
+
* Returns null if no matching agent found.
|
|
386
|
+
*/
|
|
387
|
+
export function getAgentByPath(workspacePath, absolutePath) {
|
|
388
|
+
// Normalize paths
|
|
389
|
+
const normalizedWorkspace = path.resolve(workspacePath);
|
|
390
|
+
const normalizedPath = path.resolve(absolutePath);
|
|
391
|
+
// Path must be inside workspace
|
|
392
|
+
if (!normalizedPath.startsWith(normalizedWorkspace)) {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
// Get relative path from workspace root
|
|
396
|
+
const relativePath = path.relative(normalizedWorkspace, normalizedPath);
|
|
397
|
+
const db = openWorkspaceDatabase(workspacePath);
|
|
398
|
+
const agents = db.prepare("SELECT * FROM agents WHERE status = 'active' OR status IS NULL").all();
|
|
399
|
+
db.close();
|
|
400
|
+
// Find agent whose worktree_path matches or contains the relative path
|
|
401
|
+
for (const row of agents) {
|
|
402
|
+
if (row.worktree_path) {
|
|
403
|
+
// Check if relativePath starts with or equals the agent's worktree_path
|
|
404
|
+
if (relativePath === row.worktree_path || relativePath.startsWith(row.worktree_path + '/')) {
|
|
405
|
+
return {
|
|
406
|
+
name: row.name,
|
|
407
|
+
type: (row.type || 'persistent'),
|
|
408
|
+
status: (row.status || 'active'),
|
|
409
|
+
base_name: row.base_name,
|
|
410
|
+
theme_id: row.theme_id,
|
|
411
|
+
worktree_path: row.worktree_path,
|
|
412
|
+
created_at: row.created_at,
|
|
413
|
+
cleaned_at: row.cleaned_at,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
382
420
|
/**
|
|
383
421
|
* Mark an agent as cleaned up (keeps the record for history)
|
|
384
422
|
*/
|
|
@@ -65,6 +65,9 @@ export function generateDevcontainerJson(options, config) {
|
|
|
65
65
|
GH_TOKEN: '${localEnv:GH_TOKEN}',
|
|
66
66
|
GITHUB_TOKEN: '${localEnv:GITHUB_TOKEN}',
|
|
67
67
|
PRLT_HQ_PATH: '/hq',
|
|
68
|
+
// Agent identity - allows agent to know its name and host path
|
|
69
|
+
PRLT_AGENT_NAME: options.agentName,
|
|
70
|
+
PRLT_HOST_PATH: options.agentDir,
|
|
68
71
|
// /hq/.proletariat/bin contains prlt wrapper with ESM loader for native modules
|
|
69
72
|
PATH: '/hq/.proletariat/bin:/home/node/.npm-global/bin:/usr/local/bin:/usr/bin:/bin',
|
|
70
73
|
},
|
|
@@ -639,8 +639,9 @@ export async function runDevcontainer(context, executor, config, displayMode = '
|
|
|
639
639
|
result.containerId = containerId;
|
|
640
640
|
}
|
|
641
641
|
// Set sessionId when using tmux inside the container
|
|
642
|
+
// Use buildSessionName to match the actual tmux session name format: {ticketId}-{action}-{agentName}
|
|
642
643
|
if (result.success && sessionManager === 'tmux') {
|
|
643
|
-
const sessionId = context
|
|
644
|
+
const sessionId = buildSessionName(context);
|
|
644
645
|
result.sessionId = sessionId;
|
|
645
646
|
// For terminal display mode, verify the tmux session was actually created
|
|
646
647
|
// (terminal spawns asynchronously, so we need to wait and check)
|
|
@@ -649,7 +650,7 @@ export async function runDevcontainer(context, executor, config, displayMode = '
|
|
|
649
650
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
650
651
|
// Check if tmux session exists inside the container
|
|
651
652
|
try {
|
|
652
|
-
const checkResult = execSync(`docker exec ${containerId} tmux has-session -t ${sessionId} 2>&1`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
653
|
+
const checkResult = execSync(`docker exec ${containerId} tmux has-session -t "${sessionId}" 2>&1`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
653
654
|
// Session exists - success
|
|
654
655
|
}
|
|
655
656
|
catch (err) {
|
|
@@ -57,7 +57,9 @@ interface AgentWithExecutionCount {
|
|
|
57
57
|
*/
|
|
58
58
|
export declare function getAgentsWithCounts(workspaceInfo: WorkspaceInfo, executionStorage: ExecutionStorage): AgentWithExecutionCount[];
|
|
59
59
|
/**
|
|
60
|
-
* Get agents that are not currently running any executions.
|
|
60
|
+
* Get staff agents that are not currently running any executions.
|
|
61
|
+
* Only considers persistent (staff) agents with status='active'.
|
|
62
|
+
* Cleans up stale executions before checking availability (TKT-604).
|
|
61
63
|
*/
|
|
62
64
|
export declare function getAvailableAgents(workspaceInfo: WorkspaceInfo, executionStorage: ExecutionStorage): string[];
|
|
63
65
|
/**
|
|
@@ -93,11 +93,18 @@ export function getAgentsWithCounts(workspaceInfo, executionStorage) {
|
|
|
93
93
|
});
|
|
94
94
|
}
|
|
95
95
|
/**
|
|
96
|
-
* Get agents that are not currently running any executions.
|
|
96
|
+
* Get staff agents that are not currently running any executions.
|
|
97
|
+
* Only considers persistent (staff) agents with status='active'.
|
|
98
|
+
* Cleans up stale executions before checking availability (TKT-604).
|
|
97
99
|
*/
|
|
98
100
|
export function getAvailableAgents(workspaceInfo, executionStorage) {
|
|
101
|
+
// Clean up stale executions first (TKT-604)
|
|
102
|
+
executionStorage.cleanupStaleExecutions();
|
|
103
|
+
// Filter for active staff agents only (not ephemeral agents)
|
|
99
104
|
return workspaceInfo.agents
|
|
100
|
-
.filter(agent =>
|
|
105
|
+
.filter(agent => agent.type === 'persistent' &&
|
|
106
|
+
agent.status === 'active' &&
|
|
107
|
+
executionStorage.isAgentAvailable(agent.name))
|
|
101
108
|
.map(agent => agent.name);
|
|
102
109
|
}
|
|
103
110
|
/**
|
|
@@ -64,6 +64,20 @@ export declare class ExecutionStorage {
|
|
|
64
64
|
* Check if agent is available (not running anything)
|
|
65
65
|
*/
|
|
66
66
|
isAgentAvailable(agentName: string): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Clean up stale executions where the tmux session no longer exists.
|
|
69
|
+
* This fixes the bug where agents appear "busy" after sessions terminate unexpectedly.
|
|
70
|
+
* Returns the number of stale executions cleaned up.
|
|
71
|
+
*/
|
|
72
|
+
cleanupStaleExecutions(): number;
|
|
73
|
+
/**
|
|
74
|
+
* Get list of host tmux session names
|
|
75
|
+
*/
|
|
76
|
+
private getHostTmuxSessionNames;
|
|
77
|
+
/**
|
|
78
|
+
* Get map of containerId -> tmux session names
|
|
79
|
+
*/
|
|
80
|
+
private getContainerTmuxSessionMap;
|
|
67
81
|
/**
|
|
68
82
|
* Get total execution count for an agent (historical)
|
|
69
83
|
* Used by least-busy agent selection strategy.
|