@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.
- package/dist/commands/agent/list.js +2 -3
- package/dist/commands/agent/login.js +2 -2
- package/dist/commands/agent/rebuild.js +2 -3
- package/dist/commands/agent/shell.js +2 -2
- package/dist/commands/agent/status.js +3 -3
- package/dist/commands/agent/visit.js +2 -2
- 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/staff/list.js +2 -3
- 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 +7 -6
- package/dist/commands/work/spawn.d.ts +5 -0
- package/dist/commands/work/spawn.js +195 -14
- package/dist/commands/work/start.js +79 -23
- package/dist/commands/work/watch.js +2 -2
- package/dist/lib/agents/commands.d.ts +11 -0
- package/dist/lib/agents/commands.js +40 -10
- 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 -271
- package/dist/lib/execution/spawner.js +65 -7
- 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/dist/lib/themes.js +32 -16
- package/oclif.manifest.json +2823 -2336
- 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 =
|
|
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(
|
|
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
|
-
|
|
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;
|
|
@@ -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
|
+
}
|