feat: git identity as Janet, refs task ID in commits, pm token for bucket moves, done sweep in dispatcher
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
parent
35fd9c055c
commit
acb9992208
2 changed files with 81 additions and 22 deletions
|
|
@ -47,6 +47,8 @@ LITELLM_BASE_URL = os.environ.get("LITELLM_BASE_URL", "https://llm.ctz.fyi")
|
|||
VIKUNJA_BASE_URL = os.environ.get("VIKUNJA_BASE_URL", "https://tasks.ctz.fyi")
|
||||
FORGEJO_BASE_URL = os.environ.get("FORGEJO_BASE_URL", "https://git.ctz.fyi")
|
||||
IN_REVIEW_BUCKET_ID = os.environ.get("IN_REVIEW_BUCKET_ID", "")
|
||||
VIKUNJA_PROJECT_ID = os.environ.get("VIKUNJA_PROJECT_ID", "")
|
||||
VIKUNJA_VIEW_ID = os.environ.get("VIKUNJA_VIEW_ID", "")
|
||||
|
||||
HOME = Path(os.environ.get("HOME", "/home/agent"))
|
||||
CONFIG_DIR = HOME / ".config" / "opencode"
|
||||
|
|
@ -145,6 +147,16 @@ def write_opencode_config(secrets: dict, role: str) -> None:
|
|||
log.warning("No agent file found at %s", agent_md_src)
|
||||
|
||||
|
||||
def configure_git_identity() -> None:
|
||||
"""Set git commit name to Janet, keeping the agent's service account email."""
|
||||
try:
|
||||
subprocess.run(["git", "config", "--global", "user.name", "Janet"], check=True)
|
||||
subprocess.run(["git", "config", "--global", "user.email", f"svc-ag-{AGENT_ROLE}@ad.ctz.fyi"], check=True)
|
||||
log.info("Git identity set: Janet <svc-ag-%s@ad.ctz.fyi>", AGENT_ROLE)
|
||||
except Exception as e:
|
||||
log.warning("Could not set git identity: %s", e)
|
||||
|
||||
|
||||
def build_prompt(task_id: str, task_title: str) -> str:
|
||||
return f"""You are the AutoJanet agent for role: {AGENT_ROLE}
|
||||
|
||||
|
|
@ -154,10 +166,12 @@ Your current task (Vikunja task #{task_id}):
|
|||
Instructions:
|
||||
1. Read the task carefully. Fetch full task details from Vikunja if needed.
|
||||
2. Complete the task using the tools available to you.
|
||||
3. If you wrote code, open a PR on Forgejo — do not merge it yourself.
|
||||
4. Leave a comment on the Vikunja task summarising what you did and linking any PR.
|
||||
5. Do not mark the task as done — the entrypoint will move it to In Review when you finish.
|
||||
6. Do not ask for confirmation — act autonomously within your role constraints.
|
||||
3. If you wrote code, commit with a message that includes "refs #{task_id}" so the task
|
||||
is linked in Forgejo. Example: "feat: add login endpoint refs #{task_id}"
|
||||
4. Open a PR on Forgejo — do not merge it yourself.
|
||||
5. Leave a comment on the Vikunja task summarising what you did and linking any PR.
|
||||
6. Do not mark the task as done — the entrypoint will move it to In Review when you finish.
|
||||
7. Do not ask for confirmation — act autonomously within your role constraints.
|
||||
"""
|
||||
|
||||
|
||||
|
|
@ -169,22 +183,22 @@ def run_opencode(prompt: str) -> int:
|
|||
return result.returncode
|
||||
|
||||
|
||||
def move_task_to_in_review(vikunja_token: str) -> None:
|
||||
"""Move the task bucket to In Review after work is complete."""
|
||||
if not IN_REVIEW_BUCKET_ID:
|
||||
log.warning("IN_REVIEW_BUCKET_ID not set, skipping bucket move")
|
||||
def move_task_to_bucket(pm_token: str, bucket_id: str) -> None:
|
||||
"""Move the task to a bucket using the kanban view endpoint (requires pm token)."""
|
||||
if not all([bucket_id, VIKUNJA_PROJECT_ID, VIKUNJA_VIEW_ID]):
|
||||
log.warning("Missing bucket/project/view IDs, skipping bucket move")
|
||||
return
|
||||
try:
|
||||
resp = httpx.post(
|
||||
f"{VIKUNJA_BASE_URL}/api/v1/tasks/{TASK_ID}",
|
||||
headers={"Authorization": f"Bearer {vikunja_token}", "Content-Type": "application/json"},
|
||||
json={"bucket_id": int(IN_REVIEW_BUCKET_ID)},
|
||||
f"{VIKUNJA_BASE_URL}/api/v1/projects/{VIKUNJA_PROJECT_ID}/views/{VIKUNJA_VIEW_ID}/buckets/{bucket_id}/tasks",
|
||||
headers={"Authorization": f"Bearer {pm_token}", "Content-Type": "application/json"},
|
||||
json={"task_id": int(TASK_ID)},
|
||||
timeout=10,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
log.info("Moved task %s → In Review (bucket %s)", TASK_ID, IN_REVIEW_BUCKET_ID)
|
||||
log.info("Moved task %s → bucket %s", TASK_ID, bucket_id)
|
||||
except Exception as e:
|
||||
log.warning("Failed to move task to In Review: %s", e)
|
||||
log.warning("Failed to move task to bucket %s: %s", bucket_id, e)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
|
|
@ -195,21 +209,22 @@ def main() -> None:
|
|||
|
||||
secrets = fetch_role_secrets(bao_token, AGENT_ROLE)
|
||||
write_opencode_config(secrets, AGENT_ROLE)
|
||||
configure_git_identity()
|
||||
|
||||
# Fetch vikunja token for bucket moves
|
||||
vikunja_token = ""
|
||||
# Fetch pm token for bucket moves (admin operation)
|
||||
pm_token = ""
|
||||
try:
|
||||
vikunja_token = get_secret(bao_token, f"autojanet/{AGENT_ROLE}/vikunja-token", "token")
|
||||
log.info("Fetched vikunja-token")
|
||||
pm_token = get_secret(bao_token, "autojanet/pm/vikunja-token", "token")
|
||||
log.info("Fetched pm vikunja-token")
|
||||
except Exception as e:
|
||||
log.warning("Could not fetch vikunja-token: %s", e)
|
||||
log.warning("Could not fetch pm vikunja-token: %s", e)
|
||||
|
||||
prompt = build_prompt(TASK_ID, TASK_TITLE)
|
||||
rc = run_opencode(prompt)
|
||||
|
||||
# Move to In Review regardless of exit code — work was attempted
|
||||
if vikunja_token:
|
||||
move_task_to_in_review(vikunja_token)
|
||||
if pm_token:
|
||||
move_task_to_bucket(pm_token, IN_REVIEW_BUCKET_ID)
|
||||
|
||||
if rc != 0:
|
||||
log.error("opencode exited with code %d", rc)
|
||||
|
|
|
|||
|
|
@ -187,6 +187,42 @@ def unclaim_task(vikunja_token: str, task_id: int, todo_id: int) -> None:
|
|||
log.warning("Failed to unclaim task %d: %s", task_id, e)
|
||||
|
||||
|
||||
def sweep_done_tasks(
|
||||
vikunja_token: str,
|
||||
project_id: int,
|
||||
view_id: int,
|
||||
done_bucket_id: int,
|
||||
) -> None:
|
||||
"""Find tasks marked done=True that aren't in the Done bucket and move them."""
|
||||
tasks = []
|
||||
page = 1
|
||||
while True:
|
||||
batch = vikunja_get(vikunja_token, f"projects/{project_id}/tasks", page=page, per_page=50)
|
||||
if not batch:
|
||||
break
|
||||
tasks.extend(batch)
|
||||
if len(batch) < 50:
|
||||
break
|
||||
page += 1
|
||||
|
||||
moved = 0
|
||||
for task in tasks:
|
||||
if task.get("done"):
|
||||
task_id = task["id"]
|
||||
try:
|
||||
vikunja_post(
|
||||
vikunja_token,
|
||||
f"projects/{project_id}/views/{view_id}/buckets/{done_bucket_id}/tasks",
|
||||
{"task_id": task_id},
|
||||
)
|
||||
log.info("Swept done task %d → Done bucket", task_id)
|
||||
moved += 1
|
||||
except Exception as e:
|
||||
log.warning("Failed to sweep task %d to Done: %s", task_id, e)
|
||||
|
||||
log.info("Done sweep: moved %d tasks", moved)
|
||||
|
||||
|
||||
# ── Kubernetes ────────────────────────────────────────────────────────────────
|
||||
|
||||
def load_k8s_config() -> None:
|
||||
|
|
@ -217,6 +253,8 @@ def spawn_agent_job(
|
|||
task_id: int,
|
||||
task_title: str,
|
||||
in_review_bucket_id: int,
|
||||
project_id: int,
|
||||
view_id: int,
|
||||
) -> None:
|
||||
name = job_name(role, task_id)
|
||||
if job_already_exists(batch_v1, name):
|
||||
|
|
@ -264,6 +302,8 @@ def spawn_agent_job(
|
|||
k8s_client.V1EnvVar(name="LITELLM_BASE_URL", value="https://llm.ctz.fyi"),
|
||||
k8s_client.V1EnvVar(name="FORGEJO_BASE_URL", value="https://git.ctz.fyi"),
|
||||
k8s_client.V1EnvVar(name="IN_REVIEW_BUCKET_ID", value=str(in_review_bucket_id)),
|
||||
k8s_client.V1EnvVar(name="VIKUNJA_PROJECT_ID", value=str(project_id)),
|
||||
k8s_client.V1EnvVar(name="VIKUNJA_VIEW_ID", value=str(view_id)),
|
||||
k8s_client.V1EnvVar(
|
||||
name="OPENBAO_ROLE_ID",
|
||||
value_from=k8s_client.V1EnvVarSource(
|
||||
|
|
@ -323,8 +363,9 @@ def main() -> None:
|
|||
todo_id = buckets.get(BUCKET_TODO)
|
||||
in_progress_id = buckets.get(BUCKET_IN_PROGRESS)
|
||||
in_review_id = buckets.get(BUCKET_IN_REVIEW)
|
||||
done_id = buckets.get(BUCKET_DONE)
|
||||
|
||||
if not all([todo_id, in_progress_id, in_review_id]):
|
||||
if not all([todo_id, in_progress_id, in_review_id, done_id]):
|
||||
log.error("Could not find all standard buckets. Found: %s", list(buckets.keys()))
|
||||
sys.exit(1)
|
||||
|
||||
|
|
@ -332,6 +373,9 @@ def main() -> None:
|
|||
load_k8s_config()
|
||||
batch_v1 = k8s_client.BatchV1Api()
|
||||
|
||||
# Sweep: move any done=True tasks into the Done bucket
|
||||
sweep_done_tasks(vikunja_token, project_id, view_id, done_id)
|
||||
|
||||
# Scan Todo bucket for claimable tasks
|
||||
tasks = list_todo_tasks(vikunja_token, project_id, todo_id)
|
||||
log.info("Found %d candidate tasks in Todo bucket", len(tasks))
|
||||
|
|
@ -351,7 +395,7 @@ def main() -> None:
|
|||
continue
|
||||
|
||||
try:
|
||||
spawn_agent_job(batch_v1, role, task_id, title, in_review_id)
|
||||
spawn_agent_job(batch_v1, role, task_id, title, in_review_id, project_id, view_id)
|
||||
claimed += 1
|
||||
except Exception as e:
|
||||
log.error("Failed to spawn job for task %d: %s", task_id, e)
|
||||
|
|
|
|||
Loading…
Reference in a new issue