Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .agents/skills/plan-go/scripts/loop_spec.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ $goal
- Confirm the plan is Aligned and execution is allowed.
- Treat that registered aligned plan as the goal source and implementation facts as owned by the target repository.
- If the gate fails, route to task-plan or stop for alignment before substantive work.
- Record goal tooling as an object with a non-empty runtime, `used=true`, and `mechanism=task-board-plan` when the registered task/plan is the goal.
- Use `mechanism=native-goal+task-board-plan` only when a native runtime goal mirrors that registered task/plan.
- Keep `task_id` as the auditable registered goal identifier; the runtime name does not imply native goal usage.
- Record goal tooling as an object with a non-empty runtime, 'used=true', and 'mechanism=task-board-plan' when the registered task/plan is the goal.
- Use 'mechanism=native-goal+task-board-plan' only when a native runtime goal mirrors that registered task/plan.
- Keep 'task_id' as the auditable registered goal identifier; the runtime name does not imply native goal usage.
- Reject chat-only, verbal, or arbitrary goal-mechanism claims.

## Acceptance Checks
Expand All @@ -42,7 +42,7 @@ $goal
## Role Independence Policy
- Main orchestrates and integrates: confirm the goal, hand off prompts, integrate accepted output, run mechanical checks, record evidence, and choose routes.
- Executor produces the smallest sufficient change and reports artifacts, commands/results, validation entry points, and uncertainties.
- Executor artifacts must identify either a safe existing repository-relative `local_path` or an HTTP(S) `external_url`; bare strings are not evidence.
- Executor artifacts must identify either a safe existing repository-relative 'local_path' or an HTTP(S) 'external_url'; bare strings are not evidence.
- Evaluator pressure-tests the result against the original goal and reports categorized findings with evidence.
- Main must not claim executor or evaluator output as its own.
- If independent agent tools are available, use separate executor and evaluator agents. Main-thread implementation plus self-review is not a complete adversarial loop.
Expand Down
7 changes: 7 additions & 0 deletions .agents/skills/plan-go/tests/test_plan_go.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,9 +209,16 @@ def test_adapted_source_retains_mit_notice(self):
def test_loop_spec_is_repository_relative_and_names_roles(self):
result = run(SPEC, "Execute the plan")
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual("", result.stderr)
self.assertIn("Execute the plan", result.stdout)
self.assertIn("Executor Prompt", result.stdout)
self.assertIn("Evaluator Prompt", result.stdout)
self.assertIn("'used=true'", result.stdout)
self.assertIn("'mechanism=task-board-plan'", result.stdout)
self.assertIn("'mechanism=native-goal+task-board-plan'", result.stdout)
self.assertIn("'task_id'", result.stdout)
self.assertIn("'local_path'", result.stdout)
self.assertIn("'external_url'", result.stdout)
self.assertIn("out-of-scope findings as follow-up work", result.stdout.lower())
self.assertIn("acceptance conditions are satisfied", result.stdout.lower())
self.assertNotIn("~/.codex", result.stdout)
Expand Down
2 changes: 2 additions & 0 deletions .agents/skills/task-board/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@ scripts/task validate
scripts/task list [--status STATUS] [--format table|yaml|json] [--tree]
scripts/task show TASK_ID [--format yaml|json]
scripts/task add-task --id ID --title TITLE --domain DOMAIN [options]
scripts/task set-title TASK_ID TITLE
scripts/task register-plan PLAN --id ID --domain DOMAIN --repo REPO [options]
scripts/task set-status TASK_ID STATUS
scripts/task set-plan-metadata TASK_ID PLAN [routing options]
scripts/task add-note TASK_ID NOTE
scripts/task remove-note TASK_ID NOTE
scripts/task add-evidence TASK_ID --type TYPE --status STATUS --summary TEXT [--path PATH]
scripts/task link-sub-task PARENT_TASK_ID CHILD_TASK_ID
scripts/task unlink-sub-task PARENT_TASK_ID CHILD_TASK_ID
Expand Down
57 changes: 57 additions & 0 deletions .agents/skills/task-board/scripts/task
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,29 @@ def cmd_set_status(args: argparse.Namespace) -> int:
return 0


def cmd_set_title(args: argparse.Namespace) -> int:
board, yaml, tasks, _statuses = checked_board(args.board)
task = find_task(tasks, args.task_id)
title = args.title.strip()
if not title:
raise TaskError("title must be non-empty")
old_title = task.get("title")
if old_title == title:
print(f"unchanged: {task['id']} title already matches")
return 0
now = utc_now()
task["title"] = title
task["updated_at"] = now
board["updated_at"] = now
_tasks, _statuses, errors = validate_board(board)
if errors:
task["title"] = old_title
raise TaskError("title change would make board invalid:\n" + "\n".join(f"- {error}" for error in errors))
write_board(args.board, board, yaml)
print(f"updated: {task['id']} title")
return 0


def cmd_set_plan_metadata(args: argparse.Namespace) -> int:
board, yaml, tasks, statuses = checked_board(args.board)
task = find_task(tasks, args.task_id)
Expand Down Expand Up @@ -874,6 +897,28 @@ def cmd_add_note(args: argparse.Namespace) -> int:
return 0


def cmd_remove_note(args: argparse.Namespace) -> int:
board, yaml, tasks, _statuses = checked_board(args.board)
task = find_task(tasks, args.task_id)
notes = task.setdefault("notes", [])
if args.note not in notes:
print(f"unchanged: {task['id']} does not have this note")
return 0
notes.remove(args.note)
now = utc_now()
task["updated_at"] = now
board["updated_at"] = now
_tasks, _statuses, errors = validate_board(board)
if errors:
raise TaskError(
"note removal would make board invalid:\n"
+ "\n".join(f"- {error}" for error in errors)
)
write_board(args.board, board, yaml)
print(f"updated: {task['id']} notes -= 1")
return 0


def cmd_link_sub_task(args: argparse.Namespace) -> int:
board, yaml, tasks, _statuses = checked_board(args.board)
parent = find_task(tasks, args.parent_task_id)
Expand Down Expand Up @@ -1069,6 +1114,12 @@ def build_parser() -> argparse.ArgumentParser:
set_status.add_argument("status")
set_status.set_defaults(func=cmd_set_status)

set_title = subparsers.add_parser("set-title", help="Rename a task title.")
add_board_arg(set_title)
set_title.add_argument("task_id")
set_title.add_argument("title")
set_title.set_defaults(func=cmd_set_title)

set_plan_metadata = subparsers.add_parser(
"set-plan-metadata",
help="Refresh an existing plan link/status and atomically update its execution routing.",
Expand Down Expand Up @@ -1149,6 +1200,12 @@ def build_parser() -> argparse.ArgumentParser:
add_note.add_argument("note")
add_note.set_defaults(func=cmd_add_note)

remove_note = subparsers.add_parser("remove-note", help="Remove an exact note from a task.")
add_board_arg(remove_note)
remove_note.add_argument("task_id")
remove_note.add_argument("note")
remove_note.set_defaults(func=cmd_remove_note)

link_sub_task = subparsers.add_parser(
"link-sub-task", help="Link an existing task as a child of another task."
)
Expand Down
47 changes: 46 additions & 1 deletion .agents/skills/task-board/tests/test_task_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,19 @@ def test_add_note_evidence_and_status_transitions_are_idempotent(self) -> None:
"add-note", "example.release", "Synthetic review note."
)
self.assertEqual(result, 0, stderr)
self.assertEqual(self.task("example.release")["notes"], ["Synthetic review note."])
self.assertEqual(self.task("example.release")["notes"], ["Synthetic review note."])

for _ in range(2):
result, _stdout, stderr = self.run_cli(
"remove-note", "example.release", "Synthetic review note."
)
self.assertEqual(result, 0, stderr)
self.assertEqual(self.task("example.release")["notes"], [])

result, _stdout, stderr = self.run_cli(
"add-note", "example.release", "Synthetic review note."
)
self.assertEqual(result, 0, stderr)

evidence_args = (
"add-evidence",
Expand Down Expand Up @@ -230,6 +242,25 @@ def test_add_note_evidence_and_status_transitions_are_idempotent(self) -> None:
self.assertEqual(result, 0, stderr)
self.assertEqual(self.task("example.release")["status"], "done")

def test_set_title_is_validated_and_idempotent(self) -> None:
self.add_task("example.rename")
result, _stdout, stderr = self.run_cli(
"set-title", "example.rename", "Renamed task"
)
self.assertEqual(result, 0, stderr)
self.assertEqual(self.task("example.rename")["title"], "Renamed task")

result, stdout, stderr = self.run_cli(
"set-title", "example.rename", "Renamed task"
)
self.assertEqual(result, 0, stderr)
self.assertIn("unchanged", stdout)

result, _stdout, stderr = self.run_cli("set-title", "example.rename", " ")
self.assertEqual(result, 1)
self.assertIn("non-empty", stderr)
self.assertEqual(self.task("example.rename")["title"], "Renamed task")

def test_status_failure_and_blocked_intake_are_atomic(self) -> None:
self.add_task("example.unplanned")
before = self.board_path.read_bytes()
Expand All @@ -252,6 +283,20 @@ def test_status_failure_and_blocked_intake_are_atomic(self) -> None:
self.assertEqual(result, 0, stderr)
self.assertTrue(self.task("example.blocked")["notes"])

def test_remove_sole_blocker_note_is_rejected_without_writes(self) -> None:
self.add_task("example.blocked-note", status="blocked")
notes_before = list(self.task("example.blocked-note")["notes"])
self.assertEqual(len(notes_before), 1)
before = self.board_path.read_bytes()

result, _stdout, stderr = self.run_cli(
"remove-note", "example.blocked-note", notes_before[0]
)
self.assertEqual(result, 1)
self.assertIn("would make board invalid", stderr)
self.assertEqual(self.board_path.read_bytes(), before)
self.assertEqual(self.task("example.blocked-note")["notes"], notes_before)

def test_register_plan_creates_and_attaches_with_strict_pairing(self) -> None:
aligned = self.make_plan("register.md", title="Checkout registration")
result, _stdout, stderr = self.run_cli(
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ tmp/
*.swp

# Local-only material must not be published.
.playwright-mcp/
.env
.env.*
!.env.example
Expand Down
3 changes: 3 additions & 0 deletions init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ files=(
scripts/task
tasks/board.yaml
tasks/task.schema.json
tests/__init__.py
tests/helpers.py
tests/test_claude_compat.py
.agents/skills/task-board/SKILL.md
.agents/skills/task-board/scripts/task
.agents/skills/task-plan/SKILL.md
Expand Down
14 changes: 14 additions & 0 deletions tests/test_init.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import os
import sys
import tempfile
import unittest
from pathlib import Path
Expand All @@ -26,6 +27,9 @@ def test_initializes_an_empty_workspace_with_spaces(self) -> None:
"scripts/task",
"tasks/board.yaml",
"tasks/task.schema.json",
"tests/__init__.py",
"tests/helpers.py",
"tests/test_claude_compat.py",
".agents/skills/task-board/SKILL.md",
".agents/skills/task-plan/SKILL.md",
".agents/skills/plan-go/SKILL.md",
Expand Down Expand Up @@ -65,6 +69,16 @@ def test_initializes_an_empty_workspace_with_spaces(self) -> None:
validation = run(destination / "scripts/task", "validate", cwd=destination)
self.assertEqual(0, validation.returncode, validation.stderr)

compatibility = run(
sys.executable,
"-m",
"unittest",
"tests.test_claude_compat",
"-v",
cwd=destination,
)
self.assertEqual(0, compatibility.returncode, compatibility.stderr)

def test_repeated_initialization_is_idempotent(self) -> None:
with tempfile.TemporaryDirectory() as directory:
destination = Path(directory) / "project"
Expand Down