@iloom/cli 0.9.1 → 0.10.0
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/LICENSE +1 -1
- package/README.md +179 -41
- package/dist/{BranchNamingService-K6XNWQ6C.js → BranchNamingService-ECJHBB67.js} +2 -2
- package/dist/ClaudeContextManager-QXX6ZFST.js +14 -0
- package/dist/ClaudeService-NJNK2SUH.js +13 -0
- package/dist/{GitHubService-O7T6CFAJ.js → GitHubService-MEHKHUQP.js} +4 -4
- package/dist/IssueTrackerFactory-NG53YX5S.js +14 -0
- package/dist/{LoomLauncher-3I47SUPV.js → LoomLauncher-L64HHS3T.js} +9 -9
- package/dist/{MetadataManager-W3C54UYT.js → MetadataManager-5QZSTKNN.js} +2 -2
- package/dist/{ProjectCapabilityDetector-N5L7T4IY.js → ProjectCapabilityDetector-5KSYUTBJ.js} +3 -3
- package/dist/{PromptTemplateManager-36YLQRHP.js → PromptTemplateManager-DULSVRRE.js} +2 -2
- package/dist/README.md +179 -41
- package/dist/{SettingsManager-QR7V2IW2.js → SettingsManager-BQDQA3FK.js} +4 -2
- package/dist/agents/iloom-artifact-reviewer.md +11 -0
- package/dist/agents/iloom-code-reviewer.md +14 -0
- package/dist/agents/iloom-issue-analyze-and-plan.md +55 -12
- package/dist/agents/iloom-issue-analyzer.md +49 -6
- package/dist/agents/iloom-issue-complexity-evaluator.md +47 -6
- package/dist/agents/iloom-issue-enhancer.md +86 -7
- package/dist/agents/iloom-issue-implementer.md +48 -7
- package/dist/agents/iloom-issue-planner.md +115 -62
- package/dist/{build-IC4CJRMP.js → build-5GO3XW26.js} +9 -9
- package/dist/{chunk-USSL2X4A.js → chunk-3D7WQM7I.js} +2 -2
- package/dist/chunk-4232AHNQ.js +35 -0
- package/dist/chunk-4232AHNQ.js.map +1 -0
- package/dist/{chunk-QN47QVBX.js → chunk-4WJNIR5O.js} +1 -1
- package/dist/chunk-4WJNIR5O.js.map +1 -0
- package/dist/{chunk-2JPXGGP4.js → chunk-5MWV33NN.js} +4 -4
- package/dist/{chunk-POU2UMWN.js → chunk-6EU6TCF6.js} +10 -10
- package/dist/chunk-6EU6TCF6.js.map +1 -0
- package/dist/{chunk-Y5O2ALDZ.js → chunk-FB47TIJG.js} +29 -11
- package/dist/chunk-FB47TIJG.js.map +1 -0
- package/dist/chunk-HEXKPKCK.js +1396 -0
- package/dist/chunk-HEXKPKCK.js.map +1 -0
- package/dist/{chunk-KAYXR544.js → chunk-J5S7DFYC.js} +2 -2
- package/dist/{chunk-OK7LUTRW.js → chunk-JO2LZ6EQ.js} +476 -5
- package/dist/chunk-JO2LZ6EQ.js.map +1 -0
- package/dist/{chunk-KBEIQP4G.js → chunk-KB64WNBZ.js} +43 -3
- package/dist/chunk-KB64WNBZ.js.map +1 -0
- package/dist/{chunk-Y5HSSIK2.js → chunk-KXDRI47U.js} +71 -13
- package/dist/chunk-KXDRI47U.js.map +1 -0
- package/dist/{chunk-HZXBHMVM.js → chunk-LXLMMXXY.js} +54 -14
- package/dist/chunk-LXLMMXXY.js.map +1 -0
- package/dist/{chunk-H6ST2TGP.js → chunk-MNHZB4Z2.js} +4 -4
- package/dist/{chunk-TL72BGP6.js → chunk-MORRVYPT.js} +2 -2
- package/dist/{chunk-TGRK3CHF.js → chunk-NRSWLOAZ.js} +8 -8
- package/dist/chunk-NRSWLOAZ.js.map +1 -0
- package/dist/{chunk-FO5GGFOV.js → chunk-ONQYPICO.js} +13 -5
- package/dist/chunk-ONQYPICO.js.map +1 -0
- package/dist/{chunk-7ZEHSSUP.js → chunk-P4O6EH46.js} +4 -4
- package/dist/chunk-QZWEJVWV.js +207 -0
- package/dist/chunk-QZWEJVWV.js.map +1 -0
- package/dist/chunk-RSYT7MVI.js +202 -0
- package/dist/chunk-RSYT7MVI.js.map +1 -0
- package/dist/{chunk-OAVJR4PM.js → chunk-RYWFS37M.js} +6 -6
- package/dist/chunk-RYWFS37M.js.map +1 -0
- package/dist/{chunk-B7U6OKUR.js → chunk-SF2P22EE.js} +11 -3
- package/dist/chunk-SF2P22EE.js.map +1 -0
- package/dist/{chunk-MZPRBNYC.js → chunk-SN3SQCFK.js} +10 -8
- package/dist/{chunk-MZPRBNYC.js.map → chunk-SN3SQCFK.js.map} +1 -1
- package/dist/{chunk-4ZIHFUPN.js → chunk-UD3WJDIV.js} +145 -107
- package/dist/chunk-UD3WJDIV.js.map +1 -0
- package/dist/{chunk-3P6J4IZZ.js → chunk-UKBAJ2QQ.js} +61 -7
- package/dist/chunk-UKBAJ2QQ.js.map +1 -0
- package/dist/{chunk-RD7OPXZK.js → chunk-UVD4CZKS.js} +3 -3
- package/dist/chunk-UWGVCXRF.js +207 -0
- package/dist/chunk-UWGVCXRF.js.map +1 -0
- package/dist/{chunk-JT5LZRMI.js → chunk-VECNX6VX.js} +2 -2
- package/dist/{chunk-TRUMP4DA.js → chunk-VG45TUYK.js} +75 -6
- package/dist/chunk-VG45TUYK.js.map +1 -0
- package/dist/{chunk-4GAJJUYS.js → chunk-VGGST52X.js} +2 -2
- package/dist/{chunk-4LKGCFGG.js → chunk-WWKOVDWC.js} +2 -2
- package/dist/{chunk-2HZX6AMR.js → chunk-WY4QBK43.js} +7 -7
- package/dist/chunk-WY4QBK43.js.map +1 -0
- package/dist/chunk-Y4YZTHZE.js +73 -0
- package/dist/chunk-Y4YZTHZE.js.map +1 -0
- package/dist/{chunk-VOGGLPG5.js → chunk-YQ57ORTV.js} +14 -1
- package/dist/chunk-YQ57ORTV.js.map +1 -0
- package/dist/{chunk-XFEK2X2D.js → chunk-YYAKPQBT.js} +73 -20
- package/dist/chunk-YYAKPQBT.js.map +1 -0
- package/dist/{chunk-NTTSUAVM.js → chunk-ZEWU5PZK.js} +2 -2
- package/dist/{chunk-5LVVQGB3.js → chunk-ZHPNZC75.js} +17 -17
- package/dist/chunk-ZHPNZC75.js.map +1 -0
- package/dist/{chunk-I3HMNWQQ.js → chunk-ZW2LKWWE.js} +9 -9
- package/dist/chunk-ZW2LKWWE.js.map +1 -0
- package/dist/{claude-TP2QO3BU.js → claude-P3NQR6IJ.js} +2 -2
- package/dist/{cleanup-D3CSRBBZ.js → cleanup-6UCPVMFG.js} +81 -32
- package/dist/cleanup-6UCPVMFG.js.map +1 -0
- package/dist/cli.js +640 -350
- package/dist/cli.js.map +1 -1
- package/dist/{commit-IWGT42XN.js → commit-L3EPY5QG.js} +23 -21
- package/dist/commit-L3EPY5QG.js.map +1 -0
- package/dist/{compile-EOWJORKO.js → compile-ZS4HYRX5.js} +9 -9
- package/dist/{contribute-WSJTV2RX.js → contribute-ORDDQGSL.js} +14 -6
- package/dist/contribute-ORDDQGSL.js.map +1 -0
- package/dist/{dev-server-Q6M62ATG.js → dev-server-FYZ2AQIH.js} +29 -15
- package/dist/dev-server-FYZ2AQIH.js.map +1 -0
- package/dist/{feedback-QPNDZQRV.js → feedback-TMBXSCM5.js} +15 -15
- package/dist/{git-W3XUIFTR.js → git-ET64COO3.js} +4 -4
- package/dist/hooks/iloom-hook.js +15 -0
- package/dist/ignite-CGOV3TD4.js +1393 -0
- package/dist/ignite-CGOV3TD4.js.map +1 -0
- package/dist/index.d.ts +397 -53
- package/dist/index.js +1178 -40
- package/dist/index.js.map +1 -1
- package/dist/{init-ALYWKNWG.js → init-GFQ5W7GK.js} +57 -21
- package/dist/init-GFQ5W7GK.js.map +1 -0
- package/dist/issues-T4ZZSPEG.js +179 -0
- package/dist/issues-T4ZZSPEG.js.map +1 -0
- package/dist/{lint-IHUH45OC.js → lint-6TQXDZ3T.js} +9 -9
- package/dist/mcp/issue-management-server.js +2472 -257
- package/dist/mcp/issue-management-server.js.map +1 -1
- package/dist/mcp/recap-server.js +144 -21
- package/dist/mcp/recap-server.js.map +1 -1
- package/dist/{neon-helpers-VVFFTLXE.js → neon-helpers-CQN2PB4S.js} +3 -3
- package/dist/neon-helpers-CQN2PB4S.js.map +1 -0
- package/dist/{open-KWOV2OFO.js → open-5QZGXQRF.js} +15 -15
- package/dist/open-5QZGXQRF.js.map +1 -0
- package/dist/{plan-BRJBFJHF.js → plan-U7ZQWLFY.js} +41 -25
- package/dist/plan-U7ZQWLFY.js.map +1 -0
- package/dist/{projects-LH362JZQ.js → projects-2UOXFLNZ.js} +4 -4
- package/dist/prompts/CLAUDE.md +62 -0
- package/dist/prompts/init-prompt.txt +386 -47
- package/dist/prompts/issue-prompt.txt +427 -54
- package/dist/prompts/plan-prompt.txt +97 -16
- package/dist/prompts/pr-prompt.txt +44 -1
- package/dist/prompts/regular-prompt.txt +42 -1
- package/dist/prompts/session-summary-prompt.txt +14 -0
- package/dist/prompts/swarm-orchestrator-prompt.txt +437 -0
- package/dist/{rebase-AJOJOZUG.js → rebase-DWIB77KV.js} +10 -10
- package/dist/{recap-GKJXMDXW.js → recap-MX63HAKV.js} +47 -19
- package/dist/recap-MX63HAKV.js.map +1 -0
- package/dist/{run-QEUVZF7J.js → run-O3TFNQFC.js} +15 -15
- package/dist/run-O3TFNQFC.js.map +1 -0
- package/dist/schema/package-iloom.schema.json +58 -0
- package/dist/schema/settings.schema.json +130 -15
- package/dist/{shell-DAAVG4YN.js → shell-G6VC2CYR.js} +14 -7
- package/dist/shell-G6VC2CYR.js.map +1 -0
- package/dist/{summary-ZKOA35PT.js → summary-FWHAX55O.js} +27 -25
- package/dist/summary-FWHAX55O.js.map +1 -0
- package/dist/{test-5GPWWO3P.js → test-F7JNJZYP.js} +9 -9
- package/dist/{test-git-EJUKDB7F.js → test-git-BTAOIUE2.js} +4 -4
- package/dist/test-jira-CHYNV33F.js +96 -0
- package/dist/test-jira-CHYNV33F.js.map +1 -0
- package/dist/{test-prefix-23TOBUXY.js → test-prefix-Q6TFSU6F.js} +4 -4
- package/dist/{test-webserver-CKROHFBQ.js → test-webserver-EONCG7E7.js} +6 -6
- package/dist/{vscode-6TOLFCI2.js → vscode-VA5X4P25.js} +7 -7
- package/package.json +5 -1
- package/dist/ClaudeContextManager-X2Y72GRL.js +0 -14
- package/dist/ClaudeService-7P32TTES.js +0 -13
- package/dist/chunk-2HZX6AMR.js.map +0 -1
- package/dist/chunk-3P6J4IZZ.js.map +0 -1
- package/dist/chunk-4ZIHFUPN.js.map +0 -1
- package/dist/chunk-5LVVQGB3.js.map +0 -1
- package/dist/chunk-B7U6OKUR.js.map +0 -1
- package/dist/chunk-ENGCJIYQ.js +0 -520
- package/dist/chunk-ENGCJIYQ.js.map +0 -1
- package/dist/chunk-FO5GGFOV.js.map +0 -1
- package/dist/chunk-HZXBHMVM.js.map +0 -1
- package/dist/chunk-I3HMNWQQ.js.map +0 -1
- package/dist/chunk-J7FJ6PUT.js +0 -121
- package/dist/chunk-J7FJ6PUT.js.map +0 -1
- package/dist/chunk-KBEIQP4G.js.map +0 -1
- package/dist/chunk-OAVJR4PM.js.map +0 -1
- package/dist/chunk-OK7LUTRW.js.map +0 -1
- package/dist/chunk-POU2UMWN.js.map +0 -1
- package/dist/chunk-QN47QVBX.js.map +0 -1
- package/dist/chunk-TGRK3CHF.js.map +0 -1
- package/dist/chunk-TRUMP4DA.js.map +0 -1
- package/dist/chunk-VOGGLPG5.js.map +0 -1
- package/dist/chunk-XFEK2X2D.js.map +0 -1
- package/dist/chunk-Y5HSSIK2.js.map +0 -1
- package/dist/chunk-Y5O2ALDZ.js.map +0 -1
- package/dist/cleanup-D3CSRBBZ.js.map +0 -1
- package/dist/commit-IWGT42XN.js.map +0 -1
- package/dist/contribute-WSJTV2RX.js.map +0 -1
- package/dist/dev-server-Q6M62ATG.js.map +0 -1
- package/dist/ignite-OPO6EDYT.js +0 -784
- package/dist/ignite-OPO6EDYT.js.map +0 -1
- package/dist/init-ALYWKNWG.js.map +0 -1
- package/dist/issues-L7TBUPXT.js +0 -116
- package/dist/issues-L7TBUPXT.js.map +0 -1
- package/dist/open-KWOV2OFO.js.map +0 -1
- package/dist/plan-BRJBFJHF.js.map +0 -1
- package/dist/recap-GKJXMDXW.js.map +0 -1
- package/dist/run-QEUVZF7J.js.map +0 -1
- package/dist/shell-DAAVG4YN.js.map +0 -1
- package/dist/summary-ZKOA35PT.js.map +0 -1
- /package/dist/{BranchNamingService-K6XNWQ6C.js.map → BranchNamingService-ECJHBB67.js.map} +0 -0
- /package/dist/{ClaudeContextManager-X2Y72GRL.js.map → ClaudeContextManager-QXX6ZFST.js.map} +0 -0
- /package/dist/{ClaudeService-7P32TTES.js.map → ClaudeService-NJNK2SUH.js.map} +0 -0
- /package/dist/{GitHubService-O7T6CFAJ.js.map → GitHubService-MEHKHUQP.js.map} +0 -0
- /package/dist/{MetadataManager-W3C54UYT.js.map → IssueTrackerFactory-NG53YX5S.js.map} +0 -0
- /package/dist/{LoomLauncher-3I47SUPV.js.map → LoomLauncher-L64HHS3T.js.map} +0 -0
- /package/dist/{ProjectCapabilityDetector-N5L7T4IY.js.map → MetadataManager-5QZSTKNN.js.map} +0 -0
- /package/dist/{PromptTemplateManager-36YLQRHP.js.map → ProjectCapabilityDetector-5KSYUTBJ.js.map} +0 -0
- /package/dist/{SettingsManager-QR7V2IW2.js.map → PromptTemplateManager-DULSVRRE.js.map} +0 -0
- /package/dist/{claude-TP2QO3BU.js.map → SettingsManager-BQDQA3FK.js.map} +0 -0
- /package/dist/{build-IC4CJRMP.js.map → build-5GO3XW26.js.map} +0 -0
- /package/dist/{chunk-USSL2X4A.js.map → chunk-3D7WQM7I.js.map} +0 -0
- /package/dist/{chunk-2JPXGGP4.js.map → chunk-5MWV33NN.js.map} +0 -0
- /package/dist/{chunk-KAYXR544.js.map → chunk-J5S7DFYC.js.map} +0 -0
- /package/dist/{chunk-H6ST2TGP.js.map → chunk-MNHZB4Z2.js.map} +0 -0
- /package/dist/{chunk-TL72BGP6.js.map → chunk-MORRVYPT.js.map} +0 -0
- /package/dist/{chunk-7ZEHSSUP.js.map → chunk-P4O6EH46.js.map} +0 -0
- /package/dist/{chunk-RD7OPXZK.js.map → chunk-UVD4CZKS.js.map} +0 -0
- /package/dist/{chunk-JT5LZRMI.js.map → chunk-VECNX6VX.js.map} +0 -0
- /package/dist/{chunk-4GAJJUYS.js.map → chunk-VGGST52X.js.map} +0 -0
- /package/dist/{chunk-4LKGCFGG.js.map → chunk-WWKOVDWC.js.map} +0 -0
- /package/dist/{chunk-NTTSUAVM.js.map → chunk-ZEWU5PZK.js.map} +0 -0
- /package/dist/{git-W3XUIFTR.js.map → claude-P3NQR6IJ.js.map} +0 -0
- /package/dist/{compile-EOWJORKO.js.map → compile-ZS4HYRX5.js.map} +0 -0
- /package/dist/{feedback-QPNDZQRV.js.map → feedback-TMBXSCM5.js.map} +0 -0
- /package/dist/{neon-helpers-VVFFTLXE.js.map → git-ET64COO3.js.map} +0 -0
- /package/dist/{lint-IHUH45OC.js.map → lint-6TQXDZ3T.js.map} +0 -0
- /package/dist/{projects-LH362JZQ.js.map → projects-2UOXFLNZ.js.map} +0 -0
- /package/dist/{rebase-AJOJOZUG.js.map → rebase-DWIB77KV.js.map} +0 -0
- /package/dist/{test-5GPWWO3P.js.map → test-F7JNJZYP.js.map} +0 -0
- /package/dist/{test-git-EJUKDB7F.js.map → test-git-BTAOIUE2.js.map} +0 -0
- /package/dist/{test-prefix-23TOBUXY.js.map → test-prefix-Q6TFSU6F.js.map} +0 -0
- /package/dist/{test-webserver-CKROHFBQ.js.map → test-webserver-EONCG7E7.js.map} +0 -0
- /package/dist/{vscode-6TOLFCI2.js.map → vscode-VA5X4P25.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -475,6 +475,7 @@ var init_logger = __esm({
|
|
|
475
475
|
var SettingsManager_exports = {};
|
|
476
476
|
__export(SettingsManager_exports, {
|
|
477
477
|
AgentSettingsSchema: () => AgentSettingsSchema,
|
|
478
|
+
BaseAgentSettingsSchema: () => BaseAgentSettingsSchema,
|
|
478
479
|
CapabilitiesSettingsSchema: () => CapabilitiesSettingsSchema,
|
|
479
480
|
CapabilitiesSettingsSchemaNoDefaults: () => CapabilitiesSettingsSchemaNoDefaults,
|
|
480
481
|
DatabaseProvidersSettingsSchema: () => DatabaseProvidersSettingsSchema,
|
|
@@ -495,13 +496,30 @@ import path2 from "path";
|
|
|
495
496
|
import os from "os";
|
|
496
497
|
import { z } from "zod";
|
|
497
498
|
import deepmerge from "deepmerge";
|
|
498
|
-
|
|
499
|
+
function redactSensitiveFields(obj) {
|
|
500
|
+
if (obj === null || obj === void 0) return obj;
|
|
501
|
+
if (typeof obj !== "object") return obj;
|
|
502
|
+
if (Array.isArray(obj)) return obj.map(redactSensitiveFields);
|
|
503
|
+
const sensitiveKeys = ["apitoken", "token", "secret", "password"];
|
|
504
|
+
const result = {};
|
|
505
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
506
|
+
const lowerKey = key.toLowerCase();
|
|
507
|
+
if (sensitiveKeys.some((s) => lowerKey.includes(s)) && typeof value === "string") {
|
|
508
|
+
result[key] = "[REDACTED]";
|
|
509
|
+
} else if (typeof value === "object" && value !== null) {
|
|
510
|
+
result[key] = redactSensitiveFields(value);
|
|
511
|
+
} else {
|
|
512
|
+
result[key] = value;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return result;
|
|
516
|
+
}
|
|
517
|
+
var BaseAgentSettingsSchema, AgentSettingsSchema, SpinAgentSettingsSchema, PlanCommandSettingsSchema, SummarySettingsSchema, WorkflowPermissionSchema, WorkflowPermissionSchemaNoDefaults, WorkflowsSettingsSchema, WorkflowsSettingsSchemaNoDefaults, CapabilitiesSettingsSchema, CapabilitiesSettingsSchemaNoDefaults, NeonSettingsSchema, DatabaseProvidersSettingsSchema, IloomSettingsSchema, IloomSettingsSchemaNoDefaults, SettingsManager;
|
|
499
518
|
var init_SettingsManager = __esm({
|
|
500
519
|
"src/lib/SettingsManager.ts"() {
|
|
501
520
|
"use strict";
|
|
502
521
|
init_logger();
|
|
503
|
-
|
|
504
|
-
AgentSettingsSchema = z.object({
|
|
522
|
+
BaseAgentSettingsSchema = z.object({
|
|
505
523
|
model: z.enum(["sonnet", "opus", "haiku"]).optional().describe("Claude model shorthand: sonnet, opus, or haiku"),
|
|
506
524
|
enabled: z.boolean().optional().describe("Whether this agent is enabled. Defaults to true."),
|
|
507
525
|
providers: z.record(
|
|
@@ -510,6 +528,9 @@ var init_SettingsManager = __esm({
|
|
|
510
528
|
).optional().describe('Map of review providers to model names. Keys: claude, gemini, codex. Values: model name strings (e.g., "sonnet", "gemini-3-pro-preview", "gpt-5.2-codex")'),
|
|
511
529
|
review: z.boolean().optional().describe("Whether artifacts from this agent should be reviewed before posting (defaults to false)")
|
|
512
530
|
});
|
|
531
|
+
AgentSettingsSchema = BaseAgentSettingsSchema.extend({
|
|
532
|
+
agents: z.record(z.string(), BaseAgentSettingsSchema).optional().describe("Nested per-agent model overrides for swarm mode. Configure under agents.iloom-swarm-worker.agents.<agent-name>.model to set a different model for phase agents when running inside swarm workers. Fallback chain: swarm-specific agent model > explicit swarm worker model > base agent model. Only meaningful under the iloom-swarm-worker agent entry.")
|
|
533
|
+
});
|
|
513
534
|
SpinAgentSettingsSchema = z.object({
|
|
514
535
|
model: z.enum(["sonnet", "opus", "haiku"]).default("opus").describe("Claude model shorthand for spin orchestrator")
|
|
515
536
|
});
|
|
@@ -550,19 +571,17 @@ var init_SettingsManager = __esm({
|
|
|
550
571
|
regular: WorkflowPermissionSchemaNoDefaults.optional()
|
|
551
572
|
}).optional();
|
|
552
573
|
CapabilitiesSettingsSchema = z.object({
|
|
553
|
-
capabilities: z.array(z.enum(PROJECT_CAPABILITIES)).optional().describe("Explicitly declared project capabilities (auto-detected if not specified)"),
|
|
554
574
|
web: z.object({
|
|
555
575
|
basePort: z.number().min(1, "Base port must be >= 1").max(65535, "Base port must be <= 65535").optional().describe("Base port for web workspace port calculations (default: 3000)")
|
|
556
|
-
}).optional(),
|
|
576
|
+
}).optional().describe('Web dev server settings. To declare a project as a web project, add "web" to the capabilities array in .iloom/package.iloom.json or .iloom/package.iloom.local.json.'),
|
|
557
577
|
database: z.object({
|
|
558
578
|
databaseUrlEnvVarName: z.string().min(1, "Database URL variable name cannot be empty").regex(/^[A-Z_][A-Z0-9_]*$/, "Must be valid env var name (uppercase, underscores)").optional().default("DATABASE_URL").describe("Name of environment variable for database connection URL")
|
|
559
579
|
}).optional()
|
|
560
580
|
}).optional();
|
|
561
581
|
CapabilitiesSettingsSchemaNoDefaults = z.object({
|
|
562
|
-
capabilities: z.array(z.enum(PROJECT_CAPABILITIES)).optional().describe("Explicitly declared project capabilities (auto-detected if not specified)"),
|
|
563
582
|
web: z.object({
|
|
564
583
|
basePort: z.number().min(1, "Base port must be >= 1").max(65535, "Base port must be <= 65535").optional().describe("Base port for web workspace port calculations (default: 3000)")
|
|
565
|
-
}).optional(),
|
|
584
|
+
}).optional().describe('Web dev server settings. To declare a project as a web project, add "web" to the capabilities array in .iloom/package.iloom.json or .iloom/package.iloom.local.json.'),
|
|
566
585
|
database: z.object({
|
|
567
586
|
databaseUrlEnvVarName: z.string().min(1, "Database URL variable name cannot be empty").regex(/^[A-Z_][A-Z0-9_]*$/, "Must be valid env var name (uppercase, underscores)").optional().describe("Name of environment variable for database connection URL")
|
|
568
587
|
}).optional()
|
|
@@ -606,7 +625,7 @@ var init_SettingsManager = __esm({
|
|
|
606
625
|
copyGitIgnoredPatterns: z.array(z.string().min(1, "Pattern cannot be empty")).optional().describe(`Glob patterns for gitignored files to copy to looms (e.g., ["*.db", "data/*.sqlite"]). Great for local dbs and large test data files that are too big to commit to git. Note: .env (dotenv-flow) files, iloom's and claude's local settings are automatically copied and do not need to be specified here.`),
|
|
607
626
|
workflows: WorkflowsSettingsSchema.describe("Per-workflow-type permission configurations"),
|
|
608
627
|
agents: z.record(z.string(), AgentSettingsSchema).optional().nullable().describe(
|
|
609
|
-
|
|
628
|
+
'Per-agent configuration overrides. Available agents: iloom-issue-analyzer (analyzes issues), iloom-issue-planner (creates implementation plans), iloom-issue-analyze-and-plan (combined analysis and planning), iloom-issue-complexity-evaluator (evaluates complexity), iloom-issue-enhancer (enhances issue descriptions), iloom-issue-implementer (implements code changes), iloom-code-reviewer (reviews code changes against requirements), iloom-artifact-reviewer (reviews artifacts before posting), iloom-swarm-worker (swarm worker agent, dynamically generated). The iloom-swarm-worker agent supports a nested "agents" sub-record for configuring phase agent models specifically in swarm mode.'
|
|
610
629
|
),
|
|
611
630
|
spin: SpinAgentSettingsSchema.optional().describe(
|
|
612
631
|
"Spin orchestrator configuration. Model defaults to opus when not configured."
|
|
@@ -621,7 +640,7 @@ var init_SettingsManager = __esm({
|
|
|
621
640
|
databaseProviders: DatabaseProvidersSettingsSchema.describe("Database provider configurations"),
|
|
622
641
|
issueManagement: z.object({
|
|
623
642
|
// SYNC: If this default changes, update displayDefaultsBox() in src/utils/first-run-setup.ts
|
|
624
|
-
provider: z.enum(["github", "linear"]).optional().default("github").describe("Issue tracker provider (github, linear)"),
|
|
643
|
+
provider: z.enum(["github", "linear", "jira"]).optional().default("github").describe("Issue tracker provider (github, linear, jira)"),
|
|
625
644
|
github: z.object({
|
|
626
645
|
remote: z.string().min(1, "Remote name cannot be empty").describe("Git remote name to use for GitHub operations")
|
|
627
646
|
}).optional(),
|
|
@@ -629,6 +648,17 @@ var init_SettingsManager = __esm({
|
|
|
629
648
|
teamId: z.string().min(1, "Team ID cannot be empty").describe('Linear team identifier (e.g., "ENG", "PLAT")'),
|
|
630
649
|
branchFormat: z.string().optional().describe("Branch naming template for Linear issues"),
|
|
631
650
|
apiToken: z.string().optional().describe("Linear API token (lin_api_...). SECURITY: Store in settings.local.json only, never commit to source control.")
|
|
651
|
+
}).optional(),
|
|
652
|
+
jira: z.object({
|
|
653
|
+
host: z.string().min(1, "Jira host cannot be empty").describe('Jira instance URL (e.g., "https://yourcompany.atlassian.net")'),
|
|
654
|
+
username: z.string().min(1, "Jira username/email cannot be empty").describe("Jira username or email address"),
|
|
655
|
+
apiToken: z.string().optional().describe("Jira API token. SECURITY: Store in settings.local.json only, never commit to source control. Generate at: https://id.atlassian.com/manage-profile/security/api-tokens"),
|
|
656
|
+
projectKey: z.string().min(1, "Project key cannot be empty").describe('Jira project key (e.g., "PROJ", "ENG")'),
|
|
657
|
+
boardId: z.string().optional().describe("Jira board ID for sprint/workflow operations (optional)"),
|
|
658
|
+
transitionMappings: z.record(z.string(), z.string()).optional().describe('Map iloom states to Jira transition names (e.g., {"In Review": "Start Review"})'),
|
|
659
|
+
defaultIssueType: z.string().min(1).optional().default("Task").describe('Default Jira issue type name for creating issues (e.g., "Task", "Story", "Bug")'),
|
|
660
|
+
defaultSubtaskType: z.string().min(1).optional().default("Subtask").describe('Default Jira issue type name for creating subtasks/child issues (e.g., "Subtask", "Sub-task")'),
|
|
661
|
+
doneStatuses: z.array(z.string()).optional().default(["Done"]).describe('Status names to exclude from issue lists (e.g., ["Done", "Closed", "Verify"])')
|
|
632
662
|
}).optional()
|
|
633
663
|
}).optional().describe("Issue management configuration"),
|
|
634
664
|
mergeBehavior: z.object({
|
|
@@ -637,6 +667,9 @@ var init_SettingsManager = __esm({
|
|
|
637
667
|
remote: z.string().optional(),
|
|
638
668
|
autoCommitPush: z.boolean().optional().describe(
|
|
639
669
|
"Auto-commit and push after code review in draft PR mode. Defaults to true when mode is github-draft-pr."
|
|
670
|
+
),
|
|
671
|
+
openBrowserOnFinish: z.boolean().default(true).describe(
|
|
672
|
+
"Open the PR in the default browser after finishing in github-pr or github-draft-pr mode. Use --no-browser flag to override."
|
|
640
673
|
)
|
|
641
674
|
}).optional().describe("Merge behavior configuration: local (merge locally), github-pr (create PR), or github-draft-pr (create draft PR at start, mark ready on finish)"),
|
|
642
675
|
ide: z.object({
|
|
@@ -655,7 +688,10 @@ var init_SettingsManager = __esm({
|
|
|
655
688
|
}).optional().describe("Color synchronization settings for workspace identification"),
|
|
656
689
|
attribution: z.enum(["off", "upstreamOnly", "on"]).default("upstreamOnly").describe(
|
|
657
690
|
'Controls when iloom attribution appears in session summaries. "off" - never show attribution. "upstreamOnly" - only show for contributions to external repositories (e.g., open source). "on" - always show attribution.'
|
|
658
|
-
)
|
|
691
|
+
),
|
|
692
|
+
git: z.object({
|
|
693
|
+
commitTimeout: z.number().min(1e3, "Commit timeout must be at least 1000ms").max(6e5, "Commit timeout cannot exceed 600000ms (10 minutes)").default(6e4).describe("Timeout in milliseconds for git commit operations. Increase for long-running pre-commit hooks.")
|
|
694
|
+
}).default({}).describe("Git operation settings")
|
|
659
695
|
});
|
|
660
696
|
IloomSettingsSchemaNoDefaults = z.object({
|
|
661
697
|
mainBranch: z.string().min(1, "Settings 'mainBranch' cannot be empty").optional().describe("Name of the main/primary branch for the repository"),
|
|
@@ -687,7 +723,7 @@ var init_SettingsManager = __esm({
|
|
|
687
723
|
copyGitIgnoredPatterns: z.array(z.string().min(1, "Pattern cannot be empty")).optional().describe(`Glob patterns for gitignored files to copy to looms (e.g., ["*.db", "data/*.sqlite"]). Great for local dbs and large test data files that are too big to commit to git. Note: .env (dotenv-flow) files, iloom's and claude's local settings are automatically copied and do not need to be specified here.`),
|
|
688
724
|
workflows: WorkflowsSettingsSchemaNoDefaults.describe("Per-workflow-type permission configurations"),
|
|
689
725
|
agents: z.record(z.string(), AgentSettingsSchema).optional().nullable().describe(
|
|
690
|
-
|
|
726
|
+
'Per-agent configuration overrides. Available agents: iloom-issue-analyzer (analyzes issues), iloom-issue-planner (creates implementation plans), iloom-issue-analyze-and-plan (combined analysis and planning), iloom-issue-complexity-evaluator (evaluates complexity), iloom-issue-enhancer (enhances issue descriptions), iloom-issue-implementer (implements code changes), iloom-code-reviewer (reviews code changes against requirements), iloom-artifact-reviewer (reviews artifacts before posting), iloom-swarm-worker (swarm worker agent, dynamically generated). The iloom-swarm-worker agent supports a nested "agents" sub-record for configuring phase agent models specifically in swarm mode.'
|
|
691
727
|
),
|
|
692
728
|
spin: z.object({
|
|
693
729
|
model: z.enum(["sonnet", "opus", "haiku"]).optional()
|
|
@@ -703,7 +739,7 @@ var init_SettingsManager = __esm({
|
|
|
703
739
|
capabilities: CapabilitiesSettingsSchemaNoDefaults.describe("Project capability configurations"),
|
|
704
740
|
databaseProviders: DatabaseProvidersSettingsSchema.describe("Database provider configurations"),
|
|
705
741
|
issueManagement: z.object({
|
|
706
|
-
provider: z.enum(["github", "linear"]).optional().describe("Issue tracker provider (github, linear)"),
|
|
742
|
+
provider: z.enum(["github", "linear", "jira"]).optional().describe("Issue tracker provider (github, linear, jira)"),
|
|
707
743
|
github: z.object({
|
|
708
744
|
remote: z.string().min(1, "Remote name cannot be empty").describe("Git remote name to use for GitHub operations")
|
|
709
745
|
}).optional(),
|
|
@@ -711,6 +747,17 @@ var init_SettingsManager = __esm({
|
|
|
711
747
|
teamId: z.string().min(1, "Team ID cannot be empty").describe('Linear team identifier (e.g., "ENG", "PLAT")'),
|
|
712
748
|
branchFormat: z.string().optional().describe("Branch naming template for Linear issues"),
|
|
713
749
|
apiToken: z.string().optional().describe("Linear API token (lin_api_...). SECURITY: Store in settings.local.json only, never commit to source control.")
|
|
750
|
+
}).optional(),
|
|
751
|
+
jira: z.object({
|
|
752
|
+
host: z.string().min(1, "Jira host cannot be empty").describe('Jira instance URL (e.g., "https://yourcompany.atlassian.net")'),
|
|
753
|
+
username: z.string().min(1, "Jira username/email cannot be empty").describe("Jira username or email address"),
|
|
754
|
+
apiToken: z.string().optional().describe("Jira API token. SECURITY: Store in settings.local.json only, never commit to source control. Generate at: https://id.atlassian.com/manage-profile/security/api-tokens"),
|
|
755
|
+
projectKey: z.string().min(1, "Project key cannot be empty").describe('Jira project key (e.g., "PROJ", "ENG")'),
|
|
756
|
+
boardId: z.string().optional().describe("Jira board ID for sprint/workflow operations (optional)"),
|
|
757
|
+
transitionMappings: z.record(z.string(), z.string()).optional().describe('Map iloom states to Jira transition names (e.g., {"In Review": "Start Review"})'),
|
|
758
|
+
defaultIssueType: z.string().min(1).optional().describe('Default Jira issue type name for creating issues (e.g., "Task", "Story", "Bug")'),
|
|
759
|
+
defaultSubtaskType: z.string().min(1).optional().describe('Default Jira issue type name for creating subtasks/child issues (e.g., "Subtask", "Sub-task")'),
|
|
760
|
+
doneStatuses: z.array(z.string()).optional().default(["Done"]).describe('Status names to exclude from issue lists (e.g., ["Done", "Closed", "Verify"])')
|
|
714
761
|
}).optional()
|
|
715
762
|
}).optional().describe("Issue management configuration"),
|
|
716
763
|
mergeBehavior: z.object({
|
|
@@ -718,6 +765,9 @@ var init_SettingsManager = __esm({
|
|
|
718
765
|
remote: z.string().optional(),
|
|
719
766
|
autoCommitPush: z.boolean().optional().describe(
|
|
720
767
|
"Auto-commit and push after code review in draft PR mode. Defaults to true when mode is github-draft-pr."
|
|
768
|
+
),
|
|
769
|
+
openBrowserOnFinish: z.boolean().optional().describe(
|
|
770
|
+
"Open the PR in the default browser after finishing in github-pr or github-draft-pr mode. Use --no-browser flag to override."
|
|
721
771
|
)
|
|
722
772
|
}).optional().describe("Merge behavior configuration: local (merge locally), github-pr (create PR), or github-draft-pr (create draft PR at start, mark ready on finish)"),
|
|
723
773
|
ide: z.object({
|
|
@@ -735,7 +785,10 @@ var init_SettingsManager = __esm({
|
|
|
735
785
|
}).optional().describe("Color synchronization settings for workspace identification"),
|
|
736
786
|
attribution: z.enum(["off", "upstreamOnly", "on"]).optional().describe(
|
|
737
787
|
'Controls when iloom attribution appears in session summaries. "off" - never show attribution. "upstreamOnly" - only show for contributions to external repositories (e.g., open source). "on" - always show attribution.'
|
|
738
|
-
)
|
|
788
|
+
),
|
|
789
|
+
git: z.object({
|
|
790
|
+
commitTimeout: z.number().min(1e3, "Commit timeout must be at least 1000ms").max(6e5, "Commit timeout cannot exceed 600000ms (10 minutes)").optional().describe("Timeout in milliseconds for git commit operations. Increase for long-running pre-commit hooks.")
|
|
791
|
+
}).optional().describe("Git operation settings")
|
|
739
792
|
});
|
|
740
793
|
SettingsManager = class {
|
|
741
794
|
/**
|
|
@@ -751,19 +804,19 @@ var init_SettingsManager = __esm({
|
|
|
751
804
|
const root = this.getProjectRoot(projectRoot);
|
|
752
805
|
const globalSettings = await this.loadGlobalSettingsFile();
|
|
753
806
|
const globalSettingsPath = this.getGlobalSettingsPath();
|
|
754
|
-
logger.debug(`\u{1F30D} Global settings from ${globalSettingsPath}:`, JSON.stringify(globalSettings, null, 2));
|
|
807
|
+
logger.debug(`\u{1F30D} Global settings from ${globalSettingsPath}:`, JSON.stringify(redactSensitiveFields(globalSettings), null, 2));
|
|
755
808
|
const baseSettings = await this.loadSettingsFile(root, "settings.json");
|
|
756
809
|
const baseSettingsPath = path2.join(root, ".iloom", "settings.json");
|
|
757
|
-
logger.debug(`\u{1F4C4} Base settings from ${baseSettingsPath}:`, JSON.stringify(baseSettings, null, 2));
|
|
810
|
+
logger.debug(`\u{1F4C4} Base settings from ${baseSettingsPath}:`, JSON.stringify(redactSensitiveFields(baseSettings), null, 2));
|
|
758
811
|
const localSettings = await this.loadSettingsFile(root, "settings.local.json");
|
|
759
812
|
const localSettingsPath = path2.join(root, ".iloom", "settings.local.json");
|
|
760
|
-
logger.debug(`\u{1F4C4} Local settings from ${localSettingsPath}:`, JSON.stringify(localSettings, null, 2));
|
|
813
|
+
logger.debug(`\u{1F4C4} Local settings from ${localSettingsPath}:`, JSON.stringify(redactSensitiveFields(localSettings), null, 2));
|
|
761
814
|
let merged = this.mergeSettings(this.mergeSettings(globalSettings, baseSettings), localSettings);
|
|
762
|
-
logger.debug("\u{1F504} After merging global + base + local settings:", JSON.stringify(merged, null, 2));
|
|
815
|
+
logger.debug("\u{1F504} After merging global + base + local settings:", JSON.stringify(redactSensitiveFields(merged), null, 2));
|
|
763
816
|
if (cliOverrides && Object.keys(cliOverrides).length > 0) {
|
|
764
|
-
logger.debug("\u2699\uFE0F CLI overrides to apply:", JSON.stringify(cliOverrides, null, 2));
|
|
817
|
+
logger.debug("\u2699\uFE0F CLI overrides to apply:", JSON.stringify(redactSensitiveFields(cliOverrides), null, 2));
|
|
765
818
|
merged = this.mergeSettings(merged, cliOverrides);
|
|
766
|
-
logger.debug("\u{1F504} After applying CLI overrides:", JSON.stringify(merged, null, 2));
|
|
819
|
+
logger.debug("\u{1F504} After applying CLI overrides:", JSON.stringify(redactSensitiveFields(merged), null, 2));
|
|
767
820
|
}
|
|
768
821
|
try {
|
|
769
822
|
const finalSettings = IloomSettingsSchema.parse(merged);
|
|
@@ -786,7 +839,7 @@ Note: CLI overrides were applied. Check your --set arguments.`);
|
|
|
786
839
|
* Log the final merged configuration for debugging
|
|
787
840
|
*/
|
|
788
841
|
logFinalConfiguration(settings) {
|
|
789
|
-
logger.debug("\u{1F4CB} Final merged configuration:", JSON.stringify(settings, null, 2));
|
|
842
|
+
logger.debug("\u{1F4CB} Final merged configuration:", JSON.stringify(redactSensitiveFields(settings), null, 2));
|
|
790
843
|
}
|
|
791
844
|
/**
|
|
792
845
|
* Load and parse a single settings file
|
|
@@ -1247,6 +1300,7 @@ var MetadataManager = class {
|
|
|
1247
1300
|
branchName: data.branchName ?? null,
|
|
1248
1301
|
worktreePath: data.worktreePath ?? null,
|
|
1249
1302
|
issueType: data.issueType ?? null,
|
|
1303
|
+
issueKey: data.issueKey ?? null,
|
|
1250
1304
|
issue_numbers: data.issue_numbers ?? [],
|
|
1251
1305
|
pr_numbers: data.pr_numbers ?? [],
|
|
1252
1306
|
issueTracker: data.issueTracker ?? null,
|
|
@@ -1258,7 +1312,12 @@ var MetadataManager = class {
|
|
|
1258
1312
|
draftPrNumber: data.draftPrNumber ?? null,
|
|
1259
1313
|
oneShot: data.oneShot ?? null,
|
|
1260
1314
|
capabilities: data.capabilities ?? [],
|
|
1261
|
-
|
|
1315
|
+
state: data.state ?? null,
|
|
1316
|
+
childIssueNumbers: data.childIssueNumbers ?? [],
|
|
1317
|
+
parentLoom: data.parentLoom ?? null,
|
|
1318
|
+
childIssues: data.childIssues ?? [],
|
|
1319
|
+
dependencyMap: data.dependencyMap ?? {},
|
|
1320
|
+
mcpConfigPath: data.mcpConfigPath ?? null
|
|
1262
1321
|
};
|
|
1263
1322
|
}
|
|
1264
1323
|
/**
|
|
@@ -1311,6 +1370,7 @@ var MetadataManager = class {
|
|
|
1311
1370
|
branchName: input.branchName,
|
|
1312
1371
|
worktreePath: input.worktreePath,
|
|
1313
1372
|
issueType: input.issueType,
|
|
1373
|
+
...input.issueKey && { issueKey: input.issueKey },
|
|
1314
1374
|
issue_numbers: input.issue_numbers,
|
|
1315
1375
|
pr_numbers: input.pr_numbers,
|
|
1316
1376
|
issueTracker: input.issueTracker,
|
|
@@ -1322,7 +1382,12 @@ var MetadataManager = class {
|
|
|
1322
1382
|
capabilities: input.capabilities,
|
|
1323
1383
|
...input.draftPrNumber && { draftPrNumber: input.draftPrNumber },
|
|
1324
1384
|
...input.oneShot && { oneShot: input.oneShot },
|
|
1325
|
-
...input.
|
|
1385
|
+
...input.state && { state: input.state },
|
|
1386
|
+
...input.childIssueNumbers && input.childIssueNumbers.length > 0 && { childIssueNumbers: input.childIssueNumbers },
|
|
1387
|
+
...input.parentLoom && { parentLoom: input.parentLoom },
|
|
1388
|
+
...input.childIssues && input.childIssues.length > 0 && { childIssues: input.childIssues },
|
|
1389
|
+
...input.dependencyMap && Object.keys(input.dependencyMap).length > 0 && { dependencyMap: input.dependencyMap },
|
|
1390
|
+
...input.mcpConfigPath && { mcpConfigPath: input.mcpConfigPath }
|
|
1326
1391
|
};
|
|
1327
1392
|
const filePath = this.getFilePath(worktreePath);
|
|
1328
1393
|
await fs.writeFile(filePath, JSON.stringify(content, null, 2), { mode: 420 });
|
|
@@ -1398,6 +1463,34 @@ var MetadataManager = class {
|
|
|
1398
1463
|
}
|
|
1399
1464
|
return results;
|
|
1400
1465
|
}
|
|
1466
|
+
/**
|
|
1467
|
+
* Update existing metadata for a worktree by merging new fields
|
|
1468
|
+
*
|
|
1469
|
+
* Reads the existing metadata file, merges the provided updates,
|
|
1470
|
+
* and writes back. Only provided fields are overwritten.
|
|
1471
|
+
*
|
|
1472
|
+
* @param worktreePath - Absolute path to the worktree
|
|
1473
|
+
* @param updates - Partial metadata fields to merge
|
|
1474
|
+
*/
|
|
1475
|
+
async updateMetadata(worktreePath, updates) {
|
|
1476
|
+
try {
|
|
1477
|
+
const filePath = this.getFilePath(worktreePath);
|
|
1478
|
+
if (!await fs.pathExists(filePath)) {
|
|
1479
|
+
getLogger().warn(`No metadata file to update for worktree: ${worktreePath}`);
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
1483
|
+
const data = JSON.parse(content);
|
|
1484
|
+
const merged = { ...data, ...updates };
|
|
1485
|
+
await fs.writeFile(filePath, JSON.stringify(merged, null, 2), { mode: 420 });
|
|
1486
|
+
getLogger().debug(`Metadata updated for worktree: ${worktreePath}`);
|
|
1487
|
+
} catch (error) {
|
|
1488
|
+
getLogger().warn(
|
|
1489
|
+
`Failed to update metadata for worktree: ${error instanceof Error ? error.message : String(error)}`
|
|
1490
|
+
);
|
|
1491
|
+
throw error;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1401
1494
|
/**
|
|
1402
1495
|
* Delete metadata for a worktree (spec section 3.3)
|
|
1403
1496
|
*
|
|
@@ -2665,7 +2758,7 @@ async function fetchGhPR(prNumber, repo) {
|
|
|
2665
2758
|
"view",
|
|
2666
2759
|
String(prNumber),
|
|
2667
2760
|
"--json",
|
|
2668
|
-
"number,title,body,state,headRefName,baseRefName,url,isDraft,mergeable,createdAt,updatedAt"
|
|
2761
|
+
"number,title,body,state,headRefName,baseRefName,url,isDraft,isCrossRepository,mergeable,createdAt,updatedAt"
|
|
2669
2762
|
];
|
|
2670
2763
|
if (repo) {
|
|
2671
2764
|
args.push("--repo", repo);
|
|
@@ -2763,6 +2856,65 @@ async function createIssue(title, body, options) {
|
|
|
2763
2856
|
url: issueUrl
|
|
2764
2857
|
};
|
|
2765
2858
|
}
|
|
2859
|
+
async function getIssueNodeId(issueNumber, repo) {
|
|
2860
|
+
logger.debug("Fetching GitHub issue node ID", { issueNumber, repo });
|
|
2861
|
+
const args = ["issue", "view", String(issueNumber), "--json", "id"];
|
|
2862
|
+
if (repo) {
|
|
2863
|
+
args.push("--repo", repo);
|
|
2864
|
+
}
|
|
2865
|
+
const result = await executeGhCommand(args);
|
|
2866
|
+
return result.id;
|
|
2867
|
+
}
|
|
2868
|
+
async function getSubIssues(issueNumber, repo) {
|
|
2869
|
+
var _a, _b;
|
|
2870
|
+
logger.debug("Fetching GitHub sub-issues", { issueNumber, repo });
|
|
2871
|
+
const parentNodeId = await getIssueNodeId(issueNumber, repo);
|
|
2872
|
+
const query = `
|
|
2873
|
+
query getSubIssues($parentId: ID!) {
|
|
2874
|
+
node(id: $parentId) {
|
|
2875
|
+
... on Issue {
|
|
2876
|
+
subIssues(first: 100) {
|
|
2877
|
+
nodes {
|
|
2878
|
+
number
|
|
2879
|
+
title
|
|
2880
|
+
url
|
|
2881
|
+
state
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
`;
|
|
2888
|
+
try {
|
|
2889
|
+
const result = await executeGhCommand([
|
|
2890
|
+
"api",
|
|
2891
|
+
"graphql",
|
|
2892
|
+
"-H",
|
|
2893
|
+
"GraphQL-Features: sub_issues",
|
|
2894
|
+
"-f",
|
|
2895
|
+
`query=${query}`,
|
|
2896
|
+
"-F",
|
|
2897
|
+
`parentId=${parentNodeId}`
|
|
2898
|
+
]);
|
|
2899
|
+
const subIssues = ((_b = (_a = result.data.node) == null ? void 0 : _a.subIssues) == null ? void 0 : _b.nodes) ?? [];
|
|
2900
|
+
return subIssues.map((issue) => ({
|
|
2901
|
+
id: String(issue.number),
|
|
2902
|
+
title: issue.title,
|
|
2903
|
+
url: issue.url,
|
|
2904
|
+
state: issue.state.toLowerCase()
|
|
2905
|
+
}));
|
|
2906
|
+
} catch (error) {
|
|
2907
|
+
if (error instanceof Error) {
|
|
2908
|
+
const errorMessage = error.message;
|
|
2909
|
+
const stderr = "stderr" in error ? error.stderr ?? "" : "";
|
|
2910
|
+
const combinedError = `${errorMessage} ${stderr}`;
|
|
2911
|
+
if (combinedError.includes("sub_issues") || combinedError.includes("null")) {
|
|
2912
|
+
return [];
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
throw error;
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2766
2918
|
|
|
2767
2919
|
// src/utils/prompt.ts
|
|
2768
2920
|
init_logger();
|
|
@@ -2945,6 +3097,14 @@ var GitHubService = class {
|
|
|
2945
3097
|
const issue = await fetchGhIssue(issueNumber, repo);
|
|
2946
3098
|
return issue.url;
|
|
2947
3099
|
}
|
|
3100
|
+
async getChildIssues(parentIdentifier, repo) {
|
|
3101
|
+
const issueNum = parseInt(parentIdentifier, 10);
|
|
3102
|
+
if (isNaN(issueNum)) {
|
|
3103
|
+
getLogger().warn(`Invalid GitHub issue number: ${parentIdentifier}`);
|
|
3104
|
+
return [];
|
|
3105
|
+
}
|
|
3106
|
+
return getSubIssues(issueNum, repo);
|
|
3107
|
+
}
|
|
2948
3108
|
// GitHub Projects integration
|
|
2949
3109
|
async moveIssueToInProgress(issueNumber) {
|
|
2950
3110
|
getLogger().info("Moving issue to In Progress in GitHub Projects", {
|
|
@@ -2980,7 +3140,48 @@ var GitHubService = class {
|
|
|
2980
3140
|
await this.updateIssueStatusInProject(project, issueNumber, owner);
|
|
2981
3141
|
}
|
|
2982
3142
|
}
|
|
2983
|
-
|
|
3143
|
+
// GitHub Projects integration - move to Ready for Review
|
|
3144
|
+
async moveIssueToReadyForReview(issueNumber) {
|
|
3145
|
+
getLogger().info("Moving issue to Ready for Review in GitHub Projects", {
|
|
3146
|
+
issueNumber
|
|
3147
|
+
});
|
|
3148
|
+
if (!await hasProjectScope()) {
|
|
3149
|
+
getLogger().warn("Missing project scope in GitHub CLI auth");
|
|
3150
|
+
throw new GitHubError(
|
|
3151
|
+
"MISSING_SCOPE" /* MISSING_SCOPE */,
|
|
3152
|
+
"GitHub CLI lacks project scope. Run: gh auth refresh -s project"
|
|
3153
|
+
);
|
|
3154
|
+
}
|
|
3155
|
+
let owner;
|
|
3156
|
+
try {
|
|
3157
|
+
const repoInfo = await executeGhCommand(["repo", "view", "--json", "owner,name"]);
|
|
3158
|
+
owner = repoInfo.owner.login;
|
|
3159
|
+
} catch (error) {
|
|
3160
|
+
getLogger().warn("Could not determine repository info", { error });
|
|
3161
|
+
return;
|
|
3162
|
+
}
|
|
3163
|
+
let projects;
|
|
3164
|
+
try {
|
|
3165
|
+
projects = await fetchProjectList(owner);
|
|
3166
|
+
} catch (error) {
|
|
3167
|
+
getLogger().warn("Could not fetch projects", { owner, error });
|
|
3168
|
+
return;
|
|
3169
|
+
}
|
|
3170
|
+
if (!projects.length) {
|
|
3171
|
+
getLogger().warn("No projects found", { owner });
|
|
3172
|
+
return;
|
|
3173
|
+
}
|
|
3174
|
+
for (const project of projects) {
|
|
3175
|
+
await this.updateIssueStatusInProject(
|
|
3176
|
+
project,
|
|
3177
|
+
issueNumber,
|
|
3178
|
+
owner,
|
|
3179
|
+
["Ready for Review", "In Review", "Review"],
|
|
3180
|
+
"Ready for Review"
|
|
3181
|
+
);
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
async updateIssueStatusInProject(project, issueNumber, owner, statusNames = ["In Progress", "In progress"], logLabel = "In Progress") {
|
|
2984
3185
|
var _a;
|
|
2985
3186
|
let items;
|
|
2986
3187
|
try {
|
|
@@ -3011,11 +3212,13 @@ var GitHubService = class {
|
|
|
3011
3212
|
getLogger().debug("No Status field found in project", { projectNumber: project.number });
|
|
3012
3213
|
return;
|
|
3013
3214
|
}
|
|
3014
|
-
const
|
|
3015
|
-
(o) =>
|
|
3215
|
+
const targetOption = (_a = statusField.options) == null ? void 0 : _a.find(
|
|
3216
|
+
(o) => statusNames.some(
|
|
3217
|
+
(name) => o.name.toLowerCase() === name.toLowerCase()
|
|
3218
|
+
)
|
|
3016
3219
|
);
|
|
3017
|
-
if (!
|
|
3018
|
-
getLogger().debug(
|
|
3220
|
+
if (!targetOption) {
|
|
3221
|
+
getLogger().debug(`No ${logLabel} option found in Status field`, { projectNumber: project.number });
|
|
3019
3222
|
return;
|
|
3020
3223
|
}
|
|
3021
3224
|
try {
|
|
@@ -3023,16 +3226,21 @@ var GitHubService = class {
|
|
|
3023
3226
|
item.id,
|
|
3024
3227
|
project.id,
|
|
3025
3228
|
statusField.id,
|
|
3026
|
-
|
|
3229
|
+
targetOption.id
|
|
3027
3230
|
);
|
|
3028
3231
|
getLogger().info("Updated issue status in project", {
|
|
3029
3232
|
issueNumber,
|
|
3030
|
-
projectNumber: project.number
|
|
3233
|
+
projectNumber: project.number,
|
|
3234
|
+
status: logLabel
|
|
3031
3235
|
});
|
|
3032
3236
|
} catch (error) {
|
|
3033
3237
|
getLogger().debug("Could not update project item", { item: item.id, error });
|
|
3034
3238
|
}
|
|
3035
3239
|
}
|
|
3240
|
+
// Identifier normalization - GitHub identifiers are numeric, just stringify
|
|
3241
|
+
normalizeIdentifier(identifier) {
|
|
3242
|
+
return String(identifier);
|
|
3243
|
+
}
|
|
3036
3244
|
// Utility methods
|
|
3037
3245
|
extractContext(entity) {
|
|
3038
3246
|
if ("branch" in entity) {
|
|
@@ -3064,7 +3272,8 @@ State: ${entity.state}`;
|
|
|
3064
3272
|
branch: ghPR.headRefName,
|
|
3065
3273
|
baseBranch: ghPR.baseRefName,
|
|
3066
3274
|
url: ghPR.url,
|
|
3067
|
-
isDraft: ghPR.isDraft
|
|
3275
|
+
isDraft: ghPR.isDraft,
|
|
3276
|
+
isFork: ghPR.isCrossRepository
|
|
3068
3277
|
};
|
|
3069
3278
|
}
|
|
3070
3279
|
async promptUserConfirmation(message) {
|
|
@@ -3240,6 +3449,35 @@ async function updateLinearIssueState(identifier, stateName) {
|
|
|
3240
3449
|
handleLinearError(error, "updateLinearIssueState");
|
|
3241
3450
|
}
|
|
3242
3451
|
}
|
|
3452
|
+
async function getLinearChildIssues(identifier, options) {
|
|
3453
|
+
try {
|
|
3454
|
+
logger.debug(`Fetching child issues for Linear issue: ${identifier}`);
|
|
3455
|
+
const client = createLinearClient(options == null ? void 0 : options.apiToken);
|
|
3456
|
+
const issue = await client.issue(identifier);
|
|
3457
|
+
if (!issue) {
|
|
3458
|
+
throw new LinearServiceError("NOT_FOUND", `Linear issue ${identifier} not found`);
|
|
3459
|
+
}
|
|
3460
|
+
const children = await issue.children({ first: 100 });
|
|
3461
|
+
const results = await Promise.all(
|
|
3462
|
+
children.nodes.map(async (child) => {
|
|
3463
|
+
const stateObj = await child.state;
|
|
3464
|
+
const state = (stateObj == null ? void 0 : stateObj.name) ?? "unknown";
|
|
3465
|
+
return {
|
|
3466
|
+
id: child.identifier,
|
|
3467
|
+
title: child.title,
|
|
3468
|
+
url: child.url,
|
|
3469
|
+
state
|
|
3470
|
+
};
|
|
3471
|
+
})
|
|
3472
|
+
);
|
|
3473
|
+
return results;
|
|
3474
|
+
} catch (error) {
|
|
3475
|
+
if (error instanceof LinearServiceError) {
|
|
3476
|
+
throw error;
|
|
3477
|
+
}
|
|
3478
|
+
handleLinearError(error, "getLinearChildIssues");
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
3243
3481
|
|
|
3244
3482
|
// src/lib/LinearService.ts
|
|
3245
3483
|
var LinearService = class {
|
|
@@ -3353,6 +3591,15 @@ var LinearService = class {
|
|
|
3353
3591
|
const issue = await this.fetchIssue(identifier);
|
|
3354
3592
|
return issue.url;
|
|
3355
3593
|
}
|
|
3594
|
+
/**
|
|
3595
|
+
* Fetch child issues of a Linear parent issue
|
|
3596
|
+
* @param parentIdentifier - Linear issue identifier (e.g., "ENG-123")
|
|
3597
|
+
* @param _repo - Repository (unused for Linear)
|
|
3598
|
+
* @returns Array of child issues
|
|
3599
|
+
*/
|
|
3600
|
+
async getChildIssues(parentIdentifier, _repo) {
|
|
3601
|
+
return getLinearChildIssues(parentIdentifier, this.config.apiToken ? { apiToken: this.config.apiToken } : void 0);
|
|
3602
|
+
}
|
|
3356
3603
|
/**
|
|
3357
3604
|
* Move a Linear issue to "In Progress" state
|
|
3358
3605
|
* @param identifier - Linear issue identifier
|
|
@@ -3362,6 +3609,23 @@ var LinearService = class {
|
|
|
3362
3609
|
getLogger().info(`Moving Linear issue ${identifier} to In Progress`);
|
|
3363
3610
|
await updateLinearIssueState(String(identifier), "In Progress");
|
|
3364
3611
|
}
|
|
3612
|
+
/**
|
|
3613
|
+
* Move a Linear issue to "In Review" state
|
|
3614
|
+
* @param identifier - Linear issue identifier
|
|
3615
|
+
* @throws LinearServiceError if state update fails
|
|
3616
|
+
*/
|
|
3617
|
+
async moveIssueToReadyForReview(identifier) {
|
|
3618
|
+
getLogger().info(`Moving Linear issue ${identifier} to In Review`);
|
|
3619
|
+
await updateLinearIssueState(String(identifier), "In Review");
|
|
3620
|
+
}
|
|
3621
|
+
/**
|
|
3622
|
+
* Normalize identifier to canonical form (uppercase for Linear keys)
|
|
3623
|
+
* @param identifier - Linear issue identifier (e.g., "eng-123" or "ENG-123")
|
|
3624
|
+
* @returns Uppercase identifier (e.g., "ENG-123")
|
|
3625
|
+
*/
|
|
3626
|
+
normalizeIdentifier(identifier) {
|
|
3627
|
+
return String(identifier).toUpperCase();
|
|
3628
|
+
}
|
|
3365
3629
|
/**
|
|
3366
3630
|
* Extract issue context for AI prompts
|
|
3367
3631
|
* @param entity - Issue (Linear doesn't have PRs)
|
|
@@ -3393,6 +3657,844 @@ ${issue.body}`;
|
|
|
3393
3657
|
}
|
|
3394
3658
|
};
|
|
3395
3659
|
|
|
3660
|
+
// src/lib/providers/jira/JiraApiClient.ts
|
|
3661
|
+
import https from "https";
|
|
3662
|
+
|
|
3663
|
+
// src/lib/providers/jira/AdfMarkdownConverter.ts
|
|
3664
|
+
import { Parser } from "extended-markdown-adf-parser";
|
|
3665
|
+
var parser = new Parser();
|
|
3666
|
+
function sanitizeCodeMarks(node) {
|
|
3667
|
+
var _a;
|
|
3668
|
+
if ((_a = node.marks) == null ? void 0 : _a.some((mark) => mark.type === "code")) {
|
|
3669
|
+
node.marks = [{ type: "code" }];
|
|
3670
|
+
}
|
|
3671
|
+
if (node.content && Array.isArray(node.content)) {
|
|
3672
|
+
node.content = node.content.map((child) => sanitizeCodeMarks(child));
|
|
3673
|
+
}
|
|
3674
|
+
return node;
|
|
3675
|
+
}
|
|
3676
|
+
var BLOCK_LEVEL_TYPES = /* @__PURE__ */ new Set([
|
|
3677
|
+
"paragraph",
|
|
3678
|
+
"bulletList",
|
|
3679
|
+
"orderedList",
|
|
3680
|
+
"codeBlock",
|
|
3681
|
+
"heading",
|
|
3682
|
+
"blockquote",
|
|
3683
|
+
"rule",
|
|
3684
|
+
"mediaGroup",
|
|
3685
|
+
"nestedExpand",
|
|
3686
|
+
"panel",
|
|
3687
|
+
"table",
|
|
3688
|
+
"taskList",
|
|
3689
|
+
"decisionList",
|
|
3690
|
+
"mediaSingle"
|
|
3691
|
+
]);
|
|
3692
|
+
function wrapTableCellContent(node) {
|
|
3693
|
+
if (node.content && Array.isArray(node.content)) {
|
|
3694
|
+
node.content = node.content.map((child) => wrapTableCellContent(child));
|
|
3695
|
+
}
|
|
3696
|
+
if (node.type !== "tableCell" && node.type !== "tableHeader") {
|
|
3697
|
+
return node;
|
|
3698
|
+
}
|
|
3699
|
+
if (!node.content || node.content.length === 0) {
|
|
3700
|
+
return node;
|
|
3701
|
+
}
|
|
3702
|
+
const allInline = node.content.every((child) => !BLOCK_LEVEL_TYPES.has(child.type));
|
|
3703
|
+
if (allInline) {
|
|
3704
|
+
node.content = [{ type: "paragraph", content: node.content }];
|
|
3705
|
+
} else {
|
|
3706
|
+
const newContent = [];
|
|
3707
|
+
let inlineRun = [];
|
|
3708
|
+
for (const child of node.content) {
|
|
3709
|
+
if (BLOCK_LEVEL_TYPES.has(child.type)) {
|
|
3710
|
+
if (inlineRun.length > 0) {
|
|
3711
|
+
newContent.push({ type: "paragraph", content: inlineRun });
|
|
3712
|
+
inlineRun = [];
|
|
3713
|
+
}
|
|
3714
|
+
newContent.push(child);
|
|
3715
|
+
} else {
|
|
3716
|
+
inlineRun.push(child);
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3719
|
+
if (inlineRun.length > 0) {
|
|
3720
|
+
newContent.push({ type: "paragraph", content: inlineRun });
|
|
3721
|
+
}
|
|
3722
|
+
node.content = newContent;
|
|
3723
|
+
}
|
|
3724
|
+
return node;
|
|
3725
|
+
}
|
|
3726
|
+
var taskIdCounter = 0;
|
|
3727
|
+
function getCanonicalPlainText(text) {
|
|
3728
|
+
const miniAdf = parser.markdownToAdf(text);
|
|
3729
|
+
return getPlainText(miniAdf);
|
|
3730
|
+
}
|
|
3731
|
+
function extractCheckboxBlocks(markdown) {
|
|
3732
|
+
var _a, _b;
|
|
3733
|
+
const lines = markdown.split("\n");
|
|
3734
|
+
const blocks = [];
|
|
3735
|
+
let i = 0;
|
|
3736
|
+
while (i < lines.length) {
|
|
3737
|
+
const bulletLines = [];
|
|
3738
|
+
let blockIndent = null;
|
|
3739
|
+
while (i < lines.length) {
|
|
3740
|
+
const line = lines[i] ?? "";
|
|
3741
|
+
const checkboxMatch = line.match(/^(\s*)[-*+] \[([ xX])\] (.*)$/);
|
|
3742
|
+
if (checkboxMatch) {
|
|
3743
|
+
const indent = ((_a = checkboxMatch[1]) == null ? void 0 : _a.length) ?? 0;
|
|
3744
|
+
if (blockIndent === null) {
|
|
3745
|
+
blockIndent = indent;
|
|
3746
|
+
} else if (indent !== blockIndent) {
|
|
3747
|
+
break;
|
|
3748
|
+
}
|
|
3749
|
+
const state = checkboxMatch[2] === " " ? "TODO" : "DONE";
|
|
3750
|
+
bulletLines.push({ isCheckbox: true, state, rawText: checkboxMatch[3] ?? "" });
|
|
3751
|
+
i++;
|
|
3752
|
+
} else if (line.match(/^\s*[-*+] /)) {
|
|
3753
|
+
const indentMatch = line.match(/^(\s*)/);
|
|
3754
|
+
const indent = ((_b = indentMatch == null ? void 0 : indentMatch[1]) == null ? void 0 : _b.length) ?? 0;
|
|
3755
|
+
if (blockIndent === null) {
|
|
3756
|
+
blockIndent = indent;
|
|
3757
|
+
} else if (indent !== blockIndent) {
|
|
3758
|
+
break;
|
|
3759
|
+
}
|
|
3760
|
+
bulletLines.push({ isCheckbox: false, state: null, rawText: "" });
|
|
3761
|
+
i++;
|
|
3762
|
+
} else if (bulletLines.length > 0 && line.match(/^\s/) && line.trim() !== "") {
|
|
3763
|
+
const lastItem = bulletLines[bulletLines.length - 1];
|
|
3764
|
+
if (lastItem) {
|
|
3765
|
+
lastItem.rawText += "\n" + line.trim();
|
|
3766
|
+
}
|
|
3767
|
+
i++;
|
|
3768
|
+
} else {
|
|
3769
|
+
break;
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
if (bulletLines.length > 0) {
|
|
3773
|
+
const allCheckboxes = bulletLines.every((l) => l.isCheckbox);
|
|
3774
|
+
if (allCheckboxes) {
|
|
3775
|
+
blocks.push({
|
|
3776
|
+
states: bulletLines.map((l) => l.state),
|
|
3777
|
+
texts: bulletLines.map((l) => getCanonicalPlainText(l.rawText))
|
|
3778
|
+
});
|
|
3779
|
+
}
|
|
3780
|
+
} else {
|
|
3781
|
+
i++;
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3784
|
+
return blocks;
|
|
3785
|
+
}
|
|
3786
|
+
function getPlainText(node) {
|
|
3787
|
+
if (node.type === "text" && node.text !== void 0) return node.text;
|
|
3788
|
+
if (!node.content) return "";
|
|
3789
|
+
return node.content.map(getPlainText).join("");
|
|
3790
|
+
}
|
|
3791
|
+
function convertCheckboxesToTaskList(node, blocks) {
|
|
3792
|
+
const cursor = { index: 0 };
|
|
3793
|
+
return convertCheckboxesRecursive(node, blocks, cursor);
|
|
3794
|
+
}
|
|
3795
|
+
function convertCheckboxesRecursive(node, blocks, cursor) {
|
|
3796
|
+
var _a;
|
|
3797
|
+
if (node.type === "bulletList" && node.content && node.content.length > 0 && cursor.index < blocks.length) {
|
|
3798
|
+
const block = blocks[cursor.index];
|
|
3799
|
+
if (!block) return node;
|
|
3800
|
+
if (node.content.length === block.states.length) {
|
|
3801
|
+
const plaintexts = node.content.map((listItem) => getPlainText(listItem));
|
|
3802
|
+
const matches = plaintexts.every((text, i) => text === block.texts[i]);
|
|
3803
|
+
if (matches) {
|
|
3804
|
+
const allSimple = node.content.every((item) => {
|
|
3805
|
+
var _a2, _b;
|
|
3806
|
+
return ((_a2 = item.content) == null ? void 0 : _a2.length) === 1 && ((_b = item.content[0]) == null ? void 0 : _b.type) === "paragraph";
|
|
3807
|
+
});
|
|
3808
|
+
if (!allSimple) {
|
|
3809
|
+
cursor.index++;
|
|
3810
|
+
return node;
|
|
3811
|
+
}
|
|
3812
|
+
cursor.index++;
|
|
3813
|
+
node.type = "taskList";
|
|
3814
|
+
node.attrs = { localId: `tasklist-${++taskIdCounter}` };
|
|
3815
|
+
for (const [i, listItem] of node.content.entries()) {
|
|
3816
|
+
listItem.type = "taskItem";
|
|
3817
|
+
listItem.attrs = {
|
|
3818
|
+
localId: `task-${++taskIdCounter}`,
|
|
3819
|
+
state: block.states[i]
|
|
3820
|
+
};
|
|
3821
|
+
const firstChild = (_a = listItem.content) == null ? void 0 : _a[0];
|
|
3822
|
+
if ((firstChild == null ? void 0 : firstChild.type) === "paragraph" && firstChild.content) {
|
|
3823
|
+
listItem.content = firstChild.content;
|
|
3824
|
+
}
|
|
3825
|
+
}
|
|
3826
|
+
return node;
|
|
3827
|
+
}
|
|
3828
|
+
}
|
|
3829
|
+
}
|
|
3830
|
+
if (node.content && Array.isArray(node.content)) {
|
|
3831
|
+
node.content = node.content.map((child) => convertCheckboxesRecursive(child, blocks, cursor));
|
|
3832
|
+
}
|
|
3833
|
+
return node;
|
|
3834
|
+
}
|
|
3835
|
+
function convertDetailsToExpandSyntax(markdown) {
|
|
3836
|
+
if (!markdown) return markdown;
|
|
3837
|
+
let previousText = "";
|
|
3838
|
+
let currentText = markdown;
|
|
3839
|
+
while (previousText !== currentText) {
|
|
3840
|
+
previousText = currentText;
|
|
3841
|
+
currentText = currentText.replace(
|
|
3842
|
+
/<details[^>]*>\s*<summary[^>]*>([\s\S]*?)<\/summary>([\s\S]*?)<\/details>/gi,
|
|
3843
|
+
(_match, summary, content) => {
|
|
3844
|
+
const cleanSummary = summary.trim().replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'");
|
|
3845
|
+
let cleanContent = content.trim();
|
|
3846
|
+
cleanContent = cleanContent.replace(/\n{3,}/g, "\n\n");
|
|
3847
|
+
if (cleanContent) {
|
|
3848
|
+
return `~~~expand title="${cleanSummary}"
|
|
3849
|
+
${cleanContent}
|
|
3850
|
+
~~~`;
|
|
3851
|
+
} else {
|
|
3852
|
+
return `~~~expand title="${cleanSummary}"
|
|
3853
|
+
~~~`;
|
|
3854
|
+
}
|
|
3855
|
+
}
|
|
3856
|
+
);
|
|
3857
|
+
}
|
|
3858
|
+
return currentText;
|
|
3859
|
+
}
|
|
3860
|
+
function adfToMarkdown(adf) {
|
|
3861
|
+
if (!adf) return "";
|
|
3862
|
+
if (typeof adf === "string") return adf;
|
|
3863
|
+
return parser.adfToMarkdown(adf);
|
|
3864
|
+
}
|
|
3865
|
+
function markdownToAdf(markdown) {
|
|
3866
|
+
if (!markdown) {
|
|
3867
|
+
return { type: "doc", version: 1, content: [] };
|
|
3868
|
+
}
|
|
3869
|
+
taskIdCounter = 0;
|
|
3870
|
+
const checkboxBlocks = extractCheckboxBlocks(markdown);
|
|
3871
|
+
const preprocessed = convertDetailsToExpandSyntax(markdown);
|
|
3872
|
+
const adf = parser.markdownToAdf(preprocessed);
|
|
3873
|
+
let result = sanitizeCodeMarks(adf);
|
|
3874
|
+
result = wrapTableCellContent(result);
|
|
3875
|
+
result = convertCheckboxesToTaskList(result, checkboxBlocks);
|
|
3876
|
+
return result;
|
|
3877
|
+
}
|
|
3878
|
+
|
|
3879
|
+
// src/lib/providers/jira/JiraApiClient.ts
|
|
3880
|
+
var JiraApiClient = class {
|
|
3881
|
+
constructor(config) {
|
|
3882
|
+
this.baseUrl = `${config.host.replace(/\/$/, "")}/rest/api/3`;
|
|
3883
|
+
const credentials = Buffer.from(`${config.username}:${config.apiToken}`).toString("base64");
|
|
3884
|
+
this.authHeader = `Basic ${credentials}`;
|
|
3885
|
+
}
|
|
3886
|
+
/**
|
|
3887
|
+
* Make an HTTP request to Jira API
|
|
3888
|
+
*/
|
|
3889
|
+
async request(method, endpoint, body) {
|
|
3890
|
+
const url = new URL(`${this.baseUrl}${endpoint}`);
|
|
3891
|
+
getLogger().debug(`Jira API ${method} request`, { url: url.toString() });
|
|
3892
|
+
if (body) {
|
|
3893
|
+
getLogger().debug("Jira API request body", JSON.stringify(body, null, 2));
|
|
3894
|
+
}
|
|
3895
|
+
return new Promise((resolve, reject) => {
|
|
3896
|
+
const options = {
|
|
3897
|
+
hostname: url.hostname,
|
|
3898
|
+
port: url.port || 443,
|
|
3899
|
+
path: url.pathname + url.search,
|
|
3900
|
+
method,
|
|
3901
|
+
headers: {
|
|
3902
|
+
"Authorization": this.authHeader,
|
|
3903
|
+
"Accept": "application/json",
|
|
3904
|
+
"Content-Type": "application/json"
|
|
3905
|
+
}
|
|
3906
|
+
};
|
|
3907
|
+
const req = https.request({ ...options, timeout: 3e4 }, (res) => {
|
|
3908
|
+
const chunks = [];
|
|
3909
|
+
res.on("data", (chunk) => {
|
|
3910
|
+
chunks.push(chunk);
|
|
3911
|
+
});
|
|
3912
|
+
res.on("end", () => {
|
|
3913
|
+
var _a;
|
|
3914
|
+
const data = Buffer.concat(chunks).toString("utf8");
|
|
3915
|
+
if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
|
|
3916
|
+
let errorDetail = data;
|
|
3917
|
+
try {
|
|
3918
|
+
const parsed = JSON.parse(data);
|
|
3919
|
+
const parts = [];
|
|
3920
|
+
if ((_a = parsed.errorMessages) == null ? void 0 : _a.length) {
|
|
3921
|
+
parts.push(`messages: ${parsed.errorMessages.join(", ")}`);
|
|
3922
|
+
}
|
|
3923
|
+
if (parsed.errors && Object.keys(parsed.errors).length) {
|
|
3924
|
+
parts.push(`field errors: ${JSON.stringify(parsed.errors)}`);
|
|
3925
|
+
}
|
|
3926
|
+
if (parts.length) {
|
|
3927
|
+
errorDetail = parts.join("; ");
|
|
3928
|
+
}
|
|
3929
|
+
} catch {
|
|
3930
|
+
}
|
|
3931
|
+
reject(new Error(`Jira API error (${res.statusCode}): ${errorDetail}`));
|
|
3932
|
+
return;
|
|
3933
|
+
}
|
|
3934
|
+
if (res.statusCode === 204 || !data) {
|
|
3935
|
+
resolve({});
|
|
3936
|
+
return;
|
|
3937
|
+
}
|
|
3938
|
+
try {
|
|
3939
|
+
resolve(JSON.parse(data));
|
|
3940
|
+
} catch (error) {
|
|
3941
|
+
reject(new Error(`Failed to parse Jira API response: ${error}`));
|
|
3942
|
+
}
|
|
3943
|
+
});
|
|
3944
|
+
});
|
|
3945
|
+
req.on("timeout", () => {
|
|
3946
|
+
req.destroy();
|
|
3947
|
+
reject(new Error("Jira API request timed out after 30 seconds"));
|
|
3948
|
+
});
|
|
3949
|
+
req.on("error", (error) => {
|
|
3950
|
+
reject(new Error(`Jira API request failed: ${error.message}`));
|
|
3951
|
+
});
|
|
3952
|
+
if (body) {
|
|
3953
|
+
req.write(JSON.stringify(body));
|
|
3954
|
+
}
|
|
3955
|
+
req.end();
|
|
3956
|
+
});
|
|
3957
|
+
}
|
|
3958
|
+
/**
|
|
3959
|
+
* Make a GET request to Jira API
|
|
3960
|
+
*/
|
|
3961
|
+
async get(endpoint) {
|
|
3962
|
+
return this.request("GET", endpoint);
|
|
3963
|
+
}
|
|
3964
|
+
/**
|
|
3965
|
+
* Make a POST request to Jira API
|
|
3966
|
+
*/
|
|
3967
|
+
async post(endpoint, body) {
|
|
3968
|
+
return this.request("POST", endpoint, body);
|
|
3969
|
+
}
|
|
3970
|
+
/**
|
|
3971
|
+
* Make a PUT request to Jira API
|
|
3972
|
+
*/
|
|
3973
|
+
async put(endpoint, body) {
|
|
3974
|
+
return this.request("PUT", endpoint, body);
|
|
3975
|
+
}
|
|
3976
|
+
/**
|
|
3977
|
+
* Make a DELETE request to Jira API
|
|
3978
|
+
*/
|
|
3979
|
+
async delete(endpoint) {
|
|
3980
|
+
await this.request("DELETE", endpoint);
|
|
3981
|
+
}
|
|
3982
|
+
/**
|
|
3983
|
+
* Fetch an issue by key (e.g., "PROJ-123")
|
|
3984
|
+
*/
|
|
3985
|
+
async getIssue(issueKey) {
|
|
3986
|
+
return this.get(`/issue/${issueKey}`);
|
|
3987
|
+
}
|
|
3988
|
+
/**
|
|
3989
|
+
* Add a comment to an issue
|
|
3990
|
+
* Accepts Markdown content which is converted to ADF for Jira
|
|
3991
|
+
*/
|
|
3992
|
+
async addComment(issueKey, body) {
|
|
3993
|
+
const adfBody = markdownToAdf(body);
|
|
3994
|
+
getLogger().debug("Adding comment to Jira issue", { issueKey, bodyLength: body.length });
|
|
3995
|
+
return this.post(`/issue/${issueKey}/comment`, {
|
|
3996
|
+
body: adfBody
|
|
3997
|
+
});
|
|
3998
|
+
}
|
|
3999
|
+
/**
|
|
4000
|
+
* Get all comments for an issue
|
|
4001
|
+
*/
|
|
4002
|
+
async getComments(issueKey) {
|
|
4003
|
+
const response = await this.get(`/issue/${issueKey}/comment?maxResults=5000`);
|
|
4004
|
+
if (response.total > response.comments.length) {
|
|
4005
|
+
getLogger().warn(`Comments truncated for issue ${issueKey}: returned ${response.comments.length} of ${response.total} total comments`);
|
|
4006
|
+
}
|
|
4007
|
+
return response.comments;
|
|
4008
|
+
}
|
|
4009
|
+
/**
|
|
4010
|
+
* Update a comment on an issue
|
|
4011
|
+
* Accepts Markdown content which is converted to ADF for Jira
|
|
4012
|
+
*/
|
|
4013
|
+
async updateComment(issueKey, commentId, body) {
|
|
4014
|
+
return this.put(`/issue/${issueKey}/comment/${commentId}`, {
|
|
4015
|
+
body: markdownToAdf(body)
|
|
4016
|
+
});
|
|
4017
|
+
}
|
|
4018
|
+
/**
|
|
4019
|
+
* Get available transitions for an issue
|
|
4020
|
+
*/
|
|
4021
|
+
async getTransitions(issueKey) {
|
|
4022
|
+
const response = await this.get(`/issue/${issueKey}/transitions`);
|
|
4023
|
+
return response.transitions;
|
|
4024
|
+
}
|
|
4025
|
+
/**
|
|
4026
|
+
* Transition an issue to a new state
|
|
4027
|
+
*/
|
|
4028
|
+
async transitionIssue(issueKey, transitionId) {
|
|
4029
|
+
await this.post(`/issue/${issueKey}/transitions`, {
|
|
4030
|
+
transition: {
|
|
4031
|
+
id: transitionId
|
|
4032
|
+
}
|
|
4033
|
+
});
|
|
4034
|
+
}
|
|
4035
|
+
/**
|
|
4036
|
+
* Create a new issue
|
|
4037
|
+
* Accepts Markdown description which is converted to ADF for Jira
|
|
4038
|
+
*/
|
|
4039
|
+
async createIssue(projectKey, summary, description, issueType = "Task") {
|
|
4040
|
+
return this.post("/issue", {
|
|
4041
|
+
fields: {
|
|
4042
|
+
project: {
|
|
4043
|
+
key: projectKey
|
|
4044
|
+
},
|
|
4045
|
+
summary,
|
|
4046
|
+
description: markdownToAdf(description),
|
|
4047
|
+
issuetype: {
|
|
4048
|
+
name: issueType
|
|
4049
|
+
}
|
|
4050
|
+
}
|
|
4051
|
+
});
|
|
4052
|
+
}
|
|
4053
|
+
/**
|
|
4054
|
+
* Update an issue's fields (summary, description)
|
|
4055
|
+
* @param issueKey - Jira issue key (e.g., "PROJ-123")
|
|
4056
|
+
* @param fields - Fields to update
|
|
4057
|
+
*/
|
|
4058
|
+
async updateIssue(issueKey, fields) {
|
|
4059
|
+
const updateFields = {};
|
|
4060
|
+
if (fields.summary !== void 0) {
|
|
4061
|
+
updateFields.summary = fields.summary;
|
|
4062
|
+
}
|
|
4063
|
+
if (fields.description !== void 0) {
|
|
4064
|
+
updateFields.description = markdownToAdf(fields.description);
|
|
4065
|
+
}
|
|
4066
|
+
await this.put(`/issue/${issueKey}`, { fields: updateFields });
|
|
4067
|
+
}
|
|
4068
|
+
/**
|
|
4069
|
+
* Create an issue with a parent (subtask or child issue)
|
|
4070
|
+
* Accepts Markdown description which is converted to ADF for Jira
|
|
4071
|
+
*/
|
|
4072
|
+
async createIssueWithParent(projectKey, summary, description, parentKey, issueType = "Subtask") {
|
|
4073
|
+
return this.post("/issue", {
|
|
4074
|
+
fields: {
|
|
4075
|
+
project: {
|
|
4076
|
+
key: projectKey
|
|
4077
|
+
},
|
|
4078
|
+
summary,
|
|
4079
|
+
description: markdownToAdf(description),
|
|
4080
|
+
issuetype: {
|
|
4081
|
+
name: issueType
|
|
4082
|
+
},
|
|
4083
|
+
parent: {
|
|
4084
|
+
key: parentKey
|
|
4085
|
+
}
|
|
4086
|
+
}
|
|
4087
|
+
});
|
|
4088
|
+
}
|
|
4089
|
+
/**
|
|
4090
|
+
* Create an issue link (dependency/relationship between issues)
|
|
4091
|
+
* @param inwardKey - The issue key for the inward side (e.g., the blocked issue)
|
|
4092
|
+
* @param outwardKey - The issue key for the outward side (e.g., the blocking issue)
|
|
4093
|
+
* @param linkType - The link type name (e.g., "Blocks")
|
|
4094
|
+
*/
|
|
4095
|
+
async createIssueLink(inwardKey, outwardKey, linkType) {
|
|
4096
|
+
await this.post("/issueLink", {
|
|
4097
|
+
type: {
|
|
4098
|
+
name: linkType
|
|
4099
|
+
},
|
|
4100
|
+
inwardIssue: {
|
|
4101
|
+
key: inwardKey
|
|
4102
|
+
},
|
|
4103
|
+
outwardIssue: {
|
|
4104
|
+
key: outwardKey
|
|
4105
|
+
}
|
|
4106
|
+
});
|
|
4107
|
+
}
|
|
4108
|
+
/**
|
|
4109
|
+
* Delete an issue link by ID
|
|
4110
|
+
*/
|
|
4111
|
+
async deleteIssueLink(linkId) {
|
|
4112
|
+
await this.delete(`/issueLink/${linkId}`);
|
|
4113
|
+
}
|
|
4114
|
+
/**
|
|
4115
|
+
* Search issues using JQL
|
|
4116
|
+
* Automatically paginates through all results up to MAX_SEARCH_RESULTS.
|
|
4117
|
+
*/
|
|
4118
|
+
async searchIssues(jql) {
|
|
4119
|
+
const MAX_SEARCH_RESULTS = 5e3;
|
|
4120
|
+
const allIssues = [];
|
|
4121
|
+
let nextPageToken;
|
|
4122
|
+
const maxResults = 100;
|
|
4123
|
+
while (allIssues.length < MAX_SEARCH_RESULTS) {
|
|
4124
|
+
const body = {
|
|
4125
|
+
jql,
|
|
4126
|
+
maxResults,
|
|
4127
|
+
fields: [
|
|
4128
|
+
"summary",
|
|
4129
|
+
"description",
|
|
4130
|
+
"status",
|
|
4131
|
+
"issuetype",
|
|
4132
|
+
"project",
|
|
4133
|
+
"assignee",
|
|
4134
|
+
"reporter",
|
|
4135
|
+
"labels",
|
|
4136
|
+
"created",
|
|
4137
|
+
"updated",
|
|
4138
|
+
"issuelinks",
|
|
4139
|
+
"parent"
|
|
4140
|
+
]
|
|
4141
|
+
};
|
|
4142
|
+
if (nextPageToken) {
|
|
4143
|
+
body.nextPageToken = nextPageToken;
|
|
4144
|
+
}
|
|
4145
|
+
const response = await this.post(
|
|
4146
|
+
"/search/jql",
|
|
4147
|
+
body
|
|
4148
|
+
);
|
|
4149
|
+
allIssues.push(...response.issues);
|
|
4150
|
+
if (!response.nextPageToken || response.issues.length === 0) {
|
|
4151
|
+
break;
|
|
4152
|
+
}
|
|
4153
|
+
nextPageToken = response.nextPageToken;
|
|
4154
|
+
}
|
|
4155
|
+
if (allIssues.length >= MAX_SEARCH_RESULTS) {
|
|
4156
|
+
getLogger().warn(`Search results truncated at ${MAX_SEARCH_RESULTS} issues. The query matched more results than the safety cap allows.`, { jql, returnedCount: allIssues.length });
|
|
4157
|
+
}
|
|
4158
|
+
return allIssues;
|
|
4159
|
+
}
|
|
4160
|
+
/**
|
|
4161
|
+
* Test connection to Jira API
|
|
4162
|
+
*/
|
|
4163
|
+
async testConnection() {
|
|
4164
|
+
try {
|
|
4165
|
+
await this.get("/myself");
|
|
4166
|
+
return true;
|
|
4167
|
+
} catch (error) {
|
|
4168
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4169
|
+
if (message.includes("Jira API error (401)") || message.includes("Jira API error (403)")) {
|
|
4170
|
+
getLogger().error("Jira connection test failed: authentication error", { error });
|
|
4171
|
+
return false;
|
|
4172
|
+
}
|
|
4173
|
+
throw error;
|
|
4174
|
+
}
|
|
4175
|
+
}
|
|
4176
|
+
};
|
|
4177
|
+
|
|
4178
|
+
// src/lib/providers/jira/JiraIssueTracker.ts
|
|
4179
|
+
var JiraIssueTracker = class {
|
|
4180
|
+
constructor(config, options) {
|
|
4181
|
+
this.providerName = "jira";
|
|
4182
|
+
this.supportsPullRequests = false;
|
|
4183
|
+
this.config = config;
|
|
4184
|
+
this.client = new JiraApiClient({
|
|
4185
|
+
host: config.host,
|
|
4186
|
+
username: config.username,
|
|
4187
|
+
apiToken: config.apiToken
|
|
4188
|
+
});
|
|
4189
|
+
this.prompter = (options == null ? void 0 : options.prompter) ?? promptConfirmation;
|
|
4190
|
+
}
|
|
4191
|
+
/**
|
|
4192
|
+
* Normalize identifier to canonical uppercase form
|
|
4193
|
+
* Jira issue keys are case-sensitive in the API (must be uppercase)
|
|
4194
|
+
*/
|
|
4195
|
+
normalizeIdentifier(identifier) {
|
|
4196
|
+
return String(identifier).toUpperCase();
|
|
4197
|
+
}
|
|
4198
|
+
/**
|
|
4199
|
+
* Detect input type from user input
|
|
4200
|
+
* Jira issues follow pattern: PROJECTKEY-123 (case-insensitive)
|
|
4201
|
+
*/
|
|
4202
|
+
async detectInputType(input) {
|
|
4203
|
+
const jiraPattern = /^([A-Z][A-Z0-9]+)-(\d+)$/i;
|
|
4204
|
+
const match = input.match(jiraPattern);
|
|
4205
|
+
if (!match) {
|
|
4206
|
+
return { type: "unknown", identifier: null, rawInput: input };
|
|
4207
|
+
}
|
|
4208
|
+
const issueKey = this.normalizeIdentifier(input);
|
|
4209
|
+
getLogger().debug("Checking if input is a Jira issue", { issueKey });
|
|
4210
|
+
try {
|
|
4211
|
+
await this.client.getIssue(issueKey);
|
|
4212
|
+
return { type: "issue", identifier: issueKey, rawInput: input };
|
|
4213
|
+
} catch (error) {
|
|
4214
|
+
if (error instanceof Error && (/404/.test(error.message) || /not found/i.test(error.message))) {
|
|
4215
|
+
getLogger().debug("Issue not found", { issueKey, error });
|
|
4216
|
+
return { type: "unknown", identifier: null, rawInput: input };
|
|
4217
|
+
}
|
|
4218
|
+
throw error;
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
/**
|
|
4222
|
+
* Fetch issue details
|
|
4223
|
+
*/
|
|
4224
|
+
async fetchIssue(identifier) {
|
|
4225
|
+
const issueKey = this.normalizeIdentifier(identifier);
|
|
4226
|
+
getLogger().debug("Fetching Jira issue", { issueKey });
|
|
4227
|
+
const jiraIssue = await this.client.getIssue(issueKey);
|
|
4228
|
+
return this.mapJiraIssueToIssue(jiraIssue);
|
|
4229
|
+
}
|
|
4230
|
+
/**
|
|
4231
|
+
* Check if issue exists (silent validation)
|
|
4232
|
+
*/
|
|
4233
|
+
async isValidIssue(identifier) {
|
|
4234
|
+
try {
|
|
4235
|
+
return await this.fetchIssue(identifier);
|
|
4236
|
+
} catch (error) {
|
|
4237
|
+
if (error instanceof Error && (/404/.test(error.message) || /not found/i.test(error.message))) {
|
|
4238
|
+
getLogger().debug("Issue validation failed: not found", { identifier, error });
|
|
4239
|
+
return false;
|
|
4240
|
+
}
|
|
4241
|
+
throw error;
|
|
4242
|
+
}
|
|
4243
|
+
}
|
|
4244
|
+
/**
|
|
4245
|
+
* Validate issue state
|
|
4246
|
+
* Note: Jira doesn't have a simple "closed" state - depends on workflow
|
|
4247
|
+
*/
|
|
4248
|
+
async validateIssueState(issue) {
|
|
4249
|
+
getLogger().debug("Jira issue state", { issueKey: issue.number, state: issue.state });
|
|
4250
|
+
if (issue.state === "closed") {
|
|
4251
|
+
const shouldContinue = await this.prompter(
|
|
4252
|
+
`Issue ${issue.number} is in a completed state. Continue anyway?`
|
|
4253
|
+
);
|
|
4254
|
+
if (!shouldContinue) {
|
|
4255
|
+
throw new Error("User cancelled due to completed issue");
|
|
4256
|
+
}
|
|
4257
|
+
}
|
|
4258
|
+
}
|
|
4259
|
+
/**
|
|
4260
|
+
* Create a new issue
|
|
4261
|
+
*/
|
|
4262
|
+
async createIssue(title, body, _repository, _labels) {
|
|
4263
|
+
getLogger().debug("Creating Jira issue", { title, projectKey: this.config.projectKey });
|
|
4264
|
+
const jiraIssue = await this.client.createIssue(
|
|
4265
|
+
this.config.projectKey,
|
|
4266
|
+
title,
|
|
4267
|
+
body,
|
|
4268
|
+
this.config.defaultIssueType
|
|
4269
|
+
);
|
|
4270
|
+
return {
|
|
4271
|
+
number: jiraIssue.key,
|
|
4272
|
+
url: `${this.config.host}/browse/${jiraIssue.key}`
|
|
4273
|
+
};
|
|
4274
|
+
}
|
|
4275
|
+
/**
|
|
4276
|
+
* Get issue URL
|
|
4277
|
+
*/
|
|
4278
|
+
async getIssueUrl(identifier) {
|
|
4279
|
+
const issueKey = this.normalizeIdentifier(identifier);
|
|
4280
|
+
return `${this.config.host}/browse/${issueKey}`;
|
|
4281
|
+
}
|
|
4282
|
+
/**
|
|
4283
|
+
* Move issue to "In Progress" state
|
|
4284
|
+
* Uses configured transition mapping or default transition name
|
|
4285
|
+
*/
|
|
4286
|
+
async moveIssueToInProgress(identifier) {
|
|
4287
|
+
var _a;
|
|
4288
|
+
const issueKey = this.normalizeIdentifier(identifier);
|
|
4289
|
+
getLogger().debug("Moving Jira issue to In Progress", { issueKey });
|
|
4290
|
+
const transitions = await this.client.getTransitions(issueKey);
|
|
4291
|
+
const transitionName = ((_a = this.config.transitionMappings) == null ? void 0 : _a["In Progress"]) ?? this.findTransitionByName(transitions, ["In Progress", "Start Progress", "Start"]);
|
|
4292
|
+
if (!transitionName) {
|
|
4293
|
+
throw new Error(
|
|
4294
|
+
`Could not find "In Progress" transition for ${issueKey}. Available transitions: ${transitions.map((t) => t.name).join(", ")}. Configure custom mapping in settings.json: issueManagement.jira.transitionMappings`
|
|
4295
|
+
);
|
|
4296
|
+
}
|
|
4297
|
+
const transition = transitions.find((t) => t.name === transitionName);
|
|
4298
|
+
if (!transition) {
|
|
4299
|
+
throw new Error(`Transition "${transitionName}" not found`);
|
|
4300
|
+
}
|
|
4301
|
+
await this.client.transitionIssue(issueKey, transition.id);
|
|
4302
|
+
getLogger().info("Issue transitioned successfully", { issueKey, transition: transitionName });
|
|
4303
|
+
}
|
|
4304
|
+
/**
|
|
4305
|
+
* Move issue to "Ready for Review" state
|
|
4306
|
+
* Uses configured transition mapping or default transition name
|
|
4307
|
+
*/
|
|
4308
|
+
async moveIssueToReadyForReview(identifier) {
|
|
4309
|
+
var _a;
|
|
4310
|
+
const issueKey = this.normalizeIdentifier(identifier);
|
|
4311
|
+
getLogger().debug("Moving Jira issue to Ready for Review", { issueKey });
|
|
4312
|
+
const transitions = await this.client.getTransitions(issueKey);
|
|
4313
|
+
const transitionName = ((_a = this.config.transitionMappings) == null ? void 0 : _a["Ready for Review"]) ?? this.findTransitionByName(transitions, ["Ready for Review", "In Review", "Code Review", "Review"]);
|
|
4314
|
+
if (!transitionName) {
|
|
4315
|
+
throw new Error(
|
|
4316
|
+
`Could not find "Ready for Review" transition for ${issueKey}. Available transitions: ${transitions.map((t) => t.name).join(", ")}. Configure custom mapping in settings.json: issueManagement.jira.transitionMappings`
|
|
4317
|
+
);
|
|
4318
|
+
}
|
|
4319
|
+
const transition = transitions.find((t) => t.name === transitionName);
|
|
4320
|
+
if (!transition) {
|
|
4321
|
+
throw new Error(`Transition "${transitionName}" not found`);
|
|
4322
|
+
}
|
|
4323
|
+
await this.client.transitionIssue(issueKey, transition.id);
|
|
4324
|
+
getLogger().info("Issue transitioned to Ready for Review", { issueKey, transition: transitionName });
|
|
4325
|
+
}
|
|
4326
|
+
/**
|
|
4327
|
+
* Close an issue by transitioning to "Done" state
|
|
4328
|
+
* Uses configured transition mapping or default transition names
|
|
4329
|
+
*/
|
|
4330
|
+
async closeIssue(identifier) {
|
|
4331
|
+
var _a;
|
|
4332
|
+
const issueKey = this.normalizeIdentifier(identifier);
|
|
4333
|
+
getLogger().debug("Closing Jira issue", { issueKey });
|
|
4334
|
+
const transitions = await this.client.getTransitions(issueKey);
|
|
4335
|
+
const transitionName = ((_a = this.config.transitionMappings) == null ? void 0 : _a["Done"]) ?? this.findTransitionByName(transitions, ["Done", "Close", "Closed", "Resolve", "Resolved"]);
|
|
4336
|
+
if (!transitionName) {
|
|
4337
|
+
throw new Error(
|
|
4338
|
+
`Could not find "Done" transition for ${issueKey}. Available transitions: ${transitions.map((t) => t.name).join(", ")}. Configure custom mapping in settings.json: issueManagement.jira.transitionMappings`
|
|
4339
|
+
);
|
|
4340
|
+
}
|
|
4341
|
+
const transition = transitions.find((t) => t.name === transitionName);
|
|
4342
|
+
if (!transition) {
|
|
4343
|
+
throw new Error(`Transition "${transitionName}" not found`);
|
|
4344
|
+
}
|
|
4345
|
+
await this.client.transitionIssue(issueKey, transition.id);
|
|
4346
|
+
getLogger().info("Issue closed successfully", { issueKey, transition: transitionName });
|
|
4347
|
+
}
|
|
4348
|
+
/**
|
|
4349
|
+
* Reopen an issue by transitioning back to an open state
|
|
4350
|
+
* Uses configured transition mapping or default transition names
|
|
4351
|
+
*/
|
|
4352
|
+
async reopenIssue(identifier) {
|
|
4353
|
+
var _a;
|
|
4354
|
+
const issueKey = this.normalizeIdentifier(identifier);
|
|
4355
|
+
getLogger().debug("Reopening Jira issue", { issueKey });
|
|
4356
|
+
const transitions = await this.client.getTransitions(issueKey);
|
|
4357
|
+
const transitionName = ((_a = this.config.transitionMappings) == null ? void 0 : _a["Reopen"]) ?? this.findTransitionByName(transitions, ["Reopen", "To Do", "Open", "Backlog"]);
|
|
4358
|
+
if (!transitionName) {
|
|
4359
|
+
throw new Error(
|
|
4360
|
+
`Could not find "Reopen" transition for ${issueKey}. Available transitions: ${transitions.map((t) => t.name).join(", ")}. Configure custom mapping in settings.json: issueManagement.jira.transitionMappings`
|
|
4361
|
+
);
|
|
4362
|
+
}
|
|
4363
|
+
const transition = transitions.find((t) => t.name === transitionName);
|
|
4364
|
+
if (!transition) {
|
|
4365
|
+
throw new Error(`Transition "${transitionName}" not found`);
|
|
4366
|
+
}
|
|
4367
|
+
await this.client.transitionIssue(issueKey, transition.id);
|
|
4368
|
+
getLogger().info("Issue reopened successfully", { issueKey, transition: transitionName });
|
|
4369
|
+
}
|
|
4370
|
+
/**
|
|
4371
|
+
* Extract context from issue for AI prompts
|
|
4372
|
+
*/
|
|
4373
|
+
extractContext(entity) {
|
|
4374
|
+
return `Issue: ${entity.number}
|
|
4375
|
+
Title: ${entity.title}
|
|
4376
|
+
Status: ${entity.state}
|
|
4377
|
+
URL: ${entity.url}
|
|
4378
|
+
|
|
4379
|
+
Description:
|
|
4380
|
+
${entity.body}
|
|
4381
|
+
|
|
4382
|
+
${entity.labels.length > 0 ? `Labels: ${entity.labels.join(", ")}` : ""}
|
|
4383
|
+
${entity.assignees.length > 0 ? `Assignees: ${entity.assignees.join(", ")}` : ""}`;
|
|
4384
|
+
}
|
|
4385
|
+
/**
|
|
4386
|
+
* Fetch child issues of a Jira parent issue using JQL
|
|
4387
|
+
* @param parentIdentifier - Jira issue key (e.g., "PROJ-123")
|
|
4388
|
+
* @param _repo - Repository (unused for Jira)
|
|
4389
|
+
* @returns Array of child issues
|
|
4390
|
+
*/
|
|
4391
|
+
async getChildIssues(parentIdentifier, _repo) {
|
|
4392
|
+
const parentKey = this.normalizeIdentifier(parentIdentifier);
|
|
4393
|
+
const jiraKeyPattern = /^[A-Z][A-Z0-9]+-\d+$/;
|
|
4394
|
+
if (!jiraKeyPattern.test(parentKey)) {
|
|
4395
|
+
getLogger().warn(`Invalid Jira issue key format: ${parentKey}`);
|
|
4396
|
+
return [];
|
|
4397
|
+
}
|
|
4398
|
+
const issues = await this.client.searchIssues(`parent = ${parentKey}`);
|
|
4399
|
+
return issues.map((issue) => ({
|
|
4400
|
+
id: issue.key,
|
|
4401
|
+
title: issue.fields.summary,
|
|
4402
|
+
url: `${this.config.host}/browse/${issue.key}`,
|
|
4403
|
+
state: issue.fields.status.name.toLowerCase()
|
|
4404
|
+
}));
|
|
4405
|
+
}
|
|
4406
|
+
/**
|
|
4407
|
+
* Get issue details (alias for fetchIssue for MCP compatibility)
|
|
4408
|
+
*/
|
|
4409
|
+
async getIssue(identifier) {
|
|
4410
|
+
return this.fetchIssue(identifier);
|
|
4411
|
+
}
|
|
4412
|
+
/**
|
|
4413
|
+
* Get all comments for an issue
|
|
4414
|
+
*/
|
|
4415
|
+
async getComments(identifier) {
|
|
4416
|
+
const issueKey = this.normalizeIdentifier(identifier);
|
|
4417
|
+
getLogger().debug("Fetching Jira comments", { issueKey });
|
|
4418
|
+
const comments = await this.client.getComments(issueKey);
|
|
4419
|
+
return comments.map((comment) => ({
|
|
4420
|
+
id: comment.id,
|
|
4421
|
+
body: adfToMarkdown(comment.body),
|
|
4422
|
+
author: comment.author,
|
|
4423
|
+
createdAt: comment.created,
|
|
4424
|
+
updatedAt: comment.updated
|
|
4425
|
+
}));
|
|
4426
|
+
}
|
|
4427
|
+
/**
|
|
4428
|
+
* Add a comment to an issue
|
|
4429
|
+
*/
|
|
4430
|
+
async addComment(identifier, body) {
|
|
4431
|
+
const issueKey = this.normalizeIdentifier(identifier);
|
|
4432
|
+
getLogger().debug("Adding Jira comment", { issueKey });
|
|
4433
|
+
const comment = await this.client.addComment(issueKey, body);
|
|
4434
|
+
return { id: comment.id };
|
|
4435
|
+
}
|
|
4436
|
+
/**
|
|
4437
|
+
* Update an existing comment
|
|
4438
|
+
*/
|
|
4439
|
+
async updateComment(identifier, commentId, body) {
|
|
4440
|
+
const issueKey = this.normalizeIdentifier(identifier);
|
|
4441
|
+
getLogger().debug("Updating Jira comment", { issueKey, commentId });
|
|
4442
|
+
await this.client.updateComment(issueKey, commentId, body);
|
|
4443
|
+
}
|
|
4444
|
+
/**
|
|
4445
|
+
* Get the underlying API client (for direct API access by MCP provider)
|
|
4446
|
+
*/
|
|
4447
|
+
getApiClient() {
|
|
4448
|
+
return this.client;
|
|
4449
|
+
}
|
|
4450
|
+
/**
|
|
4451
|
+
* Get configuration (for MCP provider)
|
|
4452
|
+
*/
|
|
4453
|
+
getConfig() {
|
|
4454
|
+
return this.config;
|
|
4455
|
+
}
|
|
4456
|
+
/**
|
|
4457
|
+
* Map Jira API issue to generic Issue type
|
|
4458
|
+
*/
|
|
4459
|
+
mapJiraIssueToIssue(jiraIssue) {
|
|
4460
|
+
const description = adfToMarkdown(jiraIssue.fields.description);
|
|
4461
|
+
return {
|
|
4462
|
+
id: jiraIssue.id,
|
|
4463
|
+
key: jiraIssue.key,
|
|
4464
|
+
number: jiraIssue.key,
|
|
4465
|
+
title: jiraIssue.fields.summary,
|
|
4466
|
+
body: description,
|
|
4467
|
+
state: this.mapJiraStatusToState(jiraIssue.fields.status.name),
|
|
4468
|
+
labels: jiraIssue.fields.labels,
|
|
4469
|
+
assignees: jiraIssue.fields.assignee ? [jiraIssue.fields.assignee.displayName] : [],
|
|
4470
|
+
assignee: jiraIssue.fields.assignee,
|
|
4471
|
+
author: jiraIssue.fields.reporter,
|
|
4472
|
+
url: `${this.config.host}/browse/${jiraIssue.key}`,
|
|
4473
|
+
issueType: jiraIssue.fields.issuetype.name,
|
|
4474
|
+
status: jiraIssue.fields.status.name
|
|
4475
|
+
};
|
|
4476
|
+
}
|
|
4477
|
+
mapJiraStatusToState(statusName) {
|
|
4478
|
+
const normalized = statusName.toLowerCase();
|
|
4479
|
+
const closedStatuses = ["done", "closed", "resolved", "cancelled", "canceled"];
|
|
4480
|
+
return closedStatuses.includes(normalized) ? "closed" : "open";
|
|
4481
|
+
}
|
|
4482
|
+
/**
|
|
4483
|
+
* Find a transition by name, trying multiple possible names
|
|
4484
|
+
*/
|
|
4485
|
+
findTransitionByName(transitions, names) {
|
|
4486
|
+
for (const name of names) {
|
|
4487
|
+
const transition = transitions.find(
|
|
4488
|
+
(t) => t.name.toLowerCase() === name.toLowerCase()
|
|
4489
|
+
);
|
|
4490
|
+
if (transition) {
|
|
4491
|
+
return transition.name;
|
|
4492
|
+
}
|
|
4493
|
+
}
|
|
4494
|
+
return null;
|
|
4495
|
+
}
|
|
4496
|
+
};
|
|
4497
|
+
|
|
3396
4498
|
// src/lib/IssueTrackerFactory.ts
|
|
3397
4499
|
var IssueTrackerFactory = class {
|
|
3398
4500
|
/**
|
|
@@ -3404,7 +4506,7 @@ var IssueTrackerFactory = class {
|
|
|
3404
4506
|
* @throws Error if provider type is not supported
|
|
3405
4507
|
*/
|
|
3406
4508
|
static create(settings) {
|
|
3407
|
-
var _a, _b;
|
|
4509
|
+
var _a, _b, _c;
|
|
3408
4510
|
const provider = ((_a = settings.issueManagement) == null ? void 0 : _a.provider) ?? "github";
|
|
3409
4511
|
getLogger().debug(`IssueTrackerFactory: Creating tracker for provider "${provider}"`);
|
|
3410
4512
|
getLogger().debug(`IssueTrackerFactory: issueManagement settings:`, JSON.stringify(settings.issueManagement, null, 2));
|
|
@@ -3427,6 +4529,32 @@ var IssueTrackerFactory = class {
|
|
|
3427
4529
|
getLogger().debug(`IssueTrackerFactory: Creating LinearService with config:`, JSON.stringify(linearConfig, null, 2));
|
|
3428
4530
|
return new LinearService(linearConfig);
|
|
3429
4531
|
}
|
|
4532
|
+
case "jira": {
|
|
4533
|
+
const jiraSettings = (_c = settings.issueManagement) == null ? void 0 : _c.jira;
|
|
4534
|
+
if (!(jiraSettings == null ? void 0 : jiraSettings.host)) {
|
|
4535
|
+
throw new Error("Jira host is required. Configure issueManagement.jira.host in .iloom/settings.json");
|
|
4536
|
+
}
|
|
4537
|
+
if (!(jiraSettings == null ? void 0 : jiraSettings.username)) {
|
|
4538
|
+
throw new Error("Jira username is required. Configure issueManagement.jira.username in .iloom/settings.json");
|
|
4539
|
+
}
|
|
4540
|
+
if (!(jiraSettings == null ? void 0 : jiraSettings.apiToken)) {
|
|
4541
|
+
throw new Error("Jira API token is required. Configure issueManagement.jira.apiToken in .iloom/settings.local.json");
|
|
4542
|
+
}
|
|
4543
|
+
if (!(jiraSettings == null ? void 0 : jiraSettings.projectKey)) {
|
|
4544
|
+
throw new Error("Jira project key is required. Configure issueManagement.jira.projectKey in .iloom/settings.json");
|
|
4545
|
+
}
|
|
4546
|
+
const jiraConfig = {
|
|
4547
|
+
host: jiraSettings.host,
|
|
4548
|
+
username: jiraSettings.username,
|
|
4549
|
+
apiToken: jiraSettings.apiToken,
|
|
4550
|
+
projectKey: jiraSettings.projectKey
|
|
4551
|
+
};
|
|
4552
|
+
if (jiraSettings.transitionMappings) {
|
|
4553
|
+
jiraConfig.transitionMappings = jiraSettings.transitionMappings;
|
|
4554
|
+
}
|
|
4555
|
+
getLogger().debug(`IssueTrackerFactory: Creating JiraIssueTracker for host: ${jiraSettings.host}`);
|
|
4556
|
+
return new JiraIssueTracker(jiraConfig);
|
|
4557
|
+
}
|
|
3430
4558
|
default:
|
|
3431
4559
|
throw new Error(`Unsupported issue tracker provider: ${provider}`);
|
|
3432
4560
|
}
|
|
@@ -3938,7 +5066,7 @@ function parseJsonStreamOutput(output) {
|
|
|
3938
5066
|
}
|
|
3939
5067
|
}
|
|
3940
5068
|
async function launchClaude(prompt, options = {}) {
|
|
3941
|
-
const { model, permissionMode, addDir, headless = false, appendSystemPrompt, mcpConfig, allowedTools, disallowedTools, agents, sessionId, noSessionPersistence, outputFormat, verbose, jsonMode } = options;
|
|
5069
|
+
const { model, permissionMode, addDir, headless = false, appendSystemPrompt, mcpConfig, allowedTools, disallowedTools, agents, sessionId, noSessionPersistence, outputFormat, verbose, jsonMode, env: extraEnv } = options;
|
|
3942
5070
|
const log = getLogger();
|
|
3943
5071
|
const args = [];
|
|
3944
5072
|
if (headless) {
|
|
@@ -3982,6 +5110,7 @@ async function launchClaude(prompt, options = {}) {
|
|
|
3982
5110
|
if (noSessionPersistence && headless) {
|
|
3983
5111
|
args.push("--no-session-persistence");
|
|
3984
5112
|
}
|
|
5113
|
+
const claudeEnv = { ...process.env, CLAUDECODE: "0" };
|
|
3985
5114
|
try {
|
|
3986
5115
|
if (headless) {
|
|
3987
5116
|
const isDebugMode = logger.isDebugEnabled();
|
|
@@ -3992,6 +5121,8 @@ async function launchClaude(prompt, options = {}) {
|
|
|
3992
5121
|
...addDir && { cwd: addDir },
|
|
3993
5122
|
// Run Claude in the worktree directory
|
|
3994
5123
|
verbose: isDebugMode,
|
|
5124
|
+
env: { ...claudeEnv, ...extraEnv },
|
|
5125
|
+
// CLAUDECODE=0 + any extra env vars
|
|
3995
5126
|
...isDebugMode && { stdio: ["pipe", "pipe", "pipe"] }
|
|
3996
5127
|
// Enable streaming in debug mode
|
|
3997
5128
|
};
|
|
@@ -4048,7 +5179,9 @@ async function launchClaude(prompt, options = {}) {
|
|
|
4048
5179
|
// Capture stderr to detect session conflicts
|
|
4049
5180
|
timeout: 0,
|
|
4050
5181
|
// Disable timeout
|
|
4051
|
-
verbose: logger.isDebugEnabled()
|
|
5182
|
+
verbose: logger.isDebugEnabled(),
|
|
5183
|
+
env: { ...claudeEnv, ...extraEnv }
|
|
5184
|
+
// CLAUDECODE=0 + any extra env vars
|
|
4052
5185
|
});
|
|
4053
5186
|
return;
|
|
4054
5187
|
} catch (interactiveError) {
|
|
@@ -4068,7 +5201,8 @@ async function launchClaude(prompt, options = {}) {
|
|
|
4068
5201
|
...addDir && { cwd: addDir },
|
|
4069
5202
|
stdio: "inherit",
|
|
4070
5203
|
timeout: 0,
|
|
4071
|
-
verbose: logger.isDebugEnabled()
|
|
5204
|
+
verbose: logger.isDebugEnabled(),
|
|
5205
|
+
env: claudeEnv
|
|
4072
5206
|
});
|
|
4073
5207
|
return;
|
|
4074
5208
|
}
|
|
@@ -4096,6 +5230,7 @@ async function launchClaude(prompt, options = {}) {
|
|
|
4096
5230
|
timeout: 0,
|
|
4097
5231
|
...addDir && { cwd: addDir },
|
|
4098
5232
|
verbose: isDebugMode,
|
|
5233
|
+
env: claudeEnv,
|
|
4099
5234
|
...isDebugMode && { stdio: ["pipe", "pipe", "pipe"] }
|
|
4100
5235
|
};
|
|
4101
5236
|
const subprocess = execa4("claude", resumeArgs, execaOptions);
|
|
@@ -4148,7 +5283,8 @@ async function launchClaude(prompt, options = {}) {
|
|
|
4148
5283
|
...addDir && { cwd: addDir },
|
|
4149
5284
|
stdio: "inherit",
|
|
4150
5285
|
timeout: 0,
|
|
4151
|
-
verbose: logger.isDebugEnabled()
|
|
5286
|
+
verbose: logger.isDebugEnabled(),
|
|
5287
|
+
env: claudeEnv
|
|
4152
5288
|
});
|
|
4153
5289
|
return;
|
|
4154
5290
|
}
|
|
@@ -4323,6 +5459,8 @@ var ClaudeService = class {
|
|
|
4323
5459
|
if (port !== void 0) {
|
|
4324
5460
|
variables.PORT = port;
|
|
4325
5461
|
}
|
|
5462
|
+
const isVscodeMode = process.env.ILOOM_VSCODE === "1";
|
|
5463
|
+
variables.IS_VSCODE_MODE = isVscodeMode;
|
|
4326
5464
|
const prompt = await this.templateManager.getPrompt(type, variables);
|
|
4327
5465
|
const permissionMode = this.getPermissionModeForWorkflow(type);
|
|
4328
5466
|
if (permissionMode === "bypassPermissions") {
|
|
@@ -4388,11 +5526,11 @@ var ClaudeContextManager = class {
|
|
|
4388
5526
|
if (!context.workspacePath) {
|
|
4389
5527
|
throw new Error("Workspace path is required");
|
|
4390
5528
|
}
|
|
4391
|
-
if (context.type === "issue" &&
|
|
4392
|
-
throw new Error("Issue identifier
|
|
5529
|
+
if (context.type === "issue" && context.identifier === void 0) {
|
|
5530
|
+
throw new Error("Issue identifier is required");
|
|
4393
5531
|
}
|
|
4394
|
-
if (context.type === "pr" &&
|
|
4395
|
-
throw new Error("PR identifier
|
|
5532
|
+
if (context.type === "pr" && context.identifier === void 0) {
|
|
5533
|
+
throw new Error("PR identifier is required");
|
|
4396
5534
|
}
|
|
4397
5535
|
logger.debug("Context prepared", { context });
|
|
4398
5536
|
}
|