@proletariat/cli 0.3.50 → 0.3.52

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 (87) hide show
  1. package/dist/commands/agent/status.js +1 -0
  2. package/dist/commands/asana/connect.d.ts +15 -0
  3. package/dist/commands/asana/connect.js +267 -0
  4. package/dist/commands/asana/sync.d.ts +15 -0
  5. package/dist/commands/asana/sync.js +189 -0
  6. package/dist/commands/config/index.js +7 -1
  7. package/dist/commands/execution/list.js +3 -0
  8. package/dist/commands/execution/view.js +10 -0
  9. package/dist/commands/monday/connect.d.ts +16 -0
  10. package/dist/commands/monday/connect.js +212 -0
  11. package/dist/commands/monday/sync.d.ts +14 -0
  12. package/dist/commands/monday/sync.js +178 -0
  13. package/dist/commands/orchestrator/start.d.ts +6 -0
  14. package/dist/commands/orchestrator/start.js +149 -11
  15. package/dist/commands/session/list.js +6 -5
  16. package/dist/commands/work/index.js +7 -0
  17. package/dist/commands/work/jira.d.ts +28 -0
  18. package/dist/commands/work/jira.js +225 -0
  19. package/dist/commands/work/source/set.d.ts +12 -0
  20. package/dist/commands/work/source/set.js +52 -0
  21. package/dist/commands/work/source.d.ts +11 -0
  22. package/dist/commands/work/source.js +53 -0
  23. package/dist/commands/work/spawn.d.ts +1 -0
  24. package/dist/commands/work/spawn.js +73 -8
  25. package/dist/commands/work/start.d.ts +8 -0
  26. package/dist/commands/work/start.js +241 -3
  27. package/dist/lib/asana/client.d.ts +15 -0
  28. package/dist/lib/asana/client.js +120 -0
  29. package/dist/lib/asana/config.d.ts +9 -0
  30. package/dist/lib/asana/config.js +61 -0
  31. package/dist/lib/asana/index.d.ts +5 -0
  32. package/dist/lib/asana/index.js +4 -0
  33. package/dist/lib/asana/mapper.d.ts +13 -0
  34. package/dist/lib/asana/mapper.js +70 -0
  35. package/dist/lib/asana/sync.d.ts +13 -0
  36. package/dist/lib/asana/sync.js +36 -0
  37. package/dist/lib/asana/types.d.ts +40 -0
  38. package/dist/lib/asana/types.js +1 -0
  39. package/dist/lib/database/drizzle-schema.d.ts +393 -0
  40. package/dist/lib/database/drizzle-schema.js +45 -0
  41. package/dist/lib/execution/config.d.ts +10 -0
  42. package/dist/lib/execution/config.js +19 -0
  43. package/dist/lib/execution/runners.d.ts +10 -0
  44. package/dist/lib/execution/runners.js +110 -1
  45. package/dist/lib/execution/spawner.js +26 -0
  46. package/dist/lib/execution/storage.d.ts +4 -0
  47. package/dist/lib/execution/storage.js +8 -3
  48. package/dist/lib/execution/types.d.ts +4 -0
  49. package/dist/lib/external-issues/adapters.d.ts +18 -1
  50. package/dist/lib/external-issues/adapters.js +49 -1
  51. package/dist/lib/external-issues/index.d.ts +4 -1
  52. package/dist/lib/external-issues/index.js +5 -0
  53. package/dist/lib/external-issues/jira.d.ts +23 -0
  54. package/dist/lib/external-issues/jira.js +223 -0
  55. package/dist/lib/external-issues/linear.js +4 -3
  56. package/dist/lib/external-issues/mapper.d.ts +3 -2
  57. package/dist/lib/external-issues/mapper.js +5 -2
  58. package/dist/lib/external-issues/mapping-store.d.ts +12 -0
  59. package/dist/lib/external-issues/mapping-store.js +164 -0
  60. package/dist/lib/external-issues/types.d.ts +34 -0
  61. package/dist/lib/external-issues/validation.js +11 -0
  62. package/dist/lib/external-issues/work-start.d.ts +10 -0
  63. package/dist/lib/external-issues/work-start.js +12 -0
  64. package/dist/lib/linear/mapper.d.ts +2 -0
  65. package/dist/lib/linear/mapper.js +66 -2
  66. package/dist/lib/monday/client.d.ts +14 -0
  67. package/dist/lib/monday/client.js +113 -0
  68. package/dist/lib/monday/config.d.ts +10 -0
  69. package/dist/lib/monday/config.js +64 -0
  70. package/dist/lib/monday/index.d.ts +5 -0
  71. package/dist/lib/monday/index.js +4 -0
  72. package/dist/lib/monday/mapper.d.ts +14 -0
  73. package/dist/lib/monday/mapper.js +89 -0
  74. package/dist/lib/monday/sync.d.ts +13 -0
  75. package/dist/lib/monday/sync.js +45 -0
  76. package/dist/lib/monday/types.d.ts +38 -0
  77. package/dist/lib/monday/types.js +4 -0
  78. package/dist/lib/pmo/schema.d.ts +10 -1
  79. package/dist/lib/pmo/schema.js +73 -0
  80. package/dist/lib/pmo/storage/base.js +32 -0
  81. package/dist/lib/prompt-json.d.ts +11 -0
  82. package/dist/lib/work-source/config.d.ts +14 -0
  83. package/dist/lib/work-source/config.js +70 -0
  84. package/dist/lib/work-source/index.d.ts +1 -0
  85. package/dist/lib/work-source/index.js +1 -0
  86. package/oclif.manifest.json +2531 -1964
  87. package/package.json +1 -1
