diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index bbc88e4..76025d8 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jsc-git", - "version": "0.1.0", + "version": "0.1.1", "description": "Commit 分組認可與 Push Request 建立", "skills": "./skills", "author": { @@ -16,8 +16,9 @@ ], "jsc": { "requires": { - "jsc-gitea": ">=0.1.7", - "jsc-hooks": ">=0.2.8" + "jsc-gitea": ">=0.1.8", + "jsc-hooks": ">=0.2.8", + "jsc-meta": ">=0.2.2" } } } diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 13f2850..bad6005 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,12 +1,13 @@ { "name": "jsc-git", - "version": "0.1.0", + "version": "0.1.1", "description": "Commit 分組認可與 Push Request 建立", "skills": "./skills", "jsc": { "requires": { - "jsc-gitea": ">=0.1.7", - "jsc-hooks": ">=0.2.8" + "jsc-gitea": ">=0.1.8", + "jsc-hooks": ">=0.2.8", + "jsc-meta": ">=0.2.2" } } } diff --git a/README.md b/README.md index bbfc8a6..3daa05e 100644 --- a/README.md +++ b/README.md @@ -24,19 +24,13 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安 | --- | --- | | `tools/base-branch.sh` | 決定 PR 基底分支。兩種模式:不帶旗標時,呼叫方傳入的分支最優先,沒傳才依序試 develop、main、master;`--derive [分支]` 從分支名推出階梯的上一階。一律確認分支存在於遠端,找不到就回傳非零。「沒傳」看參數個數,傳入空字串算錯誤,不會退回 develop | | `tools/slugify.sh` | 把類型與英文短語組成 ASCII 分支名 `{type}/{slug}`;輸入含非 ASCII 或 slug 化後為空,就回傳非零並要求先翻譯成英文短語。第一個參數可以帶斜線,所以連叫兩次就組得出 `feat/{功能}/{子功能}` | +| `tools/pick-type.sh` | 從一組 commit 型別中選出優先度最高的一個,是型別優先序的唯一真實來源。參數或標準輸入都收,commit 標題整行餵進來也認得出型別;沒有輸入回傳 2,輸入裡沒有階梯表型別回傳 3 | ## PR 階梯 -每個 PR 只往上一階開,禁止越級。基底一律由 `tools/base-branch.sh --derive` 推導,不手挑。 +階梯規則的唯一真實來源是 `jsc-meta` 的 `references/guidelines.md` 「PR 分支階梯」一節,本檔不再抄一份。基底一律由 `tools/base-branch.sh --derive` 推導,不手挑。 -| Commit 類型 | 階梯 | -| --- | --- | -| feat、docs、style、refactor、perf、test、chore、revert | `{類型}/{功能}/{子功能}` → `{類型}/{功能}/main` → `develop` → `master` | -| fix | `fix/{修改}` → `develop` → `master` | - -`{子功能}` 可以多層(例:`feat/a/b/c`),推導一律把最後一段換成 `main`。推不出唯一合法基底就回傳 7 並中止,由呼叫端問使用者,不猜也不退回 develop。功能主幹 `{類型}/{功能}/main` 不在遠端時,自動以 develop 為起點建立並推上去,再把建立了哪一條分支印到 stderr。 - -分支名只允許 ASCII(小寫、數字、連字號、斜線)。中文簡述先過 `tools/slugify.sh`,`--derive` 不收非 ASCII 分支名。 +腳本這一側的行為:推不出唯一合法基底就回傳 7 並中止,由呼叫端問使用者,不猜也不退回 develop。功能主幹不在遠端時,自動以 develop 為起點建立並推上去,再把建立了哪一條分支印到 stderr。分支名只允許 ASCII(小寫、數字、連字號、斜線),中文簡述先過 `tools/slugify.sh`,`--derive` 不收非 ASCII 分支名。 ## Skills 目錄 @@ -46,11 +40,11 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安 ### `commit` -追蹤所有檔案變更,依 Commit 格式 `{類型}({需求 or 功能}): {訊息}` 將同類型與同需求的變更認可在一起。認可前先跑 `jsc-hooks` 的 `comment-scope.sh sweep` 掃過工作區,攔下夾帶文件相關資訊的註解;腳本不在本機就跳過並在回報中說明,不中止認可。訊息格式三選一:完整版(What/Why/How/Who)、簡易版(依 git diff 總結一句)、自訂。認可完成後,目前分支若已有開啟中的 PR,就交給 `pr` 校準標題、描述與前置 PR 依賴。 +追蹤所有檔案變更,依 Commit 格式 `{類型}({需求 or 功能}): {訊息}` 將同類型與同需求的變更認可在一起。盤點與註解掃描兩件事同時跑,分組草擬要吃盤點的檔案清單,排在盤點之後,可以與掃描並行;掃描要求的修正仍在第一個 commit 之前完成;註解掃描跑 `jsc-hooks` 的 `comment-scope.sh sweep`,攔下夾帶文件相關資訊的註解,腳本不在本機就跳過並在回報中說明,不中止認可。`git add -A` 後單次提交與非繁中訊息由 `jsc-hooks` 的 PreToolUse `Bash` 閘門在程式層擋下,只有 claude 有這道閘門,其餘四支 CLI 仍靠技能內文的規則。訊息格式三選一:完整版(What/Why/How/Who)、簡易版(依 git diff 總結一句)、自訂。由 `pr` 呼叫進來時不查 PR,結果由 `pr` 傳入;單獨呼叫時查一次 `gitea.sh pr-of-branch`,查到就交給 `pr` 校準標題、描述與前置 PR 依賴。 ### `pr` -先認可所有變更,再依階梯命名目標分支、push、以範本描述建立 Gitea PR。基底分支由 `base-branch.sh --derive` 從分支名推出上一階;呼叫方傳入的基底與推導結果不同,就當成越級擋下並說明正確階梯,不會悄悄改目標。分支名只允許 ASCII:類型取 commit 優先度最高者,功能與標題先翻譯成英文短語再 slug 化。分支已有開啟中的 PR 時不重開,改成比對標題、描述、前置 PR 依賴三項,只有不一樣的那幾項才送出 API 呼叫。收尾回報使用 `jsc-meta/references/pr-report.md` 的 PR 資訊表格。 +呼叫方傳入的基底先驗合法性,再認可所有變更,然後依階梯命名目標分支、push、以範本描述建立 Gitea PR。基底分支由 `base-branch.sh --derive` 從分支名推出上一階;呼叫方傳入的基底與推導結果不同,就當成越級擋下並說明正確階梯,不會悄悄改目標。分支名只允許 ASCII:類型交給 `pick-type.sh` 選,功能與標題先翻譯成英文短語再 slug 化。分支命名完成就同時啟動 PR 查詢與描述草擬,不等 push。整條呼叫鏈只查一次 `gitea.sh pr-of-branch`;分支已有開啟中的 PR 時不重開,改用該次查詢帶回的標題、base 與描述比對三項,只有不一樣的那幾項才送出 API 呼叫。收尾回報使用 `jsc-meta/references/pr-report.md` 的 PR 資訊表格。 diff --git a/plugin.json b/plugin.json index 1ed9921..c8a4cf2 100644 --- a/plugin.json +++ b/plugin.json @@ -1,12 +1,13 @@ { "name": "jsc-git", - "version": "0.1.0", + "version": "0.1.1", "description": "Commit 分組認可與 Push Request 建立", "skills": "./skills/", "jsc": { "requires": { - "jsc-gitea": ">=0.1.7", - "jsc-hooks": ">=0.2.8" + "jsc-gitea": ">=0.1.8", + "jsc-hooks": ">=0.2.8", + "jsc-meta": ">=0.2.2" } } } diff --git a/skills/commit/SKILL.md b/skills/commit/SKILL.md index dbb287c..96f2342 100644 --- a/skills/commit/SKILL.md +++ b/skills/commit/SKILL.md @@ -7,15 +7,23 @@ description: Group all pending file changes by conventional type and feature, th ## Steps -1. Track every file change: inspect all changes with `git status --porcelain` first, then `git add` group by group. `git add -A` followed by one bulk commit is forbidden. Done when every path listed by `git status --porcelain` is assigned to exactly one group, before the first commit runs. -2. Sweep the comments about to be committed: run `jsc-hooks/hooks/comment-scope.sh sweep` over this working tree. A code comment states why the code is written this way, never where the work is documented; the rule text and its allow list live in one place only, `jsc-review/references/comment-scope.md`. +1. Track every file change: inspect all changes with `git status --porcelain` first. That inventory is this step's own work; the `git add` that follows is group by group, one add per group as step 4 commits it, never one bulk add here. `git add -A` followed by one bulk commit is blocked in code by the `jsc-hooks` PreToolUse `Bash` guard, which rejects that command pair before it runs. Only claude has a PreToolUse stage; on codex, copilot, antigravity and kiro that guard never fires, so on those four CLIs this step is the only thing holding the rule — add group by group there as well. Done when `git status --porcelain` has run and its full path list is held. That list is what step 3 groups, so this step never waits on the grouping. +2. Sweep the comments about to be committed: run `jsc-hooks/hooks/comment-scope.sh sweep` over this working tree. Start the sweep at the same time as step 1's inventory: both only read the working tree, and neither needs the other's result. Step 3's grouping is not part of that pair — it needs step 1's path list — but it may run while this sweep is still going. Every fix the sweep demands still lands before step 4 runs the first commit. A code comment states why the code is written this way, never where the work is documented; the rule text and its allow list live in one place only, `jsc-review/references/comment-scope.md`. - The script exits 0 in silence when it finds no git working tree and when `jsc-hooks` is not on this machine. **If the script is not found, skip this step, say so in the report and commit anyway** — missing infrastructure is not a violation. - Exit 2 means a hit. Fix every comment line the warning points at, in place, then run the sweep again until it exits 0, and commit only after that. - The warning is written for a person and a model to judge, **not a hard block**. When it is a false positive, say plainly why and carry on with the commit; never delete a useful comment just to keep the script quiet. - Done when one of these is true and reported: the sweep exited 0, or every remaining warning is reported with the reason it is a false positive, or the script was not found on this machine. -3. Group the changes by same type plus same requirement or feature. One group is one commit. Done when each group carries one type and one requirement or feature, and no path sits in two groups. +3. Group the changes by same type plus same requirement or feature. One group is one commit. Grouping takes step 1's path list as its input, so it starts once that inventory is held; step 2's sweep may still be running alongside it. Done when every path on step 1's list sits in exactly one group, and each group carries one type and one requirement or feature. 4. Commit each group with the format `{type}({requirement or feature}): {message}`. Done when `git status --porcelain` returns empty; report done only then. -5. Calibrate the open PR of this branch, when there is one: `jsc-gitea/tools/gitea.sh api GET /repos/{owner}/{repo}/pulls?state=open` and match `head.ref` against the current branch. A match → hand the PR number to `jsc-git:pr` step 9, which compares title, description, and prerequisite dependency and updates only what differs. No match → nothing to do here. Done when either the branch has no open PR, or `jsc-git:pr` reported each of the three items as matched or updated. +5. Calibrate the open PR of this branch. Who called this run decides whether a lookup happens at all: + - `jsc-git:pr` called this run → skip the lookup. That skill runs the single open-PR lookup of the chain in its own step 6 and owns the calibration from there. Done when the report names `jsc-git:pr` as the caller and states that the lookup was skipped. + - This run is standalone → look the PR up once with `jsc-gitea/tools/gitea.sh pr-of-branch {owner}/{repo} {current branch}`. On exit 0 the script prints line 1 `number{PR number}`, line 2 `title{title}`, line 3 `base{base}`, line 4 the marker `body`, and line 5 onward the description. + - Exit 0 → hand the PR number together with the returned title, base and description to `jsc-git:pr`, which compares title, description and prerequisite dependency and updates only what differs, without repeating the lookup. That hand-off does not loop back here: the tree is already clean, and `jsc-git:pr` as the caller makes this step skip its lookup. + - Exit 3 (no open PR on this branch) → nothing to calibrate. + - Exit 2 (usage error) → the `{owner}/{repo}` or the branch argument is missing or malformed. Correct the arguments and rerun. + - Exit 4 (API call failed) → report the failure and leave the PR state as unknown. Never read a failed call as "no open PR": that leaves a stale title on a branch that just gained commits. + - Any other non-zero → treat it as exit 4. + - Done when the report states exactly one of these: the lookup was skipped because `jsc-git:pr` called this run, the branch has no open PR, `jsc-git:pr` reported each of the three items as matched or updated, or the lookup failed and the failure is named. ## Type table @@ -42,5 +50,5 @@ Ask the user per the `jsc-ask:ask` rules. Skip the question when the question re ## Rules 1. The grouping and message drafting details **MUST run as a sub agent**. The main agent only confirms the grouping and runs the commits. -2. Write messages in UTF-8 Traditional Chinese (the type and scope stay in English), per the STE100 output rule. +2. Write messages in UTF-8 Traditional Chinese (the type and scope stay in English), per the STE100 output rule. The `jsc-hooks` PreToolUse `Bash` guard rejects a `git commit` whose message carries no Traditional Chinese. That guard runs on claude only; on codex, copilot, antigravity and kiro nothing blocks such a message, so on those four CLIs this rule carries the whole load. 3. Never push, and never create a PR. Step 5 only calibrates a PR that already exists; pushing and creating PRs belong to `jsc-git:pr`. diff --git a/skills/pr/SKILL.md b/skills/pr/SKILL.md index 1fc26cb..4438226 100644 --- a/skills/pr/SKILL.md +++ b/skills/pr/SKILL.md @@ -5,53 +5,82 @@ description: Commit all changes via jsc-git:commit, name a ladder-shaped target # pr — create a Push Request -## PR ladder - -Every PR targets the next rung up. Skipping a rung is forbidden. - -| Commit type | Ladder | -| --- | --- | -| feat, docs, style, refactor, perf, test, chore, revert | `{type}/{feature}/{sub-feature}` → `{type}/{feature}/main` → `develop` → `master` | -| fix | `fix/{change}` → `develop` → `master` | - -`{sub-feature}` may hold several levels (`feat/a/b/c`); the base is always the branch name with its last segment replaced by `main`. `tools/base-branch.sh --derive` owns this derivation — never hand-pick a base. +The ladder rules live in one place only: section 「PR 分支階梯」 of `jsc-meta/references/guidelines.md`. `tools/base-branch.sh --derive` is the running implementation of that section. Read the rung a branch belongs to there; never hand-pick a base, and never restate the ladder table in this file. ## Steps -1. Call `jsc-git:commit` to commit every file change first. Done when `git status --porcelain` prints nothing. -2. Build the target branch name in ladder shape: - 1. Take the type with the highest priority across all commits: `revert > fix > feat > perf > refactor > test > docs > style > chore`. Done when exactly one type is picked. +1. **Validate the caller's base first.** A caller may pass a base in (`jsc-sdlc:implement` passes the analysis page's source branch). Whether that base is legal depends on origin alone, not on the target branch name, so settle it before any naming work. Skip the whole step when no caller base was passed, and record "no caller base" for step 4. Otherwise run `tools/base-branch.sh {caller base}` and branch on its exit code: + - 0 → keep the printed name; step 4 cross-checks it against the derived base. + - 2 (too many arguments) → the base arrived as several words. Quote it as one argument and rerun. + - 3 (cannot reach origin) → stop and report that origin is unreachable. Nothing downstream works without the remote. + - 4 (caller base not on origin) → stop, report the caller base, and ask the user per the `jsc-ask:ask` rules which existing branch was meant. Never substitute another branch. + - 5 (develop, main and master all missing on origin) → the script only reaches this code with no argument, so the caller base was dropped on the way in. Stop and report that the argument never reached the script. + - 6 (empty string) → the caller's base variable is unset. Stop and ask the caller for the real branch name. Never rerun with the argument removed: that silently falls back to `develop`. + - 7, 8, 9 → these belong to `--derive` mode only. Seeing one means the wrong mode ran. Stop and report which command line was used. + - Done when the script printed one branch name, or the run is recorded as having no caller base. +2. Call `jsc-git:commit` to commit every file change. Tell it that `jsc-git:pr` is the caller, so it skips its own open-PR lookup — step 6 here is the only such lookup in this chain. Done when `git status --porcelain` prints nothing. +3. Build the target branch name in ladder shape: + 1. Pipe the subjects of step 2's commits into `tools/pick-type.sh` (`git log --format=%s {range} | tools/pick-type.sh`), which owns the type priority order. Exit 0 → take the printed type. Exit 2 (no input) → step 2 produced no commits, so there is nothing to open a PR for; stop and report. Exit 3 (no ladder type in the input) → stop, report the subjects, and correct them to `{type}({scope}): {message}` before retrying. Done when the script printed exactly one type. 2. Summarize one title from all commit messages. Done when one title line covers every commit in the range. 3. Translate the feature and the title into short English phrases, then build the name with `tools/slugify.sh`. `fix` takes one call: `tools/slugify.sh fix {change phrase}` → `fix/order-export-crash`. Every other type takes two calls, feeding the first result back in as the type: `tools/slugify.sh feat {feature phrase}` → `feat/order-export`, then `tools/slugify.sh feat/order-export {sub-feature phrase}` → `feat/order-export/report-filter`. Done when the name has the shape its ladder row requires; exit 2 (non-ASCII input) or exit 3 (empty slug) → re-translate into an English phrase and retry, never hand-build the branch name. -3. Resolve the base branch: - - Run `tools/base-branch.sh --derive {target branch}`. Done when the script prints one branch name. - - The caller passed a base in (`jsc-sdlc:implement` passes the analysis page's source branch): run `tools/base-branch.sh {caller base}` as well. Both print the same name → use it. They differ → the pair skips a rung, so stop, report the caller base, the derived base, and the ladder row that applies, and ask the user per the `jsc-ask:ask` rules whether to rename the branch or correct the caller base. Never silently retarget the PR. - - Exit 7 (no unique legal base), 8 (derived base missing on origin), or 9 (auto-create failed) → report the script's message and stop. Never fall back to `develop`. - - The feature trunk missing on origin is not an error: the script creates it from `develop`, pushes it, and prints 「已自動從 develop 建立功能主幹 {分支}」 to stderr. Capture that line; step 10 has to report it. -4. Run `git checkout -b {target branch} origin/{base branch}` so the branch starts at the remote base, then bring step 1's commits onto it (`git cherry-pick` them when they landed on another branch). Done when `git branch --show-current` prints the target branch and `git log --oneline origin/{base branch}..HEAD` lists exactly step 1's commits. -5. Run `git push -u origin {target branch}`. Done when `git ls-remote --heads origin` shows the target branch's ref. -6. Find out whether the target branch already has an open PR: `jsc-gitea/tools/gitea.sh api GET /repos/{owner}/{repo}/pulls?state=open` and match `head.ref` against the target branch. Done when you hold either one PR number or the fact that there is none; a PR number → skip step 7 and step 8 and go to step 9, because the PR already exists and only needs calibrating. -7. Create the PR: `jsc-gitea/tools/gitea.sh pr-create {owner}/{repo} {target branch} {base branch} "{branch name}" {description file}`。這一步會先要求確認。 + - The name is the only input step 6's lookup and the description draft need. Start both the moment this step ends, and run them alongside steps 4 and 5; neither waits for the push. +4. Resolve the base branch: + - Run `tools/base-branch.sh --derive {target branch}`. Exit 0 → the script printed one branch name. + - Exit 2 (too many arguments) → the branch name arrived as several words. Quote it as one argument and rerun. + - Exit 3 (cannot reach origin) → stop and report that origin is unreachable. + - Exit 4, 5 or 6 → these belong to caller mode. Seeing one means the `--derive` flag was dropped. Rerun with the flag. + - Exit 7 (no unique legal base), 8 (derived base missing on origin) or 9 (auto-create failed) → report the script's message and stop. Never fall back to `develop`. + - Step 1 held a caller base → compare the two names. Same → use it. Different → the pair skips a rung, so stop, report the caller base, the derived base, and the ladder row that applies per `jsc-meta/references/guidelines.md`, and ask the user per the `jsc-ask:ask` rules whether to rename the branch or correct the caller base. Never silently retarget the PR. + - The feature trunk missing on origin is not an error: the script creates it from `develop`, pushes it, and prints 「已自動從 develop 建立功能主幹 {分支}」 to stderr. Capture that line; step 9 has to report it. + - Done when one base name is held, and the caller base either matched it or the user settled the mismatch. +5. Put the branch on origin, in one pass: run `git checkout -b {target branch} origin/{base branch}` so the branch starts at the remote base, bring step 2's commits onto it (`git cherry-pick` them when they landed on another branch), then run `git push -u origin {target branch}`. Done when `git branch --show-current` prints the target branch, `git log --oneline origin/{base branch}..HEAD` lists exactly step 2's commits, and `git ls-remote --heads origin` shows the target branch's ref. +6. Look up the open PR of the target branch: `jsc-gitea/tools/gitea.sh pr-of-branch {owner}/{repo} {target branch}`. Launch it as soon as step 3 named the branch. This is the only open-PR lookup in the whole chain: step 8 reuses this output and never calls `pr-get`. On exit 0 the script prints line 1 `number{PR number}`, line 2 `title{title}`, line 3 `base{base}`, line 4 the marker `body`, and line 5 onward the description. + - Exit 0 → hold the PR number, title, base and body; skip step 7 and go to step 8, because the PR exists and only needs calibrating. + - Exit 3 (no open PR on that branch) → go to step 7 and create one. + - Exit 2 (usage error) → the `{owner}/{repo}` or the branch argument is missing or malformed. Correct the arguments and rerun. + - Exit 4 (API call failed) → stop and report the failure. Never read a failed call as "no open PR": that opens a second PR on a branch that already has one. + - Any other non-zero → treat it as exit 4 and stop. + - Done when either the PR number plus its title, base and body are held, or exit 3 confirmed the branch has no open PR. +7. Create the PR, then hang its prerequisite on it. Run `jsc-gitea/tools/gitea.sh pr-create {owner}/{repo} {target branch} {base branch} "{branch name}" {description file}`. Ask the user to confirm before this call runs. - Title = branch name. - Write the description into a temp file first, using `templates/pr-description.md` (a Traditional Chinese template; the generated description stays in Traditional Chinese per the STE100 output rule). - - Done when the command prints a PR URL; no URL → report the error and stop. -8. **Prerequisite PR blocking**: when the description lists a prerequisite Push Request, run + - Exit 0 → the command prints a PR URL; keep it. + - Exit 2 (description file not found) → write the description file, then rerun. + - Exit 1 → an older `gitea.sh`, whose `pr-create` never checks the description file and lets the read throw instead. Same cause and same handling as exit 2: correct the description file path, then rerun. + - Exit 4 (the API rejected the request) → report the script's message and stop. Never retry against a different base. + - Any other non-zero → treat it as exit 4 and stop. + + **Prerequisite PR blocking**: with the PR URL in hand, and only when the description lists a prerequisite Push Request, run `jsc-gitea/tools/gitea.sh pr-depend {owner}/{repo} {this PR number} {prerequisite owner}/{repo} {prerequisite PR number}` - to add the dependency. Gitea then blocks merging until the prerequisite PR closes. On exit 4 (the dependency API call failed) degrade: prefix the PR title with `WIP:`, which Gitea blocks merging on natively, and record in the PR description that the prefix comes off once the prerequisite PR closes. Done when `pr-depend` printed `OK {owner}/{repo}#{number} depends on ...`, or the PR title starts with `WIP:` and the description names the prerequisite PR. A PR created here goes straight to step 10. -9. **Calibrate an existing PR** (step 6 found a PR number): read it once with `jsc-gitea/tools/gitea.sh pr-get {owner}/{repo} {pr number}` — line 1 is `title{title}`, line 2 is `base{base}`, line 3 is the marker `body`, and line 4 onward is the description. Compare three items against what this run produced, and send an API call only for the ones that differ: - 1. Title against the target branch name. - 2. Description against a freshly drafted description from `templates/pr-description.md`. - 3. Prerequisite PR dependency against the one the description names. - - Title or description differs → write the new description to a temp file and run `jsc-gitea/tools/gitea.sh pr-edit {owner}/{repo} {pr number} "{title}" {description file}`, which carries both items in one call,且會先要求確認。 - - The dependency differs → run `pr-depend` as in step 8,這一步也會先要求確認。 - - All three match → change nothing. Every needless edit notifies every reviewer, so silence is the correct outcome here. - - Done when each of the three items is reported as either matched or updated, and the number of API calls made equals the number of items that differed. -10. If this run follows PR comment fixes and the caller supplied handled comment ids, reply to each comment now with `jsc-gitea/tools/gitea.sh comment-reply`. Rules: `jsc-meta/references/pr-report.md`. Done when every handled comment has a reply URL, or a reported reply failure reason. -11. Report the PR with the table format in `jsc-meta/references/pr-report.md`, then report the base branch, the target branch, which of the three calibration items were updated, and the feature trunk the script auto-created when step 3 printed that line. Done when the report includes the PR table columns `{owner}/{repo}`, PR number, PR URL and PR summary, and names all branch and calibration details. + to add the dependency. Gitea then blocks merging until the prerequisite PR closes. + - Exit 0 → the command prints `OK {owner}/{repo}#{number} depends on ...`. + - Exit 2 (usage error) → an argument is missing or malformed. Correct it and rerun. + - Exit 4 (the dependency API call failed) → degrade: prefix the PR title with `WIP:`, which Gitea blocks merging on natively, and record in the PR description that the prefix comes off once the prerequisite PR closes. + - Any other non-zero → treat it as exit 4 and degrade the same way. + + Done when a PR URL is held **and** the prerequisite is settled — `pr-depend` printed its `OK` line, or the PR title starts with `WIP:` and the description names the prerequisite PR, or the description names no prerequisite and that is stated. A PR created here goes straight to step 9. +8. **Calibrate an existing PR** (step 6 returned a PR number): compare three items against what this run produced, using the title, base and description step 6 already returned. Send an API call only for the items that differ. + 1. Title against the target branch name. + 2. Description against a freshly drafted description from `templates/pr-description.md`. + 3. Prerequisite PR dependency against the one the description names. + - Title or description differs → write the new description to a temp file and run `jsc-gitea/tools/gitea.sh pr-edit {owner}/{repo} {pr number} "{title}" {description file}`, which carries both items in one call. Ask the user to confirm before this call runs. Exit 0 → updated. Exit 2 (description file not found) → write the file and rerun. Exit 4 or any other non-zero → report the script's message and stop, and say plainly that the PR still holds its old title and description. + - The dependency differs → run `pr-depend` with the exit code branching of step 7. Ask the user to confirm before this call runs. + - All three match → change nothing. Every needless edit notifies every reviewer, so silence is the correct outcome here. + - Done when each of the three items is reported as either matched or updated, and the number of API calls made equals the number of items that differed. +9. Reply to the handled comments, then report the run. + + If this run follows PR comment fixes and the caller supplied handled comment ids, reply to each comment first with `jsc-gitea/tools/gitea.sh comment-reply`. The reply content and the comment type mapping live in `jsc-meta/references/pr-report.md`; the exit codes belong here. Run one call per handled comment and branch on each call's own exit code: + - Exit 0 → the command prints the reply link. Keep it against that comment id. + - Exit 2 (the reply file is missing, or the kind is not `issue`, `review` or `inline`) → correct the reply file path or the kind, then rerun that one call. + - Exit 4 (the API call failed), or any other non-zero → record that comment id with the reason it failed and carry on with the remaining comments. One failed reply never ends the round. + + Then report the PR with the table format in `jsc-meta/references/pr-report.md`, followed by the base branch, the target branch, which of the three calibration items were updated, the feature trunk the script auto-created when step 4 printed that line, and every comment that got no reply. + + Done when every handled comment holds either a reply link or a recorded failure reason, and the report includes the PR table columns `{owner}/{repo}`, PR number, PR URL and PR summary, names all branch and calibration details, and names the comments that got no reply. ## Rules 1. No template section may stay empty: fill the literal 「無」 when there is no plan page, analyze page, or prerequisite PR. 2. Read the `jsc-sdlc` plan page and analyze page through `jsc-gitea:wiki`, which resolves `JSC_WIKI_REPO_PLAN` for the plan page and `JSC_WIKI_REPO_ANALYZE` for the analyze page from the inherited environment, falls back to `JSC_WIKI_REPO`, and asks only when neither is set. Each page type reads its own variable only; a page type never borrows another type's variable. Take both links from the pages read this way; the analyze link must point at the work package heading anchor. -3. Branch title summarization (step 2.2) and description drafting MUST run as a sub agent. +3. Branch title summarization (step 3.2) and description drafting MUST run as a sub agent. The description sub agent starts right after step 3 and runs alongside steps 4 and 5, so steps 7 and 8 already hold a draft. 4. Branch names are ASCII only (`a-z0-9`, `/`, `-`). Traditional Chinese phrases go through `tools/slugify.sh` first; `tools/base-branch.sh --derive` rejects anything else. diff --git a/tools/base-branch.sh b/tools/base-branch.sh index 69feb91..ee90576 100755 --- a/tools/base-branch.sh +++ b/tools/base-branch.sh @@ -22,12 +22,23 @@ # 分支名只允許 ASCII(a-z0-9 與 /、-)。中文簡述先交給同目錄的 slugify.sh 轉成 ASCII slug, # 再組成分支名,這支腳本不接受非 ASCII 分支名。 # -# 輸出: 選中的分支名(一行)。 -# 護欄: 參數過多回傳 2;抓不到遠端回傳 3;呼叫方指定的分支不在遠端回傳 4; -# develop、main、master 都不在遠端回傳 5;傳入空字串回傳 6; -# 推不出唯一合法基底回傳 7;推導出的基底不在遠端又不能自動建立回傳 8; -# 自動建立功能主幹失敗回傳 9。 -# 錯誤訊息一律印繁中到 stderr。 +# 輸出: 選中的分支名(一行)。錯誤訊息一律印繁中到 stderr。 +# 結束碼: 0=stdout 印出一個基底分支名,兩種模式共用。直接拿它開 PR。 +# 2=參數過多(兩種模式共用)。分支名要用引號包成單一參數,再重跑。 +# 3=連不上 origin,git fetch 失敗(兩種模式共用)。先確認遠端可以連線,再重跑。 +# 4=(呼叫方模式)呼叫方指定的分支不在 origin 上。停下來問使用者原本要的是哪一條, +# 不要自行改用其他分支。 +# 5=(呼叫方模式)origin 上找不到 develop、main、master。這個碼只在完全沒傳參數時 +# 才會出現,所以真正的問題通常是基底參數在路上掉了。請由呼叫方指定基底分支。 +# 6=(呼叫方模式)傳進來的是空字串。回去補上分支變數的值,不要改成整個參數不傳—— +# 不傳會悄悄退回 develop。 +# 7=(--derive 模式)推不出唯一合法基底:站在斷頭狀態、站在 master、分支名含非 ASCII +# 或其他不允許的字元、類型不在階梯表內、feat 這類階梯少了功能層,或 fix 寫成多層。 +# 照 stderr 的訊息修分支名再重跑,不要退回 develop。 +# 8=(--derive 模式)推導出的基底不在 origin 上,而且它不是可以自動建立的功能主幹。 +# 先把那條分支建出來並推上 origin,再重跑。 +# 9=(--derive 模式)自動建立功能主幹失敗:origin 上沒有 develop,或推送被拒。 +# 先建好 develop,或確認推送權限,再重跑。 set -u MODE=caller diff --git a/tools/pick-type.sh b/tools/pick-type.sh new file mode 100755 index 0000000..d45cd65 --- /dev/null +++ b/tools/pick-type.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env sh +# pick-type.sh — 從一組 commit 型別中選出優先度最高的一個。 +# 用法: pick-type.sh {型別或 commit 標題}... +# git log --format=%s {範圍} | pick-type.sh +# +# 說明: 優先序固定為 revert > fix > feat > perf > refactor > test > docs > style > chore。 +# 這支腳本是該優先序的唯一真實來源,SKILL.md 不再抄一份。 +# 帶參數就讀參數,一個參數算一項;沒帶參數就讀標準輸入,一行算一項。 +# 每一項只取冒號、左括號、驚嘆號之前的字,所以 commit 標題整行餵進來也認得出型別。 +# 認不得的項目直接略過,不影響其他項目。 +# +# 輸出: 勝出的型別(一行,例如: feat)。 +# 結束碼: 0 選出型別;2 沒有收到任何輸入;3 輸入裡沒有階梯表內的型別。 +# 錯誤訊息一律印繁中到 stderr。 +set -u + +if [ "$#" -gt 0 ]; then + RAW=$(printf '%s\n' "$@") +else + RAW=$(cat) +fi + +if [ -z "$(printf '%s' "$RAW" | tr -d '[:space:]')" ]; then + echo "錯誤: 沒有收到任何型別。請把 git log 取出的型別集合當參數傳入,或從標準輸入餵進來。" >&2 + exit 2 +fi + +NORM=$(printf '%s\n' "$RAW" \ + | sed -e 's/[(:!].*$//' -e 's/[[:space:]]//g' \ + | tr '[:upper:]' '[:lower:]') + +for candidate in revert fix feat perf refactor test docs style chore; do + if printf '%s\n' "$NORM" | grep -qx "$candidate"; then + printf '%s\n' "$candidate" + exit 0 + fi +done + +echo "錯誤: 輸入裡沒有階梯表內的型別。請確認 commit 訊息符合 {類型}({範圍}): {訊息} 格式,型別限 revert、fix、feat、perf、refactor、test、docs、style、chore。" >&2 +exit 3 diff --git a/tools/slugify.sh b/tools/slugify.sh index 6906f7e..820dfdb 100755 --- a/tools/slugify.sh +++ b/tools/slugify.sh @@ -4,7 +4,12 @@ # 規則: 全部轉小寫,非 a-z0-9 的字元換成連字號, # 連續連字號合併成一個,並去除開頭與結尾的連字號。 # 輸出: {type}/{slug}(例如: slugify.sh feat "export report" → feat/export-report) -# 護欄: 輸入含非 ASCII 字元回傳 2;slug 化後為空回傳 3。兩者都印繁中錯誤到 stderr。 +# 結束碼: 0=stdout 印出 {type}/{slug},直接拿去當分支名。 +# 1=參數少於兩個。補上 type 與 phrase 再重跑。 +# 2=type 或 phrase 含非 ASCII 字元。先把描述翻成英文短語再重跑, +# 不要自己動手拼分支名。 +# 3=phrase slug 化之後是空字串(裡面一個 a-z0-9 都沒有)。換一句英文短語再重跑。 +# 錯誤訊息都印到 stderr,其中 2 與 3 印繁體中文。 set -u if [ "$#" -lt 2 ]; then