@proletariat/cli 0.3.96 → 0.3.98

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 (206) hide show
  1. package/dist/commands/gc.d.ts +1 -0
  2. package/dist/commands/gc.js +31 -1
  3. package/dist/commands/gc.js.map +1 -1
  4. package/dist/commands/linear/connect.d.ts +5 -0
  5. package/dist/commands/linear/connect.js +84 -0
  6. package/dist/commands/linear/connect.js.map +1 -1
  7. package/dist/commands/orchestrate/index.js +18 -10
  8. package/dist/commands/orchestrate/index.js.map +1 -1
  9. package/dist/commands/qa/index.js +1 -1
  10. package/dist/commands/session/watch.d.ts +1 -0
  11. package/dist/commands/session/watch.js +46 -2
  12. package/dist/commands/session/watch.js.map +1 -1
  13. package/dist/commands/watch/index.d.ts +28 -0
  14. package/dist/commands/watch/index.js +172 -0
  15. package/dist/commands/watch/index.js.map +1 -0
  16. package/dist/commands/work/complete.d.ts +1 -0
  17. package/dist/commands/work/complete.js +38 -31
  18. package/dist/commands/work/complete.js.map +1 -1
  19. package/dist/commands/{ticket/index.d.ts → work/drop.d.ts} +5 -4
  20. package/dist/commands/work/drop.js +215 -0
  21. package/dist/commands/work/drop.js.map +1 -0
  22. package/dist/commands/work/linear.js +1 -1
  23. package/dist/commands/work/linear.js.map +1 -1
  24. package/dist/commands/work/ready.d.ts +1 -0
  25. package/dist/commands/work/ready.js +46 -30
  26. package/dist/commands/work/ready.js.map +1 -1
  27. package/dist/commands/work/ship.d.ts +1 -0
  28. package/dist/commands/work/ship.js +56 -40
  29. package/dist/commands/work/ship.js.map +1 -1
  30. package/dist/commands/work/start.d.ts +2 -0
  31. package/dist/commands/work/start.js +238 -83
  32. package/dist/commands/work/start.js.map +1 -1
  33. package/dist/commands/work/stop.d.ts +1 -0
  34. package/dist/commands/work/stop.js +40 -0
  35. package/dist/commands/work/stop.js.map +1 -1
  36. package/dist/lib/agents/commands.js +7 -5
  37. package/dist/lib/agents/commands.js.map +1 -1
  38. package/dist/lib/asana/client.d.ts +4 -1
  39. package/dist/lib/asana/client.js +15 -0
  40. package/dist/lib/asana/client.js.map +1 -1
  41. package/dist/lib/asana/index.d.ts +1 -1
  42. package/dist/lib/asana/types.d.ts +4 -0
  43. package/dist/lib/database/credential-store.js +1 -0
  44. package/dist/lib/database/credential-store.js.map +1 -1
  45. package/dist/lib/database/drizzle-schema.d.ts +17 -0
  46. package/dist/lib/database/drizzle-schema.js +1 -0
  47. package/dist/lib/database/drizzle-schema.js.map +1 -1
  48. package/dist/lib/database/migrations/0019_gc_artifact_cleanup.d.ts +9 -0
  49. package/dist/lib/database/migrations/0019_gc_artifact_cleanup.js +23 -0
  50. package/dist/lib/database/migrations/0019_gc_artifact_cleanup.js.map +1 -0
  51. package/dist/lib/database/migrations/0020_transition_map.d.ts +2 -0
  52. package/dist/lib/database/migrations/0020_transition_map.js +27 -0
  53. package/dist/lib/database/migrations/0020_transition_map.js.map +1 -0
  54. package/dist/lib/database/migrations/index.js +4 -0
  55. package/dist/lib/database/migrations/index.js.map +1 -1
  56. package/dist/lib/execution/config.d.ts +10 -0
  57. package/dist/lib/execution/config.js +24 -0
  58. package/dist/lib/execution/config.js.map +1 -1
  59. package/dist/lib/execution/preflight.d.ts +51 -0
  60. package/dist/lib/execution/preflight.js +278 -0
  61. package/dist/lib/execution/preflight.js.map +1 -0
  62. package/dist/lib/execution/runners/prompt-builder.d.ts +6 -0
  63. package/dist/lib/execution/runners/prompt-builder.js +38 -7
  64. package/dist/lib/execution/runners/prompt-builder.js.map +1 -1
  65. package/dist/lib/execution/session-utils.d.ts +23 -0
  66. package/dist/lib/execution/session-utils.js +69 -0
  67. package/dist/lib/execution/session-utils.js.map +1 -1
  68. package/dist/lib/execution/spawner.d.ts +11 -1
  69. package/dist/lib/execution/spawner.js +44 -16
  70. package/dist/lib/execution/spawner.js.map +1 -1
  71. package/dist/lib/execution/storage.d.ts +6 -0
  72. package/dist/lib/execution/storage.js +18 -0
  73. package/dist/lib/execution/storage.js.map +1 -1
  74. package/dist/lib/execution/ticket-refs.d.ts +71 -0
  75. package/dist/lib/execution/ticket-refs.js +125 -0
  76. package/dist/lib/execution/ticket-refs.js.map +1 -0
  77. package/dist/lib/execution/types.d.ts +7 -2
  78. package/dist/lib/execution/types.js +5 -3
  79. package/dist/lib/execution/types.js.map +1 -1
  80. package/dist/lib/external-issues/index.d.ts +1 -0
  81. package/dist/lib/external-issues/index.js +2 -0
  82. package/dist/lib/external-issues/index.js.map +1 -1
  83. package/dist/lib/external-issues/linear.js +1 -1
  84. package/dist/lib/external-issues/linear.js.map +1 -1
  85. package/dist/lib/external-issues/ticket-builder.d.ts +21 -0
  86. package/dist/lib/external-issues/ticket-builder.js +43 -0
  87. package/dist/lib/external-issues/ticket-builder.js.map +1 -0
  88. package/dist/lib/external-issues/work-start.js +1 -1
  89. package/dist/lib/external-issues/work-start.js.map +1 -1
  90. package/dist/lib/gc/index.d.ts +67 -6
  91. package/dist/lib/gc/index.js +293 -15
  92. package/dist/lib/gc/index.js.map +1 -1
  93. package/dist/lib/github/client.d.ts +84 -0
  94. package/dist/lib/github/client.js +123 -0
  95. package/dist/lib/github/client.js.map +1 -0
  96. package/dist/lib/github/config.d.ts +42 -0
  97. package/dist/lib/github/config.js +115 -0
  98. package/dist/lib/github/config.js.map +1 -0
  99. package/dist/lib/github/types.d.ts +71 -0
  100. package/dist/lib/github/types.js +50 -0
  101. package/dist/lib/github/types.js.map +1 -0
  102. package/dist/lib/jira/client.d.ts +113 -0
  103. package/dist/lib/jira/client.js +208 -0
  104. package/dist/lib/jira/client.js.map +1 -0
  105. package/dist/lib/jira/config.d.ts +9 -0
  106. package/dist/lib/jira/config.js +30 -0
  107. package/dist/lib/jira/config.js.map +1 -1
  108. package/dist/lib/jira/index.d.ts +6 -2
  109. package/dist/lib/jira/index.js +4 -2
  110. package/dist/lib/jira/index.js.map +1 -1
  111. package/dist/lib/jira/types.d.ts +118 -0
  112. package/dist/lib/jira/types.js +45 -0
  113. package/dist/lib/jira/types.js.map +1 -0
  114. package/dist/lib/linear/config.d.ts +10 -0
  115. package/dist/lib/linear/config.js +33 -0
  116. package/dist/lib/linear/config.js.map +1 -1
  117. package/dist/lib/mcp/tools/index.d.ts +0 -2
  118. package/dist/lib/mcp/tools/index.js +0 -2
  119. package/dist/lib/mcp/tools/index.js.map +1 -1
  120. package/dist/lib/orchestrate/index.d.ts +2 -0
  121. package/dist/lib/orchestrate/index.js +1 -0
  122. package/dist/lib/orchestrate/index.js.map +1 -1
  123. package/dist/lib/orchestrate/simple-poller.d.ts +57 -0
  124. package/dist/lib/orchestrate/simple-poller.js +324 -0
  125. package/dist/lib/orchestrate/simple-poller.js.map +1 -0
  126. package/dist/lib/pmo/storage/index.js +16 -5
  127. package/dist/lib/pmo/storage/index.js.map +1 -1
  128. package/dist/lib/prompt-json.d.ts +31 -0
  129. package/dist/lib/prompt-json.js.map +1 -1
  130. package/dist/lib/providers/asana-provider.d.ts +27 -0
  131. package/dist/lib/providers/asana-provider.js +426 -0
  132. package/dist/lib/providers/asana-provider.js.map +1 -0
  133. package/dist/lib/providers/auto-mapper.d.ts +45 -0
  134. package/dist/lib/providers/auto-mapper.js +138 -0
  135. package/dist/lib/providers/auto-mapper.js.map +1 -0
  136. package/dist/lib/providers/github-provider.d.ts +34 -0
  137. package/dist/lib/providers/github-provider.js +418 -0
  138. package/dist/lib/providers/github-provider.js.map +1 -0
  139. package/dist/lib/providers/index.d.ts +3 -0
  140. package/dist/lib/providers/index.js +3 -0
  141. package/dist/lib/providers/index.js.map +1 -1
  142. package/dist/lib/providers/jira-provider.d.ts +31 -0
  143. package/dist/lib/providers/jira-provider.js +383 -0
  144. package/dist/lib/providers/jira-provider.js.map +1 -0
  145. package/dist/lib/providers/linear-provider.js +6 -7
  146. package/dist/lib/providers/linear-provider.js.map +1 -1
  147. package/dist/lib/providers/resolver.js +54 -0
  148. package/dist/lib/providers/resolver.js.map +1 -1
  149. package/dist/lib/providers/state-intents.d.ts +20 -0
  150. package/dist/lib/providers/state-intents.js +61 -7
  151. package/dist/lib/providers/state-intents.js.map +1 -1
  152. package/dist/lib/providers/state-resolution.d.ts +15 -11
  153. package/dist/lib/providers/state-resolution.js +54 -48
  154. package/dist/lib/providers/state-resolution.js.map +1 -1
  155. package/dist/lib/providers/transition-map.d.ts +59 -0
  156. package/dist/lib/providers/transition-map.js +113 -0
  157. package/dist/lib/providers/transition-map.js.map +1 -0
  158. package/dist/lib/providers/types.d.ts +1 -1
  159. package/dist/lib/session/index.d.ts +3 -1
  160. package/dist/lib/session/index.js +3 -1
  161. package/dist/lib/session/index.js.map +1 -1
  162. package/dist/lib/session/tmux-watchdog.d.ts +157 -0
  163. package/dist/lib/session/tmux-watchdog.js +424 -0
  164. package/dist/lib/session/tmux-watchdog.js.map +1 -0
  165. package/dist/lib/session/watcher.d.ts +22 -4
  166. package/dist/lib/session/watcher.js +66 -8
  167. package/dist/lib/session/watcher.js.map +1 -1
  168. package/dist/lib/work-lifecycle/post-execution.js +26 -1
  169. package/dist/lib/work-lifecycle/post-execution.js.map +1 -1
  170. package/dist/lib/work-lifecycle/transition.d.ts +73 -0
  171. package/dist/lib/work-lifecycle/transition.js +138 -0
  172. package/dist/lib/work-lifecycle/transition.js.map +1 -0
  173. package/dist/lib/work-source/config.d.ts +1 -1
  174. package/dist/lib/work-source/config.js +11 -1
  175. package/dist/lib/work-source/config.js.map +1 -1
  176. package/oclif.manifest.json +936 -1628
  177. package/package.json +1 -1
  178. package/dist/commands/ticket/create.d.ts +0 -44
  179. package/dist/commands/ticket/create.js +0 -760
  180. package/dist/commands/ticket/create.js.map +0 -1
  181. package/dist/commands/ticket/delete.d.ts +0 -17
  182. package/dist/commands/ticket/delete.js +0 -204
  183. package/dist/commands/ticket/delete.js.map +0 -1
  184. package/dist/commands/ticket/edit.d.ts +0 -28
  185. package/dist/commands/ticket/edit.js +0 -402
  186. package/dist/commands/ticket/edit.js.map +0 -1
  187. package/dist/commands/ticket/index.js +0 -74
  188. package/dist/commands/ticket/index.js.map +0 -1
  189. package/dist/commands/ticket/list.d.ts +0 -33
  190. package/dist/commands/ticket/list.js +0 -519
  191. package/dist/commands/ticket/list.js.map +0 -1
  192. package/dist/commands/ticket/move.d.ts +0 -27
  193. package/dist/commands/ticket/move.js +0 -413
  194. package/dist/commands/ticket/move.js.map +0 -1
  195. package/dist/commands/ticket/show.d.ts +0 -14
  196. package/dist/commands/ticket/show.js +0 -110
  197. package/dist/commands/ticket/show.js.map +0 -1
  198. package/dist/commands/ticket/update.d.ts +0 -28
  199. package/dist/commands/ticket/update.js +0 -458
  200. package/dist/commands/ticket/update.js.map +0 -1
  201. package/dist/lib/mcp/tools/action.d.ts +0 -6
  202. package/dist/lib/mcp/tools/action.js +0 -123
  203. package/dist/lib/mcp/tools/action.js.map +0 -1
  204. package/dist/lib/mcp/tools/ticket.d.ts +0 -6
  205. package/dist/lib/mcp/tools/ticket.js +0 -464
  206. package/dist/lib/mcp/tools/ticket.js.map +0 -1
