@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.
- package/dist/commands/config/index.js +39 -1
- package/dist/commands/linear/auth.d.ts +14 -0
- package/dist/commands/linear/auth.js +211 -0
- package/dist/commands/linear/import.d.ts +21 -0
- package/dist/commands/linear/import.js +260 -0
- package/dist/commands/linear/status.d.ts +11 -0
- package/dist/commands/linear/status.js +88 -0
- package/dist/commands/linear/sync.d.ts +15 -0
- package/dist/commands/linear/sync.js +233 -0
- package/dist/commands/orchestrator/attach.d.ts +9 -1
- package/dist/commands/orchestrator/attach.js +67 -13
- package/dist/commands/orchestrator/index.js +22 -7
- package/dist/commands/ticket/link/duplicates.d.ts +15 -0
- package/dist/commands/ticket/link/duplicates.js +95 -0
- package/dist/commands/ticket/link/index.js +14 -0
- package/dist/commands/ticket/link/relates.d.ts +15 -0
- package/dist/commands/ticket/link/relates.js +95 -0
- package/dist/commands/work/revise.js +4 -3
- package/dist/commands/work/spawn.d.ts +5 -0
- package/dist/commands/work/spawn.js +195 -14
- package/dist/commands/work/start.js +75 -19
- package/dist/lib/execution/config.d.ts +15 -0
- package/dist/lib/execution/config.js +54 -0
- package/dist/lib/execution/devcontainer.d.ts +6 -3
- package/dist/lib/execution/devcontainer.js +39 -12
- package/dist/lib/execution/runners.d.ts +28 -32
- package/dist/lib/execution/runners.js +345 -275
- package/dist/lib/execution/spawner.js +62 -5
- package/dist/lib/execution/types.d.ts +4 -0
- package/dist/lib/execution/types.js +3 -0
- package/dist/lib/external-issues/adapters.d.ts +26 -0
- package/dist/lib/external-issues/adapters.js +251 -0
- package/dist/lib/external-issues/index.d.ts +10 -0
- package/dist/lib/external-issues/index.js +14 -0
- package/dist/lib/external-issues/mapper.d.ts +21 -0
- package/dist/lib/external-issues/mapper.js +86 -0
- package/dist/lib/external-issues/types.d.ts +144 -0
- package/dist/lib/external-issues/types.js +26 -0
- package/dist/lib/external-issues/validation.d.ts +34 -0
- package/dist/lib/external-issues/validation.js +219 -0
- package/dist/lib/linear/client.d.ts +55 -0
- package/dist/lib/linear/client.js +254 -0
- package/dist/lib/linear/config.d.ts +37 -0
- package/dist/lib/linear/config.js +100 -0
- package/dist/lib/linear/index.d.ts +11 -0
- package/dist/lib/linear/index.js +10 -0
- package/dist/lib/linear/mapper.d.ts +67 -0
- package/dist/lib/linear/mapper.js +219 -0
- package/dist/lib/linear/sync.d.ts +37 -0
- package/dist/lib/linear/sync.js +89 -0
- package/dist/lib/linear/types.d.ts +139 -0
- package/dist/lib/linear/types.js +34 -0
- package/dist/lib/mcp/helpers.d.ts +8 -0
- package/dist/lib/mcp/helpers.js +10 -0
- package/dist/lib/mcp/tools/board.js +63 -11
- package/dist/lib/pmo/schema.d.ts +2 -0
- package/dist/lib/pmo/schema.js +20 -0
- package/dist/lib/pmo/storage/base.js +92 -13
- package/dist/lib/pmo/storage/dependencies.js +15 -0
- package/dist/lib/prompt-json.d.ts +4 -0
- package/oclif.manifest.json +2867 -2380
- 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(
|
|
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
|
-
|
|
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;
|
|
@@ -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
|
+
}
|