@@ -42,6 +42,7 @@ export default class Status extends PMOCommand {
42
42
  outputSuccessAsJson({
43
43
  agents: allStatuses,
44
44
  }, createMetadata('agent status', flags));
45
+ return;
45
46
  }
46
47
  // Agent mode config for prompts
47
48
  const agentConfig = jsonMode ? { flags, commandName: 'agent status' } : null;
@@ -0,0 +1,15 @@
1
+ import { PMOCommand } from '../../lib/pmo/index.js';
2
+ export default class AsanaConnect extends PMOCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ check: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
+ force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ disconnect: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
+ workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ };
14
+ execute(): Promise<void>;
15
+ }
@@ -0,0 +1,267 @@
1
+ import { Flags } from '@oclif/core';
2
+ import inquirer from 'inquirer';
3
+ import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
4
+ import { colors } from '../../lib/colors.js';
5
+ import { shouldOutputJson, outputSuccessAsJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
6
+ import { AsanaClient, clearAsanaConfig, getAsanaAccessToken, isAsanaConfigured, loadAsanaConfig, saveAsanaAccessToken, saveAsanaProject, saveAsanaWorkspace, } from '../../lib/asana/index.js';
7
+ function isLikelyGid(value) {
8
+ return /^\d+$/.test(value);
9
+ }
10
+ export default class AsanaConnect extends PMOCommand {
11
+ static description = 'Authenticate with Asana and configure workspace/project defaults';
12
+ static examples = [
13
+ '<%= config.bin %> <%= command.id %>',
14
+ '<%= config.bin %> <%= command.id %> --check',
15
+ '<%= config.bin %> <%= command.id %> --workspace "Product Team" --project "Roadmap"',
16
+ 'ASANA_ACCESS_TOKEN=... <%= config.bin %> <%= command.id %>',
17
+ ];
18
+ static flags = {
19
+ ...pmoBaseFlags,
20
+ check: Flags.boolean({
21
+ description: 'Only check if Asana credentials exist (do not prompt)',
22
+ default: false,
23
+ }),
24
+ force: Flags.boolean({
25
+ description: 'Force re-authentication even if credentials exist',
26
+ default: false,
27
+ }),
28
+ disconnect: Flags.boolean({
29
+ description: 'Remove stored Asana credentials',
30
+ default: false,
31
+ }),
32
+ workspace: Flags.string({
33
+ description: 'Default workspace gid or name',
34
+ }),
35
+ project: Flags.string({
36
+ description: 'Default project gid or name',
37
+ }),
38
+ };
39
+ async execute() {
40
+ const { flags } = await this.parse(AsanaConnect);
41
+ const jsonMode = shouldOutputJson(flags);
42
+ const db = this.storage.getDatabase();
43
+ if (flags.disconnect) {
44
+ clearAsanaConfig(db);
45
+ if (jsonMode) {
46
+ outputSuccessAsJson({ disconnected: true, message: 'Asana credentials removed.' }, createMetadata('asana connect', flags));
47
+ return;
48
+ }
49
+ this.log(colors.success('Asana credentials removed.'));
50
+ return;
51
+ }
52
+ if (flags.check) {
53
+ if (!isAsanaConfigured(db)) {
54
+ if (jsonMode) {
55
+ outputErrorAsJson('ASANA_NOT_CONFIGURED', 'Asana is not configured. Run "prlt asana connect".', createMetadata('asana connect', flags));
56
+ this.exit(1);
57
+ }
58
+ this.log(colors.warning('Asana is not configured.'));
59
+ this.log(colors.textMuted('Run "prlt asana connect" to authenticate.'));
60
+ this.exit(1);
61
+ }
62
+ const config = loadAsanaConfig(db);
63
+ try {
64
+ const client = new AsanaClient(config.accessToken);
65
+ const user = await client.verify();
66
+ if (jsonMode) {
67
+ outputSuccessAsJson({
68
+ authenticated: true,
69
+ user: user.name,
70
+ email: user.email ?? null,
71
+ workspace: config.workspaceName ?? config.workspaceGid ?? null,
72
+ project: config.projectName ?? config.projectGid ?? null,
73
+ }, createMetadata('asana connect', flags));
74
+ return;
75
+ }
76
+ this.log(colors.success('Asana connection is active'));
77
+ this.log(colors.textMuted(` User: ${user.name}${user.email ? ` (${user.email})` : ''}`));
78
+ if (config.workspaceName || config.workspaceGid) {
79
+ this.log(colors.textMuted(` Workspace: ${config.workspaceName ?? config.workspaceGid}`));
80
+ }
81
+ if (config.projectName || config.projectGid) {
82
+ this.log(colors.textMuted(` Project: ${config.projectName ?? config.projectGid}`));
83
+ }
84
+ }
85
+ catch {
86
+ if (jsonMode) {
87
+ outputErrorAsJson('ASANA_AUTH_INVALID', 'Stored Asana token is invalid or expired.', createMetadata('asana connect', flags));
88
+ this.exit(1);
89
+ }
90
+ this.log(colors.error('Stored Asana token is invalid or expired.'));
91
+ this.log(colors.textMuted('Run "prlt asana connect --force" to re-authenticate.'));
92
+ this.exit(1);
93
+ }
94
+ return;
95
+ }
96
+ const existingConfig = loadAsanaConfig(db);
97
+ if (existingConfig && !flags.force) {
98
+ try {
99
+ const client = new AsanaClient(existingConfig.accessToken);
100
+ const user = await client.verify();
101
+ if (jsonMode) {
102
+ outputSuccessAsJson({
103
+ authenticated: true,
104
+ user: user.name,
105
+ message: 'Already authenticated. Use --force to re-authenticate.',
106
+ }, createMetadata('asana connect', flags));
107
+ return;
108
+ }
109
+ this.log(colors.success('Already connected to Asana'));
110
+ this.log(colors.textMuted(` User: ${user.name}`));
111
+ this.log(colors.textMuted('Use --force to re-authenticate.'));
112
+ return;
113
+ }
114
+ catch {
115
+ // Continue to re-authenticate with a new token
116
+ }
117
+ }
118
+ let accessToken = getAsanaAccessToken(db);
119
+ if (!accessToken) {
120
+ if (jsonMode) {
121
+ outputErrorAsJson('ACCESS_TOKEN_REQUIRED', 'Asana access token required. Set ASANA_ACCESS_TOKEN or PRLT_ASANA_ACCESS_TOKEN.', createMetadata('asana connect', flags));
122
+ this.exit(1);
123
+ }
124
+ this.log('');
125
+ this.log(colors.primary('Asana Authentication'));
126
+ this.log('');
127
+ this.log('Create a Personal Access Token at:');
128
+ this.log(colors.textSecondary(' https://app.asana.com/0/my-apps'));
129
+ this.log('');
130
+ const { inputToken } = await inquirer.prompt([{
131
+ type: 'password',
132
+ name: 'inputToken',
133
+ message: 'Enter your Asana access token:',
134
+ mask: '*',
135
+ validate: (input) => input.trim().length > 0 || 'Access token is required',
136
+ }]);
137
+ accessToken = inputToken;
138
+ }
139
+ if (!accessToken) {
140
+ if (jsonMode) {
141
+ outputErrorAsJson('ACCESS_TOKEN_REQUIRED', 'Asana access token required. Set ASANA_ACCESS_TOKEN or PRLT_ASANA_ACCESS_TOKEN.', createMetadata('asana connect', flags));
142
+ }
143
+ else {
144
+ this.log(colors.error('Asana access token is required.'));
145
+ }
146
+ this.exit(1);
147
+ }
148
+ this.log('');
149
+ this.log(colors.textMuted('Verifying Asana access token...'));
150
+ if (!accessToken) {
151
+ if (jsonMode) {
152
+ outputErrorAsJson('ACCESS_TOKEN_REQUIRED', 'Asana access token required. Set ASANA_ACCESS_TOKEN or PRLT_ASANA_ACCESS_TOKEN.', createMetadata('asana connect', flags));
153
+ this.exit(1);
154
+ }
155
+ this.log(colors.error('Asana access token is required.'));
156
+ this.exit(1);
157
+ }
158
+ const client = new AsanaClient(accessToken);
159
+ try {
160
+ const user = await client.verify();
161
+ saveAsanaAccessToken(db, accessToken);
162
+ let workspaceGid;
163
+ let workspaceName;
164
+ let projectGid;
165
+ let projectName;
166
+ if (flags.workspace) {
167
+ const workspaces = user.workspaces;
168
+ if (isLikelyGid(flags.workspace)) {
169
+ const workspace = await client.getWorkspace(flags.workspace);
170
+ if (!workspace) {
171
+ throw new Error(`Workspace ${flags.workspace} not found`);
172
+ }
173
+ workspaceGid = workspace.gid;
174
+ workspaceName = workspace.name;
175
+ }
176
+ else {
177
+ const matched = workspaces.find((workspace) => workspace.name.toLowerCase() === flags.workspace.toLowerCase());
178
+ if (!matched) {
179
+ throw new Error(`Workspace "${flags.workspace}" not found`);
180
+ }
181
+ workspaceGid = matched.gid;
182
+ workspaceName = matched.name;
183
+ }
184
+ }
185
+ else if (!jsonMode && user.workspaces.length === 1) {
186
+ workspaceGid = user.workspaces[0].gid;
187
+ workspaceName = user.workspaces[0].name;
188
+ }
189
+ if (!workspaceGid && !jsonMode && user.workspaces.length > 1) {
190
+ const { selectedWorkspace } = await inquirer.prompt([{
191
+ type: 'list',
192
+ name: 'selectedWorkspace',
193
+ message: 'Select default Asana workspace (optional):',
194
+ choices: [
195
+ { name: 'Skip', value: null },
196
+ ...user.workspaces.map((workspace) => ({
197
+ name: workspace.name,
198
+ value: workspace.gid,
199
+ })),
200
+ ],
201
+ default: null,
202
+ }]);
203
+ if (selectedWorkspace) {
204
+ const selected = user.workspaces.find((workspace) => workspace.gid === selectedWorkspace);
205
+ workspaceGid = selected?.gid;
206
+ workspaceName = selected?.name;
207
+ }
208
+ }
209
+ if (workspaceGid) {
210
+ saveAsanaWorkspace(db, workspaceGid, workspaceName ?? workspaceGid);
211
+ }
212
+ if (flags.project) {
213
+ const projectScope = workspaceGid ?? loadAsanaConfig(db)?.workspaceGid;
214
+ if (isLikelyGid(flags.project)) {
215
+ const project = await client.getProject(flags.project);
216
+ if (!project) {
217
+ throw new Error(`Project ${flags.project} not found`);
218
+ }
219
+ projectGid = project.gid;
220
+ projectName = project.name;
221
+ }
222
+ else if (projectScope) {
223
+ const projects = await client.listProjects(projectScope);
224
+ const matched = projects.find((project) => project.name.toLowerCase() === flags.project.toLowerCase());
225
+ if (!matched) {
226
+ throw new Error(`Project "${flags.project}" not found in selected workspace`);
227
+ }
228
+ projectGid = matched.gid;
229
+ projectName = matched.name;
230
+ }
231
+ else {
232
+ throw new Error('A workspace must be configured before selecting a project by name.');
233
+ }
234
+ }
235
+ if (projectGid) {
236
+ saveAsanaProject(db, projectGid, projectName ?? projectGid);
237
+ }
238
+ if (jsonMode) {
239
+ outputSuccessAsJson({
240
+ authenticated: true,
241
+ user: user.name,
242
+ email: user.email ?? null,
243
+ workspace: workspaceName ?? null,
244
+ project: projectName ?? null,
245
+ }, createMetadata('asana connect', flags));
246
+ return;
247
+ }
248
+ this.log(colors.success('Connected to Asana'));
249
+ this.log(colors.textMuted(` User: ${user.name}${user.email ? ` (${user.email})` : ''}`));
250
+ if (workspaceName) {
251
+ this.log(colors.textMuted(` Workspace: ${workspaceName}`));
252
+ }
253
+ if (projectName) {
254
+ this.log(colors.textMuted(` Project: ${projectName}`));
255
+ }
256
+ this.log('');
257
+ this.log(colors.textMuted('Run "prlt asana sync" to sync mapped tickets to Asana tasks.'));
258
+ }
259
+ catch (error) {
260
+ if (jsonMode) {
261
+ outputErrorAsJson('ASANA_CONNECT_FAILED', `Connection failed: ${error instanceof Error ? error.message : String(error)}`, createMetadata('asana connect', flags));
262
+ this.exit(1);
263
+ }
264
+ this.error(`Connection failed: ${error instanceof Error ? error.message : String(error)}`);
265
+ }
266
+ }
267
+ }
@@ -0,0 +1,15 @@
1
+ import { PMOCommand } from '../../lib/pmo/index.js';
2
+ export default class AsanaSyncCommand extends PMOCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ ticket: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ task: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ 'create-missing': import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ 'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ };
14
+ execute(): Promise<void>;
15
+ }
@@ -0,0 +1,189 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
3
+ import { colors } from '../../lib/colors.js';
4
+ import { shouldOutputJson, outputSuccessAsJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
5
+ import { AsanaClient, AsanaMapper, AsanaSync, isAsanaConfigured, loadAsanaConfig, } from '../../lib/asana/index.js';
6
+ export default class AsanaSyncCommand extends PMOCommand {
7
+ static description = 'Sync PMO tickets to Asana tasks';
8
+ static examples = [
9
+ '<%= config.bin %> <%= command.id %> --ticket TKT-001 --task 123456789',
10
+ '<%= config.bin %> <%= command.id %> --ticket TKT-001 --create-missing',
11
+ '<%= config.bin %> <%= command.id %> --dry-run',
12
+ ];
13
+ static flags = {
14
+ ...pmoBaseFlags,
15
+ ticket: Flags.string({
16
+ description: 'PMO ticket ID to sync (syncs all mapped tickets if omitted)',
17
+ }),
18
+ task: Flags.string({
19
+ description: 'Asana task gid to map to --ticket',
20
+ }),
21
+ project: Flags.string({
22
+ description: 'Asana project gid used with --create-missing',
23
+ }),
24
+ 'create-missing': Flags.boolean({
25
+ description: 'Create Asana task when no mapping exists (requires project)',
26
+ default: false,
27
+ }),
28
+ 'dry-run': Flags.boolean({
29
+ description: 'Preview sync operations without making changes',
30
+ default: false,
31
+ }),
32
+ };
33
+ async execute() {
34
+ const { flags } = await this.parse(AsanaSyncCommand);
35
+ const jsonMode = shouldOutputJson(flags);
36
+ const db = this.storage.getDatabase();
37
+ if (!isAsanaConfigured(db)) {
38
+ if (jsonMode) {
39
+ outputErrorAsJson('ASANA_NOT_CONFIGURED', 'Asana is not configured. Run "prlt asana connect" first.', createMetadata('asana sync', flags));
40
+ this.exit(1);
41
+ }
42
+ this.error('Asana is not configured. Run "prlt asana connect" first.');
43
+ }
44
+ const config = loadAsanaConfig(db);
45
+ const client = new AsanaClient(config.accessToken);
46
+ const mapper = new AsanaMapper(db);
47
+ const sync = new AsanaSync(client, mapper);
48
+ if (flags.ticket) {
49
+ const ticket = await this.storage.getTicket(flags.ticket);
50
+ if (!ticket) {
51
+ if (jsonMode) {
52
+ outputErrorAsJson('TICKET_NOT_FOUND', `Ticket ${flags.ticket} not found.`, createMetadata('asana sync', flags));
53
+ this.exit(1);
54
+ }
55
+ this.error(`Ticket ${flags.ticket} not found.`);
56
+ }
57
+ if (flags.task) {
58
+ mapper.createOrUpdateMapping(flags.ticket, flags.task, flags.project ?? config.projectGid);
59
+ }
60
+ const mapping = mapper.getByTicketId(flags.ticket);
61
+ if (!mapping) {
62
+ if (!flags['create-missing']) {
63
+ if (jsonMode) {
64
+ outputSuccessAsJson({
65
+ synced: false,
66
+ message: `Ticket ${flags.ticket} has no Asana mapping. Use --task or --create-missing.`,
67
+ }, createMetadata('asana sync', flags));
68
+ return;
69
+ }
70
+ this.log(colors.warning(`Ticket ${flags.ticket} has no Asana mapping. Use --task or --create-missing.`));
71
+ return;
72
+ }
73
+ const projectGid = flags.project ?? config.projectGid;
74
+ if (!projectGid) {
75
+ if (jsonMode) {
76
+ outputErrorAsJson('ASANA_PROJECT_REQUIRED', 'Project gid is required for --create-missing. Use --project or run "prlt asana connect --project ...".', createMetadata('asana sync', flags));
77
+ this.exit(1);
78
+ }
79
+ this.error('Project gid is required for --create-missing. Use --project or run "prlt asana connect --project ...".');
80
+ }
81
+ if (flags['dry-run']) {
82
+ if (jsonMode) {
83
+ outputSuccessAsJson({
84
+ dryRun: true,
85
+ action: 'create-and-sync',
86
+ ticketId: flags.ticket,
87
+ projectGid,
88
+ }, createMetadata('asana sync', flags));
89
+ return;
90
+ }
91
+ this.log(colors.textMuted(`Would create Asana task in project ${projectGid} for ${flags.ticket}`));
92
+ return;
93
+ }
94
+ const taskGid = await sync.createTaskForTicket(ticket, projectGid);
95
+ await sync.syncTicket(ticket, taskGid);
96
+ if (jsonMode) {
97
+ outputSuccessAsJson({
98
+ action: 'create-and-sync',
99
+ ticketId: flags.ticket,
100
+ asanaTaskGid: taskGid,
101
+ synced: true,
102
+ }, createMetadata('asana sync', flags));
103
+ return;
104
+ }
105
+ this.log(colors.success(`Created and synced Asana task ${taskGid} for ${flags.ticket}`));
106
+ return;
107
+ }
108
+ if (flags['dry-run']) {
109
+ if (jsonMode) {
110
+ outputSuccessAsJson({
111
+ dryRun: true,
112
+ action: 'sync-ticket',
113
+ ticketId: flags.ticket,
114
+ asanaTaskGid: mapping.asanaTaskGid,
115
+ statusCategory: ticket.statusCategory,
116
+ }, createMetadata('asana sync', flags));
117
+ return;
118
+ }
119
+ this.log(colors.textMuted(`Would sync ${flags.ticket} to Asana task ${mapping.asanaTaskGid}`));
120
+ return;
121
+ }
122
+ await sync.syncTicket(ticket, mapping.asanaTaskGid);
123
+ if (jsonMode) {
124
+ outputSuccessAsJson({
125
+ action: 'sync-ticket',
126
+ ticketId: flags.ticket,
127
+ asanaTaskGid: mapping.asanaTaskGid,
128
+ synced: true,
129
+ }, createMetadata('asana sync', flags));
130
+ return;
131
+ }
132
+ this.log(colors.success(`Synced ${flags.ticket} to Asana task ${mapping.asanaTaskGid}`));
133
+ return;
134
+ }
135
+ const mappings = mapper.listMappings();
136
+ if (mappings.length === 0) {
137
+ if (jsonMode) {
138
+ outputSuccessAsJson({ synced: 0, skipped: 0, errors: 0, message: 'No mapped tickets to sync.' }, createMetadata('asana sync', flags));
139
+ return;
140
+ }
141
+ this.log(colors.textMuted('No mapped tickets to sync.'));
142
+ return;
143
+ }
144
+ if (flags['dry-run']) {
145
+ const preview = [];
146
+ for (const mapping of mappings) {
147
+ // eslint-disable-next-line no-await-in-loop
148
+ const ticket = await this.storage.getTicket(mapping.pmoTicketId);
149
+ preview.push({
150
+ ticketId: mapping.pmoTicketId,
151
+ asanaTaskGid: mapping.asanaTaskGid,
152
+ statusCategory: ticket?.statusCategory ?? null,
153
+ });
154
+ }
155
+ if (jsonMode) {
156
+ outputSuccessAsJson({ dryRun: true, action: 'sync-all', tickets: preview }, createMetadata('asana sync', flags));
157
+ return;
158
+ }
159
+ for (const item of preview) {
160
+ this.log(colors.textMuted(` ${item.ticketId} -> ${item.asanaTaskGid} (${item.statusCategory ?? 'missing'})`));
161
+ }
162
+ return;
163
+ }
164
+ let synced = 0;
165
+ let skipped = 0;
166
+ let errors = 0;
167
+ for (const mapping of mappings) {
168
+ try {
169
+ // eslint-disable-next-line no-await-in-loop
170
+ const ticket = await this.storage.getTicket(mapping.pmoTicketId);
171
+ if (!ticket) {
172
+ skipped++;
173
+ continue;
174
+ }
175
+ // eslint-disable-next-line no-await-in-loop
176
+ await sync.syncTicket(ticket, mapping.asanaTaskGid);
177
+ synced++;
178
+ }
179
+ catch {
180
+ errors++;
181
+ }
182
+ }
183
+ if (jsonMode) {
184
+ outputSuccessAsJson({ action: 'sync-all', synced, skipped, errors }, createMetadata('asana sync', flags));
185
+ return;
186
+ }
187
+ this.log(colors.success(`Asana sync complete: ${synced} synced, ${skipped} skipped, ${errors} errors`));
188
+ }
189
+ }
@@ -6,7 +6,7 @@ import { PromptCommand } from '../../lib/prompt-command.js';
6
6
  import { machineOutputFlags } from '../../lib/pmo/index.js';
7
7
  import { styles } from '../../lib/styles.js';
8
8
  import { getWorkspaceInfo } from '../../lib/agents/commands.js';
9
- import { loadExecutionConfig, saveTerminalApp, saveTerminalOpenInBackground, saveTmuxControlMode, saveShell, saveCreatePrDefault, saveFirewallAllowlistDomains, } from '../../lib/execution/config.js';
9
+ import { loadExecutionConfig, saveTerminalApp, saveTerminalOpenInBackground, saveTmuxControlMode, saveShell, getMirrorToPmoDefault, saveCreatePrDefault, saveMirrorToPmoDefault, saveFirewallAllowlistDomains, } from '../../lib/execution/config.js';
10
10
  import { shouldOutputJson, isNonTTY, outputSuccessAsJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
11
11
  export default class Config extends PromptCommand {
12
12
  static description = 'View and update workspace configuration';
@@ -56,6 +56,7 @@ export default class Config extends PromptCommand {
56
56
  try {
57
57
  // Load current config
58
58
  const config = loadExecutionConfig(db);
59
+ const mirrorToPmoDefault = getMirrorToPmoDefault(db);
59
60
  // Handle --set flag
60
61
  if (flags.set && flags.set.length > 0) {
61
62
  for (const setValue of flags.set) {
@@ -94,6 +95,7 @@ export default class Config extends PromptCommand {
94
95
  outputMode: config.outputMode,
95
96
  permissionMode: config.permissionMode,
96
97
  createPrDefault: config.createPrDefault ?? null,
98
+ mirrorToPmoDefault,
97
99
  firewall: {
98
100
  allowlistDomains: config.firewall.allowlistDomains,
99
101
  },
@@ -120,6 +122,7 @@ export default class Config extends PromptCommand {
120
122
  this.log(` outputMode: ${config.outputMode}`);
121
123
  this.log(` permissionMode: ${config.permissionMode}`);
122
124
  this.log(` createPrDefault: ${config.createPrDefault ?? 'not set (will prompt)'}`);
125
+ this.log(` mirrorToPmoDefault: ${mirrorToPmoDefault ?? 'not set (default: true)'}`);
123
126
  this.log(` firewall.allowlistDomains: ${config.firewall.allowlistDomains.join(', ') || '(none)'}`);
124
127
  this.log('');
125
128
  }
@@ -300,6 +303,9 @@ export default class Config extends PromptCommand {
300
303
  case 'execution.create_pr_default':
301
304
  saveCreatePrDefault(db, value.toLowerCase() === 'true');
302
305
  break;
306
+ case 'execution.mirror_to_pmo_default':
307
+ saveMirrorToPmoDefault(db, value.toLowerCase() === 'true');
308
+ break;
303
309
  case 'firewall.allowlistdomains': {
304
310
  const domains = value
305
311
  .split(',')
@@ -52,6 +52,9 @@ export default class ExecutionList extends PMOCommand {
52
52
  const db = new Database(dbPath);
53
53
  const executionStorage = new ExecutionStorage(db);
54
54
  try {
55
+ // Keep execution status truthful: mark missing tmux/container sessions as stopped
56
+ // before listing so "running" only reflects actually active work.
57
+ executionStorage.cleanupStaleExecutions();
55
58
  const executions = executionStorage.listExecutions({
56
59
  status: flags.status,
57
60
  agentName: flags.agent,
@@ -101,6 +101,10 @@ export default class ExecutionView extends PMOCommand {
101
101
  sessionId: execution.sessionId || null,
102
102
  host: execution.host || null,
103
103
  logPath: execution.logPath || null,
104
+ externalSource: execution.externalSource || null,
105
+ externalKey: execution.externalKey || null,
106
+ externalId: execution.externalId || null,
107
+ externalUrl: execution.externalUrl || null,
104
108
  startedAt: execution.startedAt.toISOString(),
105
109
  completedAt: execution.completedAt?.toISOString() || null,
106
110
  exitCode: execution.exitCode ?? null,
@@ -128,6 +132,12 @@ export default class ExecutionView extends PMOCommand {
128
132
  if (execution.branch) {
129
133
  this.log(`${styles.muted('Branch:')} ${execution.branch}`);
130
134
  }
135
+ if (execution.externalSource || execution.externalKey) {
136
+ this.log(`${styles.muted('External:')} ${(execution.externalSource || 'unknown')} ${execution.externalKey ? `(${execution.externalKey})` : ''}`.trimEnd());
137
+ if (execution.externalUrl) {
138
+ this.log(`${styles.muted('External URL:')} ${execution.externalUrl}`);
139
+ }
140
+ }
131
141
  this.log('');
132
142
  // Process info
133
143
  if (execution.pid || execution.containerId || execution.sessionId || execution.host) {
@@ -0,0 +1,16 @@
1
+ import { PMOCommand } from '../../lib/pmo/index.js';
2
+ export default class MondayConnect extends PMOCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ board: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ check: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
+ disconnect: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ };
14
+ execute(): Promise<void>;
15
+ private checkConnection;
16
+ }