@@ -9,7 +9,8 @@ import { enrichAgentSession } from '../../lib/telemetry/telemetry-bridge.js';
9
9
  import { registerAgent } from '../../lib/registry/index.js';
10
10
  import { shouldOutputJson, outputErrorAsJson, createMetadata, outputConfirmationNeededAsJson, outputExecutionResultAsJson, } from '../../lib/prompt-json.js';
11
11
  import { FlagResolver } from '../../lib/flags/index.js';
12
- import { getWorkColumnSetting, findColumnByName, resolveReviewGate } from '../../lib/work-lifecycle/settings.js';
12
+ import { findColumnByName, resolveReviewGate } from '../../lib/work-lifecycle/settings.js';
13
+ import { moveTicketByIntent } from '../../lib/work-lifecycle/transition.js';
13
14
  import { getTicketExternalMetadata, resolveExternalTicketId } from '../../lib/external-issues/utils.js';
14
15
  import { styles } from '../../lib/styles.js';
15
16
  import { getWorkspaceInfo, createEphemeralAgent, getTicketTmuxSession, killTmuxSession, findWorktreeForBranch, resolveAgentDir, } from '../../lib/agents/commands.js';
@@ -17,7 +18,7 @@ import { openWorkspaceDatabase } from '../../lib/database/index.js';
17
18
  import { generateBranchName, DEFAULT_EXECUTION_CONFIG, } from '../../lib/execution/types.js';
