Merge pull request 'improve: solve-issue — replace tea pr create with Gitea API' (#581) from retrospect/fix-committer-tea into main

This commit is contained in:
2026-05-30 23:37:31 +00:00
3 changed files with 111 additions and 49 deletions
+49 -18
View File
@@ -23,6 +23,12 @@ roles:
1. Store it via `uwf cas put-text "<markdown content>"` and capture the returned hash 1. Store it via `uwf cas put-text "<markdown content>"` and capture the returned hash
2. Put the hash in frontmatter.plan (required when $status=ready) 2. Put the hash in frontmatter.plan (required when $status=ready)
3. Set repoPath to the absolute path of the repository root 3. Set repoPath to the absolute path of the repository root
IMPORTANT: Extract the repo remote (owner/repo) from git:
```bash
git remote get-url origin | sed 's|.*[:/]\([^/]*/[^.]*\).*|\1|'
```
Store the result as repoRemote in your frontmatter output so downstream roles can use it for tea/API calls.
output: "Output a brief summary of the test spec. Set $status to ready (with plan hash and repoPath) or insufficient_info." output: "Output a brief summary of the test spec. Set $status to ready (with plan hash and repoPath) or insufficient_info."
frontmatter: frontmatter:
oneOf: oneOf:
@@ -30,7 +36,8 @@ roles:
$status: { const: "ready" } $status: { const: "ready" }
plan: { type: string } plan: { type: string }
repoPath: { type: string } repoPath: { type: string }
required: [$status, plan, repoPath] repoRemote: { type: string }
required: [$status, plan, repoPath, repoRemote]
- properties: - properties:
$status: { const: "insufficient_info" } $status: { const: "insufficient_info" }
required: [$status] required: [$status]
@@ -82,6 +89,7 @@ roles:
$status: { const: "done" } $status: { const: "done" }
branch: { type: string } branch: { type: string }
worktree: { type: string } worktree: { type: string }
repoRemote: { type: string }
required: [$status, branch, worktree] required: [$status, branch, worktree]
- properties: - properties:
$status: { const: "failed" } $status: { const: "failed" }
@@ -125,11 +133,13 @@ roles:
$status: { const: "approved" } $status: { const: "approved" }
branch: { type: string } branch: { type: string }
worktree: { type: string } worktree: { type: string }
repoRemote: { type: string }
required: [$status, branch, worktree] required: [$status, branch, worktree]
- properties: - properties:
$status: { const: "rejected" } $status: { const: "rejected" }
comments: { type: string } comments: { type: string }
worktree: { type: string } worktree: { type: string }
repoRemote: { type: string }
required: [$status, comments, worktree] required: [$status, comments, worktree]
tester: tester:
description: "Functional correctness verification" description: "Functional correctness verification"
@@ -153,33 +163,48 @@ roles:
$status: { const: "passed" } $status: { const: "passed" }
branch: { type: string } branch: { type: string }
worktree: { type: string } worktree: { type: string }
repoRemote: { type: string }
required: [$status, branch, worktree] required: [$status, branch, worktree]
- properties: - properties:
$status: { const: "fix_code" } $status: { const: "fix_code" }
report: { type: string } report: { type: string }
repoRemote: { type: string }
worktree: { type: string }
branch: { type: string }
required: [$status, report] required: [$status, report]
- properties: - properties:
$status: { const: "fix_spec" } $status: { const: "fix_spec" }
report: { type: string } report: { type: string }
repoRemote: { type: string }
worktree: { type: string }
branch: { type: string }
required: [$status, report] required: [$status, report]
committer: committer:
description: "Commits and creates PR" description: "Commits and creates PR"
goal: "You are a committer agent. You create a clean commit and push a PR linking the original issue." goal: "You are a committer agent. You create a clean commit and push a PR linking the original issue."
capabilities: [] capabilities: []
procedure: | procedure: |
The worktree path, branch name, and repo info are provided in your task prompt. The worktree path, branch name, and repo remote (owner/repo) are provided in your task prompt.
cd into the worktree first. cd into the worktree first.
Note: You inherit the developer's worktree and branch. Do NOT create a new branch. Note: You inherit the developer's worktree and branch. Do NOT create a new branch.
1. Stage all changes: `git add -A` 1. Check `git status` — if working tree is clean and branch is ahead of origin, skip to step 3 (push).
2. Commit with a descriptive message referencing the issue: `git commit -m "type: description\n\nFixes #N"` 2. If there are unstaged/uncommitted changes: `git add -A` then `git commit -m "type: description\n\nFixes #N"`
3. Push the branch: `git push -u origin <branch-name>` 3. Push the branch: `git push -u origin <branch-name>`
- If push hook fails: capture the error log in your output, mark hook_failed 4. **Verify push succeeded** — run `git ls-remote origin <branch-name>` and confirm it prints a commit hash.
4. On push success: create a PR via `tea pr create --repo <owner/repo> --title "..." --description "..."` - If no output or push failed: capture the error, mark hook_failed
- Extract owner/repo from: `git remote get-url origin | sed 's/.*[:/]\([^/]*\/[^.]*\).*/\1/'` 5. Create a PR using the Gitea API (do NOT use `tea pr create` — it fails in worktrees):
- PR description must include: What / Why / Changes / Ref sections, with `Fixes #N` in Ref ```bash
- On tea failure: capture stderr/stdout, include PR details for manual creation, mark hook_failed GITEA_TOKEN=$(cfg get GITEA_TOKEN)
5. After PR creation, clean up the worktree: curl -s -X POST -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" \
"https://git.shazhou.work/api/v1/repos/<owner>/<repo>/pulls" \
-d '{"title":"...","body":"...","head":"<branch>","base":"main"}'
```
- The repo remote (owner/repo format, e.g. "uncaged/workflow") is given in your task prompt — use it directly.
- PR body must include: What / Why / Changes / Ref sections, with `Fixes #N` in Ref
6. **Verify PR was created** — parse the curl response JSON: it must contain a `"number"` field. Print the PR URL.
- If curl returns an error or no number field: capture the response, mark hook_failed
7. After PR creation, clean up the worktree:
- cd to the repo root (parent of .worktrees) - cd to the repo root (parent of .worktrees)
- `git worktree remove <worktree-path>` - `git worktree remove <worktree-path>`
output: "Include PR URL on success or error log on failure. Set $status to committed (with prUrl) or hook_failed (with error)." output: "Include PR URL on success or error log on failure. Set $status to committed (with prUrl) or hook_failed (with error)."
@@ -188,27 +213,33 @@ roles:
- properties: - properties:
$status: { const: "committed" } $status: { const: "committed" }
prUrl: { type: string } prUrl: { type: string }
repoRemote: { type: string }
worktree: { type: string }
branch: { type: string }
required: [$status, prUrl] required: [$status, prUrl]
- properties: - properties:
$status: { const: "hook_failed" } $status: { const: "hook_failed" }
error: { type: string } error: { type: string }
repoRemote: { type: string }
worktree: { type: string }
branch: { type: string }
required: [$status, error] required: [$status, error]
graph: graph:
$START: $START:
_: { role: "planner", prompt: "Analyze the issue and produce an implementation plan." } _: { role: "planner", prompt: "Analyze the issue and produce an implementation plan." }
planner: planner:
insufficient_info: { role: "$END", prompt: "Insufficient information to proceed; end the workflow." } insufficient_info: { role: "$END", prompt: "Insufficient information to proceed; end the workflow." }
ready: { role: "developer", prompt: "Implement the TDD test spec (CAS hash: {{{plan}}}) in repo {{{repoPath}}}." } ready: { role: "developer", prompt: "Implement the TDD test spec (CAS hash: {{{plan}}}) in repo {{{repoPath}}}. Repo remote: {{{repoRemote}}}." }
developer: developer:
done: { role: "reviewer", prompt: "Review branch {{{branch}}} at {{{worktree}}} for code standards compliance." } done: { role: "reviewer", prompt: "Review branch {{{branch}}} at {{{worktree}}} for code standards compliance. Repo remote: {{{repoRemote}}}." }
failed: { role: "$END", prompt: "Developer failed: {{{reason}}}. Ending workflow." } failed: { role: "$END", prompt: "Developer failed: {{{reason}}}. Ending workflow." }
reviewer: reviewer:
rejected: { role: "developer", prompt: "Reviewer rejected: {{{comments}}}. Fix the issues in repo {{{worktree}}}." } rejected: { role: "developer", prompt: "Reviewer rejected: {{{comments}}}. Fix the issues in repo {{{worktree}}}. Repo remote: {{{repoRemote}}}." }
approved: { role: "tester", prompt: "Review passed. Run tests on branch {{{branch}}} at {{{worktree}}}." } approved: { role: "tester", prompt: "Review passed. Run tests on branch {{{branch}}} at {{{worktree}}}. Repo remote: {{{repoRemote}}}." }
tester: tester:
fix_code: { role: "developer", prompt: "Tests found code issues: {{{report}}}. Fix and re-submit." } fix_code: { role: "developer", prompt: "Tests found code issues: {{{report}}}. Fix and re-submit. Worktree: {{{worktree}}}. Repo remote: {{{repoRemote}}}." }
fix_spec: { role: "planner", prompt: "Tests found spec issues: {{{report}}}. Revise the test spec." } fix_spec: { role: "planner", prompt: "Tests found spec issues: {{{report}}}. Revise the test spec. Repo remote: {{{repoRemote}}}." }
passed: { role: "committer", prompt: "All tests passed. Commit and push branch {{{branch}}} from {{{worktree}}}." } passed: { role: "committer", prompt: "All tests passed. Commit and push branch {{{branch}}} from {{{worktree}}}. Repo remote (owner/repo): {{{repoRemote}}}." }
committer: committer:
hook_failed: { role: "developer", prompt: "Push hook failed: {{{error}}}. Fix and re-submit." } hook_failed: { role: "developer", prompt: "Push hook failed: {{{error}}}. Fix and re-submit. Worktree: {{{worktree}}}. Repo remote: {{{repoRemote}}}." }
committed: { role: "$END", prompt: "PR created: {{{prUrl}}}. Workflow complete." } committed: { role: "$END", prompt: "PR created: {{{prUrl}}}. Workflow complete." }
+45 -9
View File
@@ -8,22 +8,46 @@ roles:
- issue-analysis - issue-analysis
- planning - planning
procedure: | procedure: |
On first run (no previous steps): CRITICAL: First, determine which mode you are in by scanning the task prompt.
Choose EXACTLY ONE mode — do NOT default to Mode A if Mode B applies.
**How to choose:**
- If the prompt contains ANY of these keywords: "PR #", "PR#", "pulls/", "继续修复", "continue", "review feedback", "existing branch", "fix/", or mentions a branch name → **Mode B**
- If the prompt was forwarded from tester with fix_spec → **Mode C**
- Otherwise → **Mode A**
**Mode A — Fresh issue (first time, no existing PR):**
1. Read the issue and all comments from Gitea using `tea issues <number> -r <owner/repo>` 1. Read the issue and all comments from Gitea using `tea issues <number> -r <owner/repo>`
2. Look for project conventions files (CLAUDE.md, CONTRIBUTING.md, .cursor/rules/) in the repo 2. Look for project conventions files (CLAUDE.md, CONTRIBUTING.md, .cursor/rules/) in the repo
3. Assess whether the issue has enough information to produce a test spec 3. Assess whether the issue has enough information to produce a test spec
4. If insufficient info: comment on the issue via `echo "..." | tea comment <number> -r <owner/repo>` (skip if you already commented), then output $status=insufficient_info 4. If insufficient info: comment on the issue via `echo "..." | tea comment <number> -r <owner/repo>` (skip if you already commented), then output $status=insufficient_info
5. If sufficient: produce a detailed TDD test spec in markdown covering all scenarios 5. If sufficient: produce a detailed TDD test spec in markdown covering all scenarios
6. Store it via `uwf cas put-text "<markdown content>"` and capture the returned hash
7. Output **$status=ready** with plan hash and repoPath
On subsequent runs (bounced back by tester with fix_spec): **Mode B — Continue on existing PR (prompt mentions PR, branch, or review feedback):**
YOU MUST output $status=continue (NOT ready) when in this mode.
1. Extract the PR number and branch name from the prompt
2. Read the PR and its review comments from Gitea: `tea pr <number> --comments -r <owner/repo>`
3. Read the existing issue for full context: `tea issues <number> -r <owner/repo>`
4. Look for project conventions files (CLAUDE.md, CONTRIBUTING.md, .cursor/rules/) in the repo
5. Produce a TDD test spec that ONLY covers the changes requested in the review — do NOT re-spec already-implemented features
6. Store it via `uwf cas put-text "<markdown content>"` and capture the returned hash
7. Find the existing worktree: `git worktree list` and locate the branch
8. Output **$status=continue** with plan hash, repoPath, branch name, and worktree path
**Mode C — Bounced back by tester (fix_spec):**
1. Read the tester's output from the previous step to understand what's wrong with the spec 1. Read the tester's output from the previous step to understand what's wrong with the spec
2. Revise the test spec accordingly 2. Revise the test spec accordingly
3. Store it via `uwf cas put-text "<markdown content>"` and capture the returned hash
4. Output **$status=ready** with plan hash and repoPath
After producing the test spec: IMPORTANT: Extract the repo remote (owner/repo) from git:
1. Store it via `uwf cas put-text "<markdown content>"` and capture the returned hash ```bash
2. Put the hash in frontmatter.plan (required when $status=ready) git remote get-url origin | sed 's|.*[:/]\([^/]*/[^.]*\).*|\1|'
3. Set repoPath to the absolute path of the repository root ```
output: "Output a brief summary of the test spec. Set $status to ready (with plan hash and repoPath) or insufficient_info." Store the result as repoRemote in your frontmatter output so downstream roles can use it.
output: "Output a brief summary of the test spec. Set $status to ready (fresh), continue (existing PR), or insufficient_info."
frontmatter: frontmatter:
oneOf: oneOf:
- properties: - properties:
@@ -31,6 +55,13 @@ roles:
plan: { type: string } plan: { type: string }
repoPath: { type: string } repoPath: { type: string }
required: [$status, plan, repoPath] required: [$status, plan, repoPath]
- properties:
$status: { const: "continue" }
plan: { type: string }
repoPath: { type: string }
branch: { type: string }
worktree: { type: string }
required: [$status, plan, repoPath, branch, worktree]
- properties: - properties:
$status: { const: "insufficient_info" } $status: { const: "insufficient_info" }
required: [$status] required: [$status]
@@ -49,10 +80,14 @@ roles:
3. First time (no existing branch): 3. First time (no existing branch):
- `git worktree add .worktrees/fix/<issue-number>-<short-slug> -b fix/<issue-number>-<short-slug> origin/main` - `git worktree add .worktrees/fix/<issue-number>-<short-slug> -b fix/<issue-number>-<short-slug> origin/main`
- `cd .worktrees/fix/<issue-number>-<short-slug> && bun install` - `cd .worktrees/fix/<issue-number>-<short-slug> && bun install`
4. If bounced back from reviewer or tester (branch already exists): 4. If continuing on existing branch (prompt says "Continue work on existing branch" or provides a worktree path):
- cd directly into the worktree path provided in the prompt
- `git fetch origin && git rebase origin/main`
- Do NOT create a new branch or worktree
5. If bounced back from reviewer or tester (branch already exists but no explicit worktree path):
- cd into the existing worktree under `.worktrees/fix/<issue-number>-<short-slug>` - cd into the existing worktree under `.worktrees/fix/<issue-number>-<short-slug>`
- `git fetch origin && git rebase origin/main` - `git fetch origin && git rebase origin/main`
5. ALL subsequent work must happen inside the worktree directory. 6. ALL subsequent work must happen inside the worktree directory.
Then implement TDD: Then implement TDD:
6. Read the test spec from CAS: `uwf cas get <plan hash>` (find the hash from the planner's output in your task prompt) 6. Read the test spec from CAS: `uwf cas get <plan hash>` (find the hash from the planner's output in your task prompt)
@@ -183,6 +218,7 @@ graph:
planner: planner:
insufficient_info: { role: "$END", prompt: "Insufficient information to proceed; end the workflow." } insufficient_info: { role: "$END", prompt: "Insufficient information to proceed; end the workflow." }
ready: { role: "developer", prompt: "Implement the TDD test spec (CAS hash: {{{plan}}}) in repo {{{repoPath}}}." } ready: { role: "developer", prompt: "Implement the TDD test spec (CAS hash: {{{plan}}}) in repo {{{repoPath}}}." }
continue: { role: "developer", prompt: "Continue work on existing branch {{{branch}}} at worktree {{{worktree}}}. Implement the revised TDD test spec (CAS hash: {{{plan}}}) in repo {{{repoPath}}}. Do NOT create a new branch or worktree — cd into the existing worktree and work there." }
developer: developer:
done: { role: "reviewer", prompt: "Review branch {{{branch}}} at {{{worktree}}} for code standards compliance." } done: { role: "reviewer", prompt: "Review branch {{{branch}}} at {{{worktree}}} for code standards compliance." }
failed: { role: "$END", prompt: "Developer failed: {{{reason}}}. Ending workflow." } failed: { role: "$END", prompt: "Developer failed: {{{reason}}}. Ending workflow." }
@@ -8,11 +8,11 @@ import { parse } from "yaml";
* Test: Issue #474 - tea pr create fails in git worktree directories * Test: Issue #474 - tea pr create fails in git worktree directories
* *
* This test verifies that the solve-issue workflow's committer role * This test verifies that the solve-issue workflow's committer role
* includes the --repo flag when running tea pr create, which fixes * uses direct Gitea API calls via curl instead of tea pr create,
* the "path segment [0] is empty" error in worktree directories. * which fixes the "path segment [0] is empty" error in worktree directories.
*/ */
describe("solve-issue workflow: tea pr create worktree fix", () => { describe("solve-issue workflow: Gitea API PR creation", () => {
// Navigate up from packages/cli-workflow/src/__tests__ to repo root // Navigate up from packages/cli-workflow/src/__tests__ to repo root
const workflowPath = join( const workflowPath = join(
import.meta.dirname, import.meta.dirname,
@@ -24,7 +24,7 @@ describe("solve-issue workflow: tea pr create worktree fix", () => {
"solve-issue.yaml", "solve-issue.yaml",
); );
test("committer procedure should include --repo flag in tea pr create command", async () => { test("committer procedure should use curl API instead of tea pr create", async () => {
const yamlContent = await readFile(workflowPath, "utf-8"); const yamlContent = await readFile(workflowPath, "utf-8");
const workflow = parse(yamlContent) as WorkflowPayload; const workflow = parse(yamlContent) as WorkflowPayload;
@@ -32,43 +32,38 @@ describe("solve-issue workflow: tea pr create worktree fix", () => {
const committerProcedure = workflow.roles.committer?.procedure; const committerProcedure = workflow.roles.committer?.procedure;
expect(committerProcedure).toBeDefined(); expect(committerProcedure).toBeDefined();
// Verify the procedure includes tea pr create with --repo flag // Verify the procedure uses curl API, not tea pr create
expect(committerProcedure).toContain("tea pr create"); expect(committerProcedure).toContain("curl");
expect(committerProcedure).toContain("--repo"); expect(committerProcedure).toContain("api/v1/repos");
expect(committerProcedure).toContain("/pulls");
// Verify the --repo flag appears before or together with tea pr create // Verify it explicitly warns against tea pr create
// This ensures the command is: tea pr create --repo <owner/repo> ... expect(committerProcedure).toMatch(/do NOT use.*tea pr create/i);
const teaPrCreateMatch = committerProcedure?.match(/tea pr create[^\n]*/);
expect(teaPrCreateMatch).not.toBeNull();
if (teaPrCreateMatch) {
const teaCommandLine = teaPrCreateMatch[0];
expect(teaCommandLine).toContain("--repo");
}
}); });
test("committer procedure should mention repo extraction from git remote", async () => { test("committer procedure should reference repoRemote from task prompt", async () => {
const yamlContent = await readFile(workflowPath, "utf-8"); const yamlContent = await readFile(workflowPath, "utf-8");
const workflow = parse(yamlContent) as WorkflowPayload; const workflow = parse(yamlContent) as WorkflowPayload;
const committerProcedure = workflow.roles.committer?.procedure; const committerProcedure = workflow.roles.committer?.procedure;
expect(committerProcedure).toBeDefined(); expect(committerProcedure).toBeDefined();
// Verify the procedure mentions extracting repo info from git remote // Verify the procedure mentions repoRemote is provided in task prompt
// This ensures fallback logic is documented expect(committerProcedure).toMatch(/repo remote.*provided.*task prompt/i);
expect(committerProcedure).toMatch(/git remote/i); expect(committerProcedure).toMatch(/owner\/repo/i);
}); });
test("committer procedure should include error handling for tea failures", async () => { test("committer procedure should include error handling for curl failures", async () => {
const yamlContent = await readFile(workflowPath, "utf-8"); const yamlContent = await readFile(workflowPath, "utf-8");
const workflow = parse(yamlContent) as WorkflowPayload; const workflow = parse(yamlContent) as WorkflowPayload;
const committerProcedure = workflow.roles.committer?.procedure; const committerProcedure = workflow.roles.committer?.procedure;
expect(committerProcedure).toBeDefined(); expect(committerProcedure).toBeDefined();
// Verify the procedure includes error handling guidance // Verify the procedure includes error handling guidance for curl
// This ensures we capture failures and provide actionable output // This ensures we capture failures and provide actionable output
expect(committerProcedure).toMatch(/error|fail/i); expect(committerProcedure).toMatch(/error|fail/i);
expect(committerProcedure).toContain("hook_failed");
}); });
test("workflow should be parseable as valid WorkflowPayload", async () => { test("workflow should be parseable as valid WorkflowPayload", async () => {