diff --git a/skills/g3fo-commit-jira/SKILL.md b/skills/g3fo-commit-jira/SKILL.md index d8ca8b3..bfdbfc1 100644 --- a/skills/g3fo-commit-jira/SKILL.md +++ b/skills/g3fo-commit-jira/SKILL.md @@ -29,13 +29,15 @@ description: G3FO 项目 Git 提交规范自动化工具。**AI 必须严格按 1. **禁止 `git push`**。本技能只做到 amend 为止。 2. **禁止跳过** `git rev-parse `;amend 场景下禁止在 **hash ≠ HEAD** 时仍执行 amend。 -3. **禁止用 Atlassian MCP 或其它方式冒充「已上传 MD 附件」**;上传文件 **必须** 使用 `scripts/upload_attachment.py`(MCP 无可靠上传能力时不得虚构成功)。 -4. **禁止**在用户未选定 Issue、也未完成新建并取得 Key 的情况下,把某 Key 写进 commit message。 -5. **禁止**未读 `references/jira_commit_docs_policy.md`(或本 SKILL 中的条件表)就默认「三份全要」或「一律不要文档」。 +3. **禁止 amend 已推送的提交**(除非用户明确要求并确认风险)。使用 `git branch -r --contains ` 检测。 +4. **禁止用 Atlassian MCP 或其它方式冒充「已上传 MD 附件」**;上传文件 **必须** 使用 `scripts/upload_attachment.py`(MCP 无可靠上传能力时不得虚构成功)。 +5. **禁止**在用户未选定 Issue、也未完成新建并取得 Key 的情况下,把某 Key 写进 commit message。 +6. **禁止**未读 `references/jira_commit_docs_policy.md`(或本 SKILL 中的条件表)就默认「三份全要」或「一律不要文档」。 +7. **禁止**在用户已提供 Jira 编号时仍执行搜索、展示列表或创建新 Issue。 ### 3. Step 3 交互规则(易走偏) -- 用户**已给** Jira 编号 → 直接进入 Step 4,Key = 用户给的编号。 +- 用户**已给** Jira 编号 → **直接进入 Step 4**,Key = 用户给的编号,**禁止执行搜索、展示列表或创建新 Issue**。 - 用户**未给** → 搜索展示列表后,**必须等待用户输入 1~N 或 0**(或明确同意新建),**禁止**擅自替用户选一个 Issue 绑定。 - 搜索为空 → 可进入新建流程;新建成功后 Key 以 API 返回为准。 @@ -51,13 +53,16 @@ description: G3FO 项目 Git 提交规范自动化工具。**AI 必须严格按 用户必须提供一个有效的 git 修订号(如 `HEAD` 或具体的 `hash`)。 - **校验逻辑**:使用 `git rev-parse ` 检查修订号是否存在。 -- **错误处理**:如果修订号无效或找不到,**必须** 停止操作并提示用户:“找不到修订号 ``,请提供正确的 git 修订号(例如 HEAD 或 7 位以上的 commit hash)。” +- **错误处理**:如果修订号无效或找不到,**必须** 停止操作并提示用户:"找不到修订号 ``,请提供正确的 git 修订号(例如 HEAD 或 7 位以上的 commit hash)。" - **路径要求**:必须在相关的项目根目录下执行 git 命令。如果当前目录不是 git 仓库或不是目标项目,请提示用户切换到正确的项目路径。 +- **已 push 提交检测**:使用 `git branch -r --contains ` 检查提交是否已推送到远程。如果已推送,**默认跳过 Step 5(amend)**,仅完成 Jira 绑定和文档上传,并告知用户: + > "提交 `` 已推送到远程,无法 amend。已完成 Jira 绑定和文档上传。如需修改 commit message,请手动处理或提供特别说明。" +- **例外**:如果用户明确要求修改已 push 的提交(如通过 `--force` 或其他方式),需用户确认风险后再执行。 **2. Jira 编号 (可选)** 用户可以主动提供 Jira 编号(如 `G3SF-123`)。 -- 如果提供了 Jira 编号,直接引用该编号,跳过“查找/选择 Jira 任务”的步骤。 +- 如果提供了 Jira 编号,**直接使用该编号,跳过「查找/选择/新建 Jira 任务」的步骤**,进入 Step 4 上传文档和评论。 - 如果未提供,则按流程自动查找或提示用户新建。 **3. Jira 访问方式(二选一)** @@ -89,12 +94,14 @@ description: G3FO 项目 Git 提交规范自动化工具。**AI 必须严格按 ```powershell git show # 获取 diff + 元数据 git log -1 --format="%B" # 获取原始 commit message +git branch -r --contains # 检查是否已推送到远程(有输出则已 push) ``` 分析要点: - 涉及的服务模块/包名 - 新增/修改/删除的方法与接口 - 逻辑改动的核心目的 +- **是否已推送**:如果 `git branch -r --contains ` 有输出,说明已推送,Step 5 将跳过 amend ### Step 2 - 判定任务类型并确定需生成的文档 @@ -116,20 +123,27 @@ git log -1 --format="%B" # 获取原始 commit message ### Step 2(续)- 生成所需报告内容 -对上述判定为“需要”的文档,按 `references/workflow.md` 中的模板生成内容(可先存于内存或临时文件): +对上述判定为"需要"的文档,按 `references/workflow.md` 中的模板生成内容(可先存于内存或临时文件): - **开发计划**:步骤、设计方法、技术决策。 - **影响分析**:改动概览、方法级分析、影响范围、风险与回滚、验证与测试(模板见 workflow.md)。 - **任务摘要**:所完成工作的简要回顾,以及任何偏离计划之处。 -生成后先不按“公司附件名”落盘,等取得 Jira 编号后再以 `_*.md` 命名保存并上传。 +生成后先不按"公司附件名"落盘,等取得 Jira 编号后再以 `_*.md` 命名保存并上传。 + +**文档保存路径规则**: +- 所有生成的 MD 文档统一保存到项目根目录下的 `doc/{日期}` 文件夹 +- 日期格式为 `YYYY-MM-DD`(如 `doc/2026-03-26`) +- 若该文件夹不存在,**必须先创建**再保存文件 +- 示例路径:`doc/2026-03-26/G3SF-123_Impact_Analysis.md` ### Step 3 - 确定 Jira 任务 **前置**:Step 2 已完成(至少已判定文档需求;若需文档,可先草稿内容,**真实 Key 确定后再按 `_*.md` 保存**)。 **1. 如果用户已提供 Jira 编号:** -- 直接使用该编号(如 `G3SF-123`),进入 Step 4。 +- **直接使用该编号**(如 `G3SF-123`),**跳过查找、选择和新建流程**,直接进入 Step 4。 +- 不执行 JQL 搜索,不展示列表,不创建新 Issue。 **2. 如果用户未提供 Jira 编号:** - 使用 JQL 查找当前用户在 G3SF 项目下的进行中任务(limit 5): @@ -155,11 +169,15 @@ project = G3SF AND assignee = currentUser() AND statusCategory != Done ORDER BY **如果选择/新建的任务 ID > 0:** 1. **保存并上传附件**(已配置 `jira_upload.env` 且本任务需要文档时): - 将 Step 2 生成的各文档内容,按公司规范命名写入当前项目目录(或 `docs/` 等),再调用上传脚本。PowerShell 中不要用 `&&`,改用 `;` 或换行。 + 将 Step 2 生成的各文档内容,按公司规范命名写入项目 `doc/{日期}` 目录(日期格式 `YYYY-MM-DD`,若不存在则先创建),再调用上传脚本。PowerShell 中不要用 `&&`,改用 `;` 或换行。 ```powershell + # 创建日期目录(若不存在) + $dateFolder = "doc/$(Get-Date -Format 'yyyy-MM-dd')" + if (-not (Test-Path $dateFolder)) { New-Item -ItemType Directory -Path $dateFolder -Force } + # 示例:需要三份文档时,先写入 G3SF-123_Dev_Plan.md、G3SF-123_Impact_Analysis.md、G3SF-123_Task_Summary.md,再: - python skills/g3fo-commit-jira/scripts/upload_attachment.py --config "skills/g3fo-commit-jira/jira_upload.env" --issue "G3SF-123" --file "G3SF-123_Impact_Analysis.md" --file "G3SF-123_Dev_Plan.md" --file "G3SF-123_Task_Summary.md" + python skills/g3fo-commit-jira/scripts/upload_attachment.py --config "skills/g3fo-commit-jira/jira_upload.env" --issue "G3SF-123" --file "$dateFolder/G3SF-123_Impact_Analysis.md" --file "$dateFolder/G3SF-123_Dev_Plan.md" --file "$dateFolder/G3SF-123_Task_Summary.md" ``` 若本任务仅需影响分析,则只写入并上传 `G3SF-123_Impact_Analysis.md`。未配置 `jira_upload.env` 时跳过上传;有 MCP 时可仅写评论,无 MCP 则必须配置 env 才能完成评论。 @@ -193,6 +211,17 @@ project = G3SF AND assignee = currentUser() AND statusCategory != Done ORDER BY ### Step 5 - 更新 Git 提交信息 **前置**:Step 4 已按政策完成(无需文档的绑定类任务可跳过上传/评论,但须已有 Key)。 + +**检查提交是否已推送**: +```powershell +git branch -r --contains +``` +- 如果有输出,说明提交已推送到远程分支,**跳过 amend**,告知用户: + > "提交 `` 已推送到远程,无法 amend。已完成 Jira 绑定和文档上传。如需修改 commit message,请手动处理或提供特别说明。" +- 如果无输出,说明提交未推送,继续执行 amend。 + +**执行 amend(仅当未推送时)**: + 使用获取到的 Jira 编号,通过 `git commit --amend` 更新 commit message。**严禁 push**。 **注意**:在 Windows PowerShell 环境下,需确保 UTF8 编码以防止中文乱码。 @@ -211,10 +240,20 @@ git commit --amend -m "$origMsg`n`n**** Jira $jiraKey" --cleanup=verbatim 完成后告知用户: +**未推送时**: ``` 绑定成功: Jira: G3SF-123 (https://your-jira/browse/G3SF-123) Commit: + 已追加 Jira 编号到 commit message +``` + +**已推送时**: +``` +绑定完成(提交已推送,未修改 commit message): + Jira: G3SF-123 (https://your-jira/browse/G3SF-123) + Commit: + 提示:提交已推送到远程,如需修改 commit message 请手动处理 ``` --- diff --git a/skills/g3fo-commit-jira/references/agent_execution_checklist.md b/skills/g3fo-commit-jira/references/agent_execution_checklist.md index c86f2ac..8e18d4b 100644 --- a/skills/g3fo-commit-jira/references/agent_execution_checklist.md +++ b/skills/g3fo-commit-jira/references/agent_execution_checklist.md @@ -10,6 +10,7 @@ - [ ] 在 **目标业务仓库根目录** 执行 git(`git rev-parse --is-inside-work-tree` 为真) - [ ] 已执行 `git rev-parse `,修订号存在 - [ ] 若要对提交做 amend:已确认 `git rev-parse HEAD` **等于** `git rev-parse `(否则停止,提示用户) +- [ ] 已检查 `git branch -r --contains `,确认提交是否已推送(已推送则跳过 amend) --- @@ -35,15 +36,18 @@ ## Step 3 — Jira Key - [ ] 已有 **真实 Issue Key**(如 `G3SF-123`):来自用户直给、用户从列表选择、或 `create` 成功返回 +- [ ] 若用户已提供 Jira 编号:**跳过搜索、展示列表和新建流程**,直接使用该编号 - [ ] 若走列表:已 **等用户选 1~N 或 0**,未擅自替用户绑定 **未取得真实 Key 前:禁止** 使用 `_*.md` 落盘上传(禁止占位符 Key)。 +**用户已提供 Jira 时:禁止** 执行搜索、展示列表或创建新 Issue。 --- ## Step 4 — 写入 Jira - [ ] 需要附件时:文件名为 `_Dev_Plan.md` / `_Impact_Analysis.md` / `_Task_Summary.md`(仅实际上传需要的) +- [ ] 文档保存路径:`doc/{日期}`(日期格式 `YYYY-MM-DD`),若目录不存在则先创建 - [ ] 附件 **仅** 通过 `scripts/upload_attachment.py` + `jira_upload.env`(不用 MCP 冒充上传) - [ ] 需要影响分析进评论时:MCP `jira_add_comment` 或 `jira_cli.py comment`(与政策一致) - [ ] 新建 Issue 时:Summary 带 `[G3SF]`,类型 Task,描述/评论与内容一致 @@ -52,7 +56,8 @@ ## Step 5 — Git amend -- [ ] 仅 `git commit --amend`,**未** `git push` +- [ ] 已检查 `git branch -r --contains `:若已推送,**跳过 amend**,告知用户 +- [ ] 若未推送:仅 `git commit --amend`,**未** `git push` - [ ] Windows 已按 SKILL 处理 UTF-8 / `--cleanup=verbatim`(如适用) - [ ] 若 message 已含 `**** Jira`,未重复追加 @@ -67,6 +72,8 @@ | 用 MCP 上传 md | 改用 `upload_attachment.py` | | 简单拼写改却写三份文档 | 重读政策表,按类型裁剪 | | 执行了 push | 违反技能范围,后续勿再 push | +| amend 已推送的提交 | 先检查 `git branch -r --contains`,已推送则跳过 amend | +| 用户已给 Jira 却还搜索/新建 | 直接使用用户提供的编号,跳过 Step 3 的搜索流程 | --- diff --git a/skills/g3fo-commit-jira/references/jira_commit_docs_policy.md b/skills/g3fo-commit-jira/references/jira_commit_docs_policy.md index ab06860..cf4fb25 100644 --- a/skills/g3fo-commit-jira/references/jira_commit_docs_policy.md +++ b/skills/g3fo-commit-jira/references/jira_commit_docs_policy.md @@ -46,3 +46,9 @@ 示例:工单 `G3SF-123` 的三个附件命名为 `G3SF-123_Dev_Plan.md`、`G3SF-123_Impact_Analysis.md`、`G3SF-123_Task_Summary.md`。 + +**本地保存路径**: +- 所有文档统一保存到项目根目录下的 `doc/{日期}` 文件夹 +- 日期格式为 `YYYY-MM-DD`(如 `doc/2026-03-26`) +- 若该文件夹不存在,**必须先创建**再保存文件 +- 完整路径示例:`doc/2026-03-26/G3SF-123_Impact_Analysis.md` diff --git a/skills/g3fo-commit-jira/references/workflow.md b/skills/g3fo-commit-jira/references/workflow.md index 24cfa07..b1f4762 100644 --- a/skills/g3fo-commit-jira/references/workflow.md +++ b/skills/g3fo-commit-jira/references/workflow.md @@ -42,6 +42,10 @@ $head = git rev-parse HEAD $provided = git rev-parse if ($head -ne $provided) { Write-Error "提交不是 HEAD,无法 amend" } +# 检查提交是否已推送到远程 +git branch -r --contains +# 如果有输出,说明已推送,Step 5 将跳过 amend + # 获取完整 diff 和元数据 git show @@ -63,6 +67,7 @@ git diff-tree --no-commit-id -r --name-status | `+` 行 | 新增代码(方法签名、接口、配置) | | `-` 行 | 删除/替换代码 | | 文件路径中的包名 | 推断所属服务(`g3fo-trade-service`, `g3fo-margin-service` 等) | +| `git branch -r --contains` 输出 | 是否已推送(有输出 = 已推送,跳过 amend) | G3FO 服务包路径规律: - `com.afe.g3fo..*` → 对应服务 @@ -171,6 +176,17 @@ G3FO 服务包路径规律: ## Step 3: Jira 任务查找与确认 +### 用户已提供 Jira 编号 + +如果用户已提供 Jira 编号(如 `G3SF-123`): +- **直接使用该编号**,跳过 JQL 搜索、列表展示和新建流程 +- **禁止**执行搜索、展示列表或创建新 Issue +- 直接进入 Step 4 上传文档和评论 + +### 用户未提供 Jira 编号 + +执行 JQL 查询: + ### JQL 查询 ``` @@ -231,12 +247,23 @@ Summary 生成规则: - `_Dev_Plan.md` - `_Impact_Analysis.md` - `_Task_Summary.md` - 仅保存本任务类型要求的那几份(见 [2.1 按任务类型确定需生成的文档](#21-按任务类型确定需生成的文档))。保存位置:当前 Git 项目根目录或项目约定目录(如 `docs/`)。 + 仅保存本任务类型要求的那几份(见 [2.1 按任务类型确定需生成的文档](#21-按任务类型确定需生成的文档))。 + + **保存路径规则**: + - 所有文档统一保存到项目根目录下的 `doc/{日期}` 文件夹 + - 日期格式为 `YYYY-MM-DD`(如 `doc/2026-03-26`) + - 若该文件夹不存在,**必须先创建**再保存文件 + - 完整路径示例:`doc/2026-03-26/G3SF-123_Impact_Analysis.md` + 2. **上传到 Jira 附件**:Atlassian MCP 不支持上传文件,使用本技能自带脚本上传上述 MD 到当前 Issue: - 配置文件:在技能目录下配置 `jira_upload.env`(`JIRA_BASE_URL`、`JIRA_EMAIL`、`JIRA_API_TOKEN`),详见 `references/env_config.md`。 - 命令示例(PowerShell 用 `;` 连接,勿用 `&&`),按需上传多文件: ```powershell - python skills/g3fo-commit-jira/scripts/upload_attachment.py --config "skills/g3fo-commit-jira/jira_upload.env" --issue "G3SF-101" --file "G3SF-101_Impact_Analysis.md" --file "G3SF-101_Dev_Plan.md" --file "G3SF-101_Task_Summary.md" + # 创建日期目录(若不存在) + $dateFolder = "doc/$(Get-Date -Format 'yyyy-MM-dd')" + if (-not (Test-Path $dateFolder)) { New-Item -ItemType Directory -Path $dateFolder -Force } + + python skills/g3fo-commit-jira/scripts/upload_attachment.py --config "skills/g3fo-commit-jira/jira_upload.env" --issue "G3SF-101" --file "$dateFolder/G3SF-101_Impact_Analysis.md" --file "$dateFolder/G3SF-101_Dev_Plan.md" --file "$dateFolder/G3SF-101_Task_Summary.md" ``` - 未配置 `jira_upload.env` 时跳过上传,仅写评论。 @@ -259,6 +286,28 @@ Summary 生成规则: ## Step 5: Git Amend 命令详解 +### 检查提交是否已推送 + +在执行 amend 前,必须检查提交是否已推送到远程: + +```powershell +# 检查提交是否已推送 +$pushedBranches = git branch -r --contains +if ($pushedBranches) { + Write-Host "提交已推送到远程,跳过 amend" + Write-Host "已完成 Jira 绑定和文档上传" + Write-Host "如需修改 commit message,请手动处理或提供特别说明" + # 跳过 amend,直接结束 + return +} +``` + +**已推送的处理**: +- 如果 `git branch -r --contains ` 有输出,说明提交已推送到远程分支 +- **跳过 amend**,仅完成 Jira 绑定和文档上传 +- 告知用户提交已推送,无法 amend +- 例外:用户明确要求修改已推送的提交时,需确认风险后再执行 + ### PowerShell(Windows 环境) 在 Windows PowerShell 中,直接使用 `$origMsg` 可能会导致编码问题(乱码)。必须显式指定输出编码为 UTF8。 @@ -331,6 +380,7 @@ git log --oneline -1 # 查看 commit 摘要 |---------|---------|---------| | 修订号不存在 | `git rev-parse ` 报错 | **停止**,提示用户提供正确的修订号。 | | 提交 hash 不是 HEAD | `git rev-parse HEAD` ≠ `git rev-parse ` | 停止,提示用户确认 hash(只有 HEAD 才能 amend)。 | +| 提交已推送到远程 | `git branch -r --contains ` 有输出 | **跳过 amend**,仅完成 Jira 绑定和文档上传,告知用户提交已推送。 | | 项目路径不正确 | `git rev-parse --is-inside-work-tree` 失败 | 提示用户在正确的 git 项目目录下操作。 | | 无 MCP 且未配置 jira_upload.env | 无法执行 jira_cli / 上传 | **必须**配置 `jira_upload.env` 或改用 Cursor + MCP。 | | Atlassian MCP 未鉴权 | `STATUS.md` 有提示 / 工具调用返回 401 | 调用 `mcp_auth`,或改用 `jira_cli.py`。 | @@ -338,6 +388,7 @@ git log --oneline -1 # 查看 commit 摘要 | Jira 项目 G3SF 不存在 | 创建/搜索返回 404/400 | 提示用户确认 `JIRA_BASE_URL`、项目 key;`create` 可用 `--project` | | 工作区有未提交变更 | `git status --porcelain` 有输出 | 警告:amend 会将未暂存变更排除在外,建议先 `git add` 或 stash | | commit message 中已有 Jira 行 | message 中包含 `**** Jira` | 跳过追加,提示用户该提交已绑定 | +| 用户已提供 Jira 编号 | 用户输入包含 Jira key | **跳过搜索和新建**,直接使用用户提供的编号 | | Jira 搜索无结果 | 工具返回空列表 | 直接进入新建流程,无需用户确认 | | 未配置 jira_upload.env | 上传前检查配置不存在 | **有 MCP**:跳过上传,仅评论。**无 MCP**:无法完成 Jira 操作,须配置 env 或使用 MCP。 | | 任务类型判定为「无需文档」 | 按政策表属简单漏洞/琐碎改动 | 不生成报告、不写评论、不上传附件;仍执行 Jira 绑定与 commit --amend,将 Jira 编号追加到 commit message | diff --git a/skills/g3fo-commit-jira/scripts/jira_cli.py b/skills/g3fo-commit-jira/scripts/jira_cli.py index 7b31e4e..5c92d61 100644 --- a/skills/g3fo-commit-jira/scripts/jira_cli.py +++ b/skills/g3fo-commit-jira/scripts/jira_cli.py @@ -57,14 +57,18 @@ def _text_nodes(s: str) -> List[Dict[str, Any]]: def parse_inline_adf(s: str) -> List[Dict[str, Any]]: """ - Parse inline **bold**, `code`, and [text](url) into ADF text nodes with marks. + Parse inline **bold**, ~~strikethrough~~, `code`, *italic*, [text](url), ***bold italic*** + into ADF text nodes with marks. """ if not s: return [] - # Order: links, bold, code (non-greedy). Avoid ``` by not matching double backtick start. + # Order: links, bold+italic, bold, strikethrough, italic, code (non-greedy). pattern = re.compile( r"\[([^\]]+)\]\(([^)]+)\)" # [label](url) + r"|\*\*\*(.+?)\*\*\*" # ***bold italic*** r"|\*\*(.+?)\*\*" # **bold** + r"|~~(.+?)~~" # ~~strikethrough~~ + r"|(? List[Dict[str, Any]]: ) elif m.group(3) is not None: nodes.append( - {"type": "text", "text": m.group(3), "marks": [{"type": "strong"}]} + { + "type": "text", + "text": m.group(3), + "marks": [{"type": "strong"}, {"type": "em"}], + } ) elif m.group(4) is not None: nodes.append( - {"type": "text", "text": m.group(4), "marks": [{"type": "code"}]} + {"type": "text", "text": m.group(4), "marks": [{"type": "strong"}]} + ) + elif m.group(5) is not None: + nodes.append( + {"type": "text", "text": m.group(5), "marks": [{"type": "strike"}]} + ) + elif m.group(6) is not None: + nodes.append( + {"type": "text", "text": m.group(6), "marks": [{"type": "em"}]} + ) + elif m.group(7) is not None: + nodes.append( + {"type": "text", "text": m.group(7), "marks": [{"type": "code"}]} ) last = m.end() if last < len(s): @@ -98,6 +118,78 @@ def parse_inline_adf(s: str) -> List[Dict[str, Any]]: return nodes +def _split_table_row(line: str) -> List[str]: + """Split a pipe-delimited table row into cells, stripping whitespace.""" + parts = line.split("|") + cells = [] + for p in parts: + c = p.strip() + if c: + cells.append(c) + return cells + + +def _is_separator_row(cells: List[str]) -> bool: + """Check if a row is a GFM table separator (e.g., |---|---|).""" + return bool(cells) and all(re.fullmatch(r"-{3,}", c.strip()) for c in cells) + + +def _is_table_row(line: str) -> bool: + """Detect if a line looks like a GFM table row.""" + stripped = line.strip() + if not stripped.startswith("|"): + return False + cells = _split_table_row(stripped) + return len(cells) >= 1 + + +def _parse_table(lines: List[str], start: int, n: int): + """ + Parse a GFM table starting at index start. + Returns (adf_table_dict, next_index). + """ + header_cells = _split_table_row(lines[start].strip()) + num_cols = len(header_cells) + i = start + 1 + + # Skip separator row if present + if i < n and _is_separator_row(_split_table_row(lines[i].strip())): + i += 1 + + data_rows: List[List[str]] = [] + while i < n: + s = lines[i].strip() + if not s or not _is_table_row(s): + break + row_cells = _split_table_row(s) + # Pad or trim to match header column count + while len(row_cells) < num_cols: + row_cells.append("") + data_rows.append(row_cells[:num_cols]) + i += 1 + + # Build ADF table + def _make_cell(text: str, is_header: bool = False) -> Dict[str, Any]: + cell_type = "tableHeader" if is_header else "tableCell" + inline_nodes = parse_inline_adf(text) or _text_nodes(text or " ") + return { + "type": cell_type, + "content": [{"type": "paragraph", "content": inline_nodes}], + } + + def _make_row(cell_texts: List[str], is_header: bool = False) -> Dict[str, Any]: + return { + "type": "tableRow", + "content": [_make_cell(c, is_header=is_header) for c in cell_texts], + } + + table_content: List[Dict[str, Any]] = [_make_row(header_cells, is_header=True)] + for dr in data_rows: + table_content.append(_make_row(dr)) + + return {"type": "table", "content": table_content}, i + + def _paragraph_from_buffer(lines: List[str]) -> Optional[Dict[str, Any]]: if not lines: return None @@ -164,6 +256,13 @@ def markdown_to_adf(text: str) -> Dict[str, Any]: i += 1 continue + # GFM Table: | col1 | col2 | + if _is_table_row(stripped): + flush_paragraph() + table_adf, i = _parse_table(lines, i, n) + content.append(table_adf) + continue + # Fenced code block if stripped.startswith("```"): flush_paragraph() @@ -293,6 +392,22 @@ def plain_to_adf(text: str) -> Dict[str, Any]: return markdown_to_adf(text) +def unescape_text(text: str) -> str: + """ + Unescape common escape sequences that may appear as literal strings + in CLI arguments or generated content. + Converts \\n -> newline, \\t -> tab, \\\\ -> backslash, etc. + """ + if not text: + return text + result = text.replace("\\\\", "\x00ESCAPED_BACKSLASH\x00") + result = result.replace("\\n", "\n") + result = result.replace("\\t", "\t") + result = result.replace("\\r", "\r") + result = result.replace("\x00ESCAPED_BACKSLASH\x00", "\\") + return result + + def _out(data: Any, fmt: str) -> None: if fmt == "json": print(json.dumps(data, ensure_ascii=False, indent=2)) @@ -318,16 +433,16 @@ def cmd_myself(base: str, email: str, token: str, fmt: str) -> int: def cmd_search(base: str, email: str, token: str, jql: str, limit: int, fmt: str) -> int: - url = f"{base}/rest/api/3/search" - params = { + url = f"{base}/rest/api/3/search/jql" + payload = { "jql": jql, "maxResults": limit, - "fields": "key,summary,status,assignee,updated", + "fields": ["key", "summary", "status", "assignee", "updated"], } - r = requests.get( + r = requests.post( url, - headers=session_headers(email, token, json_body=False), - params=params, + headers=session_headers(email, token), + json=payload, timeout=60, ) if r.status_code != 200: @@ -455,8 +570,8 @@ def read_body(description: Optional[str], path: Optional[str]) -> str: p = Path(path) if not p.is_file(): raise FileNotFoundError(f"File not found: {p}") - return p.read_text(encoding="utf-8") - return description or "" + return unescape_text(p.read_text(encoding="utf-8")) + return unescape_text(description or "") def main() -> int: