@iloom/cli 0.9.2 → 0.10.1
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 +160 -41
- package/dist/{BranchNamingService-K6XNWQ6C.js → BranchNamingService-25KSZAEM.js} +2 -2
- package/dist/ClaudeContextManager-66GR4BGM.js +14 -0
- package/dist/ClaudeService-7KM5NA5Z.js +13 -0
- package/dist/{GitHubService-TGWJN4V4.js → GitHubService-MEHKHUQP.js} +4 -4
- package/dist/IssueTrackerFactory-NG53YX5S.js +14 -0
- package/dist/{LoomLauncher-73NXL2CL.js → LoomLauncher-TDLZSYG2.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-YOE2SIPG.js} +2 -2
- package/dist/README.md +160 -41
- package/dist/{SettingsManager-AW3JTJHD.js → SettingsManager-FNKCOZMQ.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-THZI572G.js → build-VHGEMXBA.js} +9 -9
- package/dist/chunk-4232AHNQ.js +35 -0
- package/dist/chunk-4232AHNQ.js.map +1 -0
- package/dist/chunk-4E7LCFUG.js +24 -0
- package/dist/chunk-4E7LCFUG.js.map +1 -0
- package/dist/{chunk-AR5QKYNE.js → chunk-4FGEGQW4.js} +4 -4
- package/dist/{chunk-R4YWBGY6.js → chunk-5FJWO4IT.js} +67 -22
- package/dist/chunk-5FJWO4IT.js.map +1 -0
- package/dist/{chunk-VPTAX5TR.js → chunk-5RPBYK5Q.js} +35 -30
- package/dist/chunk-5RPBYK5Q.js.map +1 -0
- package/dist/{chunk-YKFCCV6S.js → chunk-63QWFWH3.js} +7 -7
- package/dist/chunk-63QWFWH3.js.map +1 -0
- package/dist/{chunk-RI2YL6TK.js → chunk-7VHJNVLF.js} +80 -23
- package/dist/chunk-7VHJNVLF.js.map +1 -0
- package/dist/{chunk-B7U6OKUR.js → chunk-C6HNNJIV.js} +11 -3
- package/dist/chunk-C6HNNJIV.js.map +1 -0
- package/dist/{chunk-A7NJF73J.js → chunk-CVCTIDDK.js} +4 -4
- package/dist/{chunk-Z2TWEXR7.js → chunk-E6KOWMKA.js} +6 -6
- package/dist/chunk-E6KOWMKA.js.map +1 -0
- package/dist/{chunk-3I4ONZRT.js → chunk-EVPZFV3K.js} +10 -10
- package/dist/chunk-EVPZFV3K.js.map +1 -0
- package/dist/{chunk-IZIYLYPK.js → chunk-G5V75JD5.js} +2 -2
- package/dist/chunk-GRISNU6G.js +651 -0
- package/dist/chunk-GRISNU6G.js.map +1 -0
- package/dist/chunk-HEXKPKCK.js +1396 -0
- package/dist/chunk-HEXKPKCK.js.map +1 -0
- package/dist/{chunk-TC7APDKU.js → chunk-I5T677EA.js} +2 -2
- package/dist/{chunk-KBEIQP4G.js → chunk-KB64WNBZ.js} +43 -3
- package/dist/chunk-KB64WNBZ.js.map +1 -0
- package/dist/{chunk-NWMORW3U.js → chunk-KIK2ZFAL.js} +2 -2
- package/dist/{chunk-CWRI4JC3.js → chunk-KKV5WH5M.js} +30 -31
- package/dist/chunk-KKV5WH5M.js.map +1 -0
- package/dist/{chunk-DGG2VY7B.js → chunk-KVHIAWVT.js} +9 -9
- package/dist/chunk-KVHIAWVT.js.map +1 -0
- package/dist/{chunk-OFDN5NKS.js → chunk-KXDRI47U.js} +69 -12
- package/dist/chunk-KXDRI47U.js.map +1 -0
- package/dist/{chunk-NUACL52E.js → chunk-LLHXQS3C.js} +2 -2
- package/dist/chunk-LUKXJSRI.js +73 -0
- package/dist/chunk-LUKXJSRI.js.map +1 -0
- package/dist/{chunk-TL72BGP6.js → chunk-MORRVYPT.js} +2 -2
- package/dist/chunk-OTGH2HRS.js +1427 -0
- package/dist/chunk-OTGH2HRS.js.map +1 -0
- package/dist/{chunk-7ZEHSSUP.js → chunk-P4O6EH46.js} +4 -4
- package/dist/{chunk-KAYXR544.js → chunk-QVLPWNE3.js} +2 -2
- package/dist/chunk-QZWEJVWV.js +207 -0
- package/dist/chunk-QZWEJVWV.js.map +1 -0
- package/dist/chunk-RJ3VBUFK.js +781 -0
- package/dist/chunk-RJ3VBUFK.js.map +1 -0
- package/dist/chunk-RSYT7MVI.js +202 -0
- package/dist/chunk-RSYT7MVI.js.map +1 -0
- package/dist/{chunk-6IIL5M2L.js → chunk-S7PZA6IV.js} +10 -8
- package/dist/{chunk-6IIL5M2L.js.map → chunk-S7PZA6IV.js.map} +1 -1
- package/dist/chunk-SKSYYBCU.js +229 -0
- package/dist/chunk-SKSYYBCU.js.map +1 -0
- package/dist/{chunk-ULSWCPQG.js → chunk-SWSJWA2S.js} +476 -5
- package/dist/chunk-SWSJWA2S.js.map +1 -0
- package/dist/{chunk-KXGQYLFZ.js → chunk-UKBAJ2QQ.js} +61 -7
- package/dist/chunk-UKBAJ2QQ.js.map +1 -0
- package/dist/{chunk-FO5GGFOV.js → chunk-UR5DGNUO.js} +71 -9
- package/dist/chunk-UR5DGNUO.js.map +1 -0
- package/dist/{chunk-QN47QVBX.js → chunk-UUEW5KWB.js} +1 -1
- package/dist/chunk-UUEW5KWB.js.map +1 -0
- package/dist/{chunk-4CO6KG5S.js → chunk-VG45TUYK.js} +53 -7
- package/dist/{chunk-4CO6KG5S.js.map → chunk-VG45TUYK.js.map} +1 -1
- package/dist/{chunk-4LKGCFGG.js → chunk-WWKOVDWC.js} +2 -2
- package/dist/{chunk-KJTVU3HZ.js → chunk-WXIM2WS7.js} +8 -8
- package/dist/chunk-WXIM2WS7.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-SOSQILHO.js → chunk-ZNMPGMHY.js} +44 -797
- package/dist/chunk-ZNMPGMHY.js.map +1 -0
- package/dist/{claude-TP2QO3BU.js → claude-7GGEWVEM.js} +2 -2
- package/dist/{cleanup-PJRIFFU4.js → cleanup-6PVAC4NI.js} +85 -34
- package/dist/cleanup-6PVAC4NI.js.map +1 -0
- package/dist/cli.js +630 -801
- package/dist/cli.js.map +1 -1
- package/dist/{commit-IVP3M4HG.js → commit-FZR5XDQG.js} +26 -23
- package/dist/commit-FZR5XDQG.js.map +1 -0
- package/dist/{compile-R2J65HBQ.js → compile-7ALJHZ4N.js} +9 -9
- package/dist/{contribute-VDZXHK5Y.js → contribute-5GKLK3BQ.js} +14 -6
- package/dist/contribute-5GKLK3BQ.js.map +1 -0
- package/dist/{dev-server-7F622OEO.js → dev-server-7SMIB7OF.js} +29 -15
- package/dist/dev-server-7SMIB7OF.js.map +1 -0
- package/dist/{feedback-E7VET7CL.js → feedback-G2GJFN2F.js} +18 -16
- package/dist/{feedback-E7VET7CL.js.map → feedback-G2GJFN2F.js.map} +1 -1
- package/dist/{git-2QDQ2X2S.js → git-GTLKAZRJ.js} +4 -4
- package/dist/hooks/iloom-hook.js +15 -0
- package/dist/ignite-H2O5Y5A2.js +34 -0
- package/dist/ignite-H2O5Y5A2.js.map +1 -0
- package/dist/index.d.ts +482 -58
- package/dist/index.js +1340 -44
- package/dist/index.js.map +1 -1
- package/dist/{init-676DHF6R.js → init-32YOKXRL.js} +57 -21
- package/dist/init-32YOKXRL.js.map +1 -0
- package/dist/{issues-PJSOLOBJ.js → issues-4UUAQ5K6.js} +61 -20
- package/dist/issues-4UUAQ5K6.js.map +1 -0
- package/dist/{lint-CJM7BAIM.js → lint-AAN2NZWG.js} +9 -9
- package/dist/mcp/harness-server.js +140 -0
- package/dist/mcp/harness-server.js.map +1 -0
- package/dist/mcp/issue-management-server.js +2599 -262
- 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-544H7JF5.js → open-FXWW3VI4.js} +15 -15
- package/dist/open-FXWW3VI4.js.map +1 -0
- package/dist/{plan-Q7ELXDLC.js → plan-RQ5FPIGF.js} +358 -40
- package/dist/plan-RQ5FPIGF.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 +430 -34
- package/dist/prompts/issue-prompt.txt +473 -54
- package/dist/prompts/plan-prompt.txt +140 -19
- 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 +464 -0
- package/dist/{rebase-YND35CIE.js → rebase-6NVLX5V7.js} +21 -12
- package/dist/rebase-6NVLX5V7.js.map +1 -0
- package/dist/{recap-3W7COH7D.js → recap-OMBOKJST.js} +47 -19
- package/dist/recap-OMBOKJST.js.map +1 -0
- package/dist/{run-QUXJKDQQ.js → run-BBXLRIZB.js} +15 -15
- package/dist/run-BBXLRIZB.js.map +1 -0
- package/dist/schema/package-iloom.schema.json +58 -0
- package/dist/schema/settings.schema.json +149 -15
- package/dist/{shell-QGECBLST.js → shell-RF7LTND5.js} +14 -7
- package/dist/shell-RF7LTND5.js.map +1 -0
- package/dist/{summary-G2T4452H.js → summary-WTQZ7XG2.js} +27 -25
- package/dist/summary-WTQZ7XG2.js.map +1 -0
- package/dist/{test-EA5NQFDC.js → test-SGO6I5Z7.js} +9 -9
- package/dist/{test-git-M7LSLEFL.js → test-git-XM4TM65W.js} +4 -4
- package/dist/test-jira-LDTOYFSD.js +96 -0
- package/dist/test-jira-LDTOYFSD.js.map +1 -0
- package/dist/{test-prefix-64NAAUON.js → test-prefix-GBO37XCN.js} +4 -4
- package/dist/{test-webserver-OK6Z5FJM.js → test-webserver-NZ3JTVLL.js} +6 -6
- package/dist/{vscode-AR5NNXXI.js → vscode-6XUGHJKL.js} +7 -7
- package/package.json +5 -1
- package/dist/ClaudeContextManager-HR5JQKAI.js +0 -14
- package/dist/ClaudeService-TK7FMC2X.js +0 -13
- package/dist/chunk-3I4ONZRT.js.map +0 -1
- package/dist/chunk-B7U6OKUR.js.map +0 -1
- package/dist/chunk-CWRI4JC3.js.map +0 -1
- package/dist/chunk-DGG2VY7B.js.map +0 -1
- package/dist/chunk-FJDRTVJX.js +0 -520
- package/dist/chunk-FJDRTVJX.js.map +0 -1
- package/dist/chunk-FO5GGFOV.js.map +0 -1
- package/dist/chunk-KBEIQP4G.js.map +0 -1
- package/dist/chunk-KJTVU3HZ.js.map +0 -1
- package/dist/chunk-KXGQYLFZ.js.map +0 -1
- package/dist/chunk-OFDN5NKS.js.map +0 -1
- package/dist/chunk-QN47QVBX.js.map +0 -1
- package/dist/chunk-R4YWBGY6.js.map +0 -1
- package/dist/chunk-RI2YL6TK.js.map +0 -1
- package/dist/chunk-SOSQILHO.js.map +0 -1
- package/dist/chunk-ULSWCPQG.js.map +0 -1
- package/dist/chunk-VOGGLPG5.js.map +0 -1
- package/dist/chunk-VPTAX5TR.js.map +0 -1
- package/dist/chunk-W6DP5RVR.js +0 -101
- package/dist/chunk-W6DP5RVR.js.map +0 -1
- package/dist/chunk-WHI5KEOX.js +0 -121
- package/dist/chunk-WHI5KEOX.js.map +0 -1
- package/dist/chunk-YKFCCV6S.js.map +0 -1
- package/dist/chunk-Z2TWEXR7.js.map +0 -1
- package/dist/cleanup-PJRIFFU4.js.map +0 -1
- package/dist/commit-IVP3M4HG.js.map +0 -1
- package/dist/contribute-VDZXHK5Y.js.map +0 -1
- package/dist/dev-server-7F622OEO.js.map +0 -1
- package/dist/ignite-IW35CDBD.js +0 -784
- package/dist/ignite-IW35CDBD.js.map +0 -1
- package/dist/init-676DHF6R.js.map +0 -1
- package/dist/issues-PJSOLOBJ.js.map +0 -1
- package/dist/open-544H7JF5.js.map +0 -1
- package/dist/plan-Q7ELXDLC.js.map +0 -1
- package/dist/rebase-YND35CIE.js.map +0 -1
- package/dist/recap-3W7COH7D.js.map +0 -1
- package/dist/run-QUXJKDQQ.js.map +0 -1
- package/dist/shell-QGECBLST.js.map +0 -1
- package/dist/summary-G2T4452H.js.map +0 -1
- /package/dist/{BranchNamingService-K6XNWQ6C.js.map → BranchNamingService-25KSZAEM.js.map} +0 -0
- /package/dist/{ClaudeContextManager-HR5JQKAI.js.map → ClaudeContextManager-66GR4BGM.js.map} +0 -0
- /package/dist/{ClaudeService-TK7FMC2X.js.map → ClaudeService-7KM5NA5Z.js.map} +0 -0
- /package/dist/{GitHubService-TGWJN4V4.js.map → GitHubService-MEHKHUQP.js.map} +0 -0
- /package/dist/{MetadataManager-W3C54UYT.js.map → IssueTrackerFactory-NG53YX5S.js.map} +0 -0
- /package/dist/{LoomLauncher-73NXL2CL.js.map → LoomLauncher-TDLZSYG2.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-AW3JTJHD.js.map → PromptTemplateManager-YOE2SIPG.js.map} +0 -0
- /package/dist/{claude-TP2QO3BU.js.map → SettingsManager-FNKCOZMQ.js.map} +0 -0
- /package/dist/{build-THZI572G.js.map → build-VHGEMXBA.js.map} +0 -0
- /package/dist/{chunk-AR5QKYNE.js.map → chunk-4FGEGQW4.js.map} +0 -0
- /package/dist/{chunk-A7NJF73J.js.map → chunk-CVCTIDDK.js.map} +0 -0
- /package/dist/{chunk-IZIYLYPK.js.map → chunk-G5V75JD5.js.map} +0 -0
- /package/dist/{chunk-TC7APDKU.js.map → chunk-I5T677EA.js.map} +0 -0
- /package/dist/{chunk-NWMORW3U.js.map → chunk-KIK2ZFAL.js.map} +0 -0
- /package/dist/{chunk-NUACL52E.js.map → chunk-LLHXQS3C.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-KAYXR544.js.map → chunk-QVLPWNE3.js.map} +0 -0
- /package/dist/{chunk-4LKGCFGG.js.map → chunk-WWKOVDWC.js.map} +0 -0
- /package/dist/{git-2QDQ2X2S.js.map → claude-7GGEWVEM.js.map} +0 -0
- /package/dist/{compile-R2J65HBQ.js.map → compile-7ALJHZ4N.js.map} +0 -0
- /package/dist/{neon-helpers-VVFFTLXE.js.map → git-GTLKAZRJ.js.map} +0 -0
- /package/dist/{lint-CJM7BAIM.js.map → lint-AAN2NZWG.js.map} +0 -0
- /package/dist/{projects-LH362JZQ.js.map → projects-2UOXFLNZ.js.map} +0 -0
- /package/dist/{test-EA5NQFDC.js.map → test-SGO6I5Z7.js.map} +0 -0
- /package/dist/{test-git-M7LSLEFL.js.map → test-git-XM4TM65W.js.map} +0 -0
- /package/dist/{test-prefix-64NAAUON.js.map → test-prefix-GBO37XCN.js.map} +0 -0
- /package/dist/{test-webserver-OK6Z5FJM.js.map → test-webserver-NZ3JTVLL.js.map} +0 -0
- /package/dist/{vscode-AR5NNXXI.js.map → vscode-6XUGHJKL.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,14 +496,32 @@ 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"),
|
|
524
|
+
swarmModel: z.enum(["sonnet", "opus", "haiku"]).optional().describe("Model to use for this agent in swarm mode. Overrides the base model when running inside swarm workers."),
|
|
506
525
|
enabled: z.boolean().optional().describe("Whether this agent is enabled. Defaults to true."),
|
|
507
526
|
providers: z.record(
|
|
508
527
|
z.enum(["claude", "gemini", "codex"]),
|
|
@@ -510,8 +529,13 @@ var init_SettingsManager = __esm({
|
|
|
510
529
|
).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
530
|
review: z.boolean().optional().describe("Whether artifacts from this agent should be reviewed before posting (defaults to false)")
|
|
512
531
|
});
|
|
532
|
+
AgentSettingsSchema = BaseAgentSettingsSchema.extend({
|
|
533
|
+
agents: z.record(z.string(), BaseAgentSettingsSchema).optional().describe("Nested per-agent settings. Only meaningful under the iloom-swarm-worker agent entry for sub-agent timeout configuration."),
|
|
534
|
+
subAgentTimeout: z.number().min(1, "Sub-agent timeout must be at least 1 minute").max(120, "Sub-agent timeout cannot exceed 120 minutes").default(10).describe("Timeout in minutes for sub-agent claude -p invocations in swarm mode. Applies to each phase agent (evaluator, analyzer, planner, implementer) when invoked via the Bash tool. Default: 10 minutes. Only meaningful under the iloom-swarm-worker agent entry.")
|
|
535
|
+
});
|
|
513
536
|
SpinAgentSettingsSchema = z.object({
|
|
514
|
-
model: z.enum(["sonnet", "opus", "haiku"]).default("opus").describe("Claude model shorthand for spin orchestrator")
|
|
537
|
+
model: z.enum(["sonnet", "opus", "haiku"]).default("opus").describe("Claude model shorthand for spin orchestrator"),
|
|
538
|
+
swarmModel: z.enum(["sonnet", "opus", "haiku"]).optional().describe("Model for the spin orchestrator when running in swarm mode. Overrides spin.model for swarm workflows.")
|
|
515
539
|
});
|
|
516
540
|
PlanCommandSettingsSchema = z.object({
|
|
517
541
|
model: z.enum(["sonnet", "opus", "haiku"]).default("opus").describe("Claude model shorthand for plan command"),
|
|
@@ -550,19 +574,17 @@ var init_SettingsManager = __esm({
|
|
|
550
574
|
regular: WorkflowPermissionSchemaNoDefaults.optional()
|
|
551
575
|
}).optional();
|
|
552
576
|
CapabilitiesSettingsSchema = z.object({
|
|
553
|
-
capabilities: z.array(z.enum(PROJECT_CAPABILITIES)).optional().describe("Explicitly declared project capabilities (auto-detected if not specified)"),
|
|
554
577
|
web: z.object({
|
|
555
578
|
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(),
|
|
579
|
+
}).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
580
|
database: z.object({
|
|
558
581
|
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
582
|
}).optional()
|
|
560
583
|
}).optional();
|
|
561
584
|
CapabilitiesSettingsSchemaNoDefaults = z.object({
|
|
562
|
-
capabilities: z.array(z.enum(PROJECT_CAPABILITIES)).optional().describe("Explicitly declared project capabilities (auto-detected if not specified)"),
|
|
563
585
|
web: z.object({
|
|
564
586
|
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(),
|
|
587
|
+
}).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
588
|
database: z.object({
|
|
567
589
|
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
590
|
}).optional()
|
|
@@ -606,7 +628,7 @@ var init_SettingsManager = __esm({
|
|
|
606
628
|
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
629
|
workflows: WorkflowsSettingsSchema.describe("Per-workflow-type permission configurations"),
|
|
608
630
|
agents: z.record(z.string(), AgentSettingsSchema).optional().nullable().describe(
|
|
609
|
-
"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)"
|
|
631
|
+
"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). Use swarmModel on any agent to override its model in swarm mode."
|
|
610
632
|
),
|
|
611
633
|
spin: SpinAgentSettingsSchema.optional().describe(
|
|
612
634
|
"Spin orchestrator configuration. Model defaults to opus when not configured."
|
|
@@ -621,7 +643,7 @@ var init_SettingsManager = __esm({
|
|
|
621
643
|
databaseProviders: DatabaseProvidersSettingsSchema.describe("Database provider configurations"),
|
|
622
644
|
issueManagement: z.object({
|
|
623
645
|
// 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)"),
|
|
646
|
+
provider: z.enum(["github", "linear", "jira"]).optional().default("github").describe("Issue tracker provider (github, linear, jira)"),
|
|
625
647
|
github: z.object({
|
|
626
648
|
remote: z.string().min(1, "Remote name cannot be empty").describe("Git remote name to use for GitHub operations")
|
|
627
649
|
}).optional(),
|
|
@@ -629,6 +651,17 @@ var init_SettingsManager = __esm({
|
|
|
629
651
|
teamId: z.string().min(1, "Team ID cannot be empty").describe('Linear team identifier (e.g., "ENG", "PLAT")'),
|
|
630
652
|
branchFormat: z.string().optional().describe("Branch naming template for Linear issues"),
|
|
631
653
|
apiToken: z.string().optional().describe("Linear API token (lin_api_...). SECURITY: Store in settings.local.json only, never commit to source control.")
|
|
654
|
+
}).optional(),
|
|
655
|
+
jira: z.object({
|
|
656
|
+
host: z.string().min(1, "Jira host cannot be empty").describe('Jira instance URL (e.g., "https://yourcompany.atlassian.net")'),
|
|
657
|
+
username: z.string().min(1, "Jira username/email cannot be empty").describe("Jira username or email address"),
|
|
658
|
+
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"),
|
|
659
|
+
projectKey: z.string().min(1, "Project key cannot be empty").describe('Jira project key (e.g., "PROJ", "ENG")'),
|
|
660
|
+
boardId: z.string().optional().describe("Jira board ID for sprint/workflow operations (optional)"),
|
|
661
|
+
transitionMappings: z.record(z.string(), z.string()).optional().describe('Map iloom states to Jira transition names (e.g., {"In Review": "Start Review"})'),
|
|
662
|
+
defaultIssueType: z.string().min(1).optional().default("Task").describe('Default Jira issue type name for creating issues (e.g., "Task", "Story", "Bug")'),
|
|
663
|
+
defaultSubtaskType: z.string().min(1).optional().default("Subtask").describe('Default Jira issue type name for creating subtasks/child issues (e.g., "Subtask", "Sub-task")'),
|
|
664
|
+
doneStatuses: z.array(z.string()).optional().default(["Done"]).describe('Status names to exclude from issue lists (e.g., ["Done", "Closed", "Verify"])')
|
|
632
665
|
}).optional()
|
|
633
666
|
}).optional().describe("Issue management configuration"),
|
|
634
667
|
mergeBehavior: z.object({
|
|
@@ -637,6 +670,9 @@ var init_SettingsManager = __esm({
|
|
|
637
670
|
remote: z.string().optional(),
|
|
638
671
|
autoCommitPush: z.boolean().optional().describe(
|
|
639
672
|
"Auto-commit and push after code review in draft PR mode. Defaults to true when mode is github-draft-pr."
|
|
673
|
+
),
|
|
674
|
+
openBrowserOnFinish: z.boolean().default(true).describe(
|
|
675
|
+
"Open the PR in the default browser after finishing in github-pr or github-draft-pr mode. Use --no-browser flag to override."
|
|
640
676
|
)
|
|
641
677
|
}).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
678
|
ide: z.object({
|
|
@@ -690,10 +726,11 @@ var init_SettingsManager = __esm({
|
|
|
690
726
|
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.`),
|
|
691
727
|
workflows: WorkflowsSettingsSchemaNoDefaults.describe("Per-workflow-type permission configurations"),
|
|
692
728
|
agents: z.record(z.string(), AgentSettingsSchema).optional().nullable().describe(
|
|
693
|
-
"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)"
|
|
729
|
+
"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). Use swarmModel on any agent to override its model in swarm mode."
|
|
694
730
|
),
|
|
695
731
|
spin: z.object({
|
|
696
|
-
model: z.enum(["sonnet", "opus", "haiku"]).optional()
|
|
732
|
+
model: z.enum(["sonnet", "opus", "haiku"]).optional(),
|
|
733
|
+
swarmModel: z.enum(["sonnet", "opus", "haiku"]).optional()
|
|
697
734
|
}).optional().describe("Spin orchestrator configuration"),
|
|
698
735
|
plan: z.object({
|
|
699
736
|
model: z.enum(["sonnet", "opus", "haiku"]).optional(),
|
|
@@ -706,7 +743,7 @@ var init_SettingsManager = __esm({
|
|
|
706
743
|
capabilities: CapabilitiesSettingsSchemaNoDefaults.describe("Project capability configurations"),
|
|
707
744
|
databaseProviders: DatabaseProvidersSettingsSchema.describe("Database provider configurations"),
|
|
708
745
|
issueManagement: z.object({
|
|
709
|
-
provider: z.enum(["github", "linear"]).optional().describe("Issue tracker provider (github, linear)"),
|
|
746
|
+
provider: z.enum(["github", "linear", "jira"]).optional().describe("Issue tracker provider (github, linear, jira)"),
|
|
710
747
|
github: z.object({
|
|
711
748
|
remote: z.string().min(1, "Remote name cannot be empty").describe("Git remote name to use for GitHub operations")
|
|
712
749
|
}).optional(),
|
|
@@ -714,6 +751,17 @@ var init_SettingsManager = __esm({
|
|
|
714
751
|
teamId: z.string().min(1, "Team ID cannot be empty").describe('Linear team identifier (e.g., "ENG", "PLAT")'),
|
|
715
752
|
branchFormat: z.string().optional().describe("Branch naming template for Linear issues"),
|
|
716
753
|
apiToken: z.string().optional().describe("Linear API token (lin_api_...). SECURITY: Store in settings.local.json only, never commit to source control.")
|
|
754
|
+
}).optional(),
|
|
755
|
+
jira: z.object({
|
|
756
|
+
host: z.string().min(1, "Jira host cannot be empty").describe('Jira instance URL (e.g., "https://yourcompany.atlassian.net")'),
|
|
757
|
+
username: z.string().min(1, "Jira username/email cannot be empty").describe("Jira username or email address"),
|
|
758
|
+
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"),
|
|
759
|
+
projectKey: z.string().min(1, "Project key cannot be empty").describe('Jira project key (e.g., "PROJ", "ENG")'),
|
|
760
|
+
boardId: z.string().optional().describe("Jira board ID for sprint/workflow operations (optional)"),
|
|
761
|
+
transitionMappings: z.record(z.string(), z.string()).optional().describe('Map iloom states to Jira transition names (e.g., {"In Review": "Start Review"})'),
|
|
762
|
+
defaultIssueType: z.string().min(1).optional().describe('Default Jira issue type name for creating issues (e.g., "Task", "Story", "Bug")'),
|
|
763
|
+
defaultSubtaskType: z.string().min(1).optional().describe('Default Jira issue type name for creating subtasks/child issues (e.g., "Subtask", "Sub-task")'),
|
|
764
|
+
doneStatuses: z.array(z.string()).optional().default(["Done"]).describe('Status names to exclude from issue lists (e.g., ["Done", "Closed", "Verify"])')
|
|
717
765
|
}).optional()
|
|
718
766
|
}).optional().describe("Issue management configuration"),
|
|
719
767
|
mergeBehavior: z.object({
|
|
@@ -721,6 +769,9 @@ var init_SettingsManager = __esm({
|
|
|
721
769
|
remote: z.string().optional(),
|
|
722
770
|
autoCommitPush: z.boolean().optional().describe(
|
|
723
771
|
"Auto-commit and push after code review in draft PR mode. Defaults to true when mode is github-draft-pr."
|
|
772
|
+
),
|
|
773
|
+
openBrowserOnFinish: z.boolean().optional().describe(
|
|
774
|
+
"Open the PR in the default browser after finishing in github-pr or github-draft-pr mode. Use --no-browser flag to override."
|
|
724
775
|
)
|
|
725
776
|
}).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)"),
|
|
726
777
|
ide: z.object({
|
|
@@ -757,19 +808,19 @@ var init_SettingsManager = __esm({
|
|
|
757
808
|
const root = this.getProjectRoot(projectRoot);
|
|
758
809
|
const globalSettings = await this.loadGlobalSettingsFile();
|
|
759
810
|
const globalSettingsPath = this.getGlobalSettingsPath();
|
|
760
|
-
logger.debug(`\u{1F30D} Global settings from ${globalSettingsPath}:`, JSON.stringify(globalSettings, null, 2));
|
|
811
|
+
logger.debug(`\u{1F30D} Global settings from ${globalSettingsPath}:`, JSON.stringify(redactSensitiveFields(globalSettings), null, 2));
|
|
761
812
|
const baseSettings = await this.loadSettingsFile(root, "settings.json");
|
|
762
813
|
const baseSettingsPath = path2.join(root, ".iloom", "settings.json");
|
|
763
|
-
logger.debug(`\u{1F4C4} Base settings from ${baseSettingsPath}:`, JSON.stringify(baseSettings, null, 2));
|
|
814
|
+
logger.debug(`\u{1F4C4} Base settings from ${baseSettingsPath}:`, JSON.stringify(redactSensitiveFields(baseSettings), null, 2));
|
|
764
815
|
const localSettings = await this.loadSettingsFile(root, "settings.local.json");
|
|
765
816
|
const localSettingsPath = path2.join(root, ".iloom", "settings.local.json");
|
|
766
|
-
logger.debug(`\u{1F4C4} Local settings from ${localSettingsPath}:`, JSON.stringify(localSettings, null, 2));
|
|
817
|
+
logger.debug(`\u{1F4C4} Local settings from ${localSettingsPath}:`, JSON.stringify(redactSensitiveFields(localSettings), null, 2));
|
|
767
818
|
let merged = this.mergeSettings(this.mergeSettings(globalSettings, baseSettings), localSettings);
|
|
768
|
-
logger.debug("\u{1F504} After merging global + base + local settings:", JSON.stringify(merged, null, 2));
|
|
819
|
+
logger.debug("\u{1F504} After merging global + base + local settings:", JSON.stringify(redactSensitiveFields(merged), null, 2));
|
|
769
820
|
if (cliOverrides && Object.keys(cliOverrides).length > 0) {
|
|
770
|
-
logger.debug("\u2699\uFE0F CLI overrides to apply:", JSON.stringify(cliOverrides, null, 2));
|
|
821
|
+
logger.debug("\u2699\uFE0F CLI overrides to apply:", JSON.stringify(redactSensitiveFields(cliOverrides), null, 2));
|
|
771
822
|
merged = this.mergeSettings(merged, cliOverrides);
|
|
772
|
-
logger.debug("\u{1F504} After applying CLI overrides:", JSON.stringify(merged, null, 2));
|
|
823
|
+
logger.debug("\u{1F504} After applying CLI overrides:", JSON.stringify(redactSensitiveFields(merged), null, 2));
|
|
773
824
|
}
|
|
774
825
|
try {
|
|
775
826
|
const finalSettings = IloomSettingsSchema.parse(merged);
|
|
@@ -792,7 +843,7 @@ Note: CLI overrides were applied. Check your --set arguments.`);
|
|
|
792
843
|
* Log the final merged configuration for debugging
|
|
793
844
|
*/
|
|
794
845
|
logFinalConfiguration(settings) {
|
|
795
|
-
logger.debug("\u{1F4CB} Final merged configuration:", JSON.stringify(settings, null, 2));
|
|
846
|
+
logger.debug("\u{1F4CB} Final merged configuration:", JSON.stringify(redactSensitiveFields(settings), null, 2));
|
|
796
847
|
}
|
|
797
848
|
/**
|
|
798
849
|
* Load and parse a single settings file
|
|
@@ -951,9 +1002,15 @@ ${errorMessages.join("\n")}`
|
|
|
951
1002
|
* @param settings - Pre-loaded settings object
|
|
952
1003
|
* @returns Model shorthand ('opus', 'sonnet', or 'haiku')
|
|
953
1004
|
*/
|
|
954
|
-
getSpinModel(settings) {
|
|
955
|
-
var _a;
|
|
956
|
-
|
|
1005
|
+
getSpinModel(settings, mode) {
|
|
1006
|
+
var _a, _b;
|
|
1007
|
+
if (mode === "swarm") {
|
|
1008
|
+
if ((_a = settings == null ? void 0 : settings.spin) == null ? void 0 : _a.swarmModel) {
|
|
1009
|
+
return settings.spin.swarmModel;
|
|
1010
|
+
}
|
|
1011
|
+
return "opus";
|
|
1012
|
+
}
|
|
1013
|
+
return ((_b = settings == null ? void 0 : settings.spin) == null ? void 0 : _b.model) ?? SpinAgentSettingsSchema.parse({}).model;
|
|
957
1014
|
}
|
|
958
1015
|
/**
|
|
959
1016
|
* Get the plan command model with default applied
|
|
@@ -1253,6 +1310,7 @@ var MetadataManager = class {
|
|
|
1253
1310
|
branchName: data.branchName ?? null,
|
|
1254
1311
|
worktreePath: data.worktreePath ?? null,
|
|
1255
1312
|
issueType: data.issueType ?? null,
|
|
1313
|
+
issueKey: data.issueKey ?? null,
|
|
1256
1314
|
issue_numbers: data.issue_numbers ?? [],
|
|
1257
1315
|
pr_numbers: data.pr_numbers ?? [],
|
|
1258
1316
|
issueTracker: data.issueTracker ?? null,
|
|
@@ -1264,7 +1322,12 @@ var MetadataManager = class {
|
|
|
1264
1322
|
draftPrNumber: data.draftPrNumber ?? null,
|
|
1265
1323
|
oneShot: data.oneShot ?? null,
|
|
1266
1324
|
capabilities: data.capabilities ?? [],
|
|
1267
|
-
|
|
1325
|
+
state: data.state ?? null,
|
|
1326
|
+
childIssueNumbers: data.childIssueNumbers ?? [],
|
|
1327
|
+
parentLoom: data.parentLoom ?? null,
|
|
1328
|
+
childIssues: data.childIssues ?? [],
|
|
1329
|
+
dependencyMap: data.dependencyMap ?? {},
|
|
1330
|
+
mcpConfigPath: data.mcpConfigPath ?? null
|
|
1268
1331
|
};
|
|
1269
1332
|
}
|
|
1270
1333
|
/**
|
|
@@ -1317,6 +1380,7 @@ var MetadataManager = class {
|
|
|
1317
1380
|
branchName: input.branchName,
|
|
1318
1381
|
worktreePath: input.worktreePath,
|
|
1319
1382
|
issueType: input.issueType,
|
|
1383
|
+
...input.issueKey && { issueKey: input.issueKey },
|
|
1320
1384
|
issue_numbers: input.issue_numbers,
|
|
1321
1385
|
pr_numbers: input.pr_numbers,
|
|
1322
1386
|
issueTracker: input.issueTracker,
|
|
@@ -1328,7 +1392,12 @@ var MetadataManager = class {
|
|
|
1328
1392
|
capabilities: input.capabilities,
|
|
1329
1393
|
...input.draftPrNumber && { draftPrNumber: input.draftPrNumber },
|
|
1330
1394
|
...input.oneShot && { oneShot: input.oneShot },
|
|
1331
|
-
...input.
|
|
1395
|
+
...input.state && { state: input.state },
|
|
1396
|
+
...input.childIssueNumbers && input.childIssueNumbers.length > 0 && { childIssueNumbers: input.childIssueNumbers },
|
|
1397
|
+
...input.parentLoom && { parentLoom: input.parentLoom },
|
|
1398
|
+
...input.childIssues && input.childIssues.length > 0 && { childIssues: input.childIssues },
|
|
1399
|
+
...input.dependencyMap && Object.keys(input.dependencyMap).length > 0 && { dependencyMap: input.dependencyMap },
|
|
1400
|
+
...input.mcpConfigPath && { mcpConfigPath: input.mcpConfigPath }
|
|
1332
1401
|
};
|
|
1333
1402
|
const filePath = this.getFilePath(worktreePath);
|
|
1334
1403
|
await fs.writeFile(filePath, JSON.stringify(content, null, 2), { mode: 420 });
|
|
@@ -1404,6 +1473,34 @@ var MetadataManager = class {
|
|
|
1404
1473
|
}
|
|
1405
1474
|
return results;
|
|
1406
1475
|
}
|
|
1476
|
+
/**
|
|
1477
|
+
* Update existing metadata for a worktree by merging new fields
|
|
1478
|
+
*
|
|
1479
|
+
* Reads the existing metadata file, merges the provided updates,
|
|
1480
|
+
* and writes back. Only provided fields are overwritten.
|
|
1481
|
+
*
|
|
1482
|
+
* @param worktreePath - Absolute path to the worktree
|
|
1483
|
+
* @param updates - Partial metadata fields to merge
|
|
1484
|
+
*/
|
|
1485
|
+
async updateMetadata(worktreePath, updates) {
|
|
1486
|
+
try {
|
|
1487
|
+
const filePath = this.getFilePath(worktreePath);
|
|
1488
|
+
if (!await fs.pathExists(filePath)) {
|
|
1489
|
+
getLogger().warn(`No metadata file to update for worktree: ${worktreePath}`);
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
1493
|
+
const data = JSON.parse(content);
|
|
1494
|
+
const merged = { ...data, ...updates };
|
|
1495
|
+
await fs.writeFile(filePath, JSON.stringify(merged, null, 2), { mode: 420 });
|
|
1496
|
+
getLogger().debug(`Metadata updated for worktree: ${worktreePath}`);
|
|
1497
|
+
} catch (error) {
|
|
1498
|
+
getLogger().warn(
|
|
1499
|
+
`Failed to update metadata for worktree: ${error instanceof Error ? error.message : String(error)}`
|
|
1500
|
+
);
|
|
1501
|
+
throw error;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1407
1504
|
/**
|
|
1408
1505
|
* Delete metadata for a worktree (spec section 3.3)
|
|
1409
1506
|
*
|
|
@@ -2769,6 +2866,65 @@ async function createIssue(title, body, options) {
|
|
|
2769
2866
|
url: issueUrl
|
|
2770
2867
|
};
|
|
2771
2868
|
}
|
|
2869
|
+
async function getIssueNodeId(issueNumber, repo) {
|
|
2870
|
+
logger.debug("Fetching GitHub issue node ID", { issueNumber, repo });
|
|
2871
|
+
const args = ["issue", "view", String(issueNumber), "--json", "id"];
|
|
2872
|
+
if (repo) {
|
|
2873
|
+
args.push("--repo", repo);
|
|
2874
|
+
}
|
|
2875
|
+
const result = await executeGhCommand(args);
|
|
2876
|
+
return result.id;
|
|
2877
|
+
}
|
|
2878
|
+
async function getSubIssues(issueNumber, repo) {
|
|
2879
|
+
var _a, _b;
|
|
2880
|
+
logger.debug("Fetching GitHub sub-issues", { issueNumber, repo });
|
|
2881
|
+
const parentNodeId = await getIssueNodeId(issueNumber, repo);
|
|
2882
|
+
const query = `
|
|
2883
|
+
query getSubIssues($parentId: ID!) {
|
|
2884
|
+
node(id: $parentId) {
|
|
2885
|
+
... on Issue {
|
|
2886
|
+
subIssues(first: 100) {
|
|
2887
|
+
nodes {
|
|
2888
|
+
number
|
|
2889
|
+
title
|
|
2890
|
+
url
|
|
2891
|
+
state
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
`;
|
|
2898
|
+
try {
|
|
2899
|
+
const result = await executeGhCommand([
|
|
2900
|
+
"api",
|
|
2901
|
+
"graphql",
|
|
2902
|
+
"-H",
|
|
2903
|
+
"GraphQL-Features: sub_issues",
|
|
2904
|
+
"-f",
|
|
2905
|
+
`query=${query}`,
|
|
2906
|
+
"-F",
|
|
2907
|
+
`parentId=${parentNodeId}`
|
|
2908
|
+
]);
|
|
2909
|
+
const subIssues = ((_b = (_a = result.data.node) == null ? void 0 : _a.subIssues) == null ? void 0 : _b.nodes) ?? [];
|
|
2910
|
+
return subIssues.map((issue) => ({
|
|
2911
|
+
id: String(issue.number),
|
|
2912
|
+
title: issue.title,
|
|
2913
|
+
url: issue.url,
|
|
2914
|
+
state: issue.state.toLowerCase()
|
|
2915
|
+
}));
|
|
2916
|
+
} catch (error) {
|
|
2917
|
+
if (error instanceof Error) {
|
|
2918
|
+
const errorMessage = error.message;
|
|
2919
|
+
const stderr = "stderr" in error ? error.stderr ?? "" : "";
|
|
2920
|
+
const combinedError = `${errorMessage} ${stderr}`;
|
|
2921
|
+
if (combinedError.includes("sub_issues") || combinedError.includes("null")) {
|
|
2922
|
+
return [];
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
throw error;
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2772
2928
|
|
|
2773
2929
|
// src/utils/prompt.ts
|
|
2774
2930
|
init_logger();
|
|
@@ -2951,6 +3107,14 @@ var GitHubService = class {
|
|
|
2951
3107
|
const issue = await fetchGhIssue(issueNumber, repo);
|
|
2952
3108
|
return issue.url;
|
|
2953
3109
|
}
|
|
3110
|
+
async getChildIssues(parentIdentifier, repo) {
|
|
3111
|
+
const issueNum = parseInt(parentIdentifier, 10);
|
|
3112
|
+
if (isNaN(issueNum)) {
|
|
3113
|
+
getLogger().warn(`Invalid GitHub issue number: ${parentIdentifier}`);
|
|
3114
|
+
return [];
|
|
3115
|
+
}
|
|
3116
|
+
return getSubIssues(issueNum, repo);
|
|
3117
|
+
}
|
|
2954
3118
|
// GitHub Projects integration
|
|
2955
3119
|
async moveIssueToInProgress(issueNumber) {
|
|
2956
3120
|
getLogger().info("Moving issue to In Progress in GitHub Projects", {
|
|
@@ -2986,7 +3150,48 @@ var GitHubService = class {
|
|
|
2986
3150
|
await this.updateIssueStatusInProject(project, issueNumber, owner);
|
|
2987
3151
|
}
|
|
2988
3152
|
}
|
|
2989
|
-
|
|
3153
|
+
// GitHub Projects integration - move to Ready for Review
|
|
3154
|
+
async moveIssueToReadyForReview(issueNumber) {
|
|
3155
|
+
getLogger().info("Moving issue to Ready for Review in GitHub Projects", {
|
|
3156
|
+
issueNumber
|
|
3157
|
+
});
|
|
3158
|
+
if (!await hasProjectScope()) {
|
|
3159
|
+
getLogger().warn("Missing project scope in GitHub CLI auth");
|
|
3160
|
+
throw new GitHubError(
|
|
3161
|
+
"MISSING_SCOPE" /* MISSING_SCOPE */,
|
|
3162
|
+
"GitHub CLI lacks project scope. Run: gh auth refresh -s project"
|
|
3163
|
+
);
|
|
3164
|
+
}
|
|
3165
|
+
let owner;
|
|
3166
|
+
try {
|
|
3167
|
+
const repoInfo = await executeGhCommand(["repo", "view", "--json", "owner,name"]);
|
|
3168
|
+
owner = repoInfo.owner.login;
|
|
3169
|
+
} catch (error) {
|
|
3170
|
+
getLogger().warn("Could not determine repository info", { error });
|
|
3171
|
+
return;
|
|
3172
|
+
}
|
|
3173
|
+
let projects;
|
|
3174
|
+
try {
|
|
3175
|
+
projects = await fetchProjectList(owner);
|
|
3176
|
+
} catch (error) {
|
|
3177
|
+
getLogger().warn("Could not fetch projects", { owner, error });
|
|
3178
|
+
return;
|
|
3179
|
+
}
|
|
3180
|
+
if (!projects.length) {
|
|
3181
|
+
getLogger().warn("No projects found", { owner });
|
|
3182
|
+
return;
|
|
3183
|
+
}
|
|
3184
|
+
for (const project of projects) {
|
|
3185
|
+
await this.updateIssueStatusInProject(
|
|
3186
|
+
project,
|
|
3187
|
+
issueNumber,
|
|
3188
|
+
owner,
|
|
3189
|
+
["Ready for Review", "In Review", "Review"],
|
|
3190
|
+
"Ready for Review"
|
|
3191
|
+
);
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
async updateIssueStatusInProject(project, issueNumber, owner, statusNames = ["In Progress", "In progress"], logLabel = "In Progress") {
|
|
2990
3195
|
var _a;
|
|
2991
3196
|
let items;
|
|
2992
3197
|
try {
|
|
@@ -3017,11 +3222,13 @@ var GitHubService = class {
|
|
|
3017
3222
|
getLogger().debug("No Status field found in project", { projectNumber: project.number });
|
|
3018
3223
|
return;
|
|
3019
3224
|
}
|
|
3020
|
-
const
|
|
3021
|
-
(o) =>
|
|
3225
|
+
const targetOption = (_a = statusField.options) == null ? void 0 : _a.find(
|
|
3226
|
+
(o) => statusNames.some(
|
|
3227
|
+
(name) => o.name.toLowerCase() === name.toLowerCase()
|
|
3228
|
+
)
|
|
3022
3229
|
);
|
|
3023
|
-
if (!
|
|
3024
|
-
getLogger().debug(
|
|
3230
|
+
if (!targetOption) {
|
|
3231
|
+
getLogger().debug(`No ${logLabel} option found in Status field`, { projectNumber: project.number });
|
|
3025
3232
|
return;
|
|
3026
3233
|
}
|
|
3027
3234
|
try {
|
|
@@ -3029,16 +3236,21 @@ var GitHubService = class {
|
|
|
3029
3236
|
item.id,
|
|
3030
3237
|
project.id,
|
|
3031
3238
|
statusField.id,
|
|
3032
|
-
|
|
3239
|
+
targetOption.id
|
|
3033
3240
|
);
|
|
3034
3241
|
getLogger().info("Updated issue status in project", {
|
|
3035
3242
|
issueNumber,
|
|
3036
|
-
projectNumber: project.number
|
|
3243
|
+
projectNumber: project.number,
|
|
3244
|
+
status: logLabel
|
|
3037
3245
|
});
|
|
3038
3246
|
} catch (error) {
|
|
3039
3247
|
getLogger().debug("Could not update project item", { item: item.id, error });
|
|
3040
3248
|
}
|
|
3041
3249
|
}
|
|
3250
|
+
// Identifier normalization - GitHub identifiers are numeric, just stringify
|
|
3251
|
+
normalizeIdentifier(identifier) {
|
|
3252
|
+
return String(identifier);
|
|
3253
|
+
}
|
|
3042
3254
|
// Utility methods
|
|
3043
3255
|
extractContext(entity) {
|
|
3044
3256
|
if ("branch" in entity) {
|
|
@@ -3247,6 +3459,35 @@ async function updateLinearIssueState(identifier, stateName) {
|
|
|
3247
3459
|
handleLinearError(error, "updateLinearIssueState");
|
|
3248
3460
|
}
|
|
3249
3461
|
}
|
|
3462
|
+
async function getLinearChildIssues(identifier, options) {
|
|
3463
|
+
try {
|
|
3464
|
+
logger.debug(`Fetching child issues for Linear issue: ${identifier}`);
|
|
3465
|
+
const client = createLinearClient(options == null ? void 0 : options.apiToken);
|
|
3466
|
+
const issue = await client.issue(identifier);
|
|
3467
|
+
if (!issue) {
|
|
3468
|
+
throw new LinearServiceError("NOT_FOUND", `Linear issue ${identifier} not found`);
|
|
3469
|
+
}
|
|
3470
|
+
const children = await issue.children({ first: 100 });
|
|
3471
|
+
const results = await Promise.all(
|
|
3472
|
+
children.nodes.map(async (child) => {
|
|
3473
|
+
const stateObj = await child.state;
|
|
3474
|
+
const state = (stateObj == null ? void 0 : stateObj.name) ?? "unknown";
|
|
3475
|
+
return {
|
|
3476
|
+
id: child.identifier,
|
|
3477
|
+
title: child.title,
|
|
3478
|
+
url: child.url,
|
|
3479
|
+
state
|
|
3480
|
+
};
|
|
3481
|
+
})
|
|
3482
|
+
);
|
|
3483
|
+
return results;
|
|
3484
|
+
} catch (error) {
|
|
3485
|
+
if (error instanceof LinearServiceError) {
|
|
3486
|
+
throw error;
|
|
3487
|
+
}
|
|
3488
|
+
handleLinearError(error, "getLinearChildIssues");
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3250
3491
|
|
|
3251
3492
|
// src/lib/LinearService.ts
|
|
3252
3493
|
var LinearService = class {
|
|
@@ -3360,6 +3601,15 @@ var LinearService = class {
|
|
|
3360
3601
|
const issue = await this.fetchIssue(identifier);
|
|
3361
3602
|
return issue.url;
|
|
3362
3603
|
}
|
|
3604
|
+
/**
|
|
3605
|
+
* Fetch child issues of a Linear parent issue
|
|
3606
|
+
* @param parentIdentifier - Linear issue identifier (e.g., "ENG-123")
|
|
3607
|
+
* @param _repo - Repository (unused for Linear)
|
|
3608
|
+
* @returns Array of child issues
|
|
3609
|
+
*/
|
|
3610
|
+
async getChildIssues(parentIdentifier, _repo) {
|
|
3611
|
+
return getLinearChildIssues(parentIdentifier, this.config.apiToken ? { apiToken: this.config.apiToken } : void 0);
|
|
3612
|
+
}
|
|
3363
3613
|
/**
|
|
3364
3614
|
* Move a Linear issue to "In Progress" state
|
|
3365
3615
|
* @param identifier - Linear issue identifier
|
|
@@ -3369,6 +3619,23 @@ var LinearService = class {
|
|
|
3369
3619
|
getLogger().info(`Moving Linear issue ${identifier} to In Progress`);
|
|
3370
3620
|
await updateLinearIssueState(String(identifier), "In Progress");
|
|
3371
3621
|
}
|
|
3622
|
+
/**
|
|
3623
|
+
* Move a Linear issue to "In Review" state
|
|
3624
|
+
* @param identifier - Linear issue identifier
|
|
3625
|
+
* @throws LinearServiceError if state update fails
|
|
3626
|
+
*/
|
|
3627
|
+
async moveIssueToReadyForReview(identifier) {
|
|
3628
|
+
getLogger().info(`Moving Linear issue ${identifier} to In Review`);
|
|
3629
|
+
await updateLinearIssueState(String(identifier), "In Review");
|
|
3630
|
+
}
|
|
3631
|
+
/**
|
|
3632
|
+
* Normalize identifier to canonical form (uppercase for Linear keys)
|
|
3633
|
+
* @param identifier - Linear issue identifier (e.g., "eng-123" or "ENG-123")
|
|
3634
|
+
* @returns Uppercase identifier (e.g., "ENG-123")
|
|
3635
|
+
*/
|
|
3636
|
+
normalizeIdentifier(identifier) {
|
|
3637
|
+
return String(identifier).toUpperCase();
|
|
3638
|
+
}
|
|
3372
3639
|
/**
|
|
3373
3640
|
* Extract issue context for AI prompts
|
|
3374
3641
|
* @param entity - Issue (Linear doesn't have PRs)
|
|
@@ -3400,6 +3667,844 @@ ${issue.body}`;
|
|
|
3400
3667
|
}
|
|
3401
3668
|
};
|
|
3402
3669
|
|
|
3670
|
+
// src/lib/providers/jira/JiraApiClient.ts
|
|
3671
|
+
import https from "https";
|
|
3672
|
+
|
|
3673
|
+
// src/lib/providers/jira/AdfMarkdownConverter.ts
|
|
3674
|
+
import { Parser } from "extended-markdown-adf-parser";
|
|
3675
|
+
var parser = new Parser();
|
|
3676
|
+
function sanitizeCodeMarks(node) {
|
|
3677
|
+
var _a;
|
|
3678
|
+
if ((_a = node.marks) == null ? void 0 : _a.some((mark) => mark.type === "code")) {
|
|
3679
|
+
node.marks = [{ type: "code" }];
|
|
3680
|
+
}
|
|
3681
|
+
if (node.content && Array.isArray(node.content)) {
|
|
3682
|
+
node.content = node.content.map((child) => sanitizeCodeMarks(child));
|
|
3683
|
+
}
|
|
3684
|
+
return node;
|
|
3685
|
+
}
|
|
3686
|
+
var BLOCK_LEVEL_TYPES = /* @__PURE__ */ new Set([
|
|
3687
|
+
"paragraph",
|
|
3688
|
+
"bulletList",
|
|
3689
|
+
"orderedList",
|
|
3690
|
+
"codeBlock",
|
|
3691
|
+
"heading",
|
|
3692
|
+
"blockquote",
|
|
3693
|
+
"rule",
|
|
3694
|
+
"mediaGroup",
|
|
3695
|
+
"nestedExpand",
|
|
3696
|
+
"panel",
|
|
3697
|
+
"table",
|
|
3698
|
+
"taskList",
|
|
3699
|
+
"decisionList",
|
|
3700
|
+
"mediaSingle"
|
|
3701
|
+
]);
|
|
3702
|
+
function wrapTableCellContent(node) {
|
|
3703
|
+
if (node.content && Array.isArray(node.content)) {
|
|
3704
|
+
node.content = node.content.map((child) => wrapTableCellContent(child));
|
|
3705
|
+
}
|
|
3706
|
+
if (node.type !== "tableCell" && node.type !== "tableHeader") {
|
|
3707
|
+
return node;
|
|
3708
|
+
}
|
|
3709
|
+
if (!node.content || node.content.length === 0) {
|
|
3710
|
+
return node;
|
|
3711
|
+
}
|
|
3712
|
+
const allInline = node.content.every((child) => !BLOCK_LEVEL_TYPES.has(child.type));
|
|
3713
|
+
if (allInline) {
|
|
3714
|
+
node.content = [{ type: "paragraph", content: node.content }];
|
|
3715
|
+
} else {
|
|
3716
|
+
const newContent = [];
|
|
3717
|
+
let inlineRun = [];
|
|
3718
|
+
for (const child of node.content) {
|
|
3719
|
+
if (BLOCK_LEVEL_TYPES.has(child.type)) {
|
|
3720
|
+
if (inlineRun.length > 0) {
|
|
3721
|
+
newContent.push({ type: "paragraph", content: inlineRun });
|
|
3722
|
+
inlineRun = [];
|
|
3723
|
+
}
|
|
3724
|
+
newContent.push(child);
|
|
3725
|
+
} else {
|
|
3726
|
+
inlineRun.push(child);
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
if (inlineRun.length > 0) {
|
|
3730
|
+
newContent.push({ type: "paragraph", content: inlineRun });
|
|
3731
|
+
}
|
|
3732
|
+
node.content = newContent;
|
|
3733
|
+
}
|
|
3734
|
+
return node;
|
|
3735
|
+
}
|
|
3736
|
+
var taskIdCounter = 0;
|
|
3737
|
+
function getCanonicalPlainText(text) {
|
|
3738
|
+
const miniAdf = parser.markdownToAdf(text);
|
|
3739
|
+
return getPlainText(miniAdf);
|
|
3740
|
+
}
|
|
3741
|
+
function extractCheckboxBlocks(markdown) {
|
|
3742
|
+
var _a, _b;
|
|
3743
|
+
const lines = markdown.split("\n");
|
|
3744
|
+
const blocks = [];
|
|
3745
|
+
let i = 0;
|
|
3746
|
+
while (i < lines.length) {
|
|
3747
|
+
const bulletLines = [];
|
|
3748
|
+
let blockIndent = null;
|
|
3749
|
+
while (i < lines.length) {
|
|
3750
|
+
const line = lines[i] ?? "";
|
|
3751
|
+
const checkboxMatch = line.match(/^(\s*)[-*+] \[([ xX])\] (.*)$/);
|
|
3752
|
+
if (checkboxMatch) {
|
|
3753
|
+
const indent = ((_a = checkboxMatch[1]) == null ? void 0 : _a.length) ?? 0;
|
|
3754
|
+
if (blockIndent === null) {
|
|
3755
|
+
blockIndent = indent;
|
|
3756
|
+
} else if (indent !== blockIndent) {
|
|
3757
|
+
break;
|
|
3758
|
+
}
|
|
3759
|
+
const state = checkboxMatch[2] === " " ? "TODO" : "DONE";
|
|
3760
|
+
bulletLines.push({ isCheckbox: true, state, rawText: checkboxMatch[3] ?? "" });
|
|
3761
|
+
i++;
|
|
3762
|
+
} else if (line.match(/^\s*[-*+] /)) {
|
|
3763
|
+
const indentMatch = line.match(/^(\s*)/);
|
|
3764
|
+
const indent = ((_b = indentMatch == null ? void 0 : indentMatch[1]) == null ? void 0 : _b.length) ?? 0;
|
|
3765
|
+
if (blockIndent === null) {
|
|
3766
|
+
blockIndent = indent;
|
|
3767
|
+
} else if (indent !== blockIndent) {
|
|
3768
|
+
break;
|
|
3769
|
+
}
|
|
3770
|
+
bulletLines.push({ isCheckbox: false, state: null, rawText: "" });
|
|
3771
|
+
i++;
|
|
3772
|
+
} else if (bulletLines.length > 0 && line.match(/^\s/) && line.trim() !== "") {
|
|
3773
|
+
const lastItem = bulletLines[bulletLines.length - 1];
|
|
3774
|
+
if (lastItem) {
|
|
3775
|
+
lastItem.rawText += "\n" + line.trim();
|
|
3776
|
+
}
|
|
3777
|
+
i++;
|
|
3778
|
+
} else {
|
|
3779
|
+
break;
|
|
3780
|
+
}
|
|
3781
|
+
}
|
|
3782
|
+
if (bulletLines.length > 0) {
|
|
3783
|
+
const allCheckboxes = bulletLines.every((l) => l.isCheckbox);
|
|
3784
|
+
if (allCheckboxes) {
|
|
3785
|
+
blocks.push({
|
|
3786
|
+
states: bulletLines.map((l) => l.state),
|
|
3787
|
+
texts: bulletLines.map((l) => getCanonicalPlainText(l.rawText))
|
|
3788
|
+
});
|
|
3789
|
+
}
|
|
3790
|
+
} else {
|
|
3791
|
+
i++;
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
return blocks;
|
|
3795
|
+
}
|
|
3796
|
+
function getPlainText(node) {
|
|
3797
|
+
if (node.type === "text" && node.text !== void 0) return node.text;
|
|
3798
|
+
if (!node.content) return "";
|
|
3799
|
+
return node.content.map(getPlainText).join("");
|
|
3800
|
+
}
|
|
3801
|
+
function convertCheckboxesToTaskList(node, blocks) {
|
|
3802
|
+
const cursor = { index: 0 };
|
|
3803
|
+
return convertCheckboxesRecursive(node, blocks, cursor);
|
|
3804
|
+
}
|
|
3805
|
+
function convertCheckboxesRecursive(node, blocks, cursor) {
|
|
3806
|
+
var _a;
|
|
3807
|
+
if (node.type === "bulletList" && node.content && node.content.length > 0 && cursor.index < blocks.length) {
|
|
3808
|
+
const block = blocks[cursor.index];
|
|
3809
|
+
if (!block) return node;
|
|
3810
|
+
if (node.content.length === block.states.length) {
|
|
3811
|
+
const plaintexts = node.content.map((listItem) => getPlainText(listItem));
|
|
3812
|
+
const matches = plaintexts.every((text, i) => text === block.texts[i]);
|
|
3813
|
+
if (matches) {
|
|
3814
|
+
const allSimple = node.content.every((item) => {
|
|
3815
|
+
var _a2, _b;
|
|
3816
|
+
return ((_a2 = item.content) == null ? void 0 : _a2.length) === 1 && ((_b = item.content[0]) == null ? void 0 : _b.type) === "paragraph";
|
|
3817
|
+
});
|
|
3818
|
+
if (!allSimple) {
|
|
3819
|
+
cursor.index++;
|
|
3820
|
+
return node;
|
|
3821
|
+
}
|
|
3822
|
+
cursor.index++;
|
|
3823
|
+
node.type = "taskList";
|
|
3824
|
+
node.attrs = { localId: `tasklist-${++taskIdCounter}` };
|
|
3825
|
+
for (const [i, listItem] of node.content.entries()) {
|
|
3826
|
+
listItem.type = "taskItem";
|
|
3827
|
+
listItem.attrs = {
|
|
3828
|
+
localId: `task-${++taskIdCounter}`,
|
|
3829
|
+
state: block.states[i]
|
|
3830
|
+
};
|
|
3831
|
+
const firstChild = (_a = listItem.content) == null ? void 0 : _a[0];
|
|
3832
|
+
if ((firstChild == null ? void 0 : firstChild.type) === "paragraph" && firstChild.content) {
|
|
3833
|
+
listItem.content = firstChild.content;
|
|
3834
|
+
}
|
|
3835
|
+
}
|
|
3836
|
+
return node;
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
}
|
|
3840
|
+
if (node.content && Array.isArray(node.content)) {
|
|
3841
|
+
node.content = node.content.map((child) => convertCheckboxesRecursive(child, blocks, cursor));
|
|
3842
|
+
}
|
|
3843
|
+
return node;
|
|
3844
|
+
}
|
|
3845
|
+
function convertDetailsToExpandSyntax(markdown) {
|
|
3846
|
+
if (!markdown) return markdown;
|
|
3847
|
+
let previousText = "";
|
|
3848
|
+
let currentText = markdown;
|
|
3849
|
+
while (previousText !== currentText) {
|
|
3850
|
+
previousText = currentText;
|
|
3851
|
+
currentText = currentText.replace(
|
|
3852
|
+
/<details[^>]*>\s*<summary[^>]*>([\s\S]*?)<\/summary>([\s\S]*?)<\/details>/gi,
|
|
3853
|
+
(_match, summary, content) => {
|
|
3854
|
+
const cleanSummary = summary.trim().replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'");
|
|
3855
|
+
let cleanContent = content.trim();
|
|
3856
|
+
cleanContent = cleanContent.replace(/\n{3,}/g, "\n\n");
|
|
3857
|
+
if (cleanContent) {
|
|
3858
|
+
return `~~~expand title="${cleanSummary}"
|
|
3859
|
+
${cleanContent}
|
|
3860
|
+
~~~`;
|
|
3861
|
+
} else {
|
|
3862
|
+
return `~~~expand title="${cleanSummary}"
|
|
3863
|
+
~~~`;
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3866
|
+
);
|
|
3867
|
+
}
|
|
3868
|
+
return currentText;
|
|
3869
|
+
}
|
|
3870
|
+
function adfToMarkdown(adf) {
|
|
3871
|
+
if (!adf) return "";
|
|
3872
|
+
if (typeof adf === "string") return adf;
|
|
3873
|
+
return parser.adfToMarkdown(adf);
|
|
3874
|
+
}
|
|
3875
|
+
function markdownToAdf(markdown) {
|
|
3876
|
+
if (!markdown) {
|
|
3877
|
+
return { type: "doc", version: 1, content: [] };
|
|
3878
|
+
}
|
|
3879
|
+
taskIdCounter = 0;
|
|
3880
|
+
const checkboxBlocks = extractCheckboxBlocks(markdown);
|
|
3881
|
+
const preprocessed = convertDetailsToExpandSyntax(markdown);
|
|
3882
|
+
const adf = parser.markdownToAdf(preprocessed);
|
|
3883
|
+
let result = sanitizeCodeMarks(adf);
|
|
3884
|
+
result = wrapTableCellContent(result);
|
|
3885
|
+
result = convertCheckboxesToTaskList(result, checkboxBlocks);
|
|
3886
|
+
return result;
|
|
3887
|
+
}
|
|
3888
|
+
|
|
3889
|
+
// src/lib/providers/jira/JiraApiClient.ts
|
|
3890
|
+
var JiraApiClient = class {
|
|
3891
|
+
constructor(config) {
|
|
3892
|
+
this.baseUrl = `${config.host.replace(/\/$/, "")}/rest/api/3`;
|
|
3893
|
+
const credentials = Buffer.from(`${config.username}:${config.apiToken}`).toString("base64");
|
|
3894
|
+
this.authHeader = `Basic ${credentials}`;
|
|
3895
|
+
}
|
|
3896
|
+
/**
|
|
3897
|
+
* Make an HTTP request to Jira API
|
|
3898
|
+
*/
|
|
3899
|
+
async request(method, endpoint, body) {
|
|
3900
|
+
const url = new URL(`${this.baseUrl}${endpoint}`);
|
|
3901
|
+
getLogger().debug(`Jira API ${method} request`, { url: url.toString() });
|
|
3902
|
+
if (body) {
|
|
3903
|
+
getLogger().debug("Jira API request body", JSON.stringify(body, null, 2));
|
|
3904
|
+
}
|
|
3905
|
+
return new Promise((resolve, reject) => {
|
|
3906
|
+
const options = {
|
|
3907
|
+
hostname: url.hostname,
|
|
3908
|
+
port: url.port || 443,
|
|
3909
|
+
path: url.pathname + url.search,
|
|
3910
|
+
method,
|
|
3911
|
+
headers: {
|
|
3912
|
+
"Authorization": this.authHeader,
|
|
3913
|
+
"Accept": "application/json",
|
|
3914
|
+
"Content-Type": "application/json"
|
|
3915
|
+
}
|
|
3916
|
+
};
|
|
3917
|
+
const req = https.request({ ...options, timeout: 3e4 }, (res) => {
|
|
3918
|
+
const chunks = [];
|
|
3919
|
+
res.on("data", (chunk) => {
|
|
3920
|
+
chunks.push(chunk);
|
|
3921
|
+
});
|
|
3922
|
+
res.on("end", () => {
|
|
3923
|
+
var _a;
|
|
3924
|
+
const data = Buffer.concat(chunks).toString("utf8");
|
|
3925
|
+
if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
|
|
3926
|
+
let errorDetail = data;
|
|
3927
|
+
try {
|
|
3928
|
+
const parsed = JSON.parse(data);
|
|
3929
|
+
const parts = [];
|
|
3930
|
+
if ((_a = parsed.errorMessages) == null ? void 0 : _a.length) {
|
|
3931
|
+
parts.push(`messages: ${parsed.errorMessages.join(", ")}`);
|
|
3932
|
+
}
|
|
3933
|
+
if (parsed.errors && Object.keys(parsed.errors).length) {
|
|
3934
|
+
parts.push(`field errors: ${JSON.stringify(parsed.errors)}`);
|
|
3935
|
+
}
|
|
3936
|
+
if (parts.length) {
|
|
3937
|
+
errorDetail = parts.join("; ");
|
|
3938
|
+
}
|
|
3939
|
+
} catch {
|
|
3940
|
+
}
|
|
3941
|
+
reject(new Error(`Jira API error (${res.statusCode}): ${errorDetail}`));
|
|
3942
|
+
return;
|
|
3943
|
+
}
|
|
3944
|
+
if (res.statusCode === 204 || !data) {
|
|
3945
|
+
resolve({});
|
|
3946
|
+
return;
|
|
3947
|
+
}
|
|
3948
|
+
try {
|
|
3949
|
+
resolve(JSON.parse(data));
|
|
3950
|
+
} catch (error) {
|
|
3951
|
+
reject(new Error(`Failed to parse Jira API response: ${error}`));
|
|
3952
|
+
}
|
|
3953
|
+
});
|
|
3954
|
+
});
|
|
3955
|
+
req.on("timeout", () => {
|
|
3956
|
+
req.destroy();
|
|
3957
|
+
reject(new Error("Jira API request timed out after 30 seconds"));
|
|
3958
|
+
});
|
|
3959
|
+
req.on("error", (error) => {
|
|
3960
|
+
reject(new Error(`Jira API request failed: ${error.message}`));
|
|
3961
|
+
});
|
|
3962
|
+
if (body) {
|
|
3963
|
+
req.write(JSON.stringify(body));
|
|
3964
|
+
}
|
|
3965
|
+
req.end();
|
|
3966
|
+
});
|
|
3967
|
+
}
|
|
3968
|
+
/**
|
|
3969
|
+
* Make a GET request to Jira API
|
|
3970
|
+
*/
|
|
3971
|
+
async get(endpoint) {
|
|
3972
|
+
return this.request("GET", endpoint);
|
|
3973
|
+
}
|
|
3974
|
+
/**
|
|
3975
|
+
* Make a POST request to Jira API
|
|
3976
|
+
*/
|
|
3977
|
+
async post(endpoint, body) {
|
|
3978
|
+
return this.request("POST", endpoint, body);
|
|
3979
|
+
}
|
|
3980
|
+
/**
|
|
3981
|
+
* Make a PUT request to Jira API
|
|
3982
|
+
*/
|
|
3983
|
+
async put(endpoint, body) {
|
|
3984
|
+
return this.request("PUT", endpoint, body);
|
|
3985
|
+
}
|
|
3986
|
+
/**
|
|
3987
|
+
* Make a DELETE request to Jira API
|
|
3988
|
+
*/
|
|
3989
|
+
async delete(endpoint) {
|
|
3990
|
+
await this.request("DELETE", endpoint);
|
|
3991
|
+
}
|
|
3992
|
+
/**
|
|
3993
|
+
* Fetch an issue by key (e.g., "PROJ-123")
|
|
3994
|
+
*/
|
|
3995
|
+
async getIssue(issueKey) {
|
|
3996
|
+
return this.get(`/issue/${issueKey}`);
|
|
3997
|
+
}
|
|
3998
|
+
/**
|
|
3999
|
+
* Add a comment to an issue
|
|
4000
|
+
* Accepts Markdown content which is converted to ADF for Jira
|
|
4001
|
+
*/
|
|
4002
|
+
async addComment(issueKey, body) {
|
|
4003
|
+
const adfBody = markdownToAdf(body);
|
|
4004
|
+
getLogger().debug("Adding comment to Jira issue", { issueKey, bodyLength: body.length });
|
|
4005
|
+
return this.post(`/issue/${issueKey}/comment`, {
|
|
4006
|
+
body: adfBody
|
|
4007
|
+
});
|
|
4008
|
+
}
|
|
4009
|
+
/**
|
|
4010
|
+
* Get all comments for an issue
|
|
4011
|
+
*/
|
|
4012
|
+
async getComments(issueKey) {
|
|
4013
|
+
const response = await this.get(`/issue/${issueKey}/comment?maxResults=5000`);
|
|
4014
|
+
if (response.total > response.comments.length) {
|
|
4015
|
+
getLogger().warn(`Comments truncated for issue ${issueKey}: returned ${response.comments.length} of ${response.total} total comments`);
|
|
4016
|
+
}
|
|
4017
|
+
return response.comments;
|
|
4018
|
+
}
|
|
4019
|
+
/**
|
|
4020
|
+
* Update a comment on an issue
|
|
4021
|
+
* Accepts Markdown content which is converted to ADF for Jira
|
|
4022
|
+
*/
|
|
4023
|
+
async updateComment(issueKey, commentId, body) {
|
|
4024
|
+
return this.put(`/issue/${issueKey}/comment/${commentId}`, {
|
|
4025
|
+
body: markdownToAdf(body)
|
|
4026
|
+
});
|
|
4027
|
+
}
|
|
4028
|
+
/**
|
|
4029
|
+
* Get available transitions for an issue
|
|
4030
|
+
*/
|
|
4031
|
+
async getTransitions(issueKey) {
|
|
4032
|
+
const response = await this.get(`/issue/${issueKey}/transitions`);
|
|
4033
|
+
return response.transitions;
|
|
4034
|
+
}
|
|
4035
|
+
/**
|
|
4036
|
+
* Transition an issue to a new state
|
|
4037
|
+
*/
|
|
4038
|
+
async transitionIssue(issueKey, transitionId) {
|
|
4039
|
+
await this.post(`/issue/${issueKey}/transitions`, {
|
|
4040
|
+
transition: {
|
|
4041
|
+
id: transitionId
|
|
4042
|
+
}
|
|
4043
|
+
});
|
|
4044
|
+
}
|
|
4045
|
+
/**
|
|
4046
|
+
* Create a new issue
|
|
4047
|
+
* Accepts Markdown description which is converted to ADF for Jira
|
|
4048
|
+
*/
|
|
4049
|
+
async createIssue(projectKey, summary, description, issueType = "Task") {
|
|
4050
|
+
return this.post("/issue", {
|
|
4051
|
+
fields: {
|
|
4052
|
+
project: {
|
|
4053
|
+
key: projectKey
|
|
4054
|
+
},
|
|
4055
|
+
summary,
|
|
4056
|
+
description: markdownToAdf(description),
|
|
4057
|
+
issuetype: {
|
|
4058
|
+
name: issueType
|
|
4059
|
+
}
|
|
4060
|
+
}
|
|
4061
|
+
});
|
|
4062
|
+
}
|
|
4063
|
+
/**
|
|
4064
|
+
* Update an issue's fields (summary, description)
|
|
4065
|
+
* @param issueKey - Jira issue key (e.g., "PROJ-123")
|
|
4066
|
+
* @param fields - Fields to update
|
|
4067
|
+
*/
|
|
4068
|
+
async updateIssue(issueKey, fields) {
|
|
4069
|
+
const updateFields = {};
|
|
4070
|
+
if (fields.summary !== void 0) {
|
|
4071
|
+
updateFields.summary = fields.summary;
|
|
4072
|
+
}
|
|
4073
|
+
if (fields.description !== void 0) {
|
|
4074
|
+
updateFields.description = markdownToAdf(fields.description);
|
|
4075
|
+
}
|
|
4076
|
+
await this.put(`/issue/${issueKey}`, { fields: updateFields });
|
|
4077
|
+
}
|
|
4078
|
+
/**
|
|
4079
|
+
* Create an issue with a parent (subtask or child issue)
|
|
4080
|
+
* Accepts Markdown description which is converted to ADF for Jira
|
|
4081
|
+
*/
|
|
4082
|
+
async createIssueWithParent(projectKey, summary, description, parentKey, issueType = "Subtask") {
|
|
4083
|
+
return this.post("/issue", {
|
|
4084
|
+
fields: {
|
|
4085
|
+
project: {
|
|
4086
|
+
key: projectKey
|
|
4087
|
+
},
|
|
4088
|
+
summary,
|
|
4089
|
+
description: markdownToAdf(description),
|
|
4090
|
+
issuetype: {
|
|
4091
|
+
name: issueType
|
|
4092
|
+
},
|
|
4093
|
+
parent: {
|
|
4094
|
+
key: parentKey
|
|
4095
|
+
}
|
|
4096
|
+
}
|
|
4097
|
+
});
|
|
4098
|
+
}
|
|
4099
|
+
/**
|
|
4100
|
+
* Create an issue link (dependency/relationship between issues)
|
|
4101
|
+
* @param inwardKey - The issue key for the inward side (e.g., the blocked issue)
|
|
4102
|
+
* @param outwardKey - The issue key for the outward side (e.g., the blocking issue)
|
|
4103
|
+
* @param linkType - The link type name (e.g., "Blocks")
|
|
4104
|
+
*/
|
|
4105
|
+
async createIssueLink(inwardKey, outwardKey, linkType) {
|
|
4106
|
+
await this.post("/issueLink", {
|
|
4107
|
+
type: {
|
|
4108
|
+
name: linkType
|
|
4109
|
+
},
|
|
4110
|
+
inwardIssue: {
|
|
4111
|
+
key: inwardKey
|
|
4112
|
+
},
|
|
4113
|
+
outwardIssue: {
|
|
4114
|
+
key: outwardKey
|
|
4115
|
+
}
|
|
4116
|
+
});
|
|
4117
|
+
}
|
|
4118
|
+
/**
|
|
4119
|
+
* Delete an issue link by ID
|
|
4120
|
+
*/
|
|
4121
|
+
async deleteIssueLink(linkId) {
|
|
4122
|
+
await this.delete(`/issueLink/${linkId}`);
|
|
4123
|
+
}
|
|
4124
|
+
/**
|
|
4125
|
+
* Search issues using JQL
|
|
4126
|
+
* Automatically paginates through all results up to MAX_SEARCH_RESULTS.
|
|
4127
|
+
*/
|
|
4128
|
+
async searchIssues(jql) {
|
|
4129
|
+
const MAX_SEARCH_RESULTS = 5e3;
|
|
4130
|
+
const allIssues = [];
|
|
4131
|
+
let nextPageToken;
|
|
4132
|
+
const maxResults = 100;
|
|
4133
|
+
while (allIssues.length < MAX_SEARCH_RESULTS) {
|
|
4134
|
+
const body = {
|
|
4135
|
+
jql,
|
|
4136
|
+
maxResults,
|
|
4137
|
+
fields: [
|
|
4138
|
+
"summary",
|
|
4139
|
+
"description",
|
|
4140
|
+
"status",
|
|
4141
|
+
"issuetype",
|
|
4142
|
+
"project",
|
|
4143
|
+
"assignee",
|
|
4144
|
+
"reporter",
|
|
4145
|
+
"labels",
|
|
4146
|
+
"created",
|
|
4147
|
+
"updated",
|
|
4148
|
+
"issuelinks",
|
|
4149
|
+
"parent"
|
|
4150
|
+
]
|
|
4151
|
+
};
|
|
4152
|
+
if (nextPageToken) {
|
|
4153
|
+
body.nextPageToken = nextPageToken;
|
|
4154
|
+
}
|
|
4155
|
+
const response = await this.post(
|
|
4156
|
+
"/search/jql",
|
|
4157
|
+
body
|
|
4158
|
+
);
|
|
4159
|
+
allIssues.push(...response.issues);
|
|
4160
|
+
if (!response.nextPageToken || response.issues.length === 0) {
|
|
4161
|
+
break;
|
|
4162
|
+
}
|
|
4163
|
+
nextPageToken = response.nextPageToken;
|
|
4164
|
+
}
|
|
4165
|
+
if (allIssues.length >= MAX_SEARCH_RESULTS) {
|
|
4166
|
+
getLogger().warn(`Search results truncated at ${MAX_SEARCH_RESULTS} issues. The query matched more results than the safety cap allows.`, { jql, returnedCount: allIssues.length });
|
|
4167
|
+
}
|
|
4168
|
+
return allIssues;
|
|
4169
|
+
}
|
|
4170
|
+
/**
|
|
4171
|
+
* Test connection to Jira API
|
|
4172
|
+
*/
|
|
4173
|
+
async testConnection() {
|
|
4174
|
+
try {
|
|
4175
|
+
await this.get("/myself");
|
|
4176
|
+
return true;
|
|
4177
|
+
} catch (error) {
|
|
4178
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4179
|
+
if (message.includes("Jira API error (401)") || message.includes("Jira API error (403)")) {
|
|
4180
|
+
getLogger().error("Jira connection test failed: authentication error", { error });
|
|
4181
|
+
return false;
|
|
4182
|
+
}
|
|
4183
|
+
throw error;
|
|
4184
|
+
}
|
|
4185
|
+
}
|
|
4186
|
+
};
|
|
4187
|
+
|
|
4188
|
+
// src/lib/providers/jira/JiraIssueTracker.ts
|
|
4189
|
+
var JiraIssueTracker = class {
|
|
4190
|
+
constructor(config, options) {
|
|
4191
|
+
this.providerName = "jira";
|
|
4192
|
+
this.supportsPullRequests = false;
|
|
4193
|
+
this.config = config;
|
|
4194
|
+
this.client = new JiraApiClient({
|
|
4195
|
+
host: config.host,
|
|
4196
|
+
username: config.username,
|
|
4197
|
+
apiToken: config.apiToken
|
|
4198
|
+
});
|
|
4199
|
+
this.prompter = (options == null ? void 0 : options.prompter) ?? promptConfirmation;
|
|
4200
|
+
}
|
|
4201
|
+
/**
|
|
4202
|
+
* Normalize identifier to canonical uppercase form
|
|
4203
|
+
* Jira issue keys are case-sensitive in the API (must be uppercase)
|
|
4204
|
+
*/
|
|
4205
|
+
normalizeIdentifier(identifier) {
|
|
4206
|
+
return String(identifier).toUpperCase();
|
|
4207
|
+
}
|
|
4208
|
+
/**
|
|
4209
|
+
* Detect input type from user input
|
|
4210
|
+
* Jira issues follow pattern: PROJECTKEY-123 (case-insensitive)
|
|
4211
|
+
*/
|
|
4212
|
+
async detectInputType(input) {
|
|
4213
|
+
const jiraPattern = /^([A-Z][A-Z0-9]+)-(\d+)$/i;
|
|
4214
|
+
const match = input.match(jiraPattern);
|
|
4215
|
+
if (!match) {
|
|
4216
|
+
return { type: "unknown", identifier: null, rawInput: input };
|
|
4217
|
+
}
|
|
4218
|
+
const issueKey = this.normalizeIdentifier(input);
|
|
4219
|
+
getLogger().debug("Checking if input is a Jira issue", { issueKey });
|
|
4220
|
+
try {
|
|
4221
|
+
await this.client.getIssue(issueKey);
|
|
4222
|
+
return { type: "issue", identifier: issueKey, rawInput: input };
|
|
4223
|
+
} catch (error) {
|
|
4224
|
+
if (error instanceof Error && (/404/.test(error.message) || /not found/i.test(error.message))) {
|
|
4225
|
+
getLogger().debug("Issue not found", { issueKey, error });
|
|
4226
|
+
return { type: "unknown", identifier: null, rawInput: input };
|
|
4227
|
+
}
|
|
4228
|
+
throw error;
|
|
4229
|
+
}
|
|
4230
|
+
}
|
|
4231
|
+
/**
|
|
4232
|
+
* Fetch issue details
|
|
4233
|
+
*/
|
|
4234
|
+
async fetchIssue(identifier) {
|
|
4235
|
+
const issueKey = this.normalizeIdentifier(identifier);
|
|
4236
|
+
getLogger().debug("Fetching Jira issue", { issueKey });
|
|
4237
|
+
const jiraIssue = await this.client.getIssue(issueKey);
|
|
4238
|
+
return this.mapJiraIssueToIssue(jiraIssue);
|
|
4239
|
+
}
|
|
4240
|
+
/**
|
|
4241
|
+
* Check if issue exists (silent validation)
|
|
4242
|
+
*/
|
|
4243
|
+
async isValidIssue(identifier) {
|
|
4244
|
+
try {
|
|
4245
|
+
return await this.fetchIssue(identifier);
|
|
4246
|
+
} catch (error) {
|
|
4247
|
+
if (error instanceof Error && (/404/.test(error.message) || /not found/i.test(error.message))) {
|
|
4248
|
+
getLogger().debug("Issue validation failed: not found", { identifier, error });
|
|
4249
|
+
return false;
|
|
4250
|
+
}
|
|
4251
|
+
throw error;
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
4254
|
+
/**
|
|
4255
|
+
* Validate issue state
|
|
4256
|
+
* Note: Jira doesn't have a simple "closed" state - depends on workflow
|
|
4257
|
+
*/
|
|
4258
|
+
async validateIssueState(issue) {
|
|
4259
|
+
getLogger().debug("Jira issue state", { issueKey: issue.number, state: issue.state });
|
|
4260
|
+
if (issue.state === "closed") {
|
|
4261
|
+
const shouldContinue = await this.prompter(
|
|
4262
|
+
`Issue ${issue.number} is in a completed state. Continue anyway?`
|
|
4263
|
+
);
|
|
4264
|
+
if (!shouldContinue) {
|
|
4265
|
+
throw new Error("User cancelled due to completed issue");
|
|
4266
|
+
}
|
|
4267
|
+
}
|
|
4268
|
+
}
|
|
4269
|
+
/**
|
|
4270
|
+
* Create a new issue
|
|
4271
|
+
*/
|
|
4272
|
+
async createIssue(title, body, _repository, _labels) {
|
|
4273
|
+
getLogger().debug("Creating Jira issue", { title, projectKey: this.config.projectKey });
|
|
4274
|
+
const jiraIssue = await this.client.createIssue(
|
|
4275
|
+
this.config.projectKey,
|
|
4276
|
+
title,
|
|
4277
|
+
body,
|
|
4278
|
+
this.config.defaultIssueType
|
|
4279
|
+
);
|
|
4280
|
+
return {
|
|
4281
|
+
number: jiraIssue.key,
|
|
4282
|
+
url: `${this.config.host}/browse/${jiraIssue.key}`
|
|
4283
|
+
};
|
|
4284
|
+
}
|
|
4285
|
+
/**
|
|
4286
|
+
* Get issue URL
|
|
4287
|
+
*/
|
|
4288
|
+
async getIssueUrl(identifier) {
|
|
4289
|
+
const issueKey = this.normalizeIdentifier(identifier);
|
|
4290
|
+
return `${this.config.host}/browse/${issueKey}`;
|
|
4291
|
+
}
|
|
4292
|
+
/**
|
|
4293
|
+
* Move issue to "In Progress" state
|
|
4294
|
+
* Uses configured transition mapping or default transition name
|
|
4295
|
+
*/
|
|
4296
|
+
async moveIssueToInProgress(identifier) {
|
|
4297
|
+
var _a;
|
|
4298
|
+
const issueKey = this.normalizeIdentifier(identifier);
|
|
4299
|
+
getLogger().debug("Moving Jira issue to In Progress", { issueKey });
|
|
4300
|
+
const transitions = await this.client.getTransitions(issueKey);
|
|
4301
|
+
const transitionName = ((_a = this.config.transitionMappings) == null ? void 0 : _a["In Progress"]) ?? this.findTransitionByName(transitions, ["In Progress", "Start Progress", "Start"]);
|
|
4302
|
+
if (!transitionName) {
|
|
4303
|
+
throw new Error(
|
|
4304
|
+
`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`
|
|
4305
|
+
);
|
|
4306
|
+
}
|
|
4307
|
+
const transition = transitions.find((t) => t.name === transitionName);
|
|
4308
|
+
if (!transition) {
|
|
4309
|
+
throw new Error(`Transition "${transitionName}" not found`);
|
|
4310
|
+
}
|
|
4311
|
+
await this.client.transitionIssue(issueKey, transition.id);
|
|
4312
|
+
getLogger().info("Issue transitioned successfully", { issueKey, transition: transitionName });
|
|
4313
|
+
}
|
|
4314
|
+
/**
|
|
4315
|
+
* Move issue to "Ready for Review" state
|
|
4316
|
+
* Uses configured transition mapping or default transition name
|
|
4317
|
+
*/
|
|
4318
|
+
async moveIssueToReadyForReview(identifier) {
|
|
4319
|
+
var _a;
|
|
4320
|
+
const issueKey = this.normalizeIdentifier(identifier);
|
|
4321
|
+
getLogger().debug("Moving Jira issue to Ready for Review", { issueKey });
|
|
4322
|
+
const transitions = await this.client.getTransitions(issueKey);
|
|
4323
|
+
const transitionName = ((_a = this.config.transitionMappings) == null ? void 0 : _a["Ready for Review"]) ?? this.findTransitionByName(transitions, ["Ready for Review", "In Review", "Code Review", "Review"]);
|
|
4324
|
+
if (!transitionName) {
|
|
4325
|
+
throw new Error(
|
|
4326
|
+
`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`
|
|
4327
|
+
);
|
|
4328
|
+
}
|
|
4329
|
+
const transition = transitions.find((t) => t.name === transitionName);
|
|
4330
|
+
if (!transition) {
|
|
4331
|
+
throw new Error(`Transition "${transitionName}" not found`);
|
|
4332
|
+
}
|
|
4333
|
+
await this.client.transitionIssue(issueKey, transition.id);
|
|
4334
|
+
getLogger().info("Issue transitioned to Ready for Review", { issueKey, transition: transitionName });
|
|
4335
|
+
}
|
|
4336
|
+
/**
|
|
4337
|
+
* Close an issue by transitioning to "Done" state
|
|
4338
|
+
* Uses configured transition mapping or default transition names
|
|
4339
|
+
*/
|
|
4340
|
+
async closeIssue(identifier) {
|
|
4341
|
+
var _a;
|
|
4342
|
+
const issueKey = this.normalizeIdentifier(identifier);
|
|
4343
|
+
getLogger().debug("Closing Jira issue", { issueKey });
|
|
4344
|
+
const transitions = await this.client.getTransitions(issueKey);
|
|
4345
|
+
const transitionName = ((_a = this.config.transitionMappings) == null ? void 0 : _a["Done"]) ?? this.findTransitionByName(transitions, ["Done", "Close", "Closed", "Resolve", "Resolved"]);
|
|
4346
|
+
if (!transitionName) {
|
|
4347
|
+
throw new Error(
|
|
4348
|
+
`Could not find "Done" transition for ${issueKey}. Available transitions: ${transitions.map((t) => t.name).join(", ")}. Configure custom mapping in settings.json: issueManagement.jira.transitionMappings`
|
|
4349
|
+
);
|
|
4350
|
+
}
|
|
4351
|
+
const transition = transitions.find((t) => t.name === transitionName);
|
|
4352
|
+
if (!transition) {
|
|
4353
|
+
throw new Error(`Transition "${transitionName}" not found`);
|
|
4354
|
+
}
|
|
4355
|
+
await this.client.transitionIssue(issueKey, transition.id);
|
|
4356
|
+
getLogger().info("Issue closed successfully", { issueKey, transition: transitionName });
|
|
4357
|
+
}
|
|
4358
|
+
/**
|
|
4359
|
+
* Reopen an issue by transitioning back to an open state
|
|
4360
|
+
* Uses configured transition mapping or default transition names
|
|
4361
|
+
*/
|
|
4362
|
+
async reopenIssue(identifier) {
|
|
4363
|
+
var _a;
|
|
4364
|
+
const issueKey = this.normalizeIdentifier(identifier);
|
|
4365
|
+
getLogger().debug("Reopening Jira issue", { issueKey });
|
|
4366
|
+
const transitions = await this.client.getTransitions(issueKey);
|
|
4367
|
+
const transitionName = ((_a = this.config.transitionMappings) == null ? void 0 : _a["Reopen"]) ?? this.findTransitionByName(transitions, ["Reopen", "To Do", "Open", "Backlog"]);
|
|
4368
|
+
if (!transitionName) {
|
|
4369
|
+
throw new Error(
|
|
4370
|
+
`Could not find "Reopen" transition for ${issueKey}. Available transitions: ${transitions.map((t) => t.name).join(", ")}. Configure custom mapping in settings.json: issueManagement.jira.transitionMappings`
|
|
4371
|
+
);
|
|
4372
|
+
}
|
|
4373
|
+
const transition = transitions.find((t) => t.name === transitionName);
|
|
4374
|
+
if (!transition) {
|
|
4375
|
+
throw new Error(`Transition "${transitionName}" not found`);
|
|
4376
|
+
}
|
|
4377
|
+
await this.client.transitionIssue(issueKey, transition.id);
|
|
4378
|
+
getLogger().info("Issue reopened successfully", { issueKey, transition: transitionName });
|
|
4379
|
+
}
|
|
4380
|
+
/**
|
|
4381
|
+
* Extract context from issue for AI prompts
|
|
4382
|
+
*/
|
|
4383
|
+
extractContext(entity) {
|
|
4384
|
+
return `Issue: ${entity.number}
|
|
4385
|
+
Title: ${entity.title}
|
|
4386
|
+
Status: ${entity.state}
|
|
4387
|
+
URL: ${entity.url}
|
|
4388
|
+
|
|
4389
|
+
Description:
|
|
4390
|
+
${entity.body}
|
|
4391
|
+
|
|
4392
|
+
${entity.labels.length > 0 ? `Labels: ${entity.labels.join(", ")}` : ""}
|
|
4393
|
+
${entity.assignees.length > 0 ? `Assignees: ${entity.assignees.join(", ")}` : ""}`;
|
|
4394
|
+
}
|
|
4395
|
+
/**
|
|
4396
|
+
* Fetch child issues of a Jira parent issue using JQL
|
|
4397
|
+
* @param parentIdentifier - Jira issue key (e.g., "PROJ-123")
|
|
4398
|
+
* @param _repo - Repository (unused for Jira)
|
|
4399
|
+
* @returns Array of child issues
|
|
4400
|
+
*/
|
|
4401
|
+
async getChildIssues(parentIdentifier, _repo) {
|
|
4402
|
+
const parentKey = this.normalizeIdentifier(parentIdentifier);
|
|
4403
|
+
const jiraKeyPattern = /^[A-Z][A-Z0-9]+-\d+$/;
|
|
4404
|
+
if (!jiraKeyPattern.test(parentKey)) {
|
|
4405
|
+
getLogger().warn(`Invalid Jira issue key format: ${parentKey}`);
|
|
4406
|
+
return [];
|
|
4407
|
+
}
|
|
4408
|
+
const issues = await this.client.searchIssues(`parent = ${parentKey}`);
|
|
4409
|
+
return issues.map((issue) => ({
|
|
4410
|
+
id: issue.key,
|
|
4411
|
+
title: issue.fields.summary,
|
|
4412
|
+
url: `${this.config.host}/browse/${issue.key}`,
|
|
4413
|
+
state: issue.fields.status.name.toLowerCase()
|
|
4414
|
+
}));
|
|
4415
|
+
}
|
|
4416
|
+
/**
|
|
4417
|
+
* Get issue details (alias for fetchIssue for MCP compatibility)
|
|
4418
|
+
*/
|
|
4419
|
+
async getIssue(identifier) {
|
|
4420
|
+
return this.fetchIssue(identifier);
|
|
4421
|
+
}
|
|
4422
|
+
/**
|
|
4423
|
+
* Get all comments for an issue
|
|
4424
|
+
*/
|
|
4425
|
+
async getComments(identifier) {
|
|
4426
|
+
const issueKey = this.normalizeIdentifier(identifier);
|
|
4427
|
+
getLogger().debug("Fetching Jira comments", { issueKey });
|
|
4428
|
+
const comments = await this.client.getComments(issueKey);
|
|
4429
|
+
return comments.map((comment) => ({
|
|
4430
|
+
id: comment.id,
|
|
4431
|
+
body: adfToMarkdown(comment.body),
|
|
4432
|
+
author: comment.author,
|
|
4433
|
+
createdAt: comment.created,
|
|
4434
|
+
updatedAt: comment.updated
|
|
4435
|
+
}));
|
|
4436
|
+
}
|
|
4437
|
+
/**
|
|
4438
|
+
* Add a comment to an issue
|
|
4439
|
+
*/
|
|
4440
|
+
async addComment(identifier, body) {
|
|
4441
|
+
const issueKey = this.normalizeIdentifier(identifier);
|
|
4442
|
+
getLogger().debug("Adding Jira comment", { issueKey });
|
|
4443
|
+
const comment = await this.client.addComment(issueKey, body);
|
|
4444
|
+
return { id: comment.id };
|
|
4445
|
+
}
|
|
4446
|
+
/**
|
|
4447
|
+
* Update an existing comment
|
|
4448
|
+
*/
|
|
4449
|
+
async updateComment(identifier, commentId, body) {
|
|
4450
|
+
const issueKey = this.normalizeIdentifier(identifier);
|
|
4451
|
+
getLogger().debug("Updating Jira comment", { issueKey, commentId });
|
|
4452
|
+
await this.client.updateComment(issueKey, commentId, body);
|
|
4453
|
+
}
|
|
4454
|
+
/**
|
|
4455
|
+
* Get the underlying API client (for direct API access by MCP provider)
|
|
4456
|
+
*/
|
|
4457
|
+
getApiClient() {
|
|
4458
|
+
return this.client;
|
|
4459
|
+
}
|
|
4460
|
+
/**
|
|
4461
|
+
* Get configuration (for MCP provider)
|
|
4462
|
+
*/
|
|
4463
|
+
getConfig() {
|
|
4464
|
+
return this.config;
|
|
4465
|
+
}
|
|
4466
|
+
/**
|
|
4467
|
+
* Map Jira API issue to generic Issue type
|
|
4468
|
+
*/
|
|
4469
|
+
mapJiraIssueToIssue(jiraIssue) {
|
|
4470
|
+
const description = adfToMarkdown(jiraIssue.fields.description);
|
|
4471
|
+
return {
|
|
4472
|
+
id: jiraIssue.id,
|
|
4473
|
+
key: jiraIssue.key,
|
|
4474
|
+
number: jiraIssue.key,
|
|
4475
|
+
title: jiraIssue.fields.summary,
|
|
4476
|
+
body: description,
|
|
4477
|
+
state: this.mapJiraStatusToState(jiraIssue.fields.status.name),
|
|
4478
|
+
labels: jiraIssue.fields.labels,
|
|
4479
|
+
assignees: jiraIssue.fields.assignee ? [jiraIssue.fields.assignee.displayName] : [],
|
|
4480
|
+
assignee: jiraIssue.fields.assignee,
|
|
4481
|
+
author: jiraIssue.fields.reporter,
|
|
4482
|
+
url: `${this.config.host}/browse/${jiraIssue.key}`,
|
|
4483
|
+
issueType: jiraIssue.fields.issuetype.name,
|
|
4484
|
+
status: jiraIssue.fields.status.name
|
|
4485
|
+
};
|
|
4486
|
+
}
|
|
4487
|
+
mapJiraStatusToState(statusName) {
|
|
4488
|
+
const normalized = statusName.toLowerCase();
|
|
4489
|
+
const closedStatuses = ["done", "closed", "resolved", "cancelled", "canceled"];
|
|
4490
|
+
return closedStatuses.includes(normalized) ? "closed" : "open";
|
|
4491
|
+
}
|
|
4492
|
+
/**
|
|
4493
|
+
* Find a transition by name, trying multiple possible names
|
|
4494
|
+
*/
|
|
4495
|
+
findTransitionByName(transitions, names) {
|
|
4496
|
+
for (const name of names) {
|
|
4497
|
+
const transition = transitions.find(
|
|
4498
|
+
(t) => t.name.toLowerCase() === name.toLowerCase()
|
|
4499
|
+
);
|
|
4500
|
+
if (transition) {
|
|
4501
|
+
return transition.name;
|
|
4502
|
+
}
|
|
4503
|
+
}
|
|
4504
|
+
return null;
|
|
4505
|
+
}
|
|
4506
|
+
};
|
|
4507
|
+
|
|
3403
4508
|
// src/lib/IssueTrackerFactory.ts
|
|
3404
4509
|
var IssueTrackerFactory = class {
|
|
3405
4510
|
/**
|
|
@@ -3411,7 +4516,7 @@ var IssueTrackerFactory = class {
|
|
|
3411
4516
|
* @throws Error if provider type is not supported
|
|
3412
4517
|
*/
|
|
3413
4518
|
static create(settings) {
|
|
3414
|
-
var _a, _b;
|
|
4519
|
+
var _a, _b, _c;
|
|
3415
4520
|
const provider = ((_a = settings.issueManagement) == null ? void 0 : _a.provider) ?? "github";
|
|
3416
4521
|
getLogger().debug(`IssueTrackerFactory: Creating tracker for provider "${provider}"`);
|
|
3417
4522
|
getLogger().debug(`IssueTrackerFactory: issueManagement settings:`, JSON.stringify(settings.issueManagement, null, 2));
|
|
@@ -3434,6 +4539,32 @@ var IssueTrackerFactory = class {
|
|
|
3434
4539
|
getLogger().debug(`IssueTrackerFactory: Creating LinearService with config:`, JSON.stringify(linearConfig, null, 2));
|
|
3435
4540
|
return new LinearService(linearConfig);
|
|
3436
4541
|
}
|
|
4542
|
+
case "jira": {
|
|
4543
|
+
const jiraSettings = (_c = settings.issueManagement) == null ? void 0 : _c.jira;
|
|
4544
|
+
if (!(jiraSettings == null ? void 0 : jiraSettings.host)) {
|
|
4545
|
+
throw new Error("Jira host is required. Configure issueManagement.jira.host in .iloom/settings.json");
|
|
4546
|
+
}
|
|
4547
|
+
if (!(jiraSettings == null ? void 0 : jiraSettings.username)) {
|
|
4548
|
+
throw new Error("Jira username is required. Configure issueManagement.jira.username in .iloom/settings.json");
|
|
4549
|
+
}
|
|
4550
|
+
if (!(jiraSettings == null ? void 0 : jiraSettings.apiToken)) {
|
|
4551
|
+
throw new Error("Jira API token is required. Configure issueManagement.jira.apiToken in .iloom/settings.local.json");
|
|
4552
|
+
}
|
|
4553
|
+
if (!(jiraSettings == null ? void 0 : jiraSettings.projectKey)) {
|
|
4554
|
+
throw new Error("Jira project key is required. Configure issueManagement.jira.projectKey in .iloom/settings.json");
|
|
4555
|
+
}
|
|
4556
|
+
const jiraConfig = {
|
|
4557
|
+
host: jiraSettings.host,
|
|
4558
|
+
username: jiraSettings.username,
|
|
4559
|
+
apiToken: jiraSettings.apiToken,
|
|
4560
|
+
projectKey: jiraSettings.projectKey
|
|
4561
|
+
};
|
|
4562
|
+
if (jiraSettings.transitionMappings) {
|
|
4563
|
+
jiraConfig.transitionMappings = jiraSettings.transitionMappings;
|
|
4564
|
+
}
|
|
4565
|
+
getLogger().debug(`IssueTrackerFactory: Creating JiraIssueTracker for host: ${jiraSettings.host}`);
|
|
4566
|
+
return new JiraIssueTracker(jiraConfig);
|
|
4567
|
+
}
|
|
3437
4568
|
default:
|
|
3438
4569
|
throw new Error(`Unsupported issue tracker provider: ${provider}`);
|
|
3439
4570
|
}
|
|
@@ -3945,7 +5076,7 @@ function parseJsonStreamOutput(output) {
|
|
|
3945
5076
|
}
|
|
3946
5077
|
}
|
|
3947
5078
|
async function launchClaude(prompt, options = {}) {
|
|
3948
|
-
const { model, permissionMode, addDir, headless = false, appendSystemPrompt, mcpConfig, allowedTools, disallowedTools, agents, sessionId, noSessionPersistence, outputFormat, verbose, jsonMode } = options;
|
|
5079
|
+
const { model, permissionMode, addDir, headless = false, appendSystemPrompt, mcpConfig, allowedTools, disallowedTools, agents, sessionId, noSessionPersistence, outputFormat, verbose, jsonMode, passthroughStdout, env: extraEnv, signal } = options;
|
|
3949
5080
|
const log = getLogger();
|
|
3950
5081
|
const args = [];
|
|
3951
5082
|
if (headless) {
|
|
@@ -3989,7 +5120,37 @@ async function launchClaude(prompt, options = {}) {
|
|
|
3989
5120
|
if (noSessionPersistence && headless) {
|
|
3990
5121
|
args.push("--no-session-persistence");
|
|
3991
5122
|
}
|
|
5123
|
+
const claudeEnv = { ...process.env, CLAUDECODE: "0" };
|
|
5124
|
+
function attachAbortSignal(subprocess) {
|
|
5125
|
+
if (!signal) return;
|
|
5126
|
+
const onAbort = () => {
|
|
5127
|
+
subprocess.kill("SIGTERM");
|
|
5128
|
+
};
|
|
5129
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
5130
|
+
subprocess.on("exit", () => {
|
|
5131
|
+
signal.removeEventListener("abort", onAbort);
|
|
5132
|
+
});
|
|
5133
|
+
}
|
|
3992
5134
|
try {
|
|
5135
|
+
if (headless && passthroughStdout) {
|
|
5136
|
+
const subprocess = execa4("claude", args, {
|
|
5137
|
+
input: prompt,
|
|
5138
|
+
timeout: 0,
|
|
5139
|
+
...addDir && { cwd: addDir },
|
|
5140
|
+
env: { ...claudeEnv, ...extraEnv },
|
|
5141
|
+
// CLAUDECODE=0 + any extra env vars
|
|
5142
|
+
stdio: ["pipe", "inherit", "pipe"]
|
|
5143
|
+
// stdin: pipe (for prompt), stdout: inherit (passthrough), stderr: pipe (capture errors)
|
|
5144
|
+
});
|
|
5145
|
+
attachAbortSignal(subprocess);
|
|
5146
|
+
try {
|
|
5147
|
+
await subprocess;
|
|
5148
|
+
} catch (err) {
|
|
5149
|
+
if (signal == null ? void 0 : signal.aborted) return;
|
|
5150
|
+
throw err;
|
|
5151
|
+
}
|
|
5152
|
+
return;
|
|
5153
|
+
}
|
|
3993
5154
|
if (headless) {
|
|
3994
5155
|
const isDebugMode = logger.isDebugEnabled();
|
|
3995
5156
|
const execaOptions = {
|
|
@@ -3999,10 +5160,13 @@ async function launchClaude(prompt, options = {}) {
|
|
|
3999
5160
|
...addDir && { cwd: addDir },
|
|
4000
5161
|
// Run Claude in the worktree directory
|
|
4001
5162
|
verbose: isDebugMode,
|
|
5163
|
+
env: { ...claudeEnv, ...extraEnv },
|
|
5164
|
+
// CLAUDECODE=0 + any extra env vars
|
|
4002
5165
|
...isDebugMode && { stdio: ["pipe", "pipe", "pipe"] }
|
|
4003
5166
|
// Enable streaming in debug mode
|
|
4004
5167
|
};
|
|
4005
5168
|
const subprocess = execa4("claude", args, execaOptions);
|
|
5169
|
+
attachAbortSignal(subprocess);
|
|
4006
5170
|
const isJsonStreamFormat = args.includes("--output-format") && args.includes("stream-json");
|
|
4007
5171
|
let outputBuffer = "";
|
|
4008
5172
|
let isStreaming = false;
|
|
@@ -4027,7 +5191,13 @@ async function launchClaude(prompt, options = {}) {
|
|
|
4027
5191
|
}
|
|
4028
5192
|
});
|
|
4029
5193
|
}
|
|
4030
|
-
|
|
5194
|
+
let result;
|
|
5195
|
+
try {
|
|
5196
|
+
result = await subprocess;
|
|
5197
|
+
} catch (subprocessError) {
|
|
5198
|
+
if (signal == null ? void 0 : signal.aborted) return;
|
|
5199
|
+
throw subprocessError;
|
|
5200
|
+
}
|
|
4031
5201
|
if (isStreaming) {
|
|
4032
5202
|
const rawOutput = outputBuffer.trim();
|
|
4033
5203
|
if (!isDebugMode && !jsonMode) {
|
|
@@ -4049,16 +5219,26 @@ async function launchClaude(prompt, options = {}) {
|
|
|
4049
5219
|
}
|
|
4050
5220
|
} else {
|
|
4051
5221
|
try {
|
|
4052
|
-
|
|
5222
|
+
const interactiveSubprocess = execa4("claude", [...args, "--", prompt], {
|
|
4053
5223
|
...addDir && { cwd: addDir },
|
|
4054
5224
|
stdio: ["inherit", "inherit", "pipe"],
|
|
4055
5225
|
// Capture stderr to detect session conflicts
|
|
4056
5226
|
timeout: 0,
|
|
4057
5227
|
// Disable timeout
|
|
4058
|
-
verbose: logger.isDebugEnabled()
|
|
5228
|
+
verbose: logger.isDebugEnabled(),
|
|
5229
|
+
env: { ...claudeEnv, ...extraEnv }
|
|
5230
|
+
// CLAUDECODE=0 + any extra env vars
|
|
4059
5231
|
});
|
|
5232
|
+
attachAbortSignal(interactiveSubprocess);
|
|
5233
|
+
try {
|
|
5234
|
+
await interactiveSubprocess;
|
|
5235
|
+
} catch (err) {
|
|
5236
|
+
if (signal == null ? void 0 : signal.aborted) return;
|
|
5237
|
+
throw err;
|
|
5238
|
+
}
|
|
4060
5239
|
return;
|
|
4061
5240
|
} catch (interactiveError) {
|
|
5241
|
+
if (signal == null ? void 0 : signal.aborted) return;
|
|
4062
5242
|
const interactiveExecaError = interactiveError;
|
|
4063
5243
|
const interactiveErrorMessage = interactiveExecaError.stderr ?? interactiveExecaError.message ?? "";
|
|
4064
5244
|
const sessionMatch = interactiveErrorMessage.match(/Session ID ([0-9a-f-]+) is already in use/i);
|
|
@@ -4071,18 +5251,27 @@ async function launchClaude(prompt, options = {}) {
|
|
|
4071
5251
|
return true;
|
|
4072
5252
|
});
|
|
4073
5253
|
resumeArgs.push("--resume", conflictSessionId);
|
|
4074
|
-
|
|
5254
|
+
const resumeSubprocess = execa4("claude", resumeArgs, {
|
|
4075
5255
|
...addDir && { cwd: addDir },
|
|
4076
5256
|
stdio: "inherit",
|
|
4077
5257
|
timeout: 0,
|
|
4078
|
-
verbose: logger.isDebugEnabled()
|
|
5258
|
+
verbose: logger.isDebugEnabled(),
|
|
5259
|
+
env: claudeEnv
|
|
4079
5260
|
});
|
|
5261
|
+
attachAbortSignal(resumeSubprocess);
|
|
5262
|
+
try {
|
|
5263
|
+
await resumeSubprocess;
|
|
5264
|
+
} catch (err) {
|
|
5265
|
+
if (signal == null ? void 0 : signal.aborted) return;
|
|
5266
|
+
throw err;
|
|
5267
|
+
}
|
|
4080
5268
|
return;
|
|
4081
5269
|
}
|
|
4082
5270
|
throw interactiveError;
|
|
4083
5271
|
}
|
|
4084
5272
|
}
|
|
4085
5273
|
} catch (error) {
|
|
5274
|
+
if (signal == null ? void 0 : signal.aborted) return;
|
|
4086
5275
|
const execaError = error;
|
|
4087
5276
|
const errorMessage = execaError.stderr ?? execaError.message ?? "Unknown Claude CLI error";
|
|
4088
5277
|
const sessionInUseMatch = errorMessage.match(/Session ID ([0-9a-f-]+) is already in use/i);
|
|
@@ -4103,6 +5292,7 @@ async function launchClaude(prompt, options = {}) {
|
|
|
4103
5292
|
timeout: 0,
|
|
4104
5293
|
...addDir && { cwd: addDir },
|
|
4105
5294
|
verbose: isDebugMode,
|
|
5295
|
+
env: claudeEnv,
|
|
4106
5296
|
...isDebugMode && { stdio: ["pipe", "pipe", "pipe"] }
|
|
4107
5297
|
};
|
|
4108
5298
|
const subprocess = execa4("claude", resumeArgs, execaOptions);
|
|
@@ -4155,7 +5345,8 @@ async function launchClaude(prompt, options = {}) {
|
|
|
4155
5345
|
...addDir && { cwd: addDir },
|
|
4156
5346
|
stdio: "inherit",
|
|
4157
5347
|
timeout: 0,
|
|
4158
|
-
verbose: logger.isDebugEnabled()
|
|
5348
|
+
verbose: logger.isDebugEnabled(),
|
|
5349
|
+
env: claudeEnv
|
|
4159
5350
|
});
|
|
4160
5351
|
return;
|
|
4161
5352
|
}
|
|
@@ -4330,6 +5521,8 @@ var ClaudeService = class {
|
|
|
4330
5521
|
if (port !== void 0) {
|
|
4331
5522
|
variables.PORT = port;
|
|
4332
5523
|
}
|
|
5524
|
+
const isVscodeMode = process.env.ILOOM_VSCODE === "1";
|
|
5525
|
+
variables.IS_VSCODE_MODE = isVscodeMode;
|
|
4333
5526
|
const prompt = await this.templateManager.getPrompt(type, variables);
|
|
4334
5527
|
const permissionMode = this.getPermissionModeForWorkflow(type);
|
|
4335
5528
|
if (permissionMode === "bypassPermissions") {
|
|
@@ -4395,11 +5588,11 @@ var ClaudeContextManager = class {
|
|
|
4395
5588
|
if (!context.workspacePath) {
|
|
4396
5589
|
throw new Error("Workspace path is required");
|
|
4397
5590
|
}
|
|
4398
|
-
if (context.type === "issue" &&
|
|
4399
|
-
throw new Error("Issue identifier
|
|
5591
|
+
if (context.type === "issue" && context.identifier === void 0) {
|
|
5592
|
+
throw new Error("Issue identifier is required");
|
|
4400
5593
|
}
|
|
4401
|
-
if (context.type === "pr" &&
|
|
4402
|
-
throw new Error("PR identifier
|
|
5594
|
+
if (context.type === "pr" && context.identifier === void 0) {
|
|
5595
|
+
throw new Error("PR identifier is required");
|
|
4403
5596
|
}
|
|
4404
5597
|
logger.debug("Context prepared", { context });
|
|
4405
5598
|
}
|
|
@@ -4447,6 +5640,108 @@ var UserAbortedCommitError = class extends Error {
|
|
|
4447
5640
|
// src/utils/index.ts
|
|
4448
5641
|
init_logger();
|
|
4449
5642
|
|
|
5643
|
+
// src/utils/jira-wiki-sanitizer.ts
|
|
5644
|
+
var JiraWikiSanitizer = class {
|
|
5645
|
+
/**
|
|
5646
|
+
* Sanitize body text by converting unambiguous Jira Wiki patterns to Markdown.
|
|
5647
|
+
* Preserves content inside backtick-fenced code blocks.
|
|
5648
|
+
* Returns text unchanged if no Wiki patterns detected.
|
|
5649
|
+
*/
|
|
5650
|
+
static sanitize(text) {
|
|
5651
|
+
if (!text) {
|
|
5652
|
+
return "";
|
|
5653
|
+
}
|
|
5654
|
+
const segments = this.splitByCodeBlocks(text);
|
|
5655
|
+
const converted = segments.map((segment) => {
|
|
5656
|
+
if (segment.isCode) {
|
|
5657
|
+
return segment.text;
|
|
5658
|
+
}
|
|
5659
|
+
return this.convertSegment(segment.text);
|
|
5660
|
+
});
|
|
5661
|
+
return converted.join("");
|
|
5662
|
+
}
|
|
5663
|
+
/**
|
|
5664
|
+
* Check if text contains unambiguous Jira Wiki patterns.
|
|
5665
|
+
* Only checks for patterns that are safe to convert.
|
|
5666
|
+
*/
|
|
5667
|
+
static hasJiraWikiPatterns(text) {
|
|
5668
|
+
if (!text) {
|
|
5669
|
+
return false;
|
|
5670
|
+
}
|
|
5671
|
+
if (/^h[1-6]\.\s+/m.test(text)) {
|
|
5672
|
+
return true;
|
|
5673
|
+
}
|
|
5674
|
+
if (/\{code(?::[^}]*)?\}/i.test(text)) {
|
|
5675
|
+
return true;
|
|
5676
|
+
}
|
|
5677
|
+
if (/\{quote\}/i.test(text)) {
|
|
5678
|
+
return true;
|
|
5679
|
+
}
|
|
5680
|
+
if (/\[[^\]|]+\|https?:\/\/[^\]]+\]/.test(text)) {
|
|
5681
|
+
return true;
|
|
5682
|
+
}
|
|
5683
|
+
return false;
|
|
5684
|
+
}
|
|
5685
|
+
/**
|
|
5686
|
+
* Split text into segments, separating existing Markdown fenced code blocks
|
|
5687
|
+
* from the rest of the content. This ensures we don't modify content inside
|
|
5688
|
+
* code blocks (e.g., Jira Wiki examples shown in a Markdown code block).
|
|
5689
|
+
*/
|
|
5690
|
+
static splitByCodeBlocks(text) {
|
|
5691
|
+
const segments = [];
|
|
5692
|
+
const codeBlockRegex = /^(`{3,})[^\n]*\n[\s\S]*?^\1\s*$/gm;
|
|
5693
|
+
let lastIndex = 0;
|
|
5694
|
+
for (const match of text.matchAll(codeBlockRegex)) {
|
|
5695
|
+
const matchStart = match.index ?? 0;
|
|
5696
|
+
if (matchStart > lastIndex) {
|
|
5697
|
+
segments.push({ text: text.slice(lastIndex, matchStart), isCode: false });
|
|
5698
|
+
}
|
|
5699
|
+
segments.push({ text: match[0], isCode: true });
|
|
5700
|
+
lastIndex = matchStart + match[0].length;
|
|
5701
|
+
}
|
|
5702
|
+
if (lastIndex < text.length) {
|
|
5703
|
+
segments.push({ text: text.slice(lastIndex), isCode: false });
|
|
5704
|
+
}
|
|
5705
|
+
return segments;
|
|
5706
|
+
}
|
|
5707
|
+
/**
|
|
5708
|
+
* Apply all safe Jira Wiki -> Markdown conversions to a text segment.
|
|
5709
|
+
*/
|
|
5710
|
+
static convertSegment(text) {
|
|
5711
|
+
let result = text;
|
|
5712
|
+
result = result.replace(/^h([1-6])\.\s+(.*?)$/gm, (_match, level, content) => {
|
|
5713
|
+
const hashes = "#".repeat(parseInt(level, 10));
|
|
5714
|
+
return `${hashes} ${content}`;
|
|
5715
|
+
});
|
|
5716
|
+
result = result.replace(
|
|
5717
|
+
/\{code:([^}]+)\}\s*\n([\s\S]*?)\n?\s*\{code\}/gi,
|
|
5718
|
+
(_match, lang, content) => {
|
|
5719
|
+
return "```" + lang.trim() + "\n" + content + "\n```";
|
|
5720
|
+
}
|
|
5721
|
+
);
|
|
5722
|
+
result = result.replace(
|
|
5723
|
+
/\{code\}\s*\n([\s\S]*?)\n?\s*\{code\}/gi,
|
|
5724
|
+
(_match, content) => {
|
|
5725
|
+
return "```\n" + content + "\n```";
|
|
5726
|
+
}
|
|
5727
|
+
);
|
|
5728
|
+
result = result.replace(
|
|
5729
|
+
/\{quote\}\s*\n([\s\S]*?)\n?\s*\{quote\}/gi,
|
|
5730
|
+
(_match, content) => {
|
|
5731
|
+
const lines = content.split("\n");
|
|
5732
|
+
return lines.map((line) => `> ${line}`).join("\n");
|
|
5733
|
+
}
|
|
5734
|
+
);
|
|
5735
|
+
result = result.replace(
|
|
5736
|
+
/\[([^\]|]+)\|(https?:\/\/[^\]]+)\]/g,
|
|
5737
|
+
(_match, linkText, url) => {
|
|
5738
|
+
return `[${linkText}](${url})`;
|
|
5739
|
+
}
|
|
5740
|
+
);
|
|
5741
|
+
return result;
|
|
5742
|
+
}
|
|
5743
|
+
};
|
|
5744
|
+
|
|
4450
5745
|
// src/utils/linear-markup-converter.ts
|
|
4451
5746
|
import { appendFileSync } from "fs";
|
|
4452
5747
|
import { join as join2, dirname, basename, extname } from "path";
|
|
@@ -4755,6 +6050,7 @@ export {
|
|
|
4755
6050
|
GitHubService,
|
|
4756
6051
|
GitWorktreeManager,
|
|
4757
6052
|
IssueTrackerFactory,
|
|
6053
|
+
JiraWikiSanitizer,
|
|
4758
6054
|
LinearMarkupConverter,
|
|
4759
6055
|
PLACEHOLDER_COMMIT_PREFIX,
|
|
4760
6056
|
TableFormatter,
|