18
19
  import { runExecution, isDockerRunning, isGitHubTokenAvailable, isDevcontainerCliInstalled, dockerCredentialsExist, getDockerCredentialInfo, isClaudeExecutor, getExecutorDisplayName } from '../../lib/execution/runners.js';
19
20
  import { ExecutionStorage, ContainerStorage } from '../../lib/execution/storage.js';
20
- import { loadExecutionConfig, getTerminalApp, promptTerminalPreference, getShell, promptShellPreference, hasTerminalPreference, hasShellPreference, getAuthMethod, saveAuthMethod, getCreatePrDefault, getMirrorToPmoDefault, getCleanupPolicy } from '../../lib/execution/config.js';
21
+ import { loadExecutionConfig, getTerminalApp, promptTerminalPreference, getShell, promptShellPreference, hasTerminalPreference, hasShellPreference, getAuthMethod, saveAuthMethod, getCreatePrDefault, getVerifyCiDefault, getMirrorToPmoDefault, getCleanupPolicy } from '../../lib/execution/config.js';
21
22
  import { hasDevcontainerConfig } from '../../lib/execution/devcontainer.js';
22
23
  import { detectRepoWorktrees, resolveWorktreePath, buildWorkspaceRepos } from '../../lib/execution/context.js';
23
24
  import { isGHInstalled, isGHAuthenticated } from '../../lib/pr/index.js';
