@proletariat/cli 0.3.45 → 0.3.46

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 (62) hide show
  1. package/dist/commands/config/index.js +39 -1
  2. package/dist/commands/linear/auth.d.ts +14 -0
  3. package/dist/commands/linear/auth.js +211 -0
  4. package/dist/commands/linear/import.d.ts +21 -0
  5. package/dist/commands/linear/import.js +260 -0
  6. package/dist/commands/linear/status.d.ts +11 -0
  7. package/dist/commands/linear/status.js +88 -0
  8. package/dist/commands/linear/sync.d.ts +15 -0
  9. package/dist/commands/linear/sync.js +233 -0
  10. package/dist/commands/orchestrator/attach.d.ts +9 -1
  11. package/dist/commands/orchestrator/attach.js +67 -13
  12. package/dist/commands/orchestrator/index.js +22 -7
  13. package/dist/commands/ticket/link/duplicates.d.ts +15 -0
  14. package/dist/commands/ticket/link/duplicates.js +95 -0
  15. package/dist/commands/ticket/link/index.js +14 -0
  16. package/dist/commands/ticket/link/relates.d.ts +15 -0
  17. package/dist/commands/ticket/link/relates.js +95 -0
  18. package/dist/commands/work/revise.js +4 -3
  19. package/dist/commands/work/spawn.d.ts +5 -0
  20. package/dist/commands/work/spawn.js +195 -14
  21. package/dist/commands/work/start.js +75 -19
  22. package/dist/lib/execution/config.d.ts +15 -0
  23. package/dist/lib/execution/config.js +54 -0
  24. package/dist/lib/execution/devcontainer.d.ts +6 -3
  25. package/dist/lib/execution/devcontainer.js +39 -12
  26. package/dist/lib/execution/runners.d.ts +28 -32
  27. package/dist/lib/execution/runners.js +345 -275
  28. package/dist/lib/execution/spawner.js +62 -5
  29. package/dist/lib/execution/types.d.ts +4 -0
  30. package/dist/lib/execution/types.js +3 -0
  31. package/dist/lib/external-issues/adapters.d.ts +26 -0
  32. package/dist/lib/external-issues/adapters.js +251 -0
  33. package/dist/lib/external-issues/index.d.ts +10 -0
  34. package/dist/lib/external-issues/index.js +14 -0
  35. package/dist/lib/external-issues/mapper.d.ts +21 -0
  36. package/dist/lib/external-issues/mapper.js +86 -0
  37. package/dist/lib/external-issues/types.d.ts +144 -0
  38. package/dist/lib/external-issues/types.js +26 -0
  39. package/dist/lib/external-issues/validation.d.ts +34 -0
  40. package/dist/lib/external-issues/validation.js +219 -0
  41. package/dist/lib/linear/client.d.ts +55 -0
  42. package/dist/lib/linear/client.js +254 -0
  43. package/dist/lib/linear/config.d.ts +37 -0
  44. package/dist/lib/linear/config.js +100 -0
  45. package/dist/lib/linear/index.d.ts +11 -0
  46. package/dist/lib/linear/index.js +10 -0
  47. package/dist/lib/linear/mapper.d.ts +67 -0
  48. package/dist/lib/linear/mapper.js +219 -0
  49. package/dist/lib/linear/sync.d.ts +37 -0
  50. package/dist/lib/linear/sync.js +89 -0
  51. package/dist/lib/linear/types.d.ts +139 -0
  52. package/dist/lib/linear/types.js +34 -0
  53. package/dist/lib/mcp/helpers.d.ts +8 -0
  54. package/dist/lib/mcp/helpers.js +10 -0
  55. package/dist/lib/mcp/tools/board.js +63 -11
  56. package/dist/lib/pmo/schema.d.ts +2 -0
  57. package/dist/lib/pmo/schema.js +20 -0
  58. package/dist/lib/pmo/storage/base.js +92 -13
  59. package/dist/lib/pmo/storage/dependencies.js +15 -0
  60. package/dist/lib/prompt-json.d.ts +4 -0
  61. package/oclif.manifest.json +2867 -2380
  62. package/package.json +2 -1
@@ -14,7 +14,7 @@ import { findHQRoot } from '../repos/index.js';
14
14
  import { hasGitHubRemote } from '../repos/git.js';
15
15
  import { hasDevcontainerConfig } from './devcontainer.js';
16
16
  import { loadExecutionConfig, getOrPromptCoderName } from './config.js';
17
- import { runExecution, isDockerRunning, isGitHubTokenAvailable, isDevcontainerCliInstalled, runExecutorPreflight } from './runners.js';
17
+ import { runExecution, isDockerRunning, isGitHubTokenAvailable, isDevcontainerCliInstalled, runExecutorPreflight, getAgentContainerName, isContainerRunning, getContainerId, buildSessionName } from './runners.js';
18
18
  import { detectRepoWorktrees, resolveWorktreePath } from './context.js';
19
19
  import { generateBranchName, DEFAULT_EXECUTION_CONFIG, } from './types.js';
20
20
  // =============================================================================
@@ -286,7 +286,7 @@ export async function spawnAgentForTicket(ticket, agentName, storage, executionS
286
286
  // Executor preflight check (TKT-1082): verify binary is available before proceeding
287
287
  // For host environment, check immediately. For devcontainer, check happens after container start.
288
288
  if (environment === 'host') {
289
- const preflight = runExecutorPreflight(executor, environment);
289
+ const preflight = runExecutorPreflight(environment, executor);
290
290
  if (!preflight.ok) {
291
291
  return {
292
292
  success: false,
@@ -402,6 +402,54 @@ export async function spawnAgentForTicket(ticket, agentName, storage, executionS
402
402
  }
403
403
  }
404
404
  }
405
+ // TKT-1028: Clean up orphaned execution records before creating a new one.
406
+ // If the agent's container doesn't exist or isn't running, any "running"/"starting"
407
+ // execution records for this agent are orphans — their container was destroyed
408
+ // or crashed. Mark them as "stopped" to prevent downstream issues like
409
+ // `prlt docker logs` failing with "multiple running containers".
410
+ // Also clean up stale records when the container IS running but the execution's
411
+ // containerId doesn't match the current container (e.g., container was recreated).
412
+ if (environment === 'devcontainer') {
413
+ const containerName = getAgentContainerName(agentName);
414
+ const containerRunning = isContainerRunning(containerName);
415
+ const staleExecutions = executionStorage.getAgentRunningExecutions(agentName);
416
+ if (!containerRunning) {
417
+ // Container not running — all "running" executions are orphans
418
+ for (const staleExec of staleExecutions) {
419
+ log(`Marking orphaned execution ${staleExec.id} as stopped (container not running)`);
420
+ executionStorage.updateStatus(staleExec.id, 'stopped');
421
+ }
422
+ }
423
+ else if (staleExecutions.length > 0) {
424
+ // Container IS running — check for specific orphan scenarios:
425
+ // 1. containerId mismatch (container was recreated since execution started)
426
+ // 2. Same session name as incoming spawn (tmux session will be replaced)
427
+ // 3. Dead tmux sessions (crashed or killed externally)
428
+ const currentContainerId = getContainerId(containerName);
429
+ const incomingSessionName = buildSessionName(context);
430
+ for (const staleExec of staleExecutions) {
431
+ if (staleExec.containerId && currentContainerId && staleExec.containerId !== currentContainerId) {
432
+ log(`Marking orphaned execution ${staleExec.id} as stopped (containerId mismatch)`);
433
+ executionStorage.updateStatus(staleExec.id, 'stopped');
434
+ }
435
+ else if (staleExec.sessionId === incomingSessionName) {
436
+ // Same session name — will be killed when the new tmux session is created
437
+ log(`Marking execution ${staleExec.id} as stopped (session will be replaced)`);
438
+ executionStorage.updateStatus(staleExec.id, 'stopped');
439
+ }
440
+ else if (staleExec.sessionId && currentContainerId) {
441
+ // Different session — verify it still exists in the container
442
+ try {
443
+ execSync(`docker exec ${currentContainerId} tmux has-session -t "${staleExec.sessionId}"`, { stdio: 'pipe' });
444
+ }
445
+ catch {
446
+ log(`Marking orphaned execution ${staleExec.id} as stopped (tmux session gone)`);
447
+ executionStorage.updateStatus(staleExec.id, 'stopped');
448
+ }
449
+ }
450
+ }
451
+ }
452
+ }
405
453
  // Create execution record
406
454
  const execution = executionStorage.createExecution({
407
455
  ticketId: ticket.id,
@@ -415,10 +463,19 @@ export async function spawnAgentForTicket(ticket, agentName, storage, executionS
415
463
  // Load execution config (use passed config or load from db)
416
464
  const executionConfig = options.executionConfig || loadExecutionConfig(db);
417
465
  executionConfig.sandboxed = sandboxed;
418
- // Use print mode for background, interactive for terminal/tmux
419
- executionConfig.outputMode = displayMode === 'background' ? 'print' : 'interactive';
420
466
  // Run execution
421
- const sessionManager = options.sessionManager || 'direct';
467
+ // Default to tmux for session persistence (enables peek/poke/attach)
468
+ const sessionManager = options.sessionManager || 'tmux';
469
+ // Determine output mode:
470
+ // - Devcontainer with tmux: always interactive (no -p flag) so Claude runs with TUI
471
+ // inside tmux, enabling session peek/poke/attach for Docker agents
472
+ // - Otherwise: print mode for background (logs only), interactive for terminal/tmux
473
+ if (environment === 'devcontainer' && sessionManager === 'tmux') {
474
+ executionConfig.outputMode = 'interactive';
475
+ }
476
+ else {
477
+ executionConfig.outputMode = displayMode === 'background' ? 'print' : 'interactive';
478
+ }
422
479
  const result = await runExecution(environment, context, executor, executionConfig, {
423
480
  displayMode,
424
481
  sessionManager: environment === 'devcontainer' ? sessionManager : undefined,
@@ -122,6 +122,7 @@ export interface ExecutionConfig {
122
122
  outputMode: OutputMode;
123
123
  sandboxed: boolean;
124
124
  authMethod?: AuthMethod;
125
+ createPrDefault?: boolean;
125
126
  tmux: {
126
127
  session: string;
127
128
  layout: 'split' | 'window';
@@ -144,6 +145,9 @@ export interface ExecutionConfig {
144
145
  memory?: string;
145
146
  cpus?: number;
146
147
  };
148
+ firewall: {
149
+ allowlistDomains: string[];
150
+ };
147
151
  vm: {
148
152
  defaultHost?: string;
149
153
  user: string;
@@ -152,6 +152,9 @@ export const DEFAULT_EXECUTION_CONFIG = {
152
152
  image: 'claude-code:latest',
153
153
  network: 'host',
154
154
  },
155
+ firewall: {
156
+ allowlistDomains: [],
157
+ },
155
158
  vm: {
156
159
  user: 'agent',
157
160
  syncMethod: 'git',
@@ -0,0 +1,26 @@
1
+ import { type ExternalIssueAdapter, type IssueEnvelope } from './types.js';
2
+ type FetchIssueByKey = (key: string) => Promise<unknown>;
3
+ type FetchIssuesByQuery = (query: Record<string, unknown>) => Promise<unknown[]>;
4
+ interface AdapterFetchers {
5
+ fetchByKey?: FetchIssueByKey;
6
+ fetchByQuery?: FetchIssuesByQuery;
7
+ }
8
+ export declare class LinearIssueAdapter implements ExternalIssueAdapter {
9
+ readonly source: "linear";
10
+ private readonly fetchByKeyImpl?;
11
+ private readonly fetchByQueryImpl?;
12
+ constructor(fetchers?: AdapterFetchers);
13
+ normalize(raw: unknown): IssueEnvelope;
14
+ fetchByKey(key: string): Promise<IssueEnvelope>;
15
+ fetchByQuery(query: Record<string, unknown>): Promise<IssueEnvelope[]>;
16
+ }
17
+ export declare class JiraIssueAdapter implements ExternalIssueAdapter {
18
+ readonly source: "jira";
19
+ private readonly fetchByKeyImpl?;
20
+ private readonly fetchByQueryImpl?;
21
+ constructor(fetchers?: AdapterFetchers);
22
+ normalize(raw: unknown): IssueEnvelope;
23
+ fetchByKey(key: string): Promise<IssueEnvelope>;
24
+ fetchByQuery(query: Record<string, unknown>): Promise<IssueEnvelope[]>;
25
+ }
26
+ export {};
@@ -0,0 +1,251 @@
1
+ import { validateOrThrow } from './validation.js';
2
+ import { ExternalIssueError } from './types.js';
3
+ function asRecord(value) {
4
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
5
+ return {};
6
+ }
7
+ return value;
8
+ }
9
+ function asString(value) {
10
+ if (typeof value !== 'string') {
11
+ return undefined;
12
+ }
13
+ const trimmed = value.trim();
14
+ return trimmed.length > 0 ? trimmed : undefined;
15
+ }
16
+ function asNullableString(value) {
17
+ if (value === null || value === undefined) {
18
+ return null;
19
+ }
20
+ return asString(value) ?? null;
21
+ }
22
+ function deriveProjectKeyFromExternalKey(externalKey) {
23
+ if (!externalKey) {
24
+ return undefined;
25
+ }
26
+ const [prefix] = externalKey.split('-');
27
+ return prefix && prefix.trim().length > 0 ? prefix : undefined;
28
+ }
29
+ function ensureNormalized(source, candidate) {
30
+ try {
31
+ return validateOrThrow(candidate);
32
+ }
33
+ catch (error) {
34
+ if (error instanceof ExternalIssueError && error.code === 'VALIDATION_FAILED') {
35
+ throw new ExternalIssueError('NORMALIZE_FAILED', `Failed to normalize ${source} issue: ${error.message}`, source, error.validationErrors);
36
+ }
37
+ throw error;
38
+ }
39
+ }
40
+ function getFetchByKeyOrThrow(source, fetchByKey) {
41
+ if (fetchByKey) {
42
+ return fetchByKey;
43
+ }
44
+ throw new ExternalIssueError('FETCH_FAILED', `No ${source} fetchByKey implementation configured`, source);
45
+ }
46
+ function getFetchByQueryOrThrow(source, fetchByQuery) {
47
+ if (fetchByQuery) {
48
+ return fetchByQuery;
49
+ }
50
+ throw new ExternalIssueError('FETCH_FAILED', `No ${source} fetchByQuery implementation configured`, source);
51
+ }
52
+ function normalizeLinearPriority(rawPriority) {
53
+ if (rawPriority === null || rawPriority === undefined) {
54
+ return null;
55
+ }
56
+ if (typeof rawPriority === 'number') {
57
+ switch (rawPriority) {
58
+ case 1:
59
+ return 'P0';
60
+ case 2:
61
+ return 'P1';
62
+ case 3:
63
+ return 'P2';
64
+ case 4:
65
+ return 'P3';
66
+ default:
67
+ return null;
68
+ }
69
+ }
70
+ if (typeof rawPriority === 'string') {
71
+ const normalized = rawPriority.trim().toUpperCase();
72
+ return normalized.length > 0 ? normalized : null;
73
+ }
74
+ return null;
75
+ }
76
+ function normalizeJiraPriority(rawPriority) {
77
+ const name = typeof rawPriority === 'string'
78
+ ? rawPriority
79
+ : asString(asRecord(rawPriority).name);
80
+ if (!name) {
81
+ return null;
82
+ }
83
+ const normalized = name.trim().toLowerCase();
84
+ if (normalized === 'highest' || normalized === 'blocker') {
85
+ return 'P0';
86
+ }
87
+ if (normalized === 'high' || normalized === 'critical') {
88
+ return 'P1';
89
+ }
90
+ if (normalized === 'medium') {
91
+ return 'P2';
92
+ }
93
+ if (normalized === 'low' || normalized === 'lowest') {
94
+ return 'P3';
95
+ }
96
+ return name.trim();
97
+ }
98
+ function normalizeLinearLabels(rawLabels) {
99
+ if (!Array.isArray(rawLabels)) {
100
+ return [];
101
+ }
102
+ return rawLabels
103
+ .map((label) => {
104
+ if (typeof label === 'string') {
105
+ return asString(label);
106
+ }
107
+ if (typeof label === 'object' && label !== null) {
108
+ return asString(label.name);
109
+ }
110
+ return undefined;
111
+ })
112
+ .filter((label) => typeof label === 'string');
113
+ }
114
+ function normalizeJiraLabels(rawLabels) {
115
+ if (!Array.isArray(rawLabels)) {
116
+ return [];
117
+ }
118
+ return rawLabels.filter((label) => typeof label === 'string' && label.trim().length > 0);
119
+ }
120
+ function extractAdfText(node) {
121
+ if (typeof node === 'string') {
122
+ return node;
123
+ }
124
+ if (!node || typeof node !== 'object') {
125
+ return '';
126
+ }
127
+ const record = node;
128
+ const parts = [];
129
+ if (typeof record.text === 'string') {
130
+ parts.push(record.text);
131
+ }
132
+ if (Array.isArray(record.content)) {
133
+ for (const child of record.content) {
134
+ const text = extractAdfText(child);
135
+ if (text) {
136
+ parts.push(text);
137
+ }
138
+ }
139
+ }
140
+ return parts.join(' ').trim();
141
+ }
142
+ function normalizeJiraDescription(rawDescription) {
143
+ if (typeof rawDescription === 'string') {
144
+ return rawDescription;
145
+ }
146
+ if (rawDescription === null || rawDescription === undefined) {
147
+ return '';
148
+ }
149
+ return extractAdfText(rawDescription);
150
+ }
151
+ function deriveJiraUrl(raw, key) {
152
+ const directUrl = asString(raw.url);
153
+ if (directUrl) {
154
+ return directUrl;
155
+ }
156
+ const selfUrl = asString(raw.self);
157
+ if (!selfUrl || !key) {
158
+ return undefined;
159
+ }
160
+ try {
161
+ const parsed = new URL(selfUrl);
162
+ return `${parsed.origin}/browse/${key}`;
163
+ }
164
+ catch {
165
+ return undefined;
166
+ }
167
+ }
168
+ export class LinearIssueAdapter {
169
+ source = 'linear';
170
+ fetchByKeyImpl;
171
+ fetchByQueryImpl;
172
+ constructor(fetchers = {}) {
173
+ this.fetchByKeyImpl = fetchers.fetchByKey;
174
+ this.fetchByQueryImpl = fetchers.fetchByQuery;
175
+ }
176
+ normalize(raw) {
177
+ const data = asRecord(raw);
178
+ const externalKey = asString(data.identifier);
179
+ const envelope = {
180
+ source: this.source,
181
+ external_id: asString(data.id),
182
+ external_key: externalKey,
183
+ title: asString(data.title),
184
+ description: typeof data.description === 'string' ? data.description : '',
185
+ labels: normalizeLinearLabels(data.labels),
186
+ priority: normalizeLinearPriority(data.priority),
187
+ status: asString(asRecord(data.state).name) ?? asString(data.state),
188
+ url: asString(data.url),
189
+ project_key: asString(asRecord(data.team).key) ??
190
+ asString(asRecord(data.project).key) ??
191
+ deriveProjectKeyFromExternalKey(externalKey),
192
+ assignee: asNullableString(asRecord(data.assignee).displayName) ??
193
+ asNullableString(asRecord(data.assignee).name) ??
194
+ asNullableString(asRecord(data.assignee).email),
195
+ raw: data,
196
+ };
197
+ return ensureNormalized(this.source, envelope);
198
+ }
199
+ async fetchByKey(key) {
200
+ const fetchIssueByKey = getFetchByKeyOrThrow(this.source, this.fetchByKeyImpl);
201
+ const raw = await fetchIssueByKey(key);
202
+ return this.normalize(raw);
203
+ }
204
+ async fetchByQuery(query) {
205
+ const fetchIssuesByQuery = getFetchByQueryOrThrow(this.source, this.fetchByQueryImpl);
206
+ const rawIssues = await fetchIssuesByQuery(query);
207
+ return rawIssues.map((raw) => this.normalize(raw));
208
+ }
209
+ }
210
+ export class JiraIssueAdapter {
211
+ source = 'jira';
212
+ fetchByKeyImpl;
213
+ fetchByQueryImpl;
214
+ constructor(fetchers = {}) {
215
+ this.fetchByKeyImpl = fetchers.fetchByKey;
216
+ this.fetchByQueryImpl = fetchers.fetchByQuery;
217
+ }
218
+ normalize(raw) {
219
+ const data = asRecord(raw);
220
+ const fields = asRecord(data.fields);
221
+ const key = asString(data.key);
222
+ const envelope = {
223
+ source: this.source,
224
+ external_id: asString(data.id),
225
+ external_key: key,
226
+ title: asString(fields.summary),
227
+ description: normalizeJiraDescription(fields.description),
228
+ labels: normalizeJiraLabels(fields.labels),
229
+ priority: normalizeJiraPriority(fields.priority),
230
+ status: asString(asRecord(fields.status).name),
231
+ url: deriveJiraUrl(data, key),
232
+ project_key: asString(asRecord(fields.project).key) ?? deriveProjectKeyFromExternalKey(key),
233
+ assignee: asNullableString(asRecord(fields.assignee).displayName) ??
234
+ asNullableString(asRecord(fields.assignee).emailAddress) ??
235
+ asNullableString(asRecord(fields.assignee).accountId),
236
+ item_type: asNullableString(asRecord(fields.issuetype).name),
237
+ raw: data,
238
+ };
239
+ return ensureNormalized(this.source, envelope);
240
+ }
241
+ async fetchByKey(key) {
242
+ const fetchIssueByKey = getFetchByKeyOrThrow(this.source, this.fetchByKeyImpl);
243
+ const raw = await fetchIssueByKey(key);
244
+ return this.normalize(raw);
245
+ }
246
+ async fetchByQuery(query) {
247
+ const fetchIssuesByQuery = getFetchByQueryOrThrow(this.source, this.fetchByQueryImpl);
248
+ const rawIssues = await fetchIssuesByQuery(query);
249
+ return rawIssues.map((raw) => this.normalize(raw));
250
+ }
251
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * External Issue Adapter Module
3
+ *
4
+ * Shared contract for normalizing external issues (Linear, Jira) into
5
+ * a canonical IssueEnvelope format with deterministic spawn context mapping.
6
+ */
7
+ export { type IssueSource, type IssueEnvelope, type IssueSpawnContext, type IssueValidationError, type IssueValidationErrorCode, type IssueValidationResult, type ExternalIssueAdapter, type ExternalIssueErrorCode, ISSUE_SOURCES, ExternalIssueError, } from './types.js';
8
+ export { validateIssueEnvelope, validateOrThrow, } from './validation.js';
9
+ export { mapToSpawnContext, } from './mapper.js';
10
+ export { LinearIssueAdapter, JiraIssueAdapter, } from './adapters.js';
@@ -0,0 +1,14 @@
1
+ /**
2
+ * External Issue Adapter Module
3
+ *
4
+ * Shared contract for normalizing external issues (Linear, Jira) into
5
+ * a canonical IssueEnvelope format with deterministic spawn context mapping.
6
+ */
7
+ // Types and interfaces
8
+ export { ISSUE_SOURCES, ExternalIssueError, } from './types.js';
9
+ // Validation
10
+ export { validateIssueEnvelope, validateOrThrow, } from './validation.js';
11
+ // Mapper
12
+ export { mapToSpawnContext, } from './mapper.js';
13
+ // Adapters
14
+ export { LinearIssueAdapter, JiraIssueAdapter, } from './adapters.js';
@@ -0,0 +1,21 @@
1
+ /**
2
+ * IssueEnvelope → Spawn Context Mapper
3
+ *
4
+ * Deterministically maps an IssueEnvelope to spawn context data
5
+ * (prompt text + metadata) for agent execution.
6
+ */
7
+ import type { IssueEnvelope, IssueSpawnContext } from './types.js';
8
+ /**
9
+ * Map an IssueEnvelope to spawn context data.
10
+ *
11
+ * Produces:
12
+ * - A structured prompt string containing the issue details
13
+ * - Metadata key-value pairs for ticket context
14
+ *
15
+ * The mapping is deterministic: the same IssueEnvelope always produces
16
+ * the same IssueSpawnContext.
17
+ *
18
+ * @param envelope - Validated IssueEnvelope
19
+ * @returns Spawn context with prompt and metadata
20
+ */
21
+ export declare function mapToSpawnContext(envelope: IssueEnvelope): IssueSpawnContext;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * IssueEnvelope → Spawn Context Mapper
3
+ *
4
+ * Deterministically maps an IssueEnvelope to spawn context data
5
+ * (prompt text + metadata) for agent execution.
6
+ */
7
+ /**
8
+ * Map an IssueEnvelope to spawn context data.
9
+ *
10
+ * Produces:
11
+ * - A structured prompt string containing the issue details
12
+ * - Metadata key-value pairs for ticket context
13
+ *
14
+ * The mapping is deterministic: the same IssueEnvelope always produces
15
+ * the same IssueSpawnContext.
16
+ *
17
+ * @param envelope - Validated IssueEnvelope
18
+ * @returns Spawn context with prompt and metadata
19
+ */
20
+ export function mapToSpawnContext(envelope) {
21
+ const prompt = buildPrompt(envelope);
22
+ const metadata = buildMetadata(envelope);
23
+ return { prompt, metadata };
24
+ }
25
+ /**
26
+ * Build a structured prompt from an IssueEnvelope.
27
+ *
28
+ * The prompt includes all relevant issue fields formatted for agent consumption.
29
+ */
30
+ function buildPrompt(envelope) {
31
+ const lines = [];
32
+ lines.push(`# ${envelope.title}`);
33
+ lines.push('');
34
+ lines.push(`**Source:** ${envelope.source} (${envelope.external_key})`);
35
+ if (envelope.item_type) {
36
+ lines.push(`**Item Type:** ${envelope.item_type}`);
37
+ }
38
+ lines.push(`**Status:** ${envelope.status}`);
39
+ if (envelope.priority) {
40
+ lines.push(`**Priority:** ${envelope.priority}`);
41
+ }
42
+ if (envelope.assignee) {
43
+ lines.push(`**Assignee:** ${envelope.assignee}`);
44
+ }
45
+ lines.push(`**Project:** ${envelope.project_key}`);
46
+ if (envelope.labels.length > 0) {
47
+ lines.push(`**Labels:** ${envelope.labels.join(', ')}`);
48
+ }
49
+ lines.push(`**URL:** ${envelope.url}`);
50
+ if (envelope.description) {
51
+ lines.push('');
52
+ lines.push('## Description');
53
+ lines.push('');
54
+ lines.push(envelope.description);
55
+ }
56
+ return lines.join('\n');
57
+ }
58
+ /**
59
+ * Build metadata key-value pairs from an IssueEnvelope.
60
+ *
61
+ * These are stored as ticket metadata for traceability back to
62
+ * the external source.
63
+ */
64
+ function buildMetadata(envelope) {
65
+ const metadata = {
66
+ 'external_source': envelope.source,
67
+ 'external_id': envelope.external_id,
68
+ 'external_key': envelope.external_key,
69
+ 'external_url': envelope.url,
70
+ 'external_project': envelope.project_key,
71
+ 'external_status': envelope.status,
72
+ };
73
+ if (envelope.priority) {
74
+ metadata['external_priority'] = envelope.priority;
75
+ }
76
+ if (envelope.assignee) {
77
+ metadata['external_assignee'] = envelope.assignee;
78
+ }
79
+ if (envelope.labels.length > 0) {
80
+ metadata['external_labels'] = envelope.labels.join(',');
81
+ }
82
+ if (envelope.item_type) {
83
+ metadata['external_item_type'] = envelope.item_type;
84
+ }
85
+ return metadata;
86
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * External Issue Adapter Types
3
+ *
4
+ * Canonical types for normalizing issues from external sources
5
+ * (Linear and Jira) into a shared IssueEnvelope format
6
+ * that can be mapped to spawn context.
7
+ */
8
+ /**
9
+ * Supported external issue sources.
10
+ *
11
+ */
12
+ export type IssueSource = 'linear' | 'jira';
13
+ /**
14
+ * All valid issue sources as a const array.
15
+ */
16
+ export declare const ISSUE_SOURCES: readonly ["linear", "jira"];
17
+ /**
18
+ * Canonical envelope for external issues/work items.
19
+ *
20
+ * Normalizes issues from different sources into a shared structure that can be
21
+ * deterministically mapped to spawn context.
22
+ *
23
+ * Source-specific fields are preserved in the `raw` payload.
24
+ */
25
+ export interface IssueEnvelope {
26
+ /** Which external system this issue came from */
27
+ source: IssueSource;
28
+ /** Unique identifier in the external system (e.g., Linear UUID, Jira issue ID) */
29
+ external_id: string;
30
+ /** Human-readable key in the external system (e.g., "ENG-123", "PROJ-456") */
31
+ external_key: string;
32
+ /** Issue title / summary */
33
+ title: string;
34
+ /** Issue description (markdown or plain text) */
35
+ description: string;
36
+ /** Labels / tags applied to the issue */
37
+ labels: string[];
38
+ /** Priority level (normalized to P0-P3 scale) */
39
+ priority: string | null;
40
+ /** Current status name in the external system */
41
+ status: string;
42
+ /** URL to view the issue in the external system */
43
+ url: string;
44
+ /** Project key or identifier in the external system */
45
+ project_key: string;
46
+ /** Assignee display name or identifier */
47
+ assignee: string | null;
48
+ /**
49
+ * Source-native work item kind when available (e.g., issue, ticket, task).
50
+ * Optional to preserve compatibility with adapters that do not expose a
51
+ * stable item kind.
52
+ */
53
+ item_type?: string | null;
54
+ /** Original source-specific payload (preserved for source-specific logic) */
55
+ raw: Record<string, unknown>;
56
+ }
57
+ /**
58
+ * Metadata derived from an IssueEnvelope for spawn context.
59
+ * Used to populate ExecutionContext fields when spawning agent work
60
+ * from an external issue.
61
+ */
62
+ export interface IssueSpawnContext {
63
+ /** Prompt text generated from the issue for the agent */
64
+ prompt: string;
65
+ /** Metadata key-value pairs to attach to the ticket */
66
+ metadata: Record<string, string>;
67
+ }
68
+ /**
69
+ * Error codes for issue envelope validation failures.
70
+ */
71
+ export type IssueValidationErrorCode = 'MISSING_FIELD' | 'INVALID_SOURCE' | 'INVALID_FIELD_TYPE' | 'EMPTY_FIELD';
72
+ /**
73
+ * Structured validation error for issue envelope fields.
74
+ */
75
+ export interface IssueValidationError {
76
+ /** Machine-readable error code */
77
+ code: IssueValidationErrorCode;
78
+ /** The field that failed validation */
79
+ field: string;
80
+ /** Human-readable error message */
81
+ message: string;
82
+ }
83
+ /**
84
+ * Result of validating an issue envelope.
85
+ */
86
+ export type IssueValidationResult = {
87
+ valid: true;
88
+ envelope: IssueEnvelope;
89
+ } | {
90
+ valid: false;
91
+ errors: IssueValidationError[];
92
+ };
93
+ /**
94
+ * Contract for external issue source adapters.
95
+ *
96
+ * Both Linear and Jira adapters must implement this interface to normalize
97
+ * their issues into the shared IssueEnvelope format.
98
+ *
99
+ * Adapters are responsible for:
100
+ * 1. Fetching issues from their source API
101
+ * 2. Normalizing source-specific data into IssueEnvelope format
102
+ * 3. Preserving source-specific fields in the `raw` payload
103
+ */
104
+ export interface ExternalIssueAdapter {
105
+ /** Which source this adapter handles */
106
+ readonly source: IssueSource;
107
+ /**
108
+ * Normalize a raw API response into an IssueEnvelope.
109
+ *
110
+ * @param raw - Raw issue data from the source API
111
+ * @returns Validated IssueEnvelope
112
+ * @throws ExternalIssueError if the raw data cannot be normalized
113
+ */
114
+ normalize(raw: unknown): IssueEnvelope;
115
+ /**
116
+ * Fetch and normalize a single issue by its external key.
117
+ *
118
+ * @param key - External issue key (e.g., "ENG-123" for Linear, "PROJ-456" for Jira)
119
+ * @returns Normalized IssueEnvelope
120
+ * @throws ExternalIssueError if the issue cannot be fetched or normalized
121
+ */
122
+ fetchByKey(key: string): Promise<IssueEnvelope>;
123
+ /**
124
+ * Fetch and normalize multiple issues matching a query.
125
+ *
126
+ * @param query - Source-specific query parameters
127
+ * @returns Array of normalized IssueEnvelopes
128
+ * @throws ExternalIssueError if the query fails
129
+ */
130
+ fetchByQuery(query: Record<string, unknown>): Promise<IssueEnvelope[]>;
131
+ }
132
+ /**
133
+ * Error codes for external issue operations.
134
+ */
135
+ export type ExternalIssueErrorCode = 'VALIDATION_FAILED' | 'FETCH_FAILED' | 'NORMALIZE_FAILED' | 'SOURCE_NOT_SUPPORTED';
136
+ /**
137
+ * Typed error for external issue operations.
138
+ */
139
+ export declare class ExternalIssueError extends Error {
140
+ code: ExternalIssueErrorCode;
141
+ source?: IssueSource | undefined;
142
+ validationErrors?: IssueValidationError[] | undefined;
143
+ constructor(code: ExternalIssueErrorCode, message: string, source?: IssueSource | undefined, validationErrors?: IssueValidationError[] | undefined);
144
+ }