@jiraacp/cli 2026.405.4
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/README.md +283 -0
- package/dist/abort-GQE4OI5S.js +103 -0
- package/dist/abort-GQE4OI5S.js.map +1 -0
- package/dist/abort-VMRQOADY.js +96 -0
- package/dist/abort-VMRQOADY.js.map +1 -0
- package/dist/bot-WOTETAJY.js +13 -0
- package/dist/bot-WOTETAJY.js.map +1 -0
- package/dist/cancel-clarification-4G5S2HJZ.js +64 -0
- package/dist/cancel-clarification-4G5S2HJZ.js.map +1 -0
- package/dist/chunk-3U373M37.js +67 -0
- package/dist/chunk-3U373M37.js.map +1 -0
- package/dist/chunk-3YHD4SIN.js +97 -0
- package/dist/chunk-3YHD4SIN.js.map +1 -0
- package/dist/chunk-6IY6CRUJ.js +690 -0
- package/dist/chunk-6IY6CRUJ.js.map +1 -0
- package/dist/chunk-B6OA3XJK.js +1167 -0
- package/dist/chunk-B6OA3XJK.js.map +1 -0
- package/dist/chunk-BM4R6NST.js +191 -0
- package/dist/chunk-BM4R6NST.js.map +1 -0
- package/dist/chunk-FLPIU2QO.js +77 -0
- package/dist/chunk-FLPIU2QO.js.map +1 -0
- package/dist/chunk-H7YXX4UA.js +86 -0
- package/dist/chunk-H7YXX4UA.js.map +1 -0
- package/dist/chunk-IT74N3UH.js +19 -0
- package/dist/chunk-IT74N3UH.js.map +1 -0
- package/dist/chunk-JOT4UVSO.js +186 -0
- package/dist/chunk-JOT4UVSO.js.map +1 -0
- package/dist/chunk-KSJKCLEJ.js +222 -0
- package/dist/chunk-KSJKCLEJ.js.map +1 -0
- package/dist/chunk-LIEW4ULF.js +139 -0
- package/dist/chunk-LIEW4ULF.js.map +1 -0
- package/dist/chunk-M4V3YOCY.js +82 -0
- package/dist/chunk-M4V3YOCY.js.map +1 -0
- package/dist/chunk-MMWQHH25.js +207 -0
- package/dist/chunk-MMWQHH25.js.map +1 -0
- package/dist/chunk-OJ4CNF73.js +78 -0
- package/dist/chunk-OJ4CNF73.js.map +1 -0
- package/dist/chunk-PFJAC3RO.js +137 -0
- package/dist/chunk-PFJAC3RO.js.map +1 -0
- package/dist/chunk-PVKVCUNR.js +159 -0
- package/dist/chunk-PVKVCUNR.js.map +1 -0
- package/dist/chunk-RXT4WSIY.js +35 -0
- package/dist/chunk-RXT4WSIY.js.map +1 -0
- package/dist/chunk-RZK74PDF.js +34 -0
- package/dist/chunk-RZK74PDF.js.map +1 -0
- package/dist/chunk-UDTWVKRX.js +68 -0
- package/dist/chunk-UDTWVKRX.js.map +1 -0
- package/dist/chunk-VCEONSWJ.js +307 -0
- package/dist/chunk-VCEONSWJ.js.map +1 -0
- package/dist/chunk-VWBCDZWQ.js +119 -0
- package/dist/chunk-VWBCDZWQ.js.map +1 -0
- package/dist/chunk-WEJCTFQB.js +228 -0
- package/dist/chunk-WEJCTFQB.js.map +1 -0
- package/dist/chunk-YJK7IRPI.js +223 -0
- package/dist/chunk-YJK7IRPI.js.map +1 -0
- package/dist/claude-md-HQ6L4CRP.js +8 -0
- package/dist/claude-md-HQ6L4CRP.js.map +1 -0
- package/dist/cli.js +276 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands-RG45VBTZ.js +407 -0
- package/dist/commands-RG45VBTZ.js.map +1 -0
- package/dist/commands-WYVRVE5Z.js +400 -0
- package/dist/commands-WYVRVE5Z.js.map +1 -0
- package/dist/config-edit-G7O56HXO.js +50 -0
- package/dist/config-edit-G7O56HXO.js.map +1 -0
- package/dist/config-set-QN3JRNZL.js +63 -0
- package/dist/config-set-QN3JRNZL.js.map +1 -0
- package/dist/daemon-CGBV55JK.js +104 -0
- package/dist/daemon-CGBV55JK.js.map +1 -0
- package/dist/dashboard-YVFJ5DXR.js +143 -0
- package/dist/dashboard-YVFJ5DXR.js.map +1 -0
- package/dist/doctor-BPTLVLTD.js +98 -0
- package/dist/doctor-BPTLVLTD.js.map +1 -0
- package/dist/human-loop-RBTA2TYK.js +16 -0
- package/dist/human-loop-RBTA2TYK.js.map +1 -0
- package/dist/human-loop-XGWXUNCS.js +18 -0
- package/dist/human-loop-XGWXUNCS.js.map +1 -0
- package/dist/index.d.ts +583 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/loader-DGW7HCJ5.js +21 -0
- package/dist/loader-DGW7HCJ5.js.map +1 -0
- package/dist/logs-JUVQWN6C.js +93 -0
- package/dist/logs-JUVQWN6C.js.map +1 -0
- package/dist/mcp.js +132 -0
- package/dist/mcp.js.map +1 -0
- package/dist/orchestrator-3MGXX3QW.js +22 -0
- package/dist/orchestrator-3MGXX3QW.js.map +1 -0
- package/dist/orchestrator-BVUKN5N3.js +13 -0
- package/dist/orchestrator-BVUKN5N3.js.map +1 -0
- package/dist/pause-FLDZ3OD6.js +62 -0
- package/dist/pause-FLDZ3OD6.js.map +1 -0
- package/dist/projects-QMIGNW7U.js +129 -0
- package/dist/projects-QMIGNW7U.js.map +1 -0
- package/dist/replay-M4JEG4Z4.js +151 -0
- package/dist/replay-M4JEG4Z4.js.map +1 -0
- package/dist/schedule-CDHD77VZ.js +17 -0
- package/dist/schedule-CDHD77VZ.js.map +1 -0
- package/dist/serve-XI7JTIPZ.js +231 -0
- package/dist/serve-XI7JTIPZ.js.map +1 -0
- package/dist/sprint-KZZWVNK6.js +200 -0
- package/dist/sprint-KZZWVNK6.js.map +1 -0
- package/dist/status-I6GU2LWE.js +48 -0
- package/dist/status-I6GU2LWE.js.map +1 -0
- package/dist/topic-manager-4AMEPMFI.js +12 -0
- package/dist/topic-manager-4AMEPMFI.js.map +1 -0
- package/dist/triage-WNHGPVZQ.js +251 -0
- package/dist/triage-WNHGPVZQ.js.map +1 -0
- package/dist/usage-AWWBI37F.js +155 -0
- package/dist/usage-AWWBI37F.js.map +1 -0
- package/dist/wizard-CYEJJLNF.js +190 -0
- package/dist/wizard-CYEJJLNF.js.map +1 -0
- package/package.json +56 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/integrations/github/client.ts","../src/memory/context-builder.ts","../src/pipeline/stages/1-fetch.ts","../src/pipeline/runner.ts","../src/pipeline/stages/2-analyze.ts"],"sourcesContent":["import { Octokit } from \"@octokit/rest\";\n\nexport interface GitHubClient {\n createBranch(base: string, branch: string): Promise<void>;\n createPR(opts: {\n title: string;\n body: string;\n head: string;\n base: string;\n draft?: boolean;\n }): Promise<number>;\n mergePR(\n prNumber: number,\n strategy: \"squash\" | \"merge\" | \"rebase\",\n ): Promise<void>;\n addReviewers(prNumber: number, reviewers: string[]): Promise<void>;\n getPR(prNumber: number): Promise<{ state: string; merged: boolean }>;\n getRunStatus(\n branch: string,\n ): Promise<\"pending\" | \"success\" | \"failure\" | \"unknown\">;\n}\n\nexport function createGitHubClient(\n token: string,\n owner: string,\n repo: string,\n): GitHubClient {\n const octokit = new Octokit({ auth: token });\n\n return {\n async createBranch(base, branch) {\n const { data: ref } = await octokit.git.getRef({\n owner,\n repo,\n ref: `heads/${base}`,\n });\n await octokit.git.createRef({\n owner,\n repo,\n ref: `refs/heads/${branch}`,\n sha: ref.object.sha,\n });\n },\n\n async createPR({ title, body, head, base, draft = false }) {\n const { data } = await octokit.pulls.create({\n owner,\n repo,\n title,\n body,\n head,\n base,\n draft,\n });\n return data.number;\n },\n\n async mergePR(prNumber, strategy) {\n const mergeMethod =\n strategy === \"squash\"\n ? \"squash\"\n : strategy === \"rebase\"\n ? \"rebase\"\n : \"merge\";\n await octokit.pulls.merge({\n owner,\n repo,\n pull_number: prNumber,\n merge_method: mergeMethod,\n });\n },\n\n async addReviewers(prNumber, reviewers) {\n if (reviewers.length === 0) return;\n await octokit.pulls.requestReviewers({\n owner,\n repo,\n pull_number: prNumber,\n reviewers,\n });\n },\n\n async getPR(prNumber) {\n const { data } = await octokit.pulls.get({\n owner,\n repo,\n pull_number: prNumber,\n });\n return { state: data.state, merged: data.merged };\n },\n\n async getRunStatus(branch) {\n try {\n const { data } = await octokit.repos.getCombinedStatusForRef({\n owner,\n repo,\n ref: branch,\n });\n if (data.state === \"success\") return \"success\";\n if (data.state === \"failure\") return \"failure\";\n return \"pending\";\n } catch {\n return \"unknown\";\n }\n },\n };\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type { StageId } from \"../config/schema.js\";\n\nexport function writeTicketContext(\n memoryDir: string,\n ticket: {\n key: string;\n summary: string;\n description: string;\n acceptanceCriteria: string;\n priority: string;\n clarifications?: string;\n },\n): void {\n fs.mkdirSync(memoryDir, { recursive: true });\n const content = `# Ticket: ${ticket.key}\n\n## Summary\n${ticket.summary}\n\n## Description\n${ticket.description || \"(none)\"}\n\n## Acceptance Criteria\n${ticket.acceptanceCriteria || \"(none)\"}\n\n## Priority\n${ticket.priority}\n${ticket.clarifications ? `\\n## Clarifications from Team\\n${ticket.clarifications}` : \"\"}\n`;\n fs.writeFileSync(path.join(memoryDir, \"ticket-context.md\"), content);\n}\n\nexport function readTicketContext(memoryDir: string): string {\n const p = path.join(memoryDir, \"ticket-context.md\");\n return fs.existsSync(p) ? fs.readFileSync(p, \"utf8\") : \"\";\n}\n\nexport function appendClarifications(memoryDir: string, answers: string): void {\n const p = path.join(memoryDir, \"ticket-context.md\");\n if (fs.existsSync(p)) {\n fs.appendFileSync(p, `\\n## Clarifications from Team\\n${answers}\\n`);\n }\n}\n\nexport function writeReviewFeedback(\n memoryDir: string,\n feedback: {\n prNumber: number;\n issues: { severity: \"minor\" | \"major\"; message: string }[];\n autoResolved: boolean;\n },\n): void {\n const lines = [\n `# Review Results`,\n `PR: #${feedback.prNumber}`,\n `Major issues: ${feedback.issues.filter((i) => i.severity === \"major\").length}`,\n `Minor issues: ${feedback.issues.filter((i) => i.severity === \"minor\").length}`,\n `Auto-resolved: ${feedback.autoResolved}`,\n \"\",\n \"## Issues\",\n ...feedback.issues.map(\n (i) => `- [${i.severity.toUpperCase()}] ${i.message}`,\n ),\n ];\n fs.writeFileSync(\n path.join(memoryDir, \"review-feedback.md\"),\n lines.join(\"\\n\"),\n );\n}\n\nexport function getContextFilesForStage(\n projectDir: string,\n memoryDir: string,\n stage: StageId,\n): string[] {\n const claudeMd = path.join(projectDir, \".claude\", \"CLAUDE.md\");\n const ticketCtx = path.join(memoryDir, \"ticket-context.md\");\n const reviewFeedback = path.join(memoryDir, \"review-feedback.md\");\n\n const files: string[] = [];\n if (fs.existsSync(claudeMd)) files.push(claudeMd);\n\n if ([\"code\", \"git\", \"review\", \"deploy\", \"test\", \"notify\"].includes(stage)) {\n if (fs.existsSync(ticketCtx)) files.push(ticketCtx);\n }\n if (stage === \"test\" && fs.existsSync(reviewFeedback)) {\n files.push(reviewFeedback);\n }\n return files;\n}\n","import type { Stage, PipelineContext, StageOutput } from \"./types.js\";\nimport { getTasks, getTicket } from \"../../integrations/jira/tools.js\";\nimport { writeTicketContext } from \"../../memory/context-builder.js\";\n\nexport const fetchStage: Stage = {\n id: \"fetch\",\n name: \"Fetch Ticket\",\n model: \"haiku\",\n\n async run(ctx: PipelineContext): Promise<StageOutput> {\n const { config, ticketKey, memoryDir } = ctx;\n\n ctx.logger.info({ ticketKey }, \"Fetching ticket from Jira\");\n\n const raw = await getTicket({\n instance: config.jira.instance,\n ticket_key: ticketKey,\n });\n const ticket = JSON.parse(raw) as {\n key: string;\n summary: string;\n status: string;\n assignee: string;\n priority: string;\n description: string;\n acceptance_criteria: string;\n };\n\n writeTicketContext(memoryDir, {\n key: ticket.key,\n summary: ticket.summary,\n description: ticket.description ?? \"\",\n acceptanceCriteria: ticket.acceptance_criteria ?? \"\",\n priority: ticket.priority ?? \"Medium\",\n });\n\n ctx.logger.info({ ticketKey, summary: ticket.summary }, \"Ticket fetched\");\n\n return {\n summary: ticket.summary,\n description: ticket.description,\n acceptanceCriteria: ticket.acceptance_criteria,\n status: ticket.status,\n };\n },\n};\n","import { spawnSafe, buildMinimalEnv } from \"../utils/process.js\";\n\nexport type AgentModel = \"haiku\" | \"sonnet\" | \"opus\";\n\nconst MODEL_IDS: Record<AgentModel, string> = {\n haiku: \"claude-haiku-4-5-20251001\",\n sonnet: \"claude-sonnet-4-6\",\n opus: \"claude-opus-4-6\",\n};\n\nexport interface RunAgentOptions {\n prompt: string;\n workdir: string;\n model: AgentModel;\n contextFiles?: string[];\n timeoutMs?: number;\n stallTimeoutMs?: number;\n extraEnv?: Record<string, string>;\n}\n\nexport async function runAgent(opts: RunAgentOptions): Promise<string> {\n const args = [\n \"--model\",\n MODEL_IDS[opts.model],\n \"--print\",\n \"--output-format\",\n \"text\",\n ];\n\n for (const f of opts.contextFiles ?? []) {\n args.push(\"--context\", f);\n }\n\n args.push(opts.prompt);\n\n const result = await spawnSafe(\"claude\", args, {\n cwd: opts.workdir,\n env: buildMinimalEnv(opts.extraEnv),\n timeoutMs: opts.timeoutMs ?? 1_800_000,\n stallTimeoutMs: opts.stallTimeoutMs ?? 300_000,\n });\n\n if (result.exitCode !== 0) {\n throw new Error(\n `Agent exited with code ${result.exitCode}:\\n${result.stderr}`,\n );\n }\n\n return result.stdout.trim();\n}\n\n/** Run two agents in parallel and return both outputs */\nexport async function runAgentsParallel(\n a: RunAgentOptions,\n b: RunAgentOptions,\n): Promise<[string, string]> {\n return Promise.all([runAgent(a), runAgent(b)]);\n}\n\n/** Detect if a task is complex enough to warrant Opus */\nexport function detectComplexity(description: string): AgentModel {\n const complexKeywords = [\n \"auth\",\n \"payment\",\n \"stripe\",\n \"oauth\",\n \"jwt\",\n \"migration\",\n \"schema\",\n \"database\",\n \"refactor\",\n \"cross-module\",\n \"multi-service\",\n \"security\",\n \"encryption\",\n \"permission\",\n ];\n const lower = description.toLowerCase();\n const matches = complexKeywords.filter((k) => lower.includes(k));\n return matches.length >= 2 ? \"opus\" : \"sonnet\";\n}\n","import type { Stage, PipelineContext, StageOutput } from \"./types.js\";\nimport { runAgent } from \"../runner.js\";\nimport { readTicketContext } from \"../../memory/context-builder.js\";\n\nconst CLARITY_PROMPT = (ticketCtx: string, requiredFields: string[]) =>\n `\nAnalyze this Jira ticket for clarity. Score from 0.0 to 1.0.\n\nRequired fields: ${requiredFields.join(\", \")}\n\nTicket:\n${ticketCtx}\n\nReply with JSON only:\n{\n \"score\": 0.0-1.0,\n \"missing\": [\"list of missing or ambiguous items\"],\n \"questions\": [\"specific questions to ask the team\"]\n}\n`.trim();\n\nexport const analyzeStage: Stage = {\n id: \"analyze\",\n name: \"Analyze Clarity\",\n model: \"sonnet\",\n\n async run(ctx: PipelineContext): Promise<StageOutput> {\n const { config, memoryDir, ticketKey } = ctx;\n\n const ticketCtx = readTicketContext(memoryDir);\n if (!ticketCtx)\n throw new Error(\"ticket-context.md not found — run fetch stage first\");\n\n ctx.logger.info({ ticketKey }, \"Analyzing ticket clarity\");\n\n const raw = await runAgent({\n prompt: CLARITY_PROMPT(\n ticketCtx,\n config.jira.requiredFields ?? [\"description\", \"acceptanceCriteria\"],\n ),\n workdir: config.workspace.rootDir,\n model: \"haiku\",\n timeoutMs: 60_000,\n stallTimeoutMs: 30_000,\n });\n\n // Extract JSON from agent output\n const jsonMatch = raw.match(/\\{[\\s\\S]*\\}/);\n if (!jsonMatch) {\n ctx.logger.warn(\n { ticketKey },\n \"Could not parse clarity JSON — defaulting to low score\",\n );\n return { score: 0, missing: [], questions: [], needsClarification: true };\n }\n\n const result = JSON.parse(jsonMatch[0]) as {\n score: number;\n missing: string[];\n questions: string[];\n };\n\n const threshold = config.jira.clarityScoreThreshold ?? 0.7;\n const needsClarification = result.score < threshold;\n\n ctx.logger.info(\n { ticketKey, score: result.score, threshold, needsClarification },\n \"Clarity analysis done\",\n );\n\n return { ...result, needsClarification };\n },\n};\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/pipeline/state.ts
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import os from "os";
|
|
6
|
+
import path from "path";
|
|
7
|
+
var RUNS_DIR = path.join(os.homedir(), ".jira-acp", "runs");
|
|
8
|
+
var INITIAL_STATE = {
|
|
9
|
+
ticketKey: "",
|
|
10
|
+
currentStage: null,
|
|
11
|
+
completedStages: [],
|
|
12
|
+
failedStage: null,
|
|
13
|
+
pendingClarification: false,
|
|
14
|
+
pendingHumanApproval: false,
|
|
15
|
+
branchName: null,
|
|
16
|
+
prNumber: null,
|
|
17
|
+
isCompleted: false,
|
|
18
|
+
isAborted: false,
|
|
19
|
+
abortReason: null,
|
|
20
|
+
startedAt: null
|
|
21
|
+
};
|
|
22
|
+
function applyEvent(state, event) {
|
|
23
|
+
switch (event.type) {
|
|
24
|
+
case "STARTED":
|
|
25
|
+
return {
|
|
26
|
+
...state,
|
|
27
|
+
ticketKey: event.ticketKey,
|
|
28
|
+
startedAt: event.timestamp
|
|
29
|
+
};
|
|
30
|
+
case "STAGE_STARTED":
|
|
31
|
+
return { ...state, currentStage: event.stage };
|
|
32
|
+
case "STAGE_COMPLETED": {
|
|
33
|
+
const output = event.output;
|
|
34
|
+
return {
|
|
35
|
+
...state,
|
|
36
|
+
currentStage: null,
|
|
37
|
+
completedStages: [...state.completedStages, event.stage],
|
|
38
|
+
branchName: output?.branchName ?? state.branchName,
|
|
39
|
+
prNumber: output?.prNumber ?? state.prNumber
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
case "STAGE_FAILED":
|
|
43
|
+
return { ...state, currentStage: null, failedStage: event.stage };
|
|
44
|
+
case "STAGE_SKIPPED":
|
|
45
|
+
return {
|
|
46
|
+
...state,
|
|
47
|
+
completedStages: [...state.completedStages, event.stage]
|
|
48
|
+
};
|
|
49
|
+
case "CLARIFICATION_REQUESTED":
|
|
50
|
+
return { ...state, pendingClarification: true };
|
|
51
|
+
case "CLARIFICATION_RECEIVED":
|
|
52
|
+
return { ...state, pendingClarification: false };
|
|
53
|
+
case "HUMAN_APPROVAL_REQUESTED":
|
|
54
|
+
return { ...state, pendingHumanApproval: true };
|
|
55
|
+
case "HUMAN_APPROVED":
|
|
56
|
+
case "HUMAN_REJECTED":
|
|
57
|
+
return { ...state, pendingHumanApproval: false };
|
|
58
|
+
case "PIPELINE_COMPLETED":
|
|
59
|
+
return { ...state, isCompleted: true, currentStage: null };
|
|
60
|
+
case "PIPELINE_ABORTED":
|
|
61
|
+
return {
|
|
62
|
+
...state,
|
|
63
|
+
isAborted: true,
|
|
64
|
+
abortReason: event.reason,
|
|
65
|
+
currentStage: null
|
|
66
|
+
};
|
|
67
|
+
default:
|
|
68
|
+
return state;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
var StateManager = class {
|
|
72
|
+
constructor(runDir) {
|
|
73
|
+
this.runDir = runDir;
|
|
74
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
75
|
+
this.statePath = path.join(runDir, "state.json");
|
|
76
|
+
this.events = this.load();
|
|
77
|
+
}
|
|
78
|
+
runDir;
|
|
79
|
+
statePath;
|
|
80
|
+
events = [];
|
|
81
|
+
emit(event) {
|
|
82
|
+
const full = {
|
|
83
|
+
...event,
|
|
84
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
85
|
+
};
|
|
86
|
+
fs.appendFileSync(this.statePath, JSON.stringify(full) + "\n");
|
|
87
|
+
this.events.push(full);
|
|
88
|
+
}
|
|
89
|
+
get current() {
|
|
90
|
+
return this.events.reduce(applyEvent, { ...INITIAL_STATE });
|
|
91
|
+
}
|
|
92
|
+
load() {
|
|
93
|
+
if (!fs.existsSync(this.statePath)) return [];
|
|
94
|
+
return fs.readFileSync(this.statePath, "utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
function getRunDir(projectName, ticketKey) {
|
|
98
|
+
return path.join(RUNS_DIR, projectName, ticketKey);
|
|
99
|
+
}
|
|
100
|
+
function getLockPath(projectName, ticketKey) {
|
|
101
|
+
return path.join(getRunDir(projectName, ticketKey), `${ticketKey}.lock`);
|
|
102
|
+
}
|
|
103
|
+
function getMemoryDir(projectName, ticketKey) {
|
|
104
|
+
return path.join(getRunDir(projectName, ticketKey), "memory");
|
|
105
|
+
}
|
|
106
|
+
function getEvents(runDir) {
|
|
107
|
+
const storePath = path.join(runDir, "state.json");
|
|
108
|
+
if (!fs.existsSync(storePath)) return [];
|
|
109
|
+
return fs.readFileSync(storePath, "utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export {
|
|
113
|
+
StateManager,
|
|
114
|
+
getRunDir,
|
|
115
|
+
getLockPath,
|
|
116
|
+
getMemoryDir,
|
|
117
|
+
getEvents
|
|
118
|
+
};
|
|
119
|
+
//# sourceMappingURL=chunk-VWBCDZWQ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/pipeline/state.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport type { StageId } from \"../config/schema.js\";\n\nconst RUNS_DIR = path.join(os.homedir(), \".jira-acp\", \"runs\");\n\n// ── Event types ───────────────────────────────────────────────────────────\n\nexport type PipelineEvent =\n | { type: \"STARTED\"; ticketKey: string; timestamp: string }\n | { type: \"STAGE_STARTED\"; stage: StageId; timestamp: string }\n | {\n type: \"STAGE_COMPLETED\";\n stage: StageId;\n output: unknown;\n timestamp: string;\n }\n | { type: \"STAGE_FAILED\"; stage: StageId; error: string; timestamp: string }\n | { type: \"STAGE_SKIPPED\"; stage: StageId; reason: string; timestamp: string }\n | { type: \"CLARIFICATION_REQUESTED\"; questions: string[]; timestamp: string }\n | { type: \"CLARIFICATION_RECEIVED\"; answers: string; timestamp: string }\n | { type: \"HUMAN_APPROVAL_REQUESTED\"; context: unknown; timestamp: string }\n | { type: \"HUMAN_APPROVED\"; timestamp: string }\n | { type: \"HUMAN_REJECTED\"; reason: string; timestamp: string }\n | { type: \"PIPELINE_COMPLETED\"; timestamp: string }\n | { type: \"PIPELINE_ABORTED\"; reason: string; timestamp: string };\n\n// ── Derived state ─────────────────────────────────────────────────────────\n\nexport interface PipelineState {\n ticketKey: string;\n currentStage: StageId | null;\n completedStages: StageId[];\n failedStage: StageId | null;\n pendingClarification: boolean;\n pendingHumanApproval: boolean;\n branchName: string | null;\n prNumber: number | null;\n isCompleted: boolean;\n isAborted: boolean;\n abortReason: string | null;\n startedAt: string | null;\n}\n\nconst INITIAL_STATE: PipelineState = {\n ticketKey: \"\",\n currentStage: null,\n completedStages: [],\n failedStage: null,\n pendingClarification: false,\n pendingHumanApproval: false,\n branchName: null,\n prNumber: null,\n isCompleted: false,\n isAborted: false,\n abortReason: null,\n startedAt: null,\n};\n\nfunction applyEvent(state: PipelineState, event: PipelineEvent): PipelineState {\n switch (event.type) {\n case \"STARTED\":\n return {\n ...state,\n ticketKey: event.ticketKey,\n startedAt: event.timestamp,\n };\n case \"STAGE_STARTED\":\n return { ...state, currentStage: event.stage };\n case \"STAGE_COMPLETED\": {\n const output = event.output as Record<string, unknown>;\n return {\n ...state,\n currentStage: null,\n completedStages: [...state.completedStages, event.stage],\n branchName: (output?.branchName as string) ?? state.branchName,\n prNumber: (output?.prNumber as number) ?? state.prNumber,\n };\n }\n case \"STAGE_FAILED\":\n return { ...state, currentStage: null, failedStage: event.stage };\n case \"STAGE_SKIPPED\":\n return {\n ...state,\n completedStages: [...state.completedStages, event.stage],\n };\n case \"CLARIFICATION_REQUESTED\":\n return { ...state, pendingClarification: true };\n case \"CLARIFICATION_RECEIVED\":\n return { ...state, pendingClarification: false };\n case \"HUMAN_APPROVAL_REQUESTED\":\n return { ...state, pendingHumanApproval: true };\n case \"HUMAN_APPROVED\":\n case \"HUMAN_REJECTED\":\n return { ...state, pendingHumanApproval: false };\n case \"PIPELINE_COMPLETED\":\n return { ...state, isCompleted: true, currentStage: null };\n case \"PIPELINE_ABORTED\":\n return {\n ...state,\n isAborted: true,\n abortReason: event.reason,\n currentStage: null,\n };\n default:\n return state;\n }\n}\n\n// Distributes Omit over union members (standard Omit<Union, K> doesn't work on discriminated unions)\ntype DistributiveOmit<T, K extends PropertyKey> = T extends unknown\n ? Omit<T, K>\n : never;\n\n// ── State manager ─────────────────────────────────────────────────────────\n\nexport class StateManager {\n private readonly statePath: string;\n private events: PipelineEvent[] = [];\n\n constructor(private readonly runDir: string) {\n fs.mkdirSync(runDir, { recursive: true });\n this.statePath = path.join(runDir, \"state.json\");\n this.events = this.load();\n }\n\n emit(event: DistributiveOmit<PipelineEvent, \"timestamp\">): void {\n const full = {\n ...event,\n timestamp: new Date().toISOString(),\n } as PipelineEvent;\n fs.appendFileSync(this.statePath, JSON.stringify(full) + \"\\n\");\n this.events.push(full);\n }\n\n get current(): PipelineState {\n return this.events.reduce(applyEvent, { ...INITIAL_STATE });\n }\n\n private load(): PipelineEvent[] {\n if (!fs.existsSync(this.statePath)) return [];\n return fs\n .readFileSync(this.statePath, \"utf8\")\n .split(\"\\n\")\n .filter(Boolean)\n .map((line) => JSON.parse(line) as PipelineEvent);\n }\n}\n\n// ── Run directory helpers ─────────────────────────────────────────────────\n// Runs live in ~/.jira-acp/runs/<projectName>/<ticketKey>/\n\nexport function getRunDir(projectName: string, ticketKey: string): string {\n return path.join(RUNS_DIR, projectName, ticketKey);\n}\n\nexport function getLockPath(projectName: string, ticketKey: string): string {\n return path.join(getRunDir(projectName, ticketKey), `${ticketKey}.lock`);\n}\n\nexport function getMemoryDir(projectName: string, ticketKey: string): string {\n return path.join(getRunDir(projectName, ticketKey), \"memory\");\n}\n\nexport function getEvents(runDir: string): PipelineEvent[] {\n const storePath = path.join(runDir, \"state.json\");\n if (!fs.existsSync(storePath)) return [];\n return fs\n .readFileSync(storePath, \"utf8\")\n .split(\"\\n\")\n .filter(Boolean)\n .map((line) => JSON.parse(line) as PipelineEvent);\n}\n"],"mappings":";;;AAAA,OAAO,QAAQ;AACf,OAAO,QAAQ;AACf,OAAO,UAAU;AAGjB,IAAM,WAAW,KAAK,KAAK,GAAG,QAAQ,GAAG,aAAa,MAAM;AAwC5D,IAAM,gBAA+B;AAAA,EACnC,WAAW;AAAA,EACX,cAAc;AAAA,EACd,iBAAiB,CAAC;AAAA,EAClB,aAAa;AAAA,EACb,sBAAsB;AAAA,EACtB,sBAAsB;AAAA,EACtB,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,aAAa;AAAA,EACb,WAAW;AAAA,EACX,aAAa;AAAA,EACb,WAAW;AACb;AAEA,SAAS,WAAW,OAAsB,OAAqC;AAC7E,UAAQ,MAAM,MAAM;AAAA,IAClB,KAAK;AACH,aAAO;AAAA,QACL,GAAG;AAAA,QACH,WAAW,MAAM;AAAA,QACjB,WAAW,MAAM;AAAA,MACnB;AAAA,IACF,KAAK;AACH,aAAO,EAAE,GAAG,OAAO,cAAc,MAAM,MAAM;AAAA,IAC/C,KAAK,mBAAmB;AACtB,YAAM,SAAS,MAAM;AACrB,aAAO;AAAA,QACL,GAAG;AAAA,QACH,cAAc;AAAA,QACd,iBAAiB,CAAC,GAAG,MAAM,iBAAiB,MAAM,KAAK;AAAA,QACvD,YAAa,QAAQ,cAAyB,MAAM;AAAA,QACpD,UAAW,QAAQ,YAAuB,MAAM;AAAA,MAClD;AAAA,IACF;AAAA,IACA,KAAK;AACH,aAAO,EAAE,GAAG,OAAO,cAAc,MAAM,aAAa,MAAM,MAAM;AAAA,IAClE,KAAK;AACH,aAAO;AAAA,QACL,GAAG;AAAA,QACH,iBAAiB,CAAC,GAAG,MAAM,iBAAiB,MAAM,KAAK;AAAA,MACzD;AAAA,IACF,KAAK;AACH,aAAO,EAAE,GAAG,OAAO,sBAAsB,KAAK;AAAA,IAChD,KAAK;AACH,aAAO,EAAE,GAAG,OAAO,sBAAsB,MAAM;AAAA,IACjD,KAAK;AACH,aAAO,EAAE,GAAG,OAAO,sBAAsB,KAAK;AAAA,IAChD,KAAK;AAAA,IACL,KAAK;AACH,aAAO,EAAE,GAAG,OAAO,sBAAsB,MAAM;AAAA,IACjD,KAAK;AACH,aAAO,EAAE,GAAG,OAAO,aAAa,MAAM,cAAc,KAAK;AAAA,IAC3D,KAAK;AACH,aAAO;AAAA,QACL,GAAG;AAAA,QACH,WAAW;AAAA,QACX,aAAa,MAAM;AAAA,QACnB,cAAc;AAAA,MAChB;AAAA,IACF;AACE,aAAO;AAAA,EACX;AACF;AASO,IAAM,eAAN,MAAmB;AAAA,EAIxB,YAA6B,QAAgB;AAAhB;AAC3B,OAAG,UAAU,QAAQ,EAAE,WAAW,KAAK,CAAC;AACxC,SAAK,YAAY,KAAK,KAAK,QAAQ,YAAY;AAC/C,SAAK,SAAS,KAAK,KAAK;AAAA,EAC1B;AAAA,EAJ6B;AAAA,EAHZ;AAAA,EACT,SAA0B,CAAC;AAAA,EAQnC,KAAK,OAA2D;AAC9D,UAAM,OAAO;AAAA,MACX,GAAG;AAAA,MACH,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IACpC;AACA,OAAG,eAAe,KAAK,WAAW,KAAK,UAAU,IAAI,IAAI,IAAI;AAC7D,SAAK,OAAO,KAAK,IAAI;AAAA,EACvB;AAAA,EAEA,IAAI,UAAyB;AAC3B,WAAO,KAAK,OAAO,OAAO,YAAY,EAAE,GAAG,cAAc,CAAC;AAAA,EAC5D;AAAA,EAEQ,OAAwB;AAC9B,QAAI,CAAC,GAAG,WAAW,KAAK,SAAS,EAAG,QAAO,CAAC;AAC5C,WAAO,GACJ,aAAa,KAAK,WAAW,MAAM,EACnC,MAAM,IAAI,EACV,OAAO,OAAO,EACd,IAAI,CAAC,SAAS,KAAK,MAAM,IAAI,CAAkB;AAAA,EACpD;AACF;AAKO,SAAS,UAAU,aAAqB,WAA2B;AACxE,SAAO,KAAK,KAAK,UAAU,aAAa,SAAS;AACnD;AAEO,SAAS,YAAY,aAAqB,WAA2B;AAC1E,SAAO,KAAK,KAAK,UAAU,aAAa,SAAS,GAAG,GAAG,SAAS,OAAO;AACzE;AAEO,SAAS,aAAa,aAAqB,WAA2B;AAC3E,SAAO,KAAK,KAAK,UAAU,aAAa,SAAS,GAAG,QAAQ;AAC9D;AAEO,SAAS,UAAU,QAAiC;AACzD,QAAM,YAAY,KAAK,KAAK,QAAQ,YAAY;AAChD,MAAI,CAAC,GAAG,WAAW,SAAS,EAAG,QAAO,CAAC;AACvC,SAAO,GACJ,aAAa,WAAW,MAAM,EAC9B,MAAM,IAAI,EACV,OAAO,OAAO,EACd,IAAI,CAAC,SAAS,KAAK,MAAM,IAAI,CAAkB;AACpD;","names":[]}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getOrCreateTopic,
|
|
3
|
+
getVerbosity
|
|
4
|
+
} from "./chunk-BM4R6NST.js";
|
|
5
|
+
import {
|
|
6
|
+
getBot
|
|
7
|
+
} from "./chunk-M4V3YOCY.js";
|
|
8
|
+
|
|
9
|
+
// src/utils/lock.ts
|
|
10
|
+
import fs from "fs";
|
|
11
|
+
import path from "path";
|
|
12
|
+
async function acquireLock(lockPath, stage = "init") {
|
|
13
|
+
const dir = path.dirname(lockPath);
|
|
14
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
15
|
+
const data = {
|
|
16
|
+
pid: process.pid,
|
|
17
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
18
|
+
stage
|
|
19
|
+
};
|
|
20
|
+
try {
|
|
21
|
+
fs.writeFileSync(lockPath, JSON.stringify(data), { flag: "wx" });
|
|
22
|
+
} catch (err) {
|
|
23
|
+
if (isNodeError(err) && err.code === "EEXIST") {
|
|
24
|
+
const existing = readLockData(lockPath);
|
|
25
|
+
if (existing && isProcessAlive(existing.pid)) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`Pipeline already running (PID ${existing.pid}, stage: ${existing.stage})`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
fs.writeFileSync(lockPath, JSON.stringify(data));
|
|
31
|
+
} else {
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const cleanup = () => {
|
|
36
|
+
tryUnlink(lockPath);
|
|
37
|
+
};
|
|
38
|
+
process.once("exit", cleanup);
|
|
39
|
+
process.once("SIGINT", () => {
|
|
40
|
+
cleanup();
|
|
41
|
+
process.exit(130);
|
|
42
|
+
});
|
|
43
|
+
process.once("SIGTERM", () => {
|
|
44
|
+
cleanup();
|
|
45
|
+
process.exit(143);
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
release() {
|
|
49
|
+
tryUnlink(lockPath);
|
|
50
|
+
process.removeListener("exit", cleanup);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function readLockData(lockPath) {
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(fs.readFileSync(lockPath, "utf8"));
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function isProcessAlive(pid) {
|
|
62
|
+
try {
|
|
63
|
+
process.kill(pid, 0);
|
|
64
|
+
return true;
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function tryUnlink(p) {
|
|
70
|
+
try {
|
|
71
|
+
fs.unlinkSync(p);
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function isNodeError(err) {
|
|
76
|
+
return err instanceof Error && "code" in err;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/integrations/telegram/notifier.ts
|
|
80
|
+
var STAGE_IDS = [
|
|
81
|
+
"fetch",
|
|
82
|
+
"analyze",
|
|
83
|
+
"clarify",
|
|
84
|
+
"code",
|
|
85
|
+
"git",
|
|
86
|
+
"review",
|
|
87
|
+
"deploy",
|
|
88
|
+
"test",
|
|
89
|
+
"notify"
|
|
90
|
+
];
|
|
91
|
+
var STAGE_EMOJI = {
|
|
92
|
+
fetch: "\u{1F4E5}",
|
|
93
|
+
analyze: "\u{1F50D}",
|
|
94
|
+
clarify: "\u{1F4AC}",
|
|
95
|
+
code: "\u{1F4BB}",
|
|
96
|
+
git: "\u{1F500}",
|
|
97
|
+
review: "\u{1F50E}",
|
|
98
|
+
deploy: "\u{1F680}",
|
|
99
|
+
test: "\u{1F9EA}",
|
|
100
|
+
notify: "\u{1F4E2}"
|
|
101
|
+
};
|
|
102
|
+
var STATUS_ICON = {
|
|
103
|
+
pending: "\u2B1C",
|
|
104
|
+
running: "\u25B6\uFE0F",
|
|
105
|
+
done: "\u2705",
|
|
106
|
+
skipped: "\u23ED",
|
|
107
|
+
failed: "\u274C"
|
|
108
|
+
};
|
|
109
|
+
function createTelegramNotifier(token, chatId, ticketKey, projectName, fallbackTopicId) {
|
|
110
|
+
const bot = getBot(token);
|
|
111
|
+
const stageStatus = Object.fromEntries(
|
|
112
|
+
STAGE_IDS.map((id) => [id, "pending"])
|
|
113
|
+
);
|
|
114
|
+
let progressMsgId;
|
|
115
|
+
let _topicResolved = false;
|
|
116
|
+
let _topicId = fallbackTopicId;
|
|
117
|
+
async function resolveTopicId() {
|
|
118
|
+
if (_topicResolved) return _topicId;
|
|
119
|
+
_topicResolved = true;
|
|
120
|
+
const created = await getOrCreateTopic(
|
|
121
|
+
bot,
|
|
122
|
+
chatId,
|
|
123
|
+
ticketKey,
|
|
124
|
+
projectName
|
|
125
|
+
).catch(() => void 0);
|
|
126
|
+
if (created !== void 0) _topicId = created;
|
|
127
|
+
return _topicId;
|
|
128
|
+
}
|
|
129
|
+
async function makeThreadOpts() {
|
|
130
|
+
const topicId = await resolveTopicId();
|
|
131
|
+
return topicId ? { message_thread_id: topicId } : {};
|
|
132
|
+
}
|
|
133
|
+
function buildProgressText() {
|
|
134
|
+
const pills = STAGE_IDS.map((id) => {
|
|
135
|
+
const emoji = STAGE_EMOJI[id];
|
|
136
|
+
const icon = STATUS_ICON[stageStatus[id] ?? "pending"];
|
|
137
|
+
return `${emoji}${icon}`;
|
|
138
|
+
}).join(" ");
|
|
139
|
+
const current = STAGE_IDS.find((id) => stageStatus[id] === "running");
|
|
140
|
+
const statusLine = current ? `
|
|
141
|
+
Stage: <b>${current}</b> \u2014 in progress\u2026` : "";
|
|
142
|
+
return `\u{1F504} <b>${ticketKey}</b>
|
|
143
|
+
${pills}${statusLine}`;
|
|
144
|
+
}
|
|
145
|
+
async function upsertProgress() {
|
|
146
|
+
const verbosity = getVerbosity(chatId);
|
|
147
|
+
if (verbosity === "low") return;
|
|
148
|
+
const text = buildProgressText();
|
|
149
|
+
const threadOpts = await makeThreadOpts();
|
|
150
|
+
if (progressMsgId !== void 0) {
|
|
151
|
+
try {
|
|
152
|
+
await bot.api.editMessageText(chatId, progressMsgId, text, {
|
|
153
|
+
parse_mode: "HTML"
|
|
154
|
+
});
|
|
155
|
+
return;
|
|
156
|
+
} catch {
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const msg = await bot.api.sendMessage(chatId, text, {
|
|
160
|
+
parse_mode: "HTML",
|
|
161
|
+
...threadOpts
|
|
162
|
+
});
|
|
163
|
+
progressMsgId = msg.message_id;
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
async send(message) {
|
|
167
|
+
const threadOpts = await makeThreadOpts();
|
|
168
|
+
const msg = await bot.api.sendMessage(chatId, message, {
|
|
169
|
+
parse_mode: "HTML",
|
|
170
|
+
...threadOpts
|
|
171
|
+
});
|
|
172
|
+
return msg.message_id;
|
|
173
|
+
},
|
|
174
|
+
async sendError(tk, err) {
|
|
175
|
+
const text = err instanceof Error ? err.message : String(err);
|
|
176
|
+
const threadOpts = await makeThreadOpts();
|
|
177
|
+
await bot.api.sendMessage(
|
|
178
|
+
chatId,
|
|
179
|
+
`\u274C <b>${tk}</b> pipeline failed
|
|
180
|
+
<code>${text}</code>`,
|
|
181
|
+
{ parse_mode: "HTML", ...threadOpts }
|
|
182
|
+
);
|
|
183
|
+
},
|
|
184
|
+
async sendDone(tk, { summary, prNumber, deployUrl }) {
|
|
185
|
+
const lines = [`\u2705 <b>${tk}</b> completed`, summary];
|
|
186
|
+
if (prNumber) lines.push(`PR: #${prNumber}`);
|
|
187
|
+
if (deployUrl) lines.push(`Deploy: ${deployUrl}`);
|
|
188
|
+
const threadOpts = await makeThreadOpts();
|
|
189
|
+
await bot.api.sendMessage(chatId, lines.join("\n"), {
|
|
190
|
+
parse_mode: "HTML",
|
|
191
|
+
...threadOpts
|
|
192
|
+
});
|
|
193
|
+
},
|
|
194
|
+
async notifyStageStarted(stage) {
|
|
195
|
+
stageStatus[stage] = "running";
|
|
196
|
+
await upsertProgress();
|
|
197
|
+
},
|
|
198
|
+
async notifyStageCompleted(stage) {
|
|
199
|
+
stageStatus[stage] = "done";
|
|
200
|
+
await upsertProgress();
|
|
201
|
+
},
|
|
202
|
+
async notifyStageFailed(stage, error) {
|
|
203
|
+
stageStatus[stage] = "failed";
|
|
204
|
+
await upsertProgress().catch(() => void 0);
|
|
205
|
+
const verbosity = getVerbosity(chatId);
|
|
206
|
+
if (verbosity !== "low") {
|
|
207
|
+
const threadOpts = await makeThreadOpts();
|
|
208
|
+
await bot.api.sendMessage(
|
|
209
|
+
chatId,
|
|
210
|
+
`\u274C Stage <b>${stage}</b> failed:
|
|
211
|
+
<code>${error.slice(0, 300)}</code>`,
|
|
212
|
+
{ parse_mode: "HTML", ...threadOpts }
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
async notifyStageSkipped(stage) {
|
|
217
|
+
stageStatus[stage] = "skipped";
|
|
218
|
+
await upsertProgress();
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export {
|
|
224
|
+
acquireLock,
|
|
225
|
+
readLockData,
|
|
226
|
+
createTelegramNotifier
|
|
227
|
+
};
|
|
228
|
+
//# sourceMappingURL=chunk-WEJCTFQB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/lock.ts","../src/integrations/telegram/notifier.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\nexport interface Lock {\n release(): void;\n}\n\ninterface LockData {\n pid: number;\n startedAt: string;\n stage: string;\n}\n\nexport async function acquireLock(\n lockPath: string,\n stage = \"init\",\n): Promise<Lock> {\n const dir = path.dirname(lockPath);\n fs.mkdirSync(dir, { recursive: true });\n\n const data: LockData = {\n pid: process.pid,\n startedAt: new Date().toISOString(),\n stage,\n };\n\n try {\n fs.writeFileSync(lockPath, JSON.stringify(data), { flag: \"wx\" }); // O_EXCL — fail if exists\n } catch (err: unknown) {\n if (isNodeError(err) && err.code === \"EEXIST\") {\n const existing = readLockData(lockPath);\n if (existing && isProcessAlive(existing.pid)) {\n throw new Error(\n `Pipeline already running (PID ${existing.pid}, stage: ${existing.stage})`,\n );\n }\n // Dead process — steal lock\n fs.writeFileSync(lockPath, JSON.stringify(data));\n } else {\n throw err;\n }\n }\n\n // Release on exit\n const cleanup = (): void => {\n tryUnlink(lockPath);\n };\n process.once(\"exit\", cleanup);\n process.once(\"SIGINT\", () => {\n cleanup();\n process.exit(130);\n });\n process.once(\"SIGTERM\", () => {\n cleanup();\n process.exit(143);\n });\n\n return {\n release() {\n tryUnlink(lockPath);\n process.removeListener(\"exit\", cleanup);\n },\n };\n}\n\nexport function readLockData(lockPath: string): LockData | null {\n try {\n return JSON.parse(fs.readFileSync(lockPath, \"utf8\")) as LockData;\n } catch {\n return null;\n }\n}\n\nfunction isProcessAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction tryUnlink(p: string): void {\n try {\n fs.unlinkSync(p);\n } catch {\n /* ignore */\n }\n}\n\nfunction isNodeError(err: unknown): err is NodeJS.ErrnoException {\n return err instanceof Error && \"code\" in err;\n}\n","import type { StageId } from \"../../config/schema.js\";\nimport { getBot } from \"./bot.js\";\nimport { getOrCreateTopic } from \"./topic-manager.js\";\nimport { getVerbosity } from \"./prefs.js\";\n\nexport interface TelegramNotifier {\n send(message: string): Promise<number>;\n sendError(ticketKey: string, err: unknown): Promise<void>;\n sendDone(\n ticketKey: string,\n opts: { summary: string; prNumber?: number; deployUrl?: string },\n ): Promise<void>;\n notifyStageStarted(stage: StageId): Promise<void>;\n notifyStageCompleted(stage: StageId): Promise<void>;\n notifyStageFailed(stage: StageId, error: string): Promise<void>;\n notifyStageSkipped(stage: StageId): Promise<void>;\n}\n\ntype StageStatus = \"pending\" | \"running\" | \"done\" | \"skipped\" | \"failed\";\n\nconst STAGE_IDS: StageId[] = [\n \"fetch\",\n \"analyze\",\n \"clarify\",\n \"code\",\n \"git\",\n \"review\",\n \"deploy\",\n \"test\",\n \"notify\",\n];\n\nconst STAGE_EMOJI: Record<StageId, string> = {\n fetch: \"📥\",\n analyze: \"🔍\",\n clarify: \"💬\",\n code: \"💻\",\n git: \"🔀\",\n review: \"🔎\",\n deploy: \"🚀\",\n test: \"🧪\",\n notify: \"📢\",\n};\n\nconst STATUS_ICON: Record<StageStatus, string> = {\n pending: \"⬜\",\n running: \"▶️\",\n done: \"✅\",\n skipped: \"⏭\",\n failed: \"❌\",\n};\n\nexport function createTelegramNotifier(\n token: string,\n chatId: number | string,\n ticketKey: string,\n projectName: string,\n fallbackTopicId?: number,\n): TelegramNotifier {\n const bot = getBot(token);\n\n // Stage progress state\n const stageStatus: Record<string, StageStatus> = Object.fromEntries(\n STAGE_IDS.map((id) => [id, \"pending\" as StageStatus]),\n );\n let progressMsgId: number | undefined;\n\n // Lazy topic resolution — try forum topic, fall back to configured topicId\n let _topicResolved = false;\n let _topicId: number | undefined = fallbackTopicId;\n\n async function resolveTopicId(): Promise<number | undefined> {\n if (_topicResolved) return _topicId;\n _topicResolved = true;\n const created = await getOrCreateTopic(\n bot,\n chatId,\n ticketKey,\n projectName,\n ).catch(() => undefined);\n if (created !== undefined) _topicId = created;\n return _topicId;\n }\n\n async function makeThreadOpts(): Promise<\n Record<string, number> | Record<string, never>\n > {\n const topicId = await resolveTopicId();\n return topicId ? { message_thread_id: topicId } : {};\n }\n\n function buildProgressText(): string {\n const pills = STAGE_IDS.map((id) => {\n const emoji = STAGE_EMOJI[id];\n const icon = STATUS_ICON[stageStatus[id] ?? \"pending\"];\n return `${emoji}${icon}`;\n }).join(\" \");\n\n const current = STAGE_IDS.find((id) => stageStatus[id] === \"running\");\n const statusLine = current\n ? `\\nStage: <b>${current}</b> — in progress…`\n : \"\";\n\n return `🔄 <b>${ticketKey}</b>\\n${pills}${statusLine}`;\n }\n\n async function upsertProgress(): Promise<void> {\n const verbosity = getVerbosity(chatId);\n if (verbosity === \"low\") return;\n\n const text = buildProgressText();\n const threadOpts = await makeThreadOpts();\n\n if (progressMsgId !== undefined) {\n try {\n await bot.api.editMessageText(chatId, progressMsgId, text, {\n parse_mode: \"HTML\",\n });\n return;\n } catch {\n // Message deleted or too old — fall through to send new\n }\n }\n\n const msg = await bot.api.sendMessage(chatId, text, {\n parse_mode: \"HTML\",\n ...threadOpts,\n });\n progressMsgId = msg.message_id;\n }\n\n return {\n async send(message) {\n const threadOpts = await makeThreadOpts();\n const msg = await bot.api.sendMessage(chatId, message, {\n parse_mode: \"HTML\",\n ...threadOpts,\n });\n return msg.message_id;\n },\n\n async sendError(tk, err) {\n const text = err instanceof Error ? err.message : String(err);\n const threadOpts = await makeThreadOpts();\n await bot.api.sendMessage(\n chatId,\n `❌ <b>${tk}</b> pipeline failed\\n<code>${text}</code>`,\n { parse_mode: \"HTML\", ...threadOpts },\n );\n },\n\n async sendDone(tk, { summary, prNumber, deployUrl }) {\n const lines = [`✅ <b>${tk}</b> completed`, summary];\n if (prNumber) lines.push(`PR: #${prNumber}`);\n if (deployUrl) lines.push(`Deploy: ${deployUrl}`);\n const threadOpts = await makeThreadOpts();\n await bot.api.sendMessage(chatId, lines.join(\"\\n\"), {\n parse_mode: \"HTML\",\n ...threadOpts,\n });\n },\n\n async notifyStageStarted(stage) {\n stageStatus[stage] = \"running\";\n await upsertProgress();\n },\n\n async notifyStageCompleted(stage) {\n stageStatus[stage] = \"done\";\n await upsertProgress();\n },\n\n async notifyStageFailed(stage, error) {\n stageStatus[stage] = \"failed\";\n await upsertProgress().catch(() => undefined);\n\n const verbosity = getVerbosity(chatId);\n if (verbosity !== \"low\") {\n const threadOpts = await makeThreadOpts();\n await bot.api.sendMessage(\n chatId,\n `❌ Stage <b>${stage}</b> failed:\\n<code>${error.slice(0, 300)}</code>`,\n { parse_mode: \"HTML\", ...threadOpts },\n );\n }\n },\n\n async notifyStageSkipped(stage) {\n stageStatus[stage] = \"skipped\";\n await upsertProgress();\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getBot
|
|
4
|
+
} from "./chunk-OJ4CNF73.js";
|
|
5
|
+
|
|
6
|
+
// src/integrations/telegram/human-loop.ts
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
var pending = /* @__PURE__ */ new Map();
|
|
10
|
+
function loadPendingStore(storeDir) {
|
|
11
|
+
const storePath = path.join(storeDir, "pending-clarifications.json");
|
|
12
|
+
if (!fs.existsSync(storePath)) return;
|
|
13
|
+
const items = JSON.parse(
|
|
14
|
+
fs.readFileSync(storePath, "utf8")
|
|
15
|
+
);
|
|
16
|
+
for (const item of items) {
|
|
17
|
+
if (new Date(item.expiresAt) > /* @__PURE__ */ new Date()) {
|
|
18
|
+
pending.set(item.ticketKey, {
|
|
19
|
+
...item,
|
|
20
|
+
resolve: () => {
|
|
21
|
+
},
|
|
22
|
+
reject: () => {
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function savePendingStore(storeDir) {
|
|
29
|
+
const storePath = path.join(storeDir, "pending-clarifications.json");
|
|
30
|
+
const items = Array.from(pending.values()).map(
|
|
31
|
+
({ resolve: _r, reject: _j, ...rest }) => rest
|
|
32
|
+
);
|
|
33
|
+
fs.writeFileSync(storePath, JSON.stringify(items, null, 2));
|
|
34
|
+
}
|
|
35
|
+
async function requestClarification(token, chatId, ticketKey, questions, storeDir, opts) {
|
|
36
|
+
const bot = getBot(token);
|
|
37
|
+
const threadOpts = opts.topicId ? { message_thread_id: opts.topicId } : {};
|
|
38
|
+
const formatted = [
|
|
39
|
+
`\u26A0\uFE0F <b>${ticketKey}</b> \u2014 C\u1EA7n clarify tr\u01B0\u1EDBc khi code`,
|
|
40
|
+
"",
|
|
41
|
+
...questions.map((q, i) => `${i + 1}. ${q}`),
|
|
42
|
+
"",
|
|
43
|
+
"Reply v\u1EDBi:",
|
|
44
|
+
`<code>/answer ${ticketKey}</code>`,
|
|
45
|
+
...questions.map((_, i) => `${i + 1}. (your answer)`)
|
|
46
|
+
].join("\n");
|
|
47
|
+
const msg = await bot.api.sendMessage(chatId, formatted, {
|
|
48
|
+
parse_mode: "HTML",
|
|
49
|
+
...threadOpts
|
|
50
|
+
});
|
|
51
|
+
const expiresAt = new Date(Date.now() + opts.timeoutMs).toISOString();
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
pending.set(ticketKey, {
|
|
54
|
+
ticketKey,
|
|
55
|
+
messageId: msg.message_id,
|
|
56
|
+
questions,
|
|
57
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
58
|
+
expiresAt,
|
|
59
|
+
resolve,
|
|
60
|
+
reject
|
|
61
|
+
});
|
|
62
|
+
savePendingStore(storeDir);
|
|
63
|
+
setTimeout(async () => {
|
|
64
|
+
if (pending.has(ticketKey)) {
|
|
65
|
+
await bot.api.sendMessage(
|
|
66
|
+
chatId,
|
|
67
|
+
`\u23F0 <b>${ticketKey}</b> \u2014 Nh\u1EAFc l\u1EA1i: c\u1EA7n clarify (c\xF2n ${Math.round(opts.timeoutMs * 0.5 / 6e4)}min)`,
|
|
68
|
+
{ parse_mode: "HTML", ...threadOpts }
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}, opts.timeoutMs * 0.5);
|
|
72
|
+
setTimeout(async () => {
|
|
73
|
+
if (!pending.has(ticketKey)) return;
|
|
74
|
+
pending.delete(ticketKey);
|
|
75
|
+
savePendingStore(storeDir);
|
|
76
|
+
if (opts.onTimeout === "abort") {
|
|
77
|
+
reject(new Error(`Clarification timeout for ${ticketKey}`));
|
|
78
|
+
} else if (opts.onTimeout === "skip") {
|
|
79
|
+
reject(new Error(`SKIP:Clarification timeout \u2014 ticket skipped`));
|
|
80
|
+
} else {
|
|
81
|
+
resolve(
|
|
82
|
+
"(No clarification received \u2014 proceeding with original description)"
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}, opts.timeoutMs);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function registerAnswerHandler(token, storeDir) {
|
|
89
|
+
const bot = getBot(token);
|
|
90
|
+
bot.command("answer", async (ctx) => {
|
|
91
|
+
const text = ctx.message?.text ?? "";
|
|
92
|
+
const lines = text.replace(/^\/answer(?:@\S+)?\s*/, "").split("\n");
|
|
93
|
+
const ticketKey = lines[0]?.trim();
|
|
94
|
+
const answers = lines.slice(1).join("\n").trim();
|
|
95
|
+
if (!ticketKey) return;
|
|
96
|
+
const entry = pending.get(ticketKey);
|
|
97
|
+
if (entry) {
|
|
98
|
+
pending.delete(ticketKey);
|
|
99
|
+
savePendingStore(storeDir);
|
|
100
|
+
entry.resolve(answers);
|
|
101
|
+
await ctx.reply(
|
|
102
|
+
`\u2705 Answers received for ${ticketKey}. Pipeline continues.`
|
|
103
|
+
);
|
|
104
|
+
} else {
|
|
105
|
+
await ctx.reply(`No pending clarification found for ${ticketKey}.`);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
var pendingApprovals = /* @__PURE__ */ new Map();
|
|
110
|
+
async function requestApproval(token, chatId, ticketKey, issues, opts) {
|
|
111
|
+
const bot = getBot(token);
|
|
112
|
+
const threadOpts = opts.topicId ? { message_thread_id: opts.topicId } : {};
|
|
113
|
+
const issueLines = issues.filter((i) => i.severity === "major").map((i) => `\u2022 ${i.message}`).join("\n");
|
|
114
|
+
const text = [
|
|
115
|
+
`\u26A0\uFE0F <b>${ticketKey}</b> \u2014 Review found major issues`,
|
|
116
|
+
"",
|
|
117
|
+
issueLines,
|
|
118
|
+
"",
|
|
119
|
+
"Approve to merge or reject to abort pipeline.",
|
|
120
|
+
`Use /approve ${ticketKey} or /reject ${ticketKey}`
|
|
121
|
+
].join("\n");
|
|
122
|
+
const msg = await bot.api.sendMessage(chatId, text, {
|
|
123
|
+
parse_mode: "HTML",
|
|
124
|
+
reply_markup: {
|
|
125
|
+
inline_keyboard: [
|
|
126
|
+
[
|
|
127
|
+
{ text: "\u2705 Approve & Merge", callback_data: `approve:${ticketKey}` },
|
|
128
|
+
{ text: "\u274C Reject", callback_data: `reject:${ticketKey}` }
|
|
129
|
+
]
|
|
130
|
+
]
|
|
131
|
+
},
|
|
132
|
+
...threadOpts
|
|
133
|
+
});
|
|
134
|
+
const expiresAt = new Date(Date.now() + opts.timeoutMs).toISOString();
|
|
135
|
+
return new Promise((resolve, reject) => {
|
|
136
|
+
pendingApprovals.set(ticketKey, {
|
|
137
|
+
ticketKey,
|
|
138
|
+
messageId: msg.message_id,
|
|
139
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
140
|
+
expiresAt,
|
|
141
|
+
resolve,
|
|
142
|
+
reject
|
|
143
|
+
});
|
|
144
|
+
setTimeout(() => {
|
|
145
|
+
if (!pendingApprovals.has(ticketKey)) return;
|
|
146
|
+
pendingApprovals.delete(ticketKey);
|
|
147
|
+
if (opts.onTimeout === "merge-anyway") {
|
|
148
|
+
resolve(true);
|
|
149
|
+
} else {
|
|
150
|
+
reject(new Error(`REVIEW_APPROVAL_TIMEOUT:${ticketKey}`));
|
|
151
|
+
}
|
|
152
|
+
}, opts.timeoutMs);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
function registerApprovalHandlers(token) {
|
|
156
|
+
const bot = getBot(token);
|
|
157
|
+
bot.on("callback_query:data", async (ctx) => {
|
|
158
|
+
const data = ctx.callbackQuery.data;
|
|
159
|
+
if (!data) return;
|
|
160
|
+
if (data.startsWith("approve:")) {
|
|
161
|
+
const ticketKey = data.slice("approve:".length);
|
|
162
|
+
const entry = pendingApprovals.get(ticketKey);
|
|
163
|
+
if (entry) {
|
|
164
|
+
pendingApprovals.delete(ticketKey);
|
|
165
|
+
entry.resolve(true);
|
|
166
|
+
await ctx.answerCallbackQuery({ text: `\u2705 Approved ${ticketKey}` });
|
|
167
|
+
await ctx.editMessageReplyMarkup({
|
|
168
|
+
reply_markup: { inline_keyboard: [] }
|
|
169
|
+
});
|
|
170
|
+
} else {
|
|
171
|
+
await ctx.answerCallbackQuery({ text: "No pending approval found." });
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (data.startsWith("reject:")) {
|
|
176
|
+
const ticketKey = data.slice("reject:".length);
|
|
177
|
+
const entry = pendingApprovals.get(ticketKey);
|
|
178
|
+
if (entry) {
|
|
179
|
+
pendingApprovals.delete(ticketKey);
|
|
180
|
+
entry.resolve(false);
|
|
181
|
+
await ctx.answerCallbackQuery({ text: `\u274C Rejected ${ticketKey}` });
|
|
182
|
+
await ctx.editMessageReplyMarkup({
|
|
183
|
+
reply_markup: { inline_keyboard: [] }
|
|
184
|
+
});
|
|
185
|
+
} else {
|
|
186
|
+
await ctx.answerCallbackQuery({ text: "No pending approval found." });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
bot.command("approve", async (ctx) => {
|
|
191
|
+
const ticketKey = ctx.message?.text?.replace(/^\/approve(?:@\S+)?\s*/, "").trim();
|
|
192
|
+
if (!ticketKey) return;
|
|
193
|
+
const entry = pendingApprovals.get(ticketKey);
|
|
194
|
+
if (entry) {
|
|
195
|
+
pendingApprovals.delete(ticketKey);
|
|
196
|
+
entry.resolve(true);
|
|
197
|
+
await ctx.reply(`\u2705 ${ticketKey} approved \u2014 proceeding to merge.`);
|
|
198
|
+
} else {
|
|
199
|
+
await ctx.reply(`No pending approval for ${ticketKey}.`);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
bot.command("reject", async (ctx) => {
|
|
203
|
+
const ticketKey = ctx.message?.text?.replace(/^\/reject(?:@\S+)?\s*/, "").trim();
|
|
204
|
+
if (!ticketKey) return;
|
|
205
|
+
const entry = pendingApprovals.get(ticketKey);
|
|
206
|
+
if (entry) {
|
|
207
|
+
pendingApprovals.delete(ticketKey);
|
|
208
|
+
entry.resolve(false);
|
|
209
|
+
await ctx.reply(`\u274C ${ticketKey} rejected \u2014 pipeline will abort.`);
|
|
210
|
+
} else {
|
|
211
|
+
await ctx.reply(`No pending approval for ${ticketKey}.`);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export {
|
|
217
|
+
loadPendingStore,
|
|
218
|
+
requestClarification,
|
|
219
|
+
registerAnswerHandler,
|
|
220
|
+
requestApproval,
|
|
221
|
+
registerApprovalHandlers
|
|
222
|
+
};
|
|
223
|
+
//# sourceMappingURL=chunk-YJK7IRPI.js.map
|