@@ -27,12 +28,15 @@ import { buildAsanaMetadata, buildAsanaSpawnContextMessage, buildAsanaTicketDesc
27
28
  import { buildShortcutMetadata, buildShortcutSpawnContextMessage, buildShortcutTicketDescription, getShortcutStoryByKey, } from '../../lib/external-issues/shortcut.js';
28
29
  import { buildTrelloMetadata, buildTrelloSpawnContextMessage, buildTrelloTicketDescription, getTrelloCardById, } from '../../lib/external-issues/trello.js';
29
30
  import { resolveMirrorToPmo } from '../../lib/external-issues/work-start.js';
31
+ import { buildTicketFromEnvelope } from '../../lib/external-issues/ticket-builder.js';
32
+ import { TicketRefStore } from '../../lib/execution/ticket-refs.js';
30
33
  import { getLinearApiKey, loadLinearConfig } from '../../lib/linear/config.js';
31
34
  import { LinearMapper } from '../../lib/linear/mapper.js';
32
35
  import { ExternalIssueAdapterError } from '../../lib/external-issues/types.js';
33
36
  import { loadDefaultWorkSource, getConnectedIntegrations, isLocalTicketId, } from '../../lib/work-source/index.js';
34
37
  import { pruneWorktrees, checkoutBranchSafe } from '../../lib/branch/index.js';
35
38
  import { handlePostExecutionTransition } from '../../lib/work-lifecycle/index.js';
39
+ import { runPreflightChecks, formatPreflightReport } from '../../lib/execution/preflight.js';
36
40
  /**
37
41
  * Try to execute a git command, return true if successful
38
42
  */
@@ -161,6 +165,7 @@ export default class WorkStart extends PMOCommand {
161
165
  '<%= config.bin %> <%= command.id %> TKT-001 --review-gate post # Ship then human reviews after',
162
166
  '<%= config.bin %> <%= command.id %> PRLT-1085 PRLT-1086 PRLT-1087 --create-pr # Batch spawn in parallel',
163
167
  '<%= config.bin %> <%= command.id %> TKT-001 TKT-002 TKT-003 --max-parallel 2 # Limit concurrent spawns',
168
+ '<%= config.bin %> <%= command.id %> TKT-001 --dry-run # Validate environment without spawning',
164
169
  ];
165
170
  static args = {
166
171
  ticketId: Args.string({
@@ -246,6 +251,10 @@ export default class WorkStart extends PMOCommand {
246
251
  description: '[deprecated: use --create-pr instead] Skip PR creation when work is ready',
247
252
  default: false,
248
253
  }),
254
+ 'verify-ci': Flags.boolean({
255
+ description: 'Agent polls CI after pushing and fixes failures before exiting (PRLT-1126)',
256
+ default: false,
257
+ }),
249
258
  output: Flags.string({
250
259
  char: 'o',
251
260
  description: 'Output mode',
@@ -317,6 +326,10 @@ export default class WorkStart extends PMOCommand {
317
326
  description: 'Maximum number of concurrent spawns when starting multiple tickets (default: unlimited)',
318
327
  min: 1,
319
328
  }),
329
+ 'dry-run': Flags.boolean({
330
+ description: 'Validate environment and prerequisites without actually spawning an agent',
331
+ default: false,
332
+ }),
320
333
  };
321
334
  async findLinkedTicketByEnvelope(projectId, envelope) {
322
335
  const tickets = await this.storage.listTickets(projectId);
@@ -520,6 +533,8 @@ export default class WorkStart extends PMOCommand {
520
533
  let fromIssueMirror;
521
534
  let fromIssueMirrorSource;
522
535
  let sourceResolutionMeta;
536
+ // PRLT-1167: When built from envelope without PMO mirror, holds the in-memory ticket
537
+ let envelopeTicket;
523
538
  // Handle --from shorthand: parse provider:key into source + key
524
539
  let fromFlag = flags.from;
525
540
  let fromIssueActive = flags['from-issue'];
@@ -620,12 +635,33 @@ export default class WorkStart extends PMOCommand {
620
635
  linkedTicket = await this.createOrUpdateLinkedTicket(projectId, envelope, db);
621
636
  await autoExportToBoard(this.pmoPath, this.storage);
622
637
  }
638
+ else if (existingLinkedTicket) {
639
+ // Existing PMO ticket found — use it (backward compat)
640
+ linkedTicket = existingLinkedTicket;
641
+ }
623
642
  else {
624
- if (!existingLinkedTicket) {
625
- db.close();
626
- return handleError('EXTERNAL_ISSUE_NOT_MIRRORED', `No linked PMO ticket found for ${sourceAndKey.source} issue "${sourceAndKey.key}". Re-run with --mirror-to-pmo.`);
643
+ // PRLT-1167: Build Ticket from envelope directly — no PMO mirror
644
+ linkedTicket = buildTicketFromEnvelope(envelope, projectId);
645
+ envelopeTicket = linkedTicket;
646
+ // Store in ticket_refs so post-execution and other commands can find it
647
+ const ticketRefStore = new TicketRefStore(db);
648
+ ticketRefStore.upsert({
649
+ id: linkedTicket.id,
650
+ provider: envelope.source.name,
651
+ externalId: envelope.source.externalId,
652
+ externalKey: envelope.source.externalKey,
653
+ externalUrl: envelope.source.url,
654
+ title: envelope.title,
655
+ description: envelope.description,
656
+ status: envelope.status,
657
+ priority: envelope.priority,
658
+ category: envelope.category,
659
+ assignee: envelope.assignee,
660
+ projectId,
661
+ });
662
+ if (!jsonMode) {
663
+ this.log(styles.muted(`Ticket ref stored: ${linkedTicket.id} (no PMO mirror)`));
627
664
  }
628
- linkedTicket = existingLinkedTicket;
629
665
  }
630
666
  ticketId = linkedTicket.id;
631
667
  externalIssueContextMessage = buildExternalSpawnContextMessage(envelope, flags.message);
@@ -651,14 +687,65 @@ export default class WorkStart extends PMOCommand {
651
687
  }
652
688
  ticketId = selected;
653
689
  }
654
- // Get ticket
655
- const ticket = await this.storage.getTicket(ticketId);
690
+ // Get ticket — use envelope-built ticket when available (PRLT-1167: no PMO mirror)
691
+ const ticket = envelopeTicket ?? await this.storage.getTicket(ticketId);
656
692
  if (!ticket) {
657
693
  db.close();
658
694
  return handleError('TICKET_NOT_FOUND', `Ticket "${ticketId}" not found.`);
659
695
  }
660
696
  // Use resolved internal ID for all subsequent operations (external keys like PRLT-xxx resolve to TKT-xxx)
661
697
  ticketId = ticket.id;
698
+ const isExternalOnly = !!envelopeTicket;
699
+ // --dry-run: validate environment and report issues without spawning
700
+ if (flags['dry-run']) {
701
+ const dryRunEnvironment = flags['run-on-host'] ? 'host' : 'devcontainer';
702
+ const dryRunExecutor = flags.executor || DEFAULT_EXECUTION_CONFIG.defaultExecutor;
703
+ const report = runPreflightChecks({
704
+ environment: dryRunEnvironment,
705
+ executor: dryRunExecutor,
706
+ db,
707
+ ticket: { id: ticket.id, title: ticket.title },
708
+ agentDir: null, // Not yet created during dry-run
709
+ });
710
+ if (jsonMode) {
711
+ const metadata = createMetadata('work start', flags);
712
+ metadata.dryRun = true;
713
+ metadata.environment = dryRunEnvironment;
714
+ metadata.executor = dryRunExecutor;
715
+ const jsonResult = {
716
+ passed: report.passed,
717
+ checks: report.checks.map(c => ({
718
+ name: c.name,
719
+ label: c.label,
720
+ passed: c.passed,
721
+ severity: c.severity,
722
+ message: c.message,
723
+ fix: c.fix ?? null,
724
+ })),
725
+ errors: report.errors.length,
726
+ warnings: report.warnings.length,
727
+ };
728
+ outputExecutionResultAsJson([{ workId: '', ticketId: ticket.id, agent: '', status: report.passed ? 'ready' : 'blocked' }], report.passed ? 1 : 0, report.passed ? 0 : 1, { ...metadata, preflight: jsonResult });
729
+ }
730
+ else {
731
+ this.log('');
732
+ this.log(styles.header(`Dry-run: preflight validation for ${ticket.id}`));
733
+ this.log(styles.muted(` Environment: ${dryRunEnvironment}`));
734
+ this.log(styles.muted(` Executor: ${dryRunExecutor}`));
735
+ this.log('');
736
+ this.log(formatPreflightReport(report));
737
+ this.log('');
738
+ if (report.passed) {
739
+ this.log(styles.success('All preflight checks passed — ready to spawn.'));
740
+ }
741
+ else {
742
+ this.log(styles.error(`${report.errors.length} error(s) and ${report.warnings.length} warning(s) found.`));
743
+ this.log(styles.muted('Fix the errors above, then run without --dry-run to start work.'));
744
+ }
745
+ }
746
+ db.close();
747
+ return;
748
+ }
662
749
  // In JSON mode with explicit flags, implement two-step confirm-then-execute protocol
663
750
  if (jsonMode) {
664
751
  // Check if all required flags for non-interactive execution are provided
@@ -745,8 +832,8 @@ export default class WorkStart extends PMOCommand {
745
832
  // If --yes is set with all flags, continue to execution (don't return)
746
833
  // If missing flags, continue and let FlagResolver handle prompts
747
834
  }
748
- // Check if ticket is blocked by dependencies
749
- const isBlocked = await this.storage.isTicketBlocked(ticketId);
835
+ // Check if ticket is blocked by dependencies (skip for external-only tickets)
836
+ const isBlocked = isExternalOnly ? false : await this.storage.isTicketBlocked(ticketId);
750
837
  if (isBlocked && !flags.force) {
751
838
  const blockers = await this.storage.getTicketBlockers(ticketId);
752
839
  const incompleteBlockers = blockers.filter(b => b.status !== 'done' && b.status !== 'canceled');
@@ -1783,6 +1870,19 @@ export default class WorkStart extends PMOCommand {
1783
1870
  }
1784
1871
  // Add createPR to context
1785
1872
  context.createPR = createPR;
1873
+ // Resolve verify-ci: flag > workspace config > default false
1874
+ // Only meaningful when createPR is true (need a PR to poll CI on)
1875
+ let verifyCi = false;
1876
+ if (flags['verify-ci']) {
1877
+ verifyCi = true;
1878
+ }
1879
+ else {
1880
+ const configVerifyCi = getVerifyCiDefault(db);
1881
+ if (configVerifyCi !== null) {
1882
+ verifyCi = configVerifyCi;
1883
+ }
1884
+ }
1885
+ context.verifyCi = verifyCi;
1786
1886
  // Resolve review gate mode (most specific wins: spawn flag > action config > workspace default)
1787
1887
  const reviewGate = resolveReviewGate(flags['review-gate'], selectedAction?.reviewGate, db);
1788
1888
  context.reviewGate = reviewGate;
@@ -1802,6 +1902,10 @@ export default class WorkStart extends PMOCommand {
1802
1902
  };
1803
1903
  this.log(styles.warning(` Review gate: ${reviewGate} (${gateDescriptions[reviewGate]})`));
1804
1904
  }
1905
+ // Display verify-ci status
1906
+ if (!jsonMode && verifyCi && context.createPR) {
1907
+ this.log(styles.success(` CI verify: enabled — agent will poll CI and fix failures before exiting`));
1908
+ }
1805
1909
  // Handle git operations
1806
1910
  let finalBranch = branch;
1807
1911
  // Set up repo paths (needed for all action types)
@@ -1946,8 +2050,8 @@ export default class WorkStart extends PMOCommand {
1946
2050
  }
1947
2051
  }
1948
2052
  }
1949
- // Save branch to ticket
1950
- if (!isExistingBranch || finalBranch !== branch) {
2053
+ // Save branch to ticket (skip for external-only tickets — no PMO record)
2054
+ if (!isExternalOnly && (!isExistingBranch || finalBranch !== branch)) {
1951
2055
  await this.storage.updateTicket(ticket.id, { branch: finalBranch });
1952
2056
  }
1953
2057
  // Update context with final branch
@@ -2095,64 +2199,74 @@ export default class WorkStart extends PMOCommand {
2095
2199
  });
2096
2200
  }
2097
2201
  // Update ticket assignee ONLY after successful spawn
2202
+ // For external-only tickets, update ticket_ref instead of PMO storage
2098
2203
  if (!ticket.assignee || ticket.assignee !== assignedAgent) {
2099
- await this.storage.updateTicket(ticket.id, { assignee: assignedAgent });
2100
- this.log(styles.muted(` Assigned to: ${assignedAgent}`));
2101
- }
2102
- // Move ticket to target column based on action's toState
2103
- // If action has a to_state, use that state name directly; otherwise fall back to "In Progress" default
2104
- const targetStateName = selectedAction?.toState;
2105
- const board = ticket.projectId ? await this.storage.getProjectBoard(ticket.projectId) : null;
2106
- const columnNames = board ? board.columns.map(col => col.name) : [];
2107
- let targetColumnName = null;
2108
- if (targetStateName) {
2109
- // Try direct state name match first
2110
- targetColumnName = findColumnByName(columnNames, targetStateName);
2111
- if (!targetColumnName) {
2112
- const categoryMap = {
2113
- 'Backlog': 'planned',
2114
- 'Todo': 'planned',
2115
- 'In Progress': 'in_progress',
2116
- 'Done': 'done',
2117
- };
2118
- const columnType = categoryMap[targetStateName] || 'in_progress';
2119
- const workColumnName = getWorkColumnSetting(db, columnType);
2120
- targetColumnName = findColumnByName(columnNames, workColumnName);
2204
+ if (isExternalOnly) {
2205
+ // Update ticket_ref with assignee
2206
+ const ticketRefStore = new TicketRefStore(db);
2207
+ ticketRefStore.upsert({ id: ticket.id, title: ticket.title, assignee: assignedAgent });
2121
2208
  }
2209
+ else {
2210
+ await this.storage.updateTicket(ticket.id, { assignee: assignedAgent });
2211
+ }
2212
+ this.log(styles.muted(` Assigned to: ${assignedAgent}`));
2122
2213
  }
2123
- else {
2124
- // No target state specified default to "In Progress"
2125
- const workColumnName = getWorkColumnSetting(db, 'in_progress');
2126
- targetColumnName = findColumnByName(columnNames, workColumnName);
2127
- }
2128
- if (targetColumnName && ticket.statusName !== targetColumnName) {
2129
- try {
2130
- await this.storage.moveTicket(ticket.projectId, ticket.id, targetColumnName);
2131
- this.log(styles.muted(` Moved to: ${targetColumnName}`));
2214
+ // Move ticket to target column based on action's toState or default 'started' intent
2215
+ // Skip PMO board operations for external-only tickets (no PMO record to move)
2216
+ if (!isExternalOnly) {
2217
+ // If action has a to_state, try direct match first; otherwise use intent resolution
2218
+ const targetStateName = selectedAction?.toState;
2219
+ const board = ticket.projectId ? await this.storage.getProjectBoard(ticket.projectId) : null;
2220
+ const columnNames = board ? board.columns.map(col => col.name) : [];
2221
+ let targetColumnName = null;
2222
+ if (targetStateName) {
2223
+ // Try direct state name match first (backward compat with action toState)
2224
+ targetColumnName = findColumnByName(columnNames, targetStateName);
2132
2225
  }
2133
- catch (moveError) {
2134
- // Non-fatal - work can proceed even if column move fails
2135
- this.warn(`Could not move ticket to "${targetColumnName}": ${moveError instanceof Error ? moveError.message : moveError}`);
2226
+ if (!targetColumnName) {
2227
+ // Use intent-based resolution 'started' intent
2228
+ const transition = await moveTicketByIntent({
2229
+ db,
2230
+ storage: this.storage,
2231
+ ticket,
2232
+ intent: 'started',
2233
+ providerName: 'pmo',
2234
+ resolveProvider: (tid, pid) => this.resolveTicketProvider(tid, pid),
2235
+ log: (msg) => this.log(styles.muted(` ${msg}`)),
2236
+ });
2237
+ if (transition.moved) {
2238
+ this.log(styles.muted(` Moved to: ${transition.targetColumn}`));
2239
+ }
2136
2240
  }
2137
- // Sync to external provider (e.g., Linear) if ticket was imported from one
2138
- try {
2139
- const provider = await this.resolveTicketProvider(ticket.id, ticket.projectId);
2140
- if (provider.name !== 'pmo') {
2141
- const result = await provider.moveTicket(ticket.id, targetColumnName);
2142
- if (result.success) {
2143
- this.log(styles.muted(` Synced to ${result.provider}: ${targetColumnName}`));
2241
+ else if (targetColumnName && ticket.statusName !== targetColumnName) {
2242
+ try {
2243
+ await this.storage.moveTicket(ticket.projectId, ticket.id, targetColumnName);
2244
+ this.log(styles.muted(` Moved to: ${targetColumnName}`));
2245
+ }
2246
+ catch (moveError) {
2247
+ // Non-fatal - work can proceed even if column move fails
2248
+ this.warn(`Could not move ticket to "${targetColumnName}": ${moveError instanceof Error ? moveError.message : moveError}`);
2249
+ }
2250
+ // Sync to external provider (e.g., Linear) if ticket was imported from one
2251
+ try {
2252
+ const provider = await this.resolveTicketProvider(ticket.id, ticket.projectId);
2253
+ if (provider.name !== 'pmo') {
2254
+ const result = await provider.moveTicket(ticket.id, targetColumnName);
2255
+ if (result.success) {
2256
+ this.log(styles.muted(` Synced to ${result.provider}: ${targetColumnName}`));
2257
+ }
2144
2258
  }
2145
2259
  }
2260
+ catch {
2261
+ // Non-fatal — don't block work start for provider sync failures
2262
+ }
2146
2263
  }
2147
- catch {
2148
- // Non-fatal — don't block work start for provider sync failures
2149
- }
2150
- }
2151
- await autoExportToBoard(this.pmoPath, this.storage, (msg) => {
2152
- if (!jsonMode) {
2153
- this.log(styles.muted(msg));
2154
- }
2155
- });
2264
+ await autoExportToBoard(this.pmoPath, this.storage, (msg) => {
2265
+ if (!jsonMode) {
2266
+ this.log(styles.muted(msg));
2267
+ }
2268
+ });
2269
+ } // end if (!isExternalOnly)
2156
2270
  // Output results
2157
2271
  if (jsonMode) {
2158
2272
  // Output JSON execution result with resolved PR mode and source
@@ -2196,20 +2310,53 @@ export default class WorkStart extends PMOCommand {
2196
2310
  success: false,
2197
2311
  errorType: 'spawn_failure',
2198
2312
  });
2313
+ // Run post-failure diagnostics to give user actionable info
2314
+ const failureDiag = runPreflightChecks({
2315
+ environment,
2316
+ executor,
2317
+ db,
2318
+ ticket: { id: ticket.id, title: ticket.title },
2319
+ agentDir: context.agentDir,
2320
+ });
2321
+ const diagErrors = failureDiag.errors;
2199
2322
  if (jsonMode) {
2200
- // Output JSON failure result with resolved PR mode
2323
+ // Output JSON failure result with resolved PR mode and diagnostics
2201
2324
  const failMetadata = createMetadata('work start', flags);
2202
2325
  failMetadata.resolvedPRMode = createPR ? 'create-pr' : 'no-pr';
2203
2326
  failMetadata.prModeSource = prModeSource;
2327
+ failMetadata.spawnError = result.error || 'Unknown error';
2328
+ if (diagErrors.length > 0) {
2329
+ failMetadata.diagnostics = diagErrors.map(e => ({
2330
+ check: e.name,
2331
+ message: e.message,
2332
+ fix: e.fix ?? null,
2333
+ }));
2334
+ }
2204
2335
  outputExecutionResultAsJson([{
2205
2336
  workId: execution.id,
2206
2337
  ticketId: ticket.id,
2207
2338
  agent: assignedAgent,
2208
2339
  status: 'failed',
2340
+ error: result.error || 'Unknown error',
2209
2341
  }], 0, 1, failMetadata);
2210
2342
  }
2211
2343
  else {
2212
- return handleError('START_FAILED', `Failed to start work: ${result.error}`);
2344
+ // Build a detailed error message with the spawn error and any diagnostic findings
2345
+ const spawnError = result.error || 'Unknown error';
2346
+ const errorLines = [`Failed to start work on ${ticket.id}: ${spawnError}`];
2347
+ if (diagErrors.length > 0) {
2348
+ errorLines.push('');
2349
+ errorLines.push('Diagnostics found these issues:');
2350
+ for (const diag of diagErrors) {
2351
+ errorLines.push(` ✗ ${diag.label}: ${diag.message}`);
2352
+ if (diag.fix) {
2353
+ errorLines.push(` → Fix: ${diag.fix}`);
2354
+ }
2355
+ }
2356
+ }
2357
+ errorLines.push('');
2358
+ errorLines.push('Tip: Run with --dry-run to validate your environment before spawning.');
2359
+ return handleError('START_FAILED', errorLines.join('\n'));
2213
2360
  }
2214
2361
  }
2215
2362
  db.close();
@@ -2289,6 +2436,8 @@ export default class WorkStart extends PMOCommand {
2289
2436
  startArgs.push('--create-pr');
2290
2437
  if (flags['no-pr'])
2291
2438
  startArgs.push('--no-pr');
2439
+ if (flags['verify-ci'])
2440
+ startArgs.push('--verify-ci');
2292
2441
  if (flags.session)
2293
2442
  startArgs.push('--session', flags.session);
2294
2443
  if (flags.force)
@@ -2343,6 +2492,7 @@ export default class WorkStart extends PMOCommand {
2343
2492
  ticketId: ticket.id,
2344
2493
  agent: '',
2345
2494
  status: 'failed',
2495
+ error: error instanceof Error ? error.message : String(error),
2346
2496
  });
2347
2497
  }
2348
2498
  };
@@ -2827,30 +2977,35 @@ export default class WorkStart extends PMOCommand {
2827
2977
  if (!ticket.assignee || ticket.assignee !== agentName) {
2828
2978
  await this.storage.updateTicket(ticket.id, { assignee: agentName });
2829
2979
  }
2830
- // Move ticket to In Progress column ONLY after successful spawn
2831
- const targetColumnName = getWorkColumnSetting(db, 'in_progress');
2832
- const board = ticket.projectId ? await this.storage.getProjectBoard(ticket.projectId) : null;
2833
- const columnNames = board ? board.columns.map(col => col.name) : [];
2834
- const inProgressColumn = findColumnByName(columnNames, targetColumnName);
2835
- if (inProgressColumn && ticket.status !== inProgressColumn) {
2836
- await this.storage.moveTicket(ticket.projectId, ticket.id, inProgressColumn);
2837
- // Sync to external provider (e.g., Linear) if ticket was imported from one
2838
- try {
2839
- const provider = await this.resolveTicketProvider(ticket.id, ticket.projectId);
2840
- if (provider.name !== 'pmo') {
2841
- await provider.moveTicket(ticket.id, inProgressColumn);
2842
- }
2843
- }
2844
- catch {
2845
- // Non-fatal — don't block work start for provider sync failures
2846
- }
2847
- }
2980
+ // Move ticket to In Progress column ONLY after successful spawn — via intent resolution
2981
+ const transition = await moveTicketByIntent({
2982
+ db,
2983
+ storage: this.storage,
2984
+ ticket,
2985
+ intent: 'started',
2986
+ providerName: 'pmo',
2987
+ resolveProvider: (tid, pid) => this.resolveTicketProvider(tid, pid),
2988
+ });
2848
2989
  await autoExportToBoard(this.pmoPath, this.storage, () => { });
2849
2990
  this.log(styles.success(` ✓ ${ticket.id} started (${execution.id})`));
2850
2991
  }
2851
2992
  else {
2852
2993
  executionStorage.updateStatus(execution.id, 'failed');
2853
- throw new Error(result.error || 'Unknown error');
2994
+ // Include diagnostic details in the error message
2995
+ const spawnError = result.error || 'Unknown error';
2996
+ const diag = runPreflightChecks({
2997
+ environment,
2998
+ executor,
2999
+ db,
3000
+ ticket: { id: ticket.id, title: ticket.title },
3001
+ agentDir: context.agentDir,
3002
+ });
3003
+ const diagIssues = diag.errors;
3004
+ if (diagIssues.length > 0) {
3005
+ const details = diagIssues.map(e => `${e.label}: ${e.message}`).join('; ');
3006
+ throw new Error(`${spawnError} [${details}]`);
3007
+ }
3008
+ throw new Error(spawnError);
2854
3009
  }
2855
3010
  }
2856
3011
  }