@proletariat/cli 0.3.44 → 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 (73) hide show
  1. package/dist/commands/agent/list.js +2 -3
  2. package/dist/commands/agent/login.js +2 -2
  3. package/dist/commands/agent/rebuild.js +2 -3
  4. package/dist/commands/agent/shell.js +2 -2
  5. package/dist/commands/agent/status.js +3 -3
  6. package/dist/commands/agent/visit.js +2 -2
  7. package/dist/commands/config/index.js +39 -1
  8. package/dist/commands/linear/auth.d.ts +14 -0
  9. package/dist/commands/linear/auth.js +211 -0
  10. package/dist/commands/linear/import.d.ts +21 -0
  11. package/dist/commands/linear/import.js +260 -0
  12. package/dist/commands/linear/status.d.ts +11 -0
  13. package/dist/commands/linear/status.js +88 -0
  14. package/dist/commands/linear/sync.d.ts +15 -0
  15. package/dist/commands/linear/sync.js +233 -0
  16. package/dist/commands/orchestrator/attach.d.ts +9 -1
  17. package/dist/commands/orchestrator/attach.js +67 -13
  18. package/dist/commands/orchestrator/index.js +22 -7
  19. package/dist/commands/staff/list.js +2 -3
  20. package/dist/commands/ticket/link/duplicates.d.ts +15 -0
  21. package/dist/commands/ticket/link/duplicates.js +95 -0
  22. package/dist/commands/ticket/link/index.js +14 -0
  23. package/dist/commands/ticket/link/relates.d.ts +15 -0
  24. package/dist/commands/ticket/link/relates.js +95 -0
  25. package/dist/commands/work/revise.js +7 -6
  26. package/dist/commands/work/spawn.d.ts +5 -0
  27. package/dist/commands/work/spawn.js +195 -14
  28. package/dist/commands/work/start.js +79 -23
  29. package/dist/commands/work/watch.js +2 -2
  30. package/dist/lib/agents/commands.d.ts +11 -0
  31. package/dist/lib/agents/commands.js +40 -10
  32. package/dist/lib/execution/config.d.ts +15 -0
  33. package/dist/lib/execution/config.js +54 -0
  34. package/dist/lib/execution/devcontainer.d.ts +6 -3
  35. package/dist/lib/execution/devcontainer.js +39 -12
  36. package/dist/lib/execution/runners.d.ts +28 -32
  37. package/dist/lib/execution/runners.js +345 -271
  38. package/dist/lib/execution/spawner.js +65 -7
  39. package/dist/lib/execution/types.d.ts +4 -0
  40. package/dist/lib/execution/types.js +3 -0
  41. package/dist/lib/external-issues/adapters.d.ts +26 -0
  42. package/dist/lib/external-issues/adapters.js +251 -0
  43. package/dist/lib/external-issues/index.d.ts +10 -0
  44. package/dist/lib/external-issues/index.js +14 -0
  45. package/dist/lib/external-issues/mapper.d.ts +21 -0
  46. package/dist/lib/external-issues/mapper.js +86 -0
  47. package/dist/lib/external-issues/types.d.ts +144 -0
  48. package/dist/lib/external-issues/types.js +26 -0
  49. package/dist/lib/external-issues/validation.d.ts +34 -0
  50. package/dist/lib/external-issues/validation.js +219 -0
  51. package/dist/lib/linear/client.d.ts +55 -0
  52. package/dist/lib/linear/client.js +254 -0
  53. package/dist/lib/linear/config.d.ts +37 -0
  54. package/dist/lib/linear/config.js +100 -0
  55. package/dist/lib/linear/index.d.ts +11 -0
  56. package/dist/lib/linear/index.js +10 -0
  57. package/dist/lib/linear/mapper.d.ts +67 -0
  58. package/dist/lib/linear/mapper.js +219 -0
  59. package/dist/lib/linear/sync.d.ts +37 -0
  60. package/dist/lib/linear/sync.js +89 -0
  61. package/dist/lib/linear/types.d.ts +139 -0
  62. package/dist/lib/linear/types.js +34 -0
  63. package/dist/lib/mcp/helpers.d.ts +8 -0
  64. package/dist/lib/mcp/helpers.js +10 -0
  65. package/dist/lib/mcp/tools/board.js +63 -11
  66. package/dist/lib/pmo/schema.d.ts +2 -0
  67. package/dist/lib/pmo/schema.js +20 -0
  68. package/dist/lib/pmo/storage/base.js +92 -13
  69. package/dist/lib/pmo/storage/dependencies.js +15 -0
  70. package/dist/lib/prompt-json.d.ts +4 -0
  71. package/dist/lib/themes.js +32 -16
  72. package/oclif.manifest.json +2823 -2336
  73. package/package.json +2 -1
@@ -9,11 +9,12 @@ import * as path from 'node:path';
9
9
  import { execSync } from 'node:child_process';
10
10
  import { autoExportToBoard } from '../pmo/index.js';
11
11
  import { getWorkColumnSetting, findColumnByName } from '../pmo/utils.js';
12
+ import { resolveAgentDir } from '../agents/commands.js';
12
13
  import { findHQRoot } from '../repos/index.js';
13
14
  import { hasGitHubRemote } from '../repos/git.js';
14
15
  import { hasDevcontainerConfig } from './devcontainer.js';
15
16
  import { loadExecutionConfig, getOrPromptCoderName } from './config.js';
16
- import { runExecution, isDockerRunning, isGitHubTokenAvailable, isDevcontainerCliInstalled, runExecutorPreflight } from './runners.js';
17
+ import { runExecution, isDockerRunning, isGitHubTokenAvailable, isDevcontainerCliInstalled, runExecutorPreflight, getAgentContainerName, isContainerRunning, getContainerId, buildSessionName } from './runners.js';
17
18
  import { detectRepoWorktrees, resolveWorktreePath } from './context.js';
18
19
  import { generateBranchName, DEFAULT_EXECUTION_CONFIG, } from './types.js';
19
20
  // =============================================================================
@@ -182,8 +183,8 @@ export function selectAgent(strategy, availableAgents, executionStorage, roundRo
182
183
  export async function spawnAgentForTicket(ticket, agentName, storage, executionStorage, workspaceInfo, db, pmoPath, options = {}) {
183
184
  const log = options.log || (() => { });
184
185
  const executor = options.executor || DEFAULT_EXECUTION_CONFIG.defaultExecutor;
185
- // Determine agent directory and worktree path
186
- const agentDir = path.join(workspaceInfo.agentsPath, agentName);
186
+ // Determine agent directory and worktree path (handles staff and temp agents)
187
+ const agentDir = resolveAgentDir(workspaceInfo, agentName);
187
188
  if (!fs.existsSync(agentDir)) {
188
189
  return {
189
190
  success: false,
@@ -285,7 +286,7 @@ export async function spawnAgentForTicket(ticket, agentName, storage, executionS
285
286
  // Executor preflight check (TKT-1082): verify binary is available before proceeding
286
287
  // For host environment, check immediately. For devcontainer, check happens after container start.
287
288
  if (environment === 'host') {
288
- const preflight = runExecutorPreflight(executor, environment);
289
+ const preflight = runExecutorPreflight(environment, executor);
289
290
  if (!preflight.ok) {
290
291
  return {
291
292
  success: false,
@@ -401,6 +402,54 @@ export async function spawnAgentForTicket(ticket, agentName, storage, executionS
401
402
  }
402
403
  }
403
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
+ }
404
453
  // Create execution record
405
454
  const execution = executionStorage.createExecution({
406
455
  ticketId: ticket.id,
@@ -414,10 +463,19 @@ export async function spawnAgentForTicket(ticket, agentName, storage, executionS
414
463
  // Load execution config (use passed config or load from db)
415
464
  const executionConfig = options.executionConfig || loadExecutionConfig(db);
416
465
  executionConfig.sandboxed = sandboxed;
417
- // Use print mode for background, interactive for terminal/tmux
418
- executionConfig.outputMode = displayMode === 'background' ? 'print' : 'interactive';
419
466
  // Run execution
420
- 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
+ }
421
479
  const result = await runExecution(environment, context, executor, executionConfig, {
422
480
  displayMode,
423
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
+ }