Adds basic task management tools.

This commit is contained in:
2026-10-08 19:15:08 -07:00
parent 5725de2dac
commit 0c7c20b6e6
34 changed files with 676 additions and 868 deletions

View File

@@ -1,67 +1,77 @@
# Project Scripts
This directory contains repository maintenance, task management, and release automation scripts for **Old Bear Battles**.
Optional task-management tools for any kind of project. They require
Python 3.9+ and Bash or Zsh, but no third-party Python packages. They do not
prescribe the project's language, platform, or subject.
---
Tasks live in [docs/tasks.md](../docs/tasks.md). Project identity and optional
commands live in [project-config.json](../project-config.json).
## Active Task Management Tooling
## Environment
All work items and backlog features are tracked directly in repository Markdown at [docs/tasks.md](file:///Users/tom/Projects/OldBearBattles/docs/tasks.md). The Python CLI scripts below provide fast query, insertion, update, and normalization capabilities:
### `get-tasks`
Lists tasks from `docs/tasks.md` with sorting, column selection, and flexible predicate filtering:
```bash
./scripts/get-tasks # List all tasks sorted by ID (desc)
./scripts/get-tasks --accept 'Status:Testing' # List tasks currently awaiting verification
./scripts/get-tasks -p # List tasks in Triage and Planning status
./scripts/get-tasks --all # Show all tasks including Done and Cancelled
./scripts/get-tasks --sort asc # Sort in ascending order
source ./scripts/env.sh
```
### `get-task <ID>`
Displays detailed information, metadata fields, and full description for a specific task ID:
```bash
./scripts/get-task OB-206
```
This adds scripts to `PATH` and defines task aliases. It sources trusted local
shell settings from `scripts/env.sh.local`, or `.env` if the first file is
absent. Both are ignored by Git. An existing `TASKS_FILE` is preserved.
`TASK_ID_PREFIX` overrides task frontmatter; the default prefix is `EX`.
All task commands accept `-f /path/to/tasks.md`.
### `add-task <title>`
Appends a new task entry to `docs/tasks.md` with frontmatter and structured markdown anchor tags:
```bash
./scripts/add-task "Descriptive Title" -p cmn -t feature -s ready -d "Task description and criteria"
```
**Options**:
- `-p, --project`: Project code (`vtt`, `cmn`, `platform`, `shared`, `brawl`)
- `-t, --type`: Task type (`feature`, `bug`, `chore`)
- `-s, --status`: Initial status (`triage`, `planning`, `ready`, `in_progress`, `testing`, `done`, `blocked`, `cancelled`)
- `-d, --description`: Markdown description text
## Query and Edit Tasks
### `update-task <ID>`
Updates status, checks off checklist items, or appends notes to a task:
```bash
./scripts/update-task OB-206 --status testing
./scripts/update-task OB-206 --check-all
./scripts/update-task OB-206 --append-description "### Implementation Notes:\n- Added test coverage."
```
### `update-tasks`
Validates and normalizes `docs/tasks.md` tables, frontmatter, and anchor IDs:
```bash
./scripts/get-tasks --all
./scripts/get-tasks --accept 'Status:Testing'
./scripts/get-tasks --planning
./scripts/get-tasks --sort asc
./scripts/get-task EX-001
./scripts/get-task 1 --json
./scripts/add-task "Describe the first deliverable" -p example-project -t task -s triage
./scripts/update-task 1 --status ready
./scripts/work-on-task 1 --start
./scripts/update-task 1 --status testing --check-all
./scripts/update-task 1 --append-description "- [ ] Additional review criterion"
./scripts/update-tasks
```
### `triage` / `research`
Interactive helpers for rapidly reviewing tasks in triage or conducting architectural research on backlog items.
- `get-tasks`: triage tasks by default; `--all` includes completed work.
Supports column selection and filters; `--help` lists the options.
- `get-task`: metadata and full description for a prefixed ID or task number.
- `add-task`: assigns the next numeric ID. Supply a project key and type from
frontmatter; the legacy defaults are `shared` and `feature`.
- `update-task`: edits metadata, descriptions, and checklist items.
- `work-on-task`: validates readiness and optionally starts work.
- `update-tasks`: regenerates summary/enum tables and normalizes IDs and links.
---
`testing` means awaiting human verification, whether that is editorial review,
a planning checklist, or software tests. Agents do not mark their own work
`done`.
## Release Tooling
## Optional Agent Helpers
- **`build-release`**: Automated release build pipeline (runs test suites, packages production build).
- **`package-release`**: Distribution packaging script for reproducible standalone releases.
```bash
./scripts/triage 1 --hermes
./scripts/research 1 --agy
```
---
These launch an installed Hermes or Antigravity CLI. Core task commands and
tests do not need either. Select an agent with `--agent`, a shortcut above,
or `TASK_AGENT`. Missing agents are reported as errors.
## Note on `ot` (Open Tasks)
`triage` accepts `planning` / `triage`; `research` accepts `research`.
Their prompts read the shared JSON configuration and refer to
[AGENTS.md](../AGENTS.md), rather than assuming a particular project or stack.
An empty `commands` object means no project commands are configured. Configured
commands are included as guidance, not executed by these helpers.
> [!NOTE]
> Any references to `ot` (Open Tasks) refer to an upcoming standalone task management tool that is currently under development and not yet released. In the meantime, all tasks in this repository are managed directly in [docs/tasks.md](file:///Users/tom/Projects/OldBearBattles/docs/tasks.md) using the scripts documented above.
## Tooling Tests
```bash
python3 -m unittest discover -s scripts/tests -v
```
This validates the bundled tools, not your project's deliverables.
No release scripts are provided: publishing or deployment should be added only
when the project's needs are known.

View File

@@ -2,10 +2,10 @@
"""
add-task
Appends a new task to Documentation/Tasks.md and re-synchronizes the task list.
Appends a new task to docs/tasks.md and re-synchronizes the task list.
Usage:
add-task "My Title Goes Here" --project=shared --type=chore
add-task "My Title Goes Here" -p editor -t feature -s triage -d "Description text"
add-task "My Title Goes Here" -p example-project -t feature -s triage -d "Description text"
"""
import argparse

View File

@@ -1,62 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Build and verify a production release of Open Tasks
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
export PYTHONPATH="${REPO_ROOT}/src:${PYTHONPATH:-}"
VERSION=""
OUTPUT_DIR="${REPO_ROOT}/dist"
while [[ $# -gt 0 ]]; do
case "$1" in
--version|-v)
VERSION="$2"
shift 2
;;
--output-dir|-o)
OUTPUT_DIR="$2"
shift 2
;;
*)
if [ -z "$VERSION" ]; then
VERSION="$1"
fi
shift
;;
esac
done
if [ -z "$VERSION" ]; then
VERSION=$(python3 -c "import ot; print(ot.__version__)")
fi
mkdir -p "$OUTPUT_DIR"
echo "=== 1. Running Test Suite ==="
python3 -m unittest discover -s "${REPO_ROOT}/tests"
echo "=== 2. Building Release Artifact (v${VERSION}) ==="
python3 -m ot.packaging \
--version "$VERSION" \
--src-dir "${REPO_ROOT}/src" \
--output-dir "$OUTPUT_DIR" \
--artifact-name "ot-${VERSION}.zipapp"
# Also maintain canonical ot.zipapp copy in dist/
cp "${OUTPUT_DIR}/ot-${VERSION}.zipapp" "${OUTPUT_DIR}/ot.zipapp"
echo "=== 3. Generating SHA256 Checksums ==="
cd "$OUTPUT_DIR"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "ot-${VERSION}.zipapp" "ot.zipapp" > SHA256SUMS
elif command -v shasum >/dev/null 2>&1; then
shasum -a 256 "ot-${VERSION}.zipapp" "ot.zipapp" > SHA256SUMS
fi
echo "=== 4. Validating Built Release Artifact ==="
python3 "${OUTPUT_DIR}/ot-${VERSION}.zipapp" version
echo "=========================================="
echo "Release Pipeline completed successfully for v${VERSION}!"
echo "Artifacts created in ${OUTPUT_DIR}:"
ls -la "${OUTPUT_DIR}"

View File

@@ -1,173 +0,0 @@
#!/usr/bin/env bash
# ==============================================================================
# Old Bear Battles - Production Container Build & CI Publish Script
# Sourced from single source of truth: VERSION file in repository root
# ==============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "${ROOT_DIR}"
# 1. Read version from single source of truth (VERSION file in repo root)
APP_VERSION=""
if [ -f "${ROOT_DIR}/VERSION" ]; then
APP_VERSION="$(tr -d ' \r\n' < "${ROOT_DIR}/VERSION")"
fi
if [ -z "${APP_VERSION}" ]; then
APP_VERSION="0.1.0-alpha5"
fi
# 2. Get short git commit hash
GIT_COMMIT_SHORT="$(git rev-parse --short HEAD 2>/dev/null || echo "dev")"
# Default configuration (can be overridden via CLI args or environment variables)
IMAGE_REGISTRY="${IMAGE_REGISTRY:-ghcr.io/headhunter45/}"
IMAGE_REGISTRY="$(echo "${IMAGE_REGISTRY}" | tr '[:upper:]' '[:lower:]')"
if [ -n "${IMAGE_REGISTRY}" ] && [[ "${IMAGE_REGISTRY}" != */ ]]; then
IMAGE_REGISTRY="${IMAGE_REGISTRY}/"
fi
IMAGE_TAG="${IMAGE_TAG:-${APP_VERSION}}"
DO_PUSH=false
RUN_TESTS=false
# Parse command line options
while [[ $# -gt 0 ]]; do
case "$1" in
--registry)
IMAGE_REGISTRY="$(echo "$2" | tr '[:upper:]' '[:lower:]')"
if [ -n "${IMAGE_REGISTRY}" ] && [[ "${IMAGE_REGISTRY}" != */ ]]; then
IMAGE_REGISTRY="${IMAGE_REGISTRY}/"
fi
shift 2
;;
--tag)
IMAGE_TAG="$2"
shift 2
;;
--push)
DO_PUSH=true
shift
;;
--test)
RUN_TESTS=true
shift
;;
-h|--help)
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Options:"
echo " --registry <URL> Container registry prefix (default: ${IMAGE_REGISTRY})"
echo " --tag <TAG> Container image tag (default: ${IMAGE_TAG} from VERSION file)"
echo " --push Push container images to registry after building"
echo " --test Run test suites before building images"
echo " -h, --help Show this help message"
echo ""
echo "Version info:"
echo " VERSION file : ${APP_VERSION}"
echo " Git commit hash : ${GIT_COMMIT_SHORT}"
echo ""
echo "Examples:"
echo " $0"
echo " $0 --push"
echo " $0 --registry ghcr.io/headhunter45/ --tag v0.1.0 --push"
exit 0
;;
*)
echo "Unknown option: $1" >&2
exit 1
;;
esac
done
echo "======================================================================"
echo " Old Bear Battles - Production Container Build"
echo "======================================================================"
echo " Application Ver : ${APP_VERSION} (from VERSION)"
echo " Git Commit Hash : ${GIT_COMMIT_SHORT}"
echo " Primary Tag : ${IMAGE_TAG}"
echo " Registry Prefix : ${IMAGE_REGISTRY:-<local docker>}"
echo " Push to Registry: ${DO_PUSH}"
echo " Project Root : ${ROOT_DIR}"
echo "======================================================================"
# Optional test run
if [ "${RUN_TESTS}" = true ]; then
echo ""
echo "==> Running automated test suites..."
npm test
fi
CLIENT_IMAGE="${IMAGE_REGISTRY}oldbear_client:${IMAGE_TAG}"
CLIENT_IMAGE_COMMIT="${IMAGE_REGISTRY}oldbear_client:${GIT_COMMIT_SHORT}"
CLIENT_IMAGE_LATEST="${IMAGE_REGISTRY}oldbear_client:latest"
SERVER_IMAGE="${IMAGE_REGISTRY}oldbear_server:${IMAGE_TAG}"
SERVER_IMAGE_COMMIT="${IMAGE_REGISTRY}oldbear_server:${GIT_COMMIT_SHORT}"
SERVER_IMAGE_LATEST="${IMAGE_REGISTRY}oldbear_server:latest"
NGINX_IMAGE="${IMAGE_REGISTRY}oldbear_nginx:${IMAGE_TAG}"
NGINX_IMAGE_COMMIT="${IMAGE_REGISTRY}oldbear_nginx:${GIT_COMMIT_SHORT}"
NGINX_IMAGE_LATEST="${IMAGE_REGISTRY}oldbear_nginx:latest"
echo ""
echo "==> 1/3 Building Client container (${CLIENT_IMAGE})..."
docker build \
-t "${CLIENT_IMAGE}" \
-t "${CLIENT_IMAGE_COMMIT}" \
-t "${CLIENT_IMAGE_LATEST}" \
-f docker/Dockerfile.client \
--build-arg CLIENT_PORT=80 \
--build-arg APP_VERSION="${APP_VERSION}" \
--build-arg GIT_COMMIT_HASH="${GIT_COMMIT_SHORT}" \
.
echo ""
echo "==> 2/3 Building Server container (${SERVER_IMAGE})..."
docker build \
-t "${SERVER_IMAGE}" \
-t "${SERVER_IMAGE_COMMIT}" \
-t "${SERVER_IMAGE_LATEST}" \
-f docker/Dockerfile.server \
--build-arg SERVER_PORT=3001 \
--build-arg APP_VERSION="${APP_VERSION}" \
--build-arg GIT_COMMIT_HASH="${GIT_COMMIT_SHORT}" \
.
echo ""
echo "==> 3/3 Building Nginx proxy container (${NGINX_IMAGE})..."
docker build \
-t "${NGINX_IMAGE}" \
-t "${NGINX_IMAGE_COMMIT}" \
-t "${NGINX_IMAGE_LATEST}" \
-f docker/Dockerfile.nginx \
.
echo ""
echo "======================================================================"
echo " Build Completed Successfully!"
echo " Images built:"
echo " - ${CLIENT_IMAGE}, ${CLIENT_IMAGE_COMMIT}, ${CLIENT_IMAGE_LATEST}"
echo " - ${SERVER_IMAGE}, ${SERVER_IMAGE_COMMIT}, ${SERVER_IMAGE_LATEST}"
echo " - ${NGINX_IMAGE}, ${NGINX_IMAGE_COMMIT}, ${NGINX_IMAGE_LATEST}"
echo "======================================================================"
if [ "${DO_PUSH}" = true ]; then
echo ""
echo "==> Pushing images to registry: ${IMAGE_REGISTRY}..."
docker push "${CLIENT_IMAGE}"
docker push "${CLIENT_IMAGE_COMMIT}"
docker push "${CLIENT_IMAGE_LATEST}"
docker push "${SERVER_IMAGE}"
docker push "${SERVER_IMAGE_COMMIT}"
docker push "${SERVER_IMAGE_LATEST}"
docker push "${NGINX_IMAGE}"
docker push "${NGINX_IMAGE_COMMIT}"
docker push "${NGINX_IMAGE_LATEST}"
echo ""
echo "======================================================================"
echo " All images successfully pushed to ${IMAGE_REGISTRY}!"
echo "======================================================================"
fi

View File

@@ -31,13 +31,12 @@ esac
# Export Repo & Tasks Context
export REPO_ROOT
export TASKS_FILE="$REPO_ROOT/docs/tasks.md"
# Optional document-wide prefix override; unset uses frontmatter, then OT.
# export TASK_ID_PREFIX="OT"
export TASKS_FILE="${TASKS_FILE:-$REPO_ROOT/docs/tasks.md}"
# Optional document-wide prefix override; unset uses frontmatter, then EX.
# export TASK_ID_PREFIX="EX"
# Define Aliases
alias run-editor='(cd Editor/App && npm run tauri dev -- -- "../../Content/Episodes/Episode 1/Episode.xml")'
alias get-ready-tasks="$SCRIPTS_DIR/get-tasks | sed -E 's/ +\\|$/ |/; s/-{10,}/---/'"
alias get-ready-tasks="$SCRIPTS_DIR/get-tasks --accept 'Status:Ready' | sed -E 's/ +\\|$/ |/; s/-{10,}/---/'"
alias get-planning-tasks="$SCRIPTS_DIR/get-tasks --planning | sed -E 's/ +\\|$/ |/; s/-{10,}/---/'"
alias add-task="$SCRIPTS_DIR/add-task"
alias get-task="$SCRIPTS_DIR/get-task"

View File

@@ -2,11 +2,11 @@
"""
get-task
Retrieves detailed information and metadata for a specific task in Documentation/Tasks.md.
Retrieves detailed information and metadata for a specific task in docs/tasks.md.
Usage:
./scripts/get-task OT-018
./scripts/get-task EX-018
./scripts/get-task 18 --json
./scripts/get-task OT-099 --description-only
./scripts/get-task EX-099 --description-only
"""
import argparse

View File

@@ -2,9 +2,9 @@
#
# get-tasks
#
# Lists tasks from Documentation/Tasks.md sorted by ID.
# Lists tasks from docs/tasks.md sorted by ID.
# Supports --accept, --reject, and --sort [asc|desc] (defaults to descending).
# Defaults to Pending tasks, or Triage & Planning with -p / --planning.
# Defaults to Triage tasks, or Triage & Planning with -p / --planning.
# Uses filter-table if available, with a fast built-in fallback.
#

View File

@@ -1 +1 @@
"""Shared automation library for PlanB task tooling."""
"""Shared automation library for project task tooling."""

View File

@@ -1,6 +1,7 @@
import argparse
import importlib.machinery
import importlib.util
import json
import os
import sys
from pathlib import Path
@@ -8,6 +9,27 @@ from typing import Any, Dict, List, Optional
from agents import BaseAgent, get_agent, get_default_agent, list_agents
def load_project_config(config_path: Optional[Path] = None) -> Dict[str, Any]:
path = config_path if config_path is not None else Path(__file__).resolve().parents[2] / "project-config.json"
try:
config = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as error:
raise ValueError(f"Cannot read project configuration {path}: {error}") from error
if not isinstance(config, dict):
raise ValueError(f"Project configuration {path} must be a JSON object")
for field in ("name", "description"):
if not isinstance(config.get(field), str) or not config[field].strip():
raise ValueError(f"Project configuration {path} requires a non-empty '{field}' string")
commands = config.get("commands")
if not isinstance(commands, dict) or any(
not isinstance(name, str) or not name.strip()
or not isinstance(command, str) or not command.strip()
for name, command in commands.items()
):
raise ValueError(f"Project configuration {path} requires 'commands' to map names to non-empty command strings")
return config
def load_get_task_module():
"""Dynamically load scripts/get-task."""
repo_root = Path(__file__).resolve().parent.parent.parent
@@ -53,12 +75,19 @@ def build_workflow_prompt(
extra_context: str = "",
) -> str:
"""Construct a standardized, high-fidelity prompt for an AI agent."""
config = load_project_config()
commands = "\n".join(f"- {name}: `{command}`" for name, command in config["commands"].items())
if not commands:
commands = "- No commands configured. Agree on relevant verification criteria with the user."
desc = task["description"].strip() if task.get("description") else "(No description provided yet)"
subproj_line = f"- Subproject: {task['subproject']}\n" if task.get("subproject") else ""
epic_line = f"- Epic: {task.get('epic_label', task['epic'])} ({task['epic']})\n" if task.get("epic") else ""
epic_arg = f" --epic={task['epic']}" if task.get("epic") else ""
prompt = f"""You are {workflow_name} task {task['id']} in the PlanBForGreatJustice repository.
prompt = f"""You are {workflow_name} task {task['id']} for {config['name']}.
### Project:
{config['description']}
### Target Task:
- ID: {task['id']}
@@ -71,23 +100,25 @@ def build_workflow_prompt(
{desc}
### Repository Context & Guidelines:
1. SSoT & Task Management (GEMINI.md):
1. SSoT & Task Management (AGENTS.md):
- You MUST use `./scripts/update-task {task['id']} ...` to update task metadata, description, title, or checklist items.
- If this task needs to be split into subtasks, use `./scripts/add-task "Subtask Title" --project={task['project']} --type={task['type']}{epic_arg}`.
- Do NOT edit docs/tasks.md manually.
2. Architecture Overview:
- C++ Story Engine: Engine/PlanB/Engine/ (Actions, Expressions, Sequences, GameState, StoryModel).
- Desktop Editor: Editor/App/ (Tauri + Vite + React, Sequence Simulator, Expression Graph Canvas, XML converters).
- Content: Content/Episode 1/Episode1/Episode.xml.
- Specifications: docs/Schema/ (e.g., PBScript_Specification.md, grammar files).
- Legacy Reference: Editor/Sources/ (WPF C# editor for comparison if applicable).
2. Project Context:
- Read AGENTS.md, README.md, and relevant project documents before starting.
- Do not assume this is software or prescribe a language, platform, or architecture.
- Use the project's agreed verification criteria, including human review when appropriate.
### Optional Project Commands (guidance, not automatically executed):
{commands}
3. Objectives for this session:
{workflow_objective}
"""
if extra_context:
prompt += f"\n### Additional Context:\n{extra_context}\n"
prompt += f"\nBegin by reviewing the task, inspecting the relevant code, and collaborating interactively with the user.\n"
prompt += "\nBegin by reviewing the task, inspecting the relevant project materials, and collaborating interactively with the user.\n"
return prompt

View File

@@ -1,8 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Package Open Tasks release artifact
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
export PYTHONPATH="${REPO_ROOT}/src:${PYTHONPATH:-}"
python3 -m ot.packaging "$@"

View File

@@ -2,13 +2,13 @@
"""
research
Researches a task from Documentation/Tasks.md using an AI CLI agent (Antigravity or Hermes).
Validates that the task is in 'research' status, prepares task and codebase context,
Researches a task from docs/tasks.md using an AI CLI agent (Antigravity or Hermes).
Validates that the task is in 'research' status, prepares task and project context,
and launches the agent interactively.
Usage:
./scripts/research 037
./scripts/research OT-037 --hermes
./scripts/research EX-037 --hermes
./scripts/research 037 --agy
./scripts/research --help
"""
@@ -28,17 +28,17 @@ from task_context import (
resolve_selected_agent,
)
RESEARCH_OBJECTIVES = """- Deeply investigate the technical requirements, architecture, and code models needed for this task.
- Inspect existing codebase, grammar definitions, or schemas to identify gaps or technical hurdles.
- Perform web searches or documentation lookups if external formats, libraries, or protocols are involved.
- Collaborate interactively with the user to explore technical trade-offs and possible approaches.
- Document research findings, technical recommendations, and proposed solutions.
- When research is complete, update the task description/notes via `./scripts/update-task {task_id}` and determine if it's ready to transition to 'planning' or 'pending'."""
RESEARCH_OBJECTIVES = """- Investigate the requirements, background, and evidence needed for this task.
- Inspect existing project materials to identify gaps or challenges.
- Consult relevant sources when needed and record citations.
- Collaborate interactively with the user to explore trade-offs and possible approaches.
- Document research findings, recommendations, and proposed next steps.
- When research is complete, update the task description/notes via `./scripts/update-task {task_id}` and determine if it's ready to transition to 'planning' or 'ready'."""
def main():
parser = argparse.ArgumentParser(
description="Research a task in Documentation/Tasks.md using an AI CLI agent. Requires status to be 'research'."
description="Research a task in docs/tasks.md using an AI CLI agent. Requires status to be 'research'."
)
parser.add_argument("task_id", help="Task number or prefixed ID (e.g. 37, PREFIX-037)")
add_common_agent_arguments(parser)

View File

@@ -21,7 +21,7 @@ def tracker(prefix=None, epics=True, number=1):
label: "Team's Release"
description: 'Ship the product.'
""" if epics else ""
anchor_prefix = prefix.lower() if prefix else "ot"
anchor_prefix = prefix.lower() if prefix else "ex"
return f"""---
{prefix_line}projects:
- value: 'shared'
@@ -101,7 +101,7 @@ class TaskScriptsTest(unittest.TestCase):
def test_epic_add_update_clear_and_preserve(self):
self.run_cli("add-task", "New task", "--epic", "Proof of Concept", "--subproject", "engine")
task = self.get_task("OT-002")
task = self.get_task("EX-002")
self.assertEqual((task["epic"], task["epic_label"]), ("poc", "Proof of Concept"))
self.assertEqual(task["subproject"], "engine")
self.assertIn('data-epic="poc"', task["raw_markdown"])
@@ -181,7 +181,7 @@ class TaskScriptsTest(unittest.TestCase):
def test_default_prefix_without_epics_and_environment_file(self):
self.path.write_text(tracker(epics=False))
self.run_cli("update-tasks")
self.assertEqual(self.get_task()["id"], "OT-001")
self.assertEqual(self.get_task()["id"], "EX-001")
self.assertEqual(self.get_task()["epic"], "")
self.assertNotIn("| Epic", self.path.read_text())
env = {**self.env, "TASKS_FILE": str(self.path)}
@@ -190,7 +190,7 @@ class TaskScriptsTest(unittest.TestCase):
env=env, text=True, capture_output=True,
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout.strip(), "OT-001")
self.assertEqual(result.stdout.strip(), "EX-001")
def test_invalid_prefix_and_duplicate_numbers(self):
for prefix in ("", "1BAD", "BAD SPACE", "BAD/"):
@@ -216,7 +216,7 @@ class TaskScriptsTest(unittest.TestCase):
self.assertNotIn("<<<<<<<", updated)
self.assertNotIn("=======", updated)
self.assertNotIn(">>>>>>>", updated)
self.assertIn("OT-001", updated)
self.assertIn("EX-001", updated)
self.assertIn("Original", updated)
@@ -226,30 +226,30 @@ class TaskScriptsTest(unittest.TestCase):
env = {**self.env, "PATH": "/usr/bin:/bin"}
output = self.run_cli("get-tasks", "--all", "--sort", "asc", env=env).stdout
self.assertIn("Epic", output)
self.assertLess(output.index("OT-001"), output.index("OT-002"))
self.assertLess(output.index("EX-001"), output.index("EX-002"))
output = self.run_cli(
"get-tasks", "--all", "--accept", "Epic:Team's Release", "--select", "ID,Epic", env=env,
).stdout
self.assertIn("OT-001", output)
self.assertNotIn("OT-002", output)
self.assertIn("EX-001", output)
self.assertNotIn("EX-002", output)
self.assertNotIn("Title", output)
output = self.run_cli("get-tasks", "--all", "--reject", "Epic:Proof of Concept", env=env).stdout
self.assertNotIn("OT-002", output)
self.assertIn("OT-001", output)
self.assertNotIn("EX-002", output)
self.assertIn("EX-001", output)
for value in ("poc", "POC", "Proof of Concept"):
with self.subTest(value=value):
output = self.run_cli("get-tasks", "--accept", f"Epic:{value}", env=env).stdout
self.assertIn("OT-002", output)
self.assertNotIn("OT-001", output)
self.assertIn("EX-002", output)
self.assertNotIn("EX-001", output)
output = self.run_cli("get-tasks", "--all", "--reject", "Epic:poc", env=env).stdout
self.assertNotIn("OT-002", output)
self.assertIn("OT-001", output)
self.assertNotIn("EX-002", output)
self.assertIn("EX-001", output)
output = self.run_cli(
"get-tasks", "--accept", "Epic:poc,release", "--accept", "Status:Triage", env=env,
).stdout
self.assertIn("OT-002", output)
self.assertNotIn("OT-001", output)
self.assertNotIn("OT-001", self.run_cli("get-tasks", "--accept", "Epic:missing", env=env).stdout)
self.assertIn("EX-002", output)
self.assertNotIn("EX-001", output)
self.assertNotIn("EX-001", self.run_cli("get-tasks", "--accept", "Epic:missing", env=env).stdout)
def test_listing_external_filter_table_selects_epic(self):
self.run_cli("update-tasks")
@@ -291,6 +291,117 @@ class TaskScriptsTest(unittest.TestCase):
self.assertIn("- Epic: Proof of Concept (poc)", prompt)
self.assertIn("--epic=poc", prompt)
def test_ready_task_can_be_started(self):
self.run_cli("update-task", "1", "--status", "ready")
self.run_cli("work-on-task", "1", "--start")
self.assertEqual(self.get_task()["status"], "in_progress")
self.run_cli("work-on-task", "1")
def test_hyphenated_in_progress_is_already_started(self):
self.run_cli("update-task", "1", "--status", "in-progress")
before = self.path.read_text()
self.run_cli("work-on-task", "1", "--start")
self.assertEqual(before, self.path.read_text())
def test_repository_sample_and_config_are_consistent(self):
config = json.loads((SCRIPTS.parent / "project-config.json").read_text())
sample = SCRIPTS.parent / "docs" / "tasks.md"
result = subprocess.run(
[sys.executable, str(SCRIPTS / "get-task"), "1", "-f", str(sample), "--json"],
env=self.env, text=True, capture_output=True,
)
self.assertEqual(result.returncode, 0, result.stderr)
task = json.loads(result.stdout)
self.assertIsInstance(config["name"], str)
sys.path.insert(0, str(SCRIPTS / "lib"))
try:
import task_context
update = task_context.load_get_task_module().load_update_tasks_module()
finally:
sys.path.pop(0)
frontmatter = update.read_frontmatter(sample.read_text())
project = next(item for item in frontmatter["projects"] if item["value"] == task["project"])
self.assertIn(f"**Project:** {project['label']}", task["raw_markdown"])
self.assertEqual(task["id"].rsplit("-", 1)[0], update.get_task_id_prefix(frontmatter))
copy = Path(self.temp.name) / "sample.md"
copy.write_text(sample.read_text())
result = subprocess.run(
[sys.executable, str(SCRIPTS / "update-tasks"), "-f", str(copy)],
env=self.env, text=True, capture_output=True,
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(sample.read_text(), copy.read_text())
def test_environment_preserves_external_tracker(self):
env = {**self.env, "TASKS_FILE": str(self.path)}
result = subprocess.run(
["bash", "-c", 'source "$1" >/dev/null; printf "%s\\n" "$TASKS_FILE"; command -v get-task',
"bash", str(SCRIPTS / "env.sh")],
env=env, text=True, capture_output=True,
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout.splitlines(), [str(self.path), str(SCRIPTS / "get-task")])
class ProjectConfigTest(unittest.TestCase):
def setUp(self):
sys.path.insert(0, str(SCRIPTS / "lib"))
try:
import task_context
self.context = task_context
finally:
sys.path.pop(0)
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.path = Path(self.temp.name) / "project-config.json"
self.config = {
"name": "The Lighthouse Novel",
"description": "A novel about a coastal community.",
"commands": {},
}
self.path.write_text(json.dumps(self.config))
def test_nonsoftware_configuration_in_prompt(self):
task = {
"id": "BOOK-001", "title": "Outline chapter one", "project": "manuscript",
"status": "triage", "type": "task", "description": "Review the narrative arc.",
}
with patch.object(self.context, "load_project_config", return_value=self.context.load_project_config(self.path)):
prompt = self.context.build_workflow_prompt(task, "triaging", "Outline the chapter")
self.assertIn("for The Lighthouse Novel.", prompt)
self.assertIn(self.config["description"], prompt)
self.assertIn("No commands configured", prompt)
self.assertIn("Do not assume this is software", prompt)
self.assertIn("AGENTS.md", prompt)
def test_arbitrary_named_commands_are_guidance(self):
self.config["commands"] = {"word-count": "wc -w manuscript.txt"}
self.path.write_text(json.dumps(self.config))
config = self.context.load_project_config(self.path)
task = {"id": "1", "title": "Review", "project": "manuscript", "status": "triage", "type": "task"}
with patch.object(self.context, "load_project_config", return_value=config):
prompt = self.context.build_workflow_prompt(task, "reviewing", "Review")
self.assertIn("- word-count: `wc -w manuscript.txt`", prompt)
self.assertIn("not automatically executed", prompt)
def test_invalid_configuration_is_reported(self):
invalid_configs = (
[], {}, {**self.config, "name": ""}, {**self.config, "description": None},
{**self.config, "commands": []}, {**self.config, "commands": {"test": None}},
{**self.config, "commands": {"": "echo ok"}},
{**self.config, "commands": {"review": " "}},
)
for config in invalid_configs:
with self.subTest(config=config):
self.path.write_text(json.dumps(config))
with self.assertRaisesRegex(ValueError, "Project configuration"):
self.context.load_project_config(self.path)
self.path.write_text("{")
with self.assertRaisesRegex(ValueError, "Cannot read project configuration"):
self.context.load_project_config(self.path)
with self.assertRaisesRegex(ValueError, "Cannot read project configuration"):
self.context.load_project_config(self.path.parent / "missing.json")
if __name__ == "__main__":
unittest.main()

View File

@@ -2,13 +2,13 @@
"""
triage
Triages a task from Documentation/Tasks.md using an AI CLI agent (Antigravity or Hermes).
Triages a task from docs/tasks.md using an AI CLI agent (Antigravity or Hermes).
Validates that the task is in 'planning' or 'triage' status, prepares full
architectural and task context, and launches the agent interactively.
Usage:
./scripts/triage 094
./scripts/triage OT-120 --hermes
./scripts/triage EX-120 --hermes
./scripts/triage 094 --agy
./scripts/triage --help
"""
@@ -28,17 +28,17 @@ from task_context import (
resolve_selected_agent,
)
TRIAGE_OBJECTIVES = """- Inspect the codebase to understand the current implementation and scope for this task.
TRIAGE_OBJECTIVES = """- Inspect the project materials to understand the current work and scope for this task.
- Clarify ambiguities: ask the user any critical design or scoping questions interactively.
- Formulate a clear, actionable checklist of engineering deliverables (- [ ]).
- Formulate a clear, actionable checklist of deliverables and review criteria (- [ ]).
- Document any open decisions, architectural considerations, or trade-offs in triage notes.
- Once scoped, update the task using `./scripts/update-task {task_id}`.
- If ready for implementation, propose moving status to 'pending', or keep in 'triage'/'planning' if awaiting user decisions."""
- If ready for implementation, propose moving status to 'ready', or keep in 'triage'/'planning' if awaiting user decisions."""
def main():
parser = argparse.ArgumentParser(
description="Triage a task in Documentation/Tasks.md using an AI CLI agent. Requires status to be 'planning' or 'triage'."
description="Triage a task in docs/tasks.md using an AI CLI agent. Requires status to be 'planning' or 'triage'."
)
parser.add_argument("task_id", help="Task number or prefixed ID (e.g. 94, PREFIX-094)")
add_common_agent_arguments(parser)

View File

@@ -2,15 +2,15 @@
"""
update-task
Modifies metadata, title, and/or description of a specific task in Documentation/Tasks.md
Modifies metadata, title, and/or description of a specific task in docs/tasks.md
and automatically synchronizes the document tables and sorting.
Usage:
./scripts/update-task OT-018 --status pending
./scripts/update-task OT-018 --title "Updated Title"
./scripts/update-task OT-018 --description "New description..."
./scripts/update-task OT-018 --append-description "- [ ] Additional checklist item"
./scripts/update-task OT-018 --check-all
./scripts/update-task EX-018 --status ready
./scripts/update-task EX-018 --title "Updated Title"
./scripts/update-task EX-018 --description "New description..."
./scripts/update-task EX-018 --append-description "- [ ] Additional checklist item"
./scripts/update-task EX-018 --check-all
"""
import argparse

View File

@@ -2,17 +2,17 @@
"""
update-tasks
Synchronizes and regenerates Documentation/Tasks.md:
Synchronizes and regenerates docs/tasks.md:
1. Parses YAML frontmatter for projects, epics, task-statuses, task-types, and task-id-prefix.
2. Extracts all task chunks and sorts the Detailed Tasks section strictly by numeric task ID.
3. Updates the Detailed Tasks section:
- Normalizes all task IDs to uniform digit width (minimum 3 digits, e.g. OT-001).
- Normalizes all task IDs to uniform digit width (minimum 3 digits, e.g. EX-001).
- Synchronizes task detail blocks (including optional **Epic:**) with anchor tags (SSOT).
4. Re-renders the Overall Progress Table (tasks-list) from the sorted, updated task details:
- Bold (**Title**) for pending tasks
- Italic (*Title*) for planning and triage tasks
- Plain (Title) for fixed/done/other tasks
- Type column links ([Type](#ot-xxx))
- Type column links ([Type](#ex-xxx))
5. Re-renders the metadata derived enum tables at the end of the document:
- Projects (Rendered from frontmatter projects)
- Task Statuses (Rendered from frontmatter task-statuses)
@@ -106,7 +106,7 @@ def read_frontmatter(text):
def get_task_id_prefix(fm_data):
prefix = os.environ.get("TASK_ID_PREFIX", fm_data.get("task-id-prefix", "OT"))
prefix = os.environ.get("TASK_ID_PREFIX", fm_data.get("task-id-prefix", "EX"))
if not isinstance(prefix, str) or not re.fullmatch(r"[A-Za-z][A-Za-z0-9_-]*", prefix):
raise ValueError("Task ID prefix must start with a letter and contain only letters, digits, underscores, or hyphens")
return prefix.upper()

View File

@@ -6,9 +6,9 @@ Validates a task's status for active development, displays its requirements,
and optionally transitions it to 'in_progress'.
Usage:
./scripts/work-on-task OT-001
./scripts/work-on-task OT-001 --start
./scripts/work-on-task OT-001 --force
./scripts/work-on-task EX-001
./scripts/work-on-task EX-001 --start
./scripts/work-on-task EX-001 --force
"""
import argparse
@@ -24,7 +24,7 @@ def main():
parser = argparse.ArgumentParser(description="Validate and begin work on a task.")
parser.add_argument("task_id", help="Task number or prefixed ID (e.g., 1 or PREFIX-001)")
parser.add_argument("--start", action="store_true", help="Transition status to 'in_progress'")
parser.add_argument("--force", action="store_true", help="Proceed even if status is not pending/in_progress")
parser.add_argument("--force", action="store_true", help="Proceed even if status is not ready/pending/in_progress")
parser.add_argument("-f", "--file", dest="file", default=None, help="Path to tasks.md")
args = parser.parse_args()
@@ -35,9 +35,7 @@ def main():
sys.exit(1)
status = task.get("status", "").strip().lower()
valid_work_statuses = ["pending", "in_progress"]
invalid_statuses = ["planning", "triage", "research", "done", "cancelled", "blocked"]
valid_work_statuses = ["ready", "pending", "in_progress", "in-progress"]
if status not in valid_work_statuses and not args.force:
sys.stderr.write(
f"WARNING: Task {task['id']} is currently in '{task['status']}' status.\n"
@@ -56,7 +54,7 @@ def main():
print("\nRequirements & Description:")
print(task["description"])
if args.start and status != "in_progress":
if args.start and status not in ("in_progress", "in-progress"):
mod = load_get_task_module()
update_mod = mod.load_update_tasks_module()
script_dir = Path(__file__).resolve().parent