From 5725de2dacd2e9dbaef36540f230db3e995633a9 Mon Sep 17 00:00:00 2001 From: Tom Hicks Date: Thu, 8 Oct 2026 18:29:45 -0700 Subject: [PATCH] temp --- .agents/skills/execute-tasks/SKILL.md | 130 ++++ .agents/skills/task-management/SKILL.md | 155 +++++ AGENTS.md | 121 ++++ docs/tasks.md | 201 ++++++ scripts/README.md | 67 ++ scripts/__pycache__/get-taskcpython-314.pyc | Bin 0 -> 11016 bytes .../__pycache__/update-taskcpython-314.pyc | Bin 0 -> 15997 bytes .../__pycache__/update-tasks.cpython-314.pyc | Bin 0 -> 18229 bytes .../__pycache__/update-taskscpython-314.pyc | Bin 0 -> 33532 bytes scripts/add-task | 243 ++++++++ scripts/build-release | 62 ++ scripts/ci-build.sh | 173 ++++++ scripts/env.sh | 83 +++ scripts/get-task | 173 ++++++ scripts/get-tasks | 296 +++++++++ scripts/lib/__init__.py | 1 + .../__pycache__/task_context.cpython-314.pyc | Bin 0 -> 9294 bytes scripts/lib/agents/__init__.py | 52 ++ .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 3225 bytes .../__pycache__/antigravity.cpython-314.pyc | Bin 0 -> 1886 bytes .../agents/__pycache__/base.cpython-314.pyc | Bin 0 -> 2188 bytes .../agents/__pycache__/hermes.cpython-314.pyc | Bin 0 -> 1862 bytes scripts/lib/agents/antigravity.py | 25 + scripts/lib/agents/base.py | 22 + scripts/lib/agents/hermes.py | 25 + scripts/lib/filter_predicates.py | 36 ++ scripts/lib/task_context.py | 132 ++++ scripts/package-release | 8 + scripts/research | 63 ++ .../test_task_scripts.cpython-314.pyc | Bin 0 -> 21767 bytes scripts/tests/test_task_scripts.py | 296 +++++++++ scripts/triage | 63 ++ scripts/update-task | 283 +++++++++ scripts/update-tasks | 574 ++++++++++++++++++ scripts/work-on-task | 76 +++ 35 files changed, 3360 insertions(+) create mode 100644 .agents/skills/execute-tasks/SKILL.md create mode 100644 .agents/skills/task-management/SKILL.md create mode 100644 AGENTS.md create mode 100644 docs/tasks.md create mode 100644 scripts/README.md create mode 100644 scripts/__pycache__/get-taskcpython-314.pyc create mode 100644 scripts/__pycache__/update-taskcpython-314.pyc create mode 100644 scripts/__pycache__/update-tasks.cpython-314.pyc create mode 100644 scripts/__pycache__/update-taskscpython-314.pyc create mode 100755 scripts/add-task create mode 100755 scripts/build-release create mode 100755 scripts/ci-build.sh create mode 100755 scripts/env.sh create mode 100755 scripts/get-task create mode 100755 scripts/get-tasks create mode 100644 scripts/lib/__init__.py create mode 100644 scripts/lib/__pycache__/task_context.cpython-314.pyc create mode 100644 scripts/lib/agents/__init__.py create mode 100644 scripts/lib/agents/__pycache__/__init__.cpython-314.pyc create mode 100644 scripts/lib/agents/__pycache__/antigravity.cpython-314.pyc create mode 100644 scripts/lib/agents/__pycache__/base.cpython-314.pyc create mode 100644 scripts/lib/agents/__pycache__/hermes.cpython-314.pyc create mode 100644 scripts/lib/agents/antigravity.py create mode 100644 scripts/lib/agents/base.py create mode 100644 scripts/lib/agents/hermes.py create mode 100644 scripts/lib/filter_predicates.py create mode 100644 scripts/lib/task_context.py create mode 100755 scripts/package-release create mode 100755 scripts/research create mode 100644 scripts/tests/__pycache__/test_task_scripts.cpython-314.pyc create mode 100644 scripts/tests/test_task_scripts.py create mode 100755 scripts/triage create mode 100755 scripts/update-task create mode 100755 scripts/update-tasks create mode 100755 scripts/work-on-task diff --git a/.agents/skills/execute-tasks/SKILL.md b/.agents/skills/execute-tasks/SKILL.md new file mode 100644 index 0000000..33ad050 --- /dev/null +++ b/.agents/skills/execute-tasks/SKILL.md @@ -0,0 +1,130 @@ +--- +name: execute-tasks +description: >- + Execute and complete a specific list of tasks one by one. Use this skill + whenever the user asks to work on, implement, execute, or do a specific set + or batch of task IDs (e.g. "Do tasks OT-001, OT-002", "Work on these tasks + one at a time"). Validates task statuses, executes implementation + sequentially, updates documentation, transitions tasks to testing, records + follow-up tasks, includes tasks.md changes in git commits, logs verification + instructions in docs/task-verification.md, and checks project-description timestamps. +--- + +# Execute Tasks Workflow Skill + +This skill defines the standardized procedure for executing a specific set of tasks sequentially with strict status validation, documentation updates, follow-up tracking, inclusion of `tasks.md` changes, per-task commits, and comprehensive verification instructions. + +--- + +## 1. Task Status Validation & Confirmation + +Before starting execution on any given task ID, inspect its status using `./scripts/get-task `: + +- **Valid Statuses to Work On**: + - `ready` / `pending` (Ready for implementation) + - `in_progress` (Work currently active) + +- **Invalid / Ambiguous Statuses**: + - `planning`: Scoping incomplete; requirements still unknown. + - `triage`: Requirements known, but prioritization or technical approach unconfirmed. + - `research`: Technical investigation required before planning. + - `testing`: Feature is already implemented and currently awaiting human verification. + - `done` / `fixed`: Task is already completed and verified. + - `cancelled`: Task was discarded or rejected. + - `blocked`: Task is waiting on external dependencies or decisions. + +> [!WARNING] +> **Status Check Rule**: If any requested task is in an invalid status (such as `planning`, `triage`, `research`, `testing`, `done`, `cancelled`, or `blocked`), you **must stop, report the task ID and its current status to the user, explain why it is not ready for implementation, and ask for explicit confirmation** before proceeding with that task. + +--- + +## 2. Step-by-Step Task Execution Loop + +Process each validated task **strictly one at a time** in the requested order: + +```mermaid +graph TD + A[Inspect & Validate Task Status] --> B[Mark Task In Progress] + B --> C[Implement Code & Fixes] + C --> D[Run Build & Verification Tests] + D --> E[Update Relevant Documentation] + E --> F[Check Off Requirements & Transition to Testing] + F --> G[Add Verification Steps to docs/task-verification.md] + G --> H[Check 20-Commit Rule on docs/project-description.md] + H --> I[Stage tasks.md & Create Git Commit per Task] + I --> J{More Tasks in Batch?} + J -- Yes --> A + J -- No --> K[Output Verification Summary to User] +``` + +### Step A: Mark In Progress +Transition the task to active work: +```bash +./scripts/update-task --status in_progress +``` + +### Step B: Implement Requirements +1. Read the task requirements using `./scripts/get-task `. +2. Inspect the relevant source files and implement the requested changes adhering to project architecture and best practices. +3. If partial implementation is required or edge cases emerge, record notes directly in the task description. + +### Step C: Verify & Test +Run build verification and automated tests to ensure no regressions: +```bash +npm test -w @oldbear/client +npm test -w @oldbear/shared +``` + +### Step D: Update Documentation +Check if the changes affect: +- [README.md](file:///Users/tom/Projects/OldBearBattles/README.md) +- `docs/` specifications or architecture guides +Update the corresponding documentation files accordingly. + +### Step E: Transition Task to Testing (Not Done) +Check off completed checklist items and transition status to `testing`: +```bash +./scripts/update-task --status testing --check-all +``` +If specific items need implementation notes: +```bash +./scripts/update-task --status testing --append-description "### Implementation Notes:\n- Completed core logic." +``` +> [!IMPORTANT] +> **Testing vs Done**: The agent sets the status to `testing`. The user will manually test the feature and create their own commit transitioning the status to `done` once verified. + +### Step F: Record Newly Discovered Tasks +If during the work any new bugs, edge cases, refactor opportunities, or follow-ups are discovered, log them immediately in `planning` status: +```bash +./scripts/add-task "Follow-up: " --project= --type= --status=planning -d "Discovered during work on :\n- [ ] Detail" +``` + +### Step G: Update `docs/task-verification.md` +Add clear, step-by-step verification instructions for the completed task into [docs/task-verification.md](file:///Users/tom/Projects/OldBearBattles/docs/task-verification.md) under the appropriate section: +- Include task ID, title, where to find it in the UI, exact steps to execute, and pass/fail criteria. +- *(Note: Once a human tester verifies a feature and marks it `done`, the verification steps are migrated from `docs/task-verification.md` into `docs/human-tests.md` as permanent regression tests).* + +### Step H: Check 20-Commit Rule on Project Description +Check the number of commits since `docs/project-description.md` was last modified: +```bash +LAST_COMMIT=$(git log -n 1 --pretty=format:"%H" -- docs/project-description.md) +git rev-list --count ${LAST_COMMIT}..HEAD +``` +If the count is $\ge 20$ (or if major features/technologies were added): +1. Update `docs/project-description.md` with current UTC timestamp (`- Last Verified UTC: YYYY-MM-DDTHH:MM:SSZ`) and any major feature changes. +2. Update `docs/project-description.json` with `"lastVerifiedUtc": "YYYY-MM-DDTHH:MM:SSZ"`. +3. Include these files in the commit (resetting the 20-commit counter). + +### Step I: Include tasks.md & Create Git Commit +When creating the commit for the task: +1. **Always include `docs/tasks.md` and `docs/task-verification.md`**: Check `git status`. Ensure all changes to `docs/tasks.md` (including user edits or pre-staged edits) and `docs/task-verification.md` are staged: +```bash +git add docs/tasks.md docs/task-verification.md +git commit -m "(): " +``` +*(At least one commit must be made per completed task before moving to the next task).* + +### Step J: Verification Instructions in Response +Always provide clear verification instructions so the user can easily test the work: +1. **Single Task / Small Batch**: Include clear, actionable step-by-step verification instructions directly in your response to the user. +2. **Batch / Many Tasks**: If executing a batch of multiple tasks, create a dedicated verification report in `docs/` (e.g. `docs/verification-report.md`) detailing the changes, commits, and verification steps for each task, and link to it in your final response. diff --git a/.agents/skills/task-management/SKILL.md b/.agents/skills/task-management/SKILL.md new file mode 100644 index 0000000..048d6ed --- /dev/null +++ b/.agents/skills/task-management/SKILL.md @@ -0,0 +1,155 @@ +--- +name: task-management +description: >- + Manage, query, create, triage, research, and update project tasks using the + repository's task CLI scripts in scripts/ (get-tasks, get-task, add-task, + update-task, update-tasks, triage, research) and docs/tasks.md. Activate this + skill whenever reading, creating, editing, prioritizing, or updating tasks, + checklists, or project progress. +--- + +# Task Management Skill + +This skill provides operational guidelines and procedures for managing project tasks using the automation scripts in `scripts/` and the Single Source of Truth (SSOT) document `docs/tasks.md`. + +--- + +## Quick Reference & Environment Setup + +To make scripts available on your `PATH` and load shell aliases: + +```bash +source ./scripts/env.sh +``` + +> **Note**: For local configurations that should not be committed (such as custom environment variables, agent endpoints, or tokens), create a `scripts/env.local.sh` or `.env.local` file. It will be automatically sourced by `env.sh` and is ignored by Git. + +--- + +## Core Procedures + +### 1. Querying and Inspecting Tasks + +- **List active pending tasks**: + ```bash + ./scripts/get-tasks + ``` +- **List tasks in Planning and Triage**: + ```bash + ./scripts/get-tasks --planning + # or alias: get-planning-tasks + ``` +- **List all tasks**: + ```bash + ./scripts/get-tasks --all + ``` +- **Filter tasks**: + ```bash + ./scripts/get-tasks --accept "Status:In Progress" + ./scripts/get-tasks --reject "Project:Android" + ./scripts/get-tasks --sort asc + ``` +- **Inspect a specific task**: + ```bash + ./scripts/get-task OT-001 + # Output as JSON: + ./scripts/get-task OT-001 --json + # View description and checklist only: + ./scripts/get-task OT-001 --description-only + ``` + +--- + +### 2. Creating New Tasks + +Use `add-task` to add a new entry to `docs/tasks.md`. The script automatically assigns the next monotonic ID and re-synchronizes the summary table: + +```bash +./scripts/add-task "Task Title" --project= --type= --status= -d "Description text" +``` + +**Common Flags**: +- `-p`, `--project`: Project value from frontmatter (e.g., `example-project`, `android`, `ios`, `shared`). +- `-t`, `--type`: Task type (`feature`, `bug`, `chore`, `lint`). Default: `feature`. +- `-s`, `--status`: Initial status (`planning`, `triage`, `pending`, `in_progress`, `done`, `blocked`, `cancelled`, `research`). Default: `triage`. +- `-d`, `--description`: Initial description and markdown checklist items. +- `--subproject`: Subproject name if applicable. + +--- + +### 3. Updating Tasks + +Modify task attributes, title, status, and checklist items using `update-task`: + +- **Change status**: + ```bash + ./scripts/update-task OT-001 --status in_progress + ./scripts/update-task OT-001 --status done + ``` +- **Update title or description**: + ```bash + ./scripts/update-task OT-001 --title "Updated Descriptive Title" + ./scripts/update-task OT-001 --description "New full description text" + ``` +- **Append checklist items or notes**: + ```bash + ./scripts/update-task OT-001 --append-description "- [ ] New requirement step" + ``` +- **Check/uncheck checklist items**: + ```bash + # Check all items: + ./scripts/update-task OT-001 --check-all + # Check specific item by keyword: + ./scripts/update-task OT-001 --check-items "Define initial requirements" + # Uncheck all items: + ./scripts/update-task OT-001 --uncheck-all + ``` + +--- + +### 4. Synchronizing the Document + +If tasks are modified or if table alignment needs re-calculation, run `update-tasks`: + +```bash +./scripts/update-tasks +``` + +This script: +1. Re-indexes and sorts all detailed task chunks strictly by numeric ID. +2. Re-renders the **Overall Progress Table** (`#tasks-list`). +3. Re-renders the metadata enum tables (**Projects**, **Task Statuses**, **Task Types**) based on frontmatter. + +--- + +### 5. Running Triage & Research Workflows + +- **Triage a task**: + ```bash + ./scripts/triage OT-001 + ./scripts/triage OT-001 --agy + ``` +- **Research a task**: + ```bash + ./scripts/research OT-001 + ./scripts/research OT-001 --agy + ``` + +--- + +## Operational Rules & Best Practices + +1. **Single Source of Truth (SSOT)**: + - The `` anchor tag in `## Detailed Tasks` is the canonical record. + - **Never manually edit the summary table** in `docs/tasks.md`. Always modify task entries using `update-task` or run `update-tasks` after editing details. +2. **Lifecycle Discipline**: + - `planning`: Scoping and defining requirements. + - `triage`: Prioritizing and deciding architecture. + - `pending`: Clear scope, ready for execution. + - `in_progress`: Work actively being executed. + - `done`: All requirements verified and complete. + - `blocked`: Waiting on an unresolved external dependency. + - `cancelled`: No longer applicable. + - `research`: Technical investigation needed before planning. +3. **Verification**: + - Always run `./scripts/get-tasks` or `./scripts/get-task ` after updating to verify that status transitions and checklist updates have persisted accurately. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ba9fa76 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,121 @@ +# AGENTS.md — Old Bear Battles Operational & Architecture Guide + +Welcome to **Old Bear Battles** (`OldBearBattles`). This document serves as the primary operational orientation, architectural reference, and workflow guide for all AI agents working in this repository. + +--- + +## 1. Project Overview & Core Tenets + +OldBearBattles is a lightweight, mobile-friendly, zero-install Virtual Tabletop (VTT) and tactical tabletop wargaming platform (`Brawl`). + +### Core Design Tenets +1. **Local-First & Zero-Mandatory Auth**: All player and scene data (maps, tokens, audio, character sheets) is stored in local browser storage (`IndexedDB` / `localStorage`). Users can join a room by URL without account friction. +2. **Hybrid P2P & Relay Networking**: Fast WebRTC mesh audio and room synchronization over WebSocket signaling, with graceful server relay fallback. +3. **High-Performance 2D Canvas**: Custom 60 FPS HTML5 Canvas engine designed for seamless trackpad gestures (two-finger pan, pinch-to-zoom) and touch usability without heavy external canvas frameworks. +4. **System-Agnostic Entity Architecture**: Attacks, spells, items, and abilities share a unified `EntityAction` and `EntityStatBlock` schema, enabling flexible play across D&D 5e, Pathfinder 2e, and homebrew systems. +5. **Dual Game Modes (`VTT` & `Brawl`)**: + - `vtt`: Traditional TTRPG mode (initiative tracker, D&D/PF2e character sheets, spellbooks, fog of war). + - `brawl`: Tabletop wargaming mode (armies, units, coherency graph halos, chess clocks, round phase stepper, victory point scoreboard). + - Configured via `GAME_MODE` environment variable (`vtt`, `brawl`, or multi-mode `vtt,brawl` with in-app toggle). + +--- + +## 2. Monorepo Structure + +``` +OldBearBattles/ +├── packages/ +│ ├── client/ # React 18 + Vite frontend +│ │ └── src/ +│ │ ├── common/ # Shared canvas engine, networking, dice, status, widgets +│ │ ├── vtt/ # TTRPG VTT application (AppVtt, character sheets, tokens) +│ │ └── brawl/ # Tabletop wargaming platform (AppBrawl, armies, coherency) +│ ├── server/ # Node.js + WebSocket signaling & relay server +│ └── shared/ # Shared TypeScript schemas, protocol payloads, entity types +├── docs/ # Specifications, architecture guides, task tracking +├── scripts/ # Python CLI tools for task management (get-tasks, update-task, etc.) +└── .agents/ # Agent skills (execute-tasks, task-management) +``` + +--- + +## 3. Mandatory Development & Testing Commands + +Before committing any code or declaring a task ready for testing: + +- **Run Client Tests**: + ```bash + npm test -w @oldbear/client + ``` +- **Run Shared Tests**: + ```bash + npm test -w @oldbear/shared + ``` +- **Start Local Dev Server**: + ```bash + npm run dev + ``` +- **Build Production Bundle** (only when validating build cleanliness): + ```bash + npm run build + ``` + +--- + +## 4. Task Management & Execution Workflow + +All project tasks are tracked directly in [docs/tasks.md](file:///Users/tom/Projects/OldBearBattles/docs/tasks.md). Always use the `execute-tasks` or `task-management` skills and CLI scripts. + +### Task Status Lifecycles +- `triage` / `planning`: Under research or requirements definition. +- `ready`: Ready for implementation. +- `in_progress`: Actively being worked on by an agent or developer. +- `testing`: Feature implementation is complete and currently awaiting human verification. +- `done`: Human tester has verified the feature and committed the completion. +- `blocked` / `cancelled`: Paused or abandoned. + +### Rules for Agent Task Execution +1. **Transition to `testing` (Not `done`)**: + When an agent finishes implementing a task and all tests pass: + ```bash + ./scripts/update-task --status testing --check-all + ``` + Do **NOT** transition tasks directly to `done`. The human user verifies the feature and commits the transition to `done`. +2. **Log Verification in [docs/task-verification.md](file:///Users/tom/Projects/OldBearBattles/docs/task-verification.md)**: + Add step-by-step instructions to `docs/task-verification.md` for testing the feature. +3. **Permanent Human Regression Tests in [docs/human-tests.md](file:///Users/tom/Projects/OldBearBattles/docs/human-tests.md)**: + When a feature is verified and moved from `testing` to `done`, its verification steps are migrated from `docs/task-verification.md` to `docs/human-tests.md` as an ongoing regression test suite. +4. **Always Stage `docs/tasks.md` and `docs/task-verification.md`**: + Before committing, always check `git status`. Ensure that any changes to `docs/tasks.md` (including user edits or pre-staged items) and `docs/task-verification.md` are included: + ```bash + git add docs/tasks.md docs/task-verification.md + git commit -m "(): ()" + ``` +5. **The 20-Commit Project Description Rule**: + `docs/project-description.md` and `docs/project-description.json` are consumed by external repositories and catalog scanners. + - Count commits since `docs/project-description.md` was last modified: + ```bash + LAST_COMMIT=$(git log -n 1 --pretty=format:"%H" -- docs/project-description.md) + git rev-list --count ${LAST_COMMIT}..HEAD + ``` + - If $\ge 20$ commits have elapsed (or if major features/technologies were added), update the `- Last Verified UTC: YYYY-MM-DDTHH:MM:SSZ` timestamp in `docs/project-description.md` and `"lastVerifiedUtc"` in `docs/project-description.json`, stage them, and commit. This resets the 20-commit counter. + +--- + +## 5. Documentation Sitemap & Index + +When working in specific functional areas, refer to these authoritative domain documents: + +| Topic / Domain | Authoritative Documentation | +| :--- | :--- | +| **All Implemented VTT Features** | [Features.md](file:///Users/tom/Projects/OldBearBattles/Features.md) | +| **Wargaming Platform (Brawl)** | [docs/brawl-project-plan.md](file:///Users/tom/Projects/OldBearBattles/docs/brawl-project-plan.md) | +| **System-Agnostic Entity & Imports** | [docs/system-agnostic-import.md](file:///Users/tom/Projects/OldBearBattles/docs/system-agnostic-import.md) | +| **Active Task Backlog** | [docs/tasks.md](file:///Users/tom/Projects/OldBearBattles/docs/tasks.md) | +| **Features Awaiting Verification** | [docs/task-verification.md](file:///Users/tom/Projects/OldBearBattles/docs/task-verification.md) | +| **Human Regression Testing Suites** | [docs/human-tests.md](file:///Users/tom/Projects/OldBearBattles/docs/human-tests.md) | +| **HelpTip Component & Copy Guide** | [docs/helptip-locations.md](file:///Users/tom/Projects/OldBearBattles/docs/helptip-locations.md) | +| **Codebase Refactoring Plan** | [docs/refactor-1.md](file:///Users/tom/Projects/OldBearBattles/docs/refactor-1.md) | +| **Backlog Research & Architecture** | [docs/backlog-implementation-guide.md](file:///Users/tom/Projects/OldBearBattles/docs/backlog-implementation-guide.md) | +| **Task CLI Tooling Guide** | [scripts/README.md](file:///Users/tom/Projects/OldBearBattles/scripts/README.md) | +| **Historical Vision & Kickstarter Plans** | [docs/archive/idea.md](file:///Users/tom/Projects/OldBearBattles/docs/archive/idea.md), [docs/archive/initial-project-plan.md](file:///Users/tom/Projects/OldBearBattles/docs/archive/initial-project-plan.md) | diff --git a/docs/tasks.md b/docs/tasks.md new file mode 100644 index 0000000..4be1c8d --- /dev/null +++ b/docs/tasks.md @@ -0,0 +1,201 @@ +--- +# Most projects only have one or two projects. label is used in prose and value is used as an index. Setup those task-id-prefix and projects values. + +task-id-prefix: 'OB' +projects: + - label: 'Old Bear VTT' + path: '.' + prefix: 'OBV' + value: 'old-bear-vtt' + - label: 'Old Bear Brawl' + path: '.' + prefix: 'OBB' + value: 'old-bear-brawl' + - label: 'Common' + path: '.' + prefix: 'CMN' + value: 'cmn' +task-statuses: + - label: 'Planning' + value: 'planning' + description: 'We do not yet know what it should do.' + - label: 'Triage' + value: 'triage' + description: 'We do not yet know how to do it.' + - label: 'Ready' + value: 'ready' + description: 'Task is ready to be worked on.' + - label: 'In Progress' + value: 'in-progress' + description: 'Task is currently being worked on.' + - label: 'Testing' + value: 'testing' + description: 'Task is currently being tested.' + - label: 'Blocked' + value: 'blocked' + description: 'Task is waiting on external dependencies.' + - label: 'Done' + value: 'done' + description: 'Task is completed and verified.' +task-types: + - value: 'bug' + prefix: 'BUG' + label: 'Bug' + description: 'The task is a bug to fix.' + - value: 'feature' + prefix: 'ENH' + label: 'Feature' + description: 'The task is a new feature to implement.' + - value: 'chore' + prefix: 'CHR' + label: 'Chore' + description: 'The task is a routine maintenance or administrative task.' +--- + +# Project Task Tracker + +## Instructions for Working with Tasks + +### Quick Start (Environment Setup) + +Before running task commands, source the helper environment script in your shell to load convenient aliases and paths: + +```bash +source ./scripts/env.sh +``` + +### Task ID Configuration + +- Set the document-wide `task-id-prefix` in YAML frontmatter (this tracker uses `OB`). +- Optionally export `TASK_ID_PREFIX` in your shell or in the local settings sourced by `scripts/env.sh` (`scripts/env.sh.local` or `.env`). It overrides frontmatter; when neither is set, the default is `OT`. +- Prefixes must start with a letter and contain only letters, digits, underscores, or hyphens. Display IDs are uppercase and anchors are lowercase. +- Numbers remain globally monotonic across the document, with at least three digits. Project and task-type `prefix` fields do not control these IDs. +- After changing the prefix, run `update-tasks` to normalize existing IDs and internal anchor links. Numeric IDs can still be used with task commands. +- All task commands accept `-f /path/to/tasks.md`; `TASKS_FILE` supplies the default path when exported. + +### Command Reference + +You can manage tasks using the provided automation scripts in `./scripts/` (or via their aliases): + +- **List tasks**: + - `get-tasks` (or `./scripts/get-tasks`): Show active pending tasks. + - `get-planning-tasks` (or `get-tasks --planning`): Show tasks in _Planning_ or _Triage_ status. + - `get-tasks --all`: List all tasks regardless of status. + - `get-tasks --accept "Status:In Progress"` / `get-tasks --reject "Project:Example"`: Filter tasks. + - `get-tasks --all --accept "Epic:poc" --select "ID,Title,Epic"`: Filter or select the Epic column. Epic filters accept frontmatter values (`poc`) or labels (`Proof of Concept`), case-insensitively. + - `get-tasks --all --reject "Epic:poc"`: Exclude an Epic by value or label; comma-separated lists such as `Epic:poc,bootstrap` also work. +- **View a specific task**: + - `get-task OT-001` (or `./scripts/get-task 1`): Inspect task details, requirements, and metadata. + - `get-task OT-001 --json`: Output task data in JSON format, including `epic` (value) and `epic_label`. + - `get-task OT-001 --epic`: Output only the canonical Epic value. +- **Add a new task**: + - `add-task "Task Title" --project=example-project --type=feature --status=triage` + - (Optionally supply `-d "Description and checklist"`) + - Add `--epic=poc` (or `--epic="Proof of Concept"`) to assign an Epic defined in frontmatter. +- **Update an existing task**: + - `update-task OT-001 --status in_progress` + - `update-task OT-001 --title "New Title"` + - `update-task OT-001 --status done --check-all` + - `update-task OT-001 --append-description "- [ ] Additional subtask"` + - `update-task OT-001 --epic=poc` / `update-task OT-001 --clear-epic`: Assign or remove an Epic. +- **Synchronize document**: + - `update-tasks` (or `./scripts/update-tasks`): Re-indexes task IDs, re-renders the summary progress table, and regenerates metadata enum tables. + +### Workflow Guidelines + +1. **Do Not Edit the Summary Table Manually**: The summary table below is automatically regenerated from the detailed task entries. Use `./scripts/add-task` and `./scripts/update-task`, or edit the detailed task blocks directly and run `./scripts/update-tasks`. +2. **Anchor Tags as SSOT**: Each task in `## Detailed Tasks` is defined by an anchor tag: + `` + The attributes on this anchor tag are the canonical source of truth for the task's state. + `data-epic` is optional; its value comes from frontmatter `epics`. Summary and detail views display the corresponding label. +3. **Task Lifecycle**: + - **Triage**: Conceptualization and scoping phase. + - **Ready**: Ready to be picked up for implementation. + - **In Progress**: Actively being worked on. + - **Testing**: Code complete and ready for verification. + - **Done**: Completed and verified. + - **Blocked**: Waiting on external dependencies. + + + +## Overall Progress + + + +| ID | Title | Project | Status | Type | +|:-----|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------|:-------|:-----------------| +| OB-001 | In voice and audio settings the 4th button for streaming is mostly off screen | Old Bear VTT | Done | [Feature](#ob-001) | +| OB-002 | When adding a new token to the board try to not put it on top of an existing one | Old Bear VTT | Done | [Feature](#ob-002) | +| OB-003 | When uploading files allow selecting multiple files at once | Old Bear VTT | Done | [Feature](#ob-003) | | Common | Ready | [Feature](#ob-211) | + +--- + + + +## Detailed Tasks + + +### In voice and audio settings the 4th button for streaming is mostly off screen +**ID:** OB-001 +**Project:** Old Bear VTT +**Status:** Done +**Type:** Feature + + +**Description:** + +In voice and audio settings the 4th button for streaming is mostly off screen + + +### When adding a new token to the board try to not put it on top of an existing one +**ID:** OB-002 +**Project:** Old Bear VTT +**Status:** Done +**Type:** Feature + + +**Description:** + +When adding a new token to the board try to not put it on top of an existing one + + +### When uploading files allow selecting multiple files at once +**ID:** OB-003 +**Project:** Old Bear VTT +**Status:** Done +**Type:** Feature + + +**Description:** + +When uploading files allow selecting multiple files at once + +--- + +## Notes + +#### Projects (Rendered from frontmatter projects) + +| Value | Label | Prefix | Path | +| :------------- | :------------- | :----- | :--- | +| old-bear-vtt | Old Bear VTT | OBV | . | +| old-bear-brawl | Old Bear Brawl | OBB | . | + +#### Task Statuses (Rendered from frontmatter task-statuses) + +| Value | Label | Description | +| :---------- | :---------- | :---------------------------------------- | +| triage | Triage | Task is still being defined. | +| ready | Ready | Task is ready to be worked on. | +| in-progress | In Progress | Task is currently being worked on. | +| testing | Testing | Task is currently being tested. | +| blocked | Blocked | Task is waiting on external dependencies. | +| done | Done | Task is completed and verified. | + +#### Task Types (Rendered from frontmatter task-types) + +| Value | Label | Prefix | Description | +| :------ | :------ | :----- | :-------------------------------------------------------- | +| bug | Bug | BUG | The task is a bug to fix. | +| feature | Feature | ENH | The task is a new feature to implement. | +| chore | Chore | CHR | The task is a routine maintenance or administrative task. | diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..c0bced2 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,67 @@ +# Project Scripts + +This directory contains repository maintenance, task management, and release automation scripts for **Old Bear Battles**. + +--- + +## Active Task Management Tooling + +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 +``` + +### `get-task ` +Displays detailed information, metadata fields, and full description for a specific task ID: +```bash +./scripts/get-task OB-206 +``` + +### `add-task ` +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 + +### `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/update-tasks +``` + +### `triage` / `research` +Interactive helpers for rapidly reviewing tasks in triage or conducting architectural research on backlog items. + +--- + +## Release Tooling + +- **`build-release`**: Automated release build pipeline (runs test suites, packages production build). +- **`package-release`**: Distribution packaging script for reproducible standalone releases. + +--- + +## Note on `ot` (Open Tasks) + +> [!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. diff --git a/scripts/__pycache__/get-taskcpython-314.pyc b/scripts/__pycache__/get-taskcpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0490f6976d49b11a6d6d3267d4a54a0a86fef190 GIT binary patch literal 11016 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 literal 0 HcmV?d00001 diff --git a/scripts/__pycache__/update-taskcpython-314.pyc b/scripts/__pycache__/update-taskcpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4dd155215f4b8407fa170a12b975ced533f3b307 GIT binary patch literal 15997 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 literal 0 HcmV?d00001 diff --git a/scripts/__pycache__/update-tasks.cpython-314.pyc b/scripts/__pycache__/update-tasks.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0849c57b0758938e9a43d10e14ef2c685b93bc77 GIT binary patch literal 18229 zcmd6PZBQFYmSB~>&?oxh8vzBF5Ah+sY-2FS#s+K*2D*&PZW~)IBLT7jl}p0L8qbY3 z_TobBo;%~75Ag0xw0dSPMjf+rO-J-bdt-0+n7iG$?z_FX!UZjL4HM18bX;Fw+z;CB zj@h2OxVx8CDoJ30J#l|FZK^V}UcP+!Ugpb}nJ-nJZO~#6a!>u_#=V0W_B)i2f-Dhv z7Jx_yBe4K>4I}XZJb(*tSwJSd<pDXoWdn*c%7C&ABQ>NvADh7gDpHY;1+pNm6w+!^ z1!)bL1yAi&9jQKY3B%k_zYV*pEytjGApi!vQuwoLT{#L2)6UawSHRO4aQScg9rRtT z_Wa$siD||+=M8%Nglo=CFrF#ToQDA@Khf)(n4k5`1zZ8IZ?0)fC^YNt)V4T?Q5WL} zh*x{g4H1)2J}~PF1Uw8e>0=0*@!jxD1pEyIs;AKp<>vvoLBKC$1@6)wzeC&VAo>;p zj0<21*UXFnPE1VC&)pPh@%tF~5}5W7y`F%}JL7Q^0uh8C&;v<?Kfrh=0yB4s>vxGc zAQe&~l7U`_w#`9Y5SqImicT#-5RJsJkC}DNh)j?O8R&(AK4N~(3yqp3+}<f~fVksz z2d0VIS?`>8c7B#<6SJXh9><h}=s$h7vAMa$?hvu=U@mGn;TGCKT%YkxKpSf-D=UdH zZ(zpLK-AR@^mf+Op~$GnzeMW1z`jIc3>XJdJJ11SxIlKm?|^FxaGgItG6oeNbP%Mc zk@3vAJ&f4!k=r0nXcVBGVmy97@bLPKhp0sa`5R}v{(v2ca8_V6V!6lNK!_d1Epk(& z+MnRnN#Bf{sI99L2uHLCGKUJib5jDmUu^wA0601UkRo6LZpJk?hX7Gu1L?V@Jn3ad zXI$PnqE;v&7MS!dc-&2H-<+q(2R#9`7RVNBK-?fEd^7X2bHt1n^_to%DDP@*<>b_j z#)X9iyIp(eIfl%F5MUU%KpKqqHgqZv^c}zpT!DTU5cmMiA$*fU)SVawiRLGy19g!C zlcBu8z4TEajR1RsP9%C!j!?rs<xty_<0ZW?YSoKN1`w28d)Ptr`2C()u|3d`JB$|% zXEcx`xk`<4Xou}MuNZX&rYG=(+CVQQ{K?_(+5d#x5Jq7&C>Mge6=|9ABrJs;OvGuN z!bee=T|v@DNelpE!zj>9<ER!PkWhmjYn4g3rtwq_<0=VC`Z}tSa#C-pmQg_qr}9yZ zmW?MfM)k>PGLq7mB;KG)W`ib9PrWpb9i=E)CW`i}hm!60)uEu}l-z{%mq5D|l;UML zJ|%E`uo%NA#k{NxtHd-|X*&1i5@)AzyCPV0p7u<5C%vd=gVH7Dd;u8a^K<S_f;a4# z6N2h7G#VYV?qJO+-#q9A1PjUyrjI>l4|^7aW<x&9>vtmTytyBq!@fX2s-cfzd<?I4 z3ZI-#J_{6<Z|1g#SJ5ueX#rk7amQ^}@&?ZW4a&tEaMI9od7S38{t3oQ2b^v%!)qr% zyLiz|=jZVWKk8-Gpymm_zv+S>)M8V>H`^qQ<0jBKb0@`TzmGZ1cwB+A^ZtN$!qX(; z^f&FAHhJTuch2n;G?73lV}>d~AejGe;Ss_<&C1!-n-)treOVZf>+}yg?sq&m`ufqh zH79&#GrNqf=z4th@d!%{aoOis^|`N9m?iJ~)88wNnQPX}HJsTVGdHi9n?q+mQ|lk7 zUsu1OU36}!4@80+>Xyx9*5wU#SzK+2sSDTCh07xw>iX!74fPSoN<nXHlonO!>@z)< zU64l0cM&bGpZQV=*<YzKosG5iaoT=X(f<rufMwPNsIP^vCXB{O7*p^kr?AnaCP3xH zAdN*6)`$1MF80&p=szor{*3*W#HD^HK~J?Mpp%+Hssn@3xf|MT6QN0r%)+tJtQ4#i zlPIHQ4(y_(8nRI<1~0(KkWGrC<O&)k`<7Kw$|QYanR%fM#CHcWIkKb@?ms~^0uSXE zEe%B}_)I#DN|r7Inn=YC`2AX^B4w1S1CYB<3mj36rfD^)PEvLaKbWvg@4%Z`pw#=d zJJUB(Daqhsd#3LazY(YV@Ilbq@B|H<S#Oal37f>NDKJ`Xs0<*W<dagW@r5!<av+HT zI6?hW;lsbg1PzSlK)ZUF#|a+q3@VTz+W7^v5gQ&3$|`D54nchJe<9dapx{8`1;ES! zrKSa&8cbtk1oCRILd2L7nZq=%a?v#8@k*h9UBhQh&N>60g#fQk*v)?4I5E#4ed<i) z@Omi&EQMKKfvN#;gpc;PdD+d|JbshMZ~M{AE9g>08e@VKp9iYh?{VIB&CWQ}j35Td z5QZ=2KMg7(gvE{K(3yB%QD`J?%?}N2net<%iZxS3%yb~E_*7?HR=j0=WQ@vV1r1LM z8dh>XD0#2su|L*w>PgF~cxgqXEpl?%`<CyKk1K5okHl;1!e`d>Rq^7o*N5T;+q$7P zo?pBad>H)Rz3`b&t;L_13gSlVgYVq`&hqV;vHFRzI$l=3sE?ZpHca-Isd>%Rykc87 zb;olHmn(j7Y3bU-Yg}&CqAYIBTdw$iN4&V?ts{?)L{3G`Tyg!w-o>7{qv_qLccy;f zeb@Jn@3Dt-^eztlJpb+dpBAp?)IU{XIVIaji!YTuEQ{pD%(WZl+UWR3V;@wobot@s z?~NyLFa5E&zqDiKs_h0$pA#BkEWpr-bVY;A9h81H4IRgo?J|bIv`P*iK|PT$;it^0 zG=mM)w+XudCV>N+m-S~OMV$txkOt$hY!C#EOon+Gt?(Hs9L$g780O0!Pf~I|TY`pf zFgyi?LZuA6^F~UUuD(+BjAsG$LZFhvU&1A+CsZ1#kijOAj=`S90Fq6DVza>#*MU>= zhO|inrY9LG&!|+5Z8%y^&!|(>r+8vYE}kVC8WPl@7pY87(OC|RPREj*kbnn#f(Nhe z&4X)lseQDT(qvT8NsdSoO(=BDs3DE4MD%&}>24O(zk1Y|37X8JwAW+}X$EsT2h&?h zs?t*uXUecaH1WMGE%tqAL6qQoQL<<4??dhTwQWy~d$eX#>{X<iRA=ltYN_Sv<w;F? ziq=7&&<i8vG#Vj$_QGt+*d`;jLO(>-qYzRP{vXv34U|5UgeikWFGy-YY}>_erBh2{ z8QZCs%B90r!sss(G=LYg>v-%Biq6Ky#-QvT5tI}6i03Rur23!r!lMhXz{+3(7{mH= z6NAD6YaHynoWj#p0$TqWumvmxJ70tO2~ofdc}VWX-;wcn*YG=-UBSy7%{=bq@o8Qq zY{vNgp!m?76cVQ+O&8k0TA-P3zUKP8cgF3Ubul;HzB_YZu)~tr(S_D0GyZNz67U)z z@S_p-k6!?tY~{tZUh1!UANNFVMa|K}E9JlJh}K69Z1v&w1MOTd^^H*DpOz3fYPpty zR$)Be$m92b8`{i_cnx5>WJyL;g04+l;+~i7WQD~47ci-6CzBW?E?CXWVcxbY7|@j% z0}24+<&5tR1M7Ay!K#qRDh7#&K_=P^`c@AQkpaY|kBqA=KRXqbM|*y$`sv^bzEb_z z{FnBX;^?>8%46&0-T&E`U_!kE2Ma?!<C)_%Xd6Y?IppQDt_9`@;NulHeBL>x5TXn+ zb{VkHF<!~I=B7M$6Q4EhaiM)GUI7$>SpyqJQ*&s?(a);^nG^PQ{7effC${6ZYlhc} zWt@VEV)T2!`fvq&3@ik}BI0oukKf_5Q`rKT2G9!y{7ePHhD>9TAuzFF)zwLh28@~$ zC&Ye~ld1DCuzJB>fj|HIVAOmAD?Od@6QlVvqh+!EjeDUppOjW{hU!OC%jWM+ESCSP z$wyObhU!rNuZ`BQd^4{sBLCss{r+&zW<l|Cf28Hd13&ZsMO)PWiz7e%Hdk;c+`riJ z#=tW<mS6Ry95dx?t1-3ZNmgMz&$f6eqTa}@4Qsb7dCR$P6+J3iue|to10PPU_flNt zMXvDThUFrA`LzwpYoWn-UO{MN(^R-z_pzz!z9Nio>Wz!4#alnnEVpp_5^)vSb-!y_ z9@E*L=xh=5&x_tJik|v;*-y&i`r??rVoeYBR@4e|@A*sRFH1Hm4?nhURCax=KNdG+ zKe&AV^0NGa^L1xBaCy}A%c_5UY^4o=Ki0Qz?SThAeQCtZ)!Qa4uW-3du(3wC;)5Hx z2Uq4da=SvqTP90rKwucyZ;PI#8xL=65SRWz`Qh|G7&+n+mv?Eyc!|B@+AzApin!Vw zQx~kM3zl;}R+qwRR^XaGe?wOjt$4TQothOpSJ$<n>tYpM-#l%@vTKChfFI?YX;SPM zs8*1vU`AcfLB?vzJV}`bG?s2Kz#0=LW$g(A=qT>Q=VkM9XVQEScEi#P62LD6X+;DE ziX8nrn7~kmrS`c-)e>~-U5=58OR_YWv}s#{l5n9NBFSKw(xYj2JZUbZR&3K?GG&@J zU{wfx8wD^2Q&UsY9He3WJBs+VE08J&mNaNod%%Fnl!VFJ14f+&qo!0~qH5-4O~^zA zg>~yu#=146R>C`q^zp7BaUG16OqA<VrLW5d4~c4(*3ud(3u@P;)&AER%fD>xx{ReW z*RH2^q@L1~24R;1#j;V1QUgVXG>U$c5$DTNWXM=LGez07fi%)a*wKX9d3T_U$zEfk zOe86sNbT->CJAO5C(X2Z4@_nWindUeJuq1$7{Fwut$SdyN>FqTm9qz?90>--1B^+^ zgkyAWW_)BWWs-Usok!*EfiW)~Bb7&4#?kyI1S;7!G*?RjYRso(FNULX0J0!W2c+#E zNw`2y<WmJnDhsJX=r2WyB_2%ibP-iFinc0u1!;sN^#<rA8z$@-?+Xu$Ls;&i#UWh? zxC@izcgwyY#T77-ixFM@exS#e)}GQ`L_reTQ%I%55YNLT^<D{8lB_>(m)<L)Y-I6n zdoxATc%_7wD%oEvN@eI$s(2Jku-$<wrEG1<rJ%S-Li_<}iMX^Pq%9vKOWG3}7xwvz zB_KEGUN|QyCdqWFuVLw_bNDr!E*poXl^CwcQ+;h58FWIR$|RbT@5H_zO@J)Q;je-$ zr7Njo*s+!M|FvkP)1@yVo3fXd%>n9w)Uqj9j_#O~2F=?`^ae;LqKFYGntR8?{m zRKF0qA`QCch0v90(6v;x)Gjyd&Va<K_ow|FP9C7`RL!nZH7_hxMb}ZayGqr*uv9f& z|HAgv?2ux*YD)B=<Opk^8j=(cLTelLr(i!_b523lijt=rNjp_a)=`8WBkNn`23QNy z4zl67ye3%cW|+aLx_xK;x>2+k+7%>?m+U3Ylr|%7`@XpCFNM2>%Fc+peqY@6FNM36 zY^0iiZw|^1&nBu4p3PJ}JX@$nc(zgwc(ze3@H|L0!}Ac;3ggW}x4o!mP?eNT;!jfU z!YNByNwhZG70NLv*+$u};pE|Vv_6MGA3Qfswo^9YTmrTaVWikb;Y@OOAlp;t60j+h zHph*E{kJ<vQp||y$X=LW3~K2^R2%T3gE|DfkdK1Zw>wC<>4Q{xMooWE(x;%eD>6Zl zozR1iQVz0<Y9fzOjbt~~LLR4@$rDs-Q&y^UpKK6L4$&+@c8s>~rUR_LatW3`?7+zL zBRJg-TKfo9A)K+@0Pc2B9ptfeZqc1o=O|+Ku0ZxkxRMer5TwyLdUO{kB%g<#g0Td$ z%Z&TvbGU!95ANro-`EHD^K^sdAyD#Cbh`zMfa<&^Bgf!$a333p?i_pzc>a<tOq{Mi z7D{BI<hfQRO01z0Fha-|QuE&cvV%OHF5Ug~27Qz|N}duda9Byg_IMA~2|c~1RZe%E zGr_kD!Wb04ofN*E6u!yEI_WO57wS5ZUKf<@gS@U?dHt<Q`WSU2Q@;o3qv_C87vyyB z$l28%>X;*WmRN>P{AR`RFn&zxlT<r(1o}QTf|IAEb$6ywOCFK>Al(gCbq*;9tNA#T zya=P_gtVR%jyzyf6xMi|*Mw;+s68OkR!Mt6oJp-K_kb8kK{ym(OAjV&=^k2>YWbxT zR1eU9X#|(%;eC7WNq~J7+ImV_y$WrG?KoOTWnO`%w>2|FdRsF?oK0(q99q(uXh|=v zOKZs~su!rc{E{u{1K2Cjl74B`o0OV?%Di$-Z%Jl|^p<3XNS9h>i1fCmOYJmmNNa09 zb^4Ex+8J_?9w3J<orKYL?ouB-hp9909BIv>&%SWB9iYxq$J3xP><*Em$(eYN%B8x~ z%{(eUJw+Z#Pf>@{Q)EYaiW<x`C#ObWIxIc3WB=@xm)Ji8D`9t_hNRIA-mSZL&{FH{ z3JIzbBe43U_u3S!du*5&4l+vMOalGu(+vI1QF#gGh5b9>cthir4mxznm?0S}O2+np z+rPFUQmo59&f`o_IUks8JOVrRaE3qOL;jP)LDMxjRT-bFb<`cVkI$hKmg?u6Y?BUI z1UO4d9$36u@;VF}cK8tnwL(&G4&;@vg+A|L(1MXy&bY37W_V>ka<Sx<f~O^~K(3ZS z#XVx6H(0Zm>nQqaPx*xfE#Swx(?wSsM82&%^9Zzak9<Fq^aqVvXrWdMpx^=K@oR(S zQZ(t~I}4{b;P#m+qtXh6f~9?;kEVZq7GUm*4xDLl*4pF8x?JP_`YYGEuhxxU_qvbS zgVn=6!GlzCGZy^X5F2-r8q5ReD;484SCJ!W?YO(%E~J9xDK|Id1191c=tUR=$K_6K z=kOOl1nv&+*j`4<ZM1B8mIIFvI6LlK#)BIO{snx&7zaF{L*NU3v?EY09!03TW?Zw^ z-7YxM5<Srpu4hRn&eV}s0`Pq(>_@xEAuNhV%m0l+a2!BqLpMElgKzF)OR}NRP9ZLE zth=kp)jjU78=nJF3TzyoyIMa!H(oc6e3jAb%C%Hh!k_NB{DlAVZgAw$ng$!J6=<qW z$tp8Z_U>SLPtxs~nDzSos7C?&Q70E=Km-Zo-rJZM5273aGZLpH$1(u|+-SfxJtIJc z0FWFxnP7O8e;T|J-MmV4WClkY@Ex3IJVE_6k!7`xdi#N(atx6cR3Ptg9*5IQcTj=4 zRWPf|ML<8R2$ob36EiNq|5yd8uwp0q6+vSnCqZsS(2~eVkyjDaRR{uuu~bO}HQIzf zIT6f;s3e=ZIwEL*bV4R5se_~-3rIl!P)>jwW+13ZX7ac;DAQ_rIh@DwGOwGLyWpV= zAg^^kE5TymguqE&fwW9eR#(U4b?_D3kU%;-ekCXepKKnt+i|7`dL_7XG9J)3yh3o_ z)VV!=!Fv;Y`h%B|S9~S%Ru?W25aav!ODC%LkAteTgT}1wDsOBAjfni)&x0pBF))Xa zpsNCnjotQO+fLLkf!VGN9!L5KC=#S7a_%L3u|<X;ckEzCIs(M&m-gfiDzw`1RwS0m z8F2DvP6O{4wD}G$u(E+(UMV=r^H~XI@hXu#UV&=);{PDWF&Q(C7_O!hj`wyYQ-`o* zc);Phl0wr1uN)F)2Gyt_CBb1A4wN$$kuwVj+g((Lt~Vt{yL=Zolutr3K~&~j@URo2 zH?YD7eFaW|VS?ag3=ja<NhaWgzjtJ;XK0Am3S^1S!OAJdH%|-RWxRaK6M$*VcgMr< z3Z#!2usSgYE$x_MctF4OxEMgB9XLHaLiU~NIp4?2y>kKa99n^HJ1|`c8mwcC*POdB zG&V3aFx-b+h-ojnz!3DndHVcyUNw{Ou49BZgO(*}=h|mcd8OcJZ$HNiGp;}3n>-1l zLh!x=H$0(P4BF-vyz-Ea;q{4L3<uQy0Iw4}j+oG(;Uq*EJu0J<KxbVvkBfeQpuhkv zB*DK=07nUn;OgK6Tueale*gyQ((?Em@lJ;`ahNVnvb;W#E0p8)Fz0#@ax?++Ix!`F zLhjfBr;FE!IFY-dm^Hz}U`BU!oY#HsyQoh8O{aLJhk;#Rj5h%*(2XCDJCO}42i^GL z@mc0N0?>l*fYa-CB7LJ1F5Nf<KMy`Xad*KfR**uFBYhAmV>}Z+#_i{G5=|5C+8|ND z^`tpoFW#9E1py8R!c+p&=@e2)z8lV2<n@Rq1&FCcc7l?EFU%AoP8mRXe3qd8oU@Dv zWg6zB+e2<3oX<{6B3)?&XB1FEK(B~da9$TP5aP6?fZH_Zxg&{1pq)2J?GqUVqXha% zu6R=?#SGMVaO_N2LkSFiUN2<`9u^7&#p@<!JT7pD5p+1T*C~!5Bf95=_yn%)!l&%C z4Ag8hB2B_4NM7(h6`f0sQr{H$4mC>|Le1HUlwb710T&IC%@VZK??qtoBXmBo7QG+| z2~->v0-z3kkf5x{0Yxk!j6Ff%MnrSk2i_!c6FRL(xdDMp&d<yUN&h*JX}UuQ@^t<G z3ZfC0E7lA2ft;L@>BhvMHU|Fu|5bymwM}#G;tkGR9y+_JF@$d{S4Oj9n&u5n^GZ+L zXnJt_{_Qsw!iwL^F>MRD?VBtQ7G7Uq3+kg|@3(%?@m>dKJQh0hsm{8YUBDLhaM>qW z^~tU5ya#jl=aygLvdcoJH?t2!E^^t8q0?K|qNSq`k1hwJt(?^n8j4#Am+BwZFZ-el zXK4jD5>x(??xAjZC|bdp8bbqLs<0va63$sJeblw;S~~Rb&~kpHg0oh|to3Wwdd}Jy zz4h+=JM$|SA7AI1dY)K&KI#e$vX)DqoV~!AsgLScUs<YpShbuLv2d0HF-zT=rH->S zL|yNC-tnxQeoS(X6HhEBKB^C$WzE!95%3^<XHAhGFC-v#drgt|+cU3lM)D)gYRi)H zp>lC*nc<NCaP69@mNV5wd*1DPr*Ea^aVyu*{lwJ$ktTG8HIm?!TSTymyg2yg+8^3k zn}f47LF=+Ci`Un)3ItA_<g!n(>Qh_A&8(sT+>@6&9(J%LEu6I#*pg*kWY)6^1;B1D z`#7sUzGbaN{1Ol@4{;XzE__{F_Ayp{Y|CI?Y*;+X8p=ZbaZ}Ec=AmY}KJqQj)D{|u zXPFjH{`2n$SUb4vPFCHC!0pR3e{r5|>EjyvH%j|i!)YjK$y>blvkA7Yo2x#)k$-%( zp0x~z2I0<#HFv4@&04m&W#uSmIUXAPq`C#(Z{LiVS6=zx;(HfAc=h`oi!Dp-58Hp( z@%Bw5i{qc`@^<u%{6bcd^J}v$tlZRRKNxs@fX!=K$^Eyoe`okB1E=o~<DVK$n-&{e z*0Wmmq2llBS4-HE5zaEo>PI)T&5PwrH4kf+%fDCmhA%9Ss|#Z4vNd&C#2WEL^dGAm zztmxt5^(iV=Ef9-Yl^~0bF89pUD5b?Ll<i<k5p`!t0JzC&9$qjzguz_T|ct^=zOH; z&z)RhBWH1h20?gb<*cF@+_6D2+X~EfO9qk8@~ZD^!}9Qj&HTcpyKmlQ%ZEPntY5eo zyKsH&!gVfxBCP+^SS-+eV%55;WJ~%v%V}1BI-Xk|%dJ_<t%>4XZe3XWnc2Ej_+}wn z+`X#h%!6UoCh)Z5O%VR^)gsP16jpCa5GPjaIP<x%>hp@)@a+fp?%!MP`R;e3)8M;a z?N}^WDt%ZQ!Pm_dD`�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 Dyl;{d literal 0 HcmV?d00001 diff --git a/scripts/__pycache__/update-taskscpython-314.pyc b/scripts/__pycache__/update-taskscpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8c47a85e41647e7a6c99a8825e5a2eaf472e136b GIT binary patch literal 33532 zcmd75YgAiFnjm=f0wD=Kc|XM)0TKpmV=%_X1`Ouu3%<y<01G5z**uh2;D^##MfdFP z$eA;1oLSY#T{Xwb&f1g8>8eR`C;KG1Rl7;fnLXv&?mctm3n``2Gj{jX**f*l`KVK! zHM_I>MO<C!f`#nLn$u@xa~~03eDTE>UwjeqMZ|aO)tL$ce(4|n^6G~+g80wqh5w0T z4_|M=!zw`%%fu8xiuy&%B77Gwi}78uEP=bYU)m>ImX#8uo|NPg^P=T6QkqLFr<1Z} zIhi&oZ{YF5olF<LEoYE&fK-qfa8;59Qi0Q}NF}^yk}9~W$xOIv0Jj>)X-N&d>qsqJ z^^*otcf?8%4k$|zF{v*j4#x5VYD5+I&y;0XNC`r*;&#}Vo#th`=bA^M7`wGNdxc(F zba|Z~qkYj~q@8olMJEj)9%J{??8<_3aoN7?T3T!zM=2}|j&?<p#W-rGJpggOYiQ7T z8PYE=*q4``wDIy1ZFJL1SDmxVo(7}S?V5!f%FFBlG%JuM_PTt_jh<1KINIefyJ_cT z*G-F}*<$RuxlG#uh0#7gkK>KAS5_9UaU^<{Xoy+9;xu+Um+h{3r^AS8GkPFbpw#GD zrd_kk^S6vMw~UKW0(j$)^mkhnEf(WB%z{)X+7(8l(QF)Eq8IG*9B+6+`nw_Fl5u6x z1!Y|@I$U$EW#bK(WBH1)cEPpiT3A^yws66S1C}|9ab(<l;6Rhf!Xe$oa#VJs0~f<M zGru$oMXYr#&d#qmT#IwYB{wRMeco7C*8{9<ud6dbR*;h&Y7IEOY@Y-8v9XbHKz`6- zB%Nm3x#)1xT-iphLq!7Jqx8}o?eutnO*8XOV=c<mW1e?;mQASg7BDXnSDlUqBPSnR zt;8tv#Mp6iY2IP1t*aY%Ezdg<Ex5KJLqsb^%9XHx8JIQ;kQ`tPZr;AQhyZbw(=Pj* zQ<!FS-tJm7*5VXg0+0o#qtUUn=xkhq%0{I{B8w|#9HhrMyEMPDuxOljA;GA<h{7go zE0&hcH*elFnG}bf<HrK<8HC>s#iCu;L2R5L9e@^?0+O|i*#M<6E?vftj+pqzN)KGb zVr{G?F^?fVW}Z;|dC@n;$V9a}78Cqb=}^()Eku|Y6$%Qse~vgMZcHY{VT-ZL<8dx< z<$;3Spk1gPqo%@FD_<y!V%Q{#N=NO>S7t>5#iW5}3H*J10F=clF-{y6)x@Ibj7Ynb z5}+nyzfoR8@&Rxkl?h&Ak0fz0mWP|52p*rK8R4^~^YByxN<@i9Gx+eto!4q{3_xiS zL4k&|DS0STL=h^YS<L5nMU=?FrsARa*ils8JwHB|QSF`xcy)_yX?*CYo_|W*6PlM4 zH^gZIeJ>HdeMcP9?}S4pO!Xb<*p1`6(ed3luq!W0oUBe#ar&iFyi6XuIk2lN`{c1Z z#hyG;)4V5kZ`$3M_s~M_j@^~kqIOFtiI(Us21%Au(wAvtbGVHS6cPj_T@jZO6+{M6 zB5ZGId|SFAGNnfq<6UC|W7EC;gFW7YF}HKpb=ie9C3Mrq#ieEFGFBEH?Z&7S6p_a( z$2uJb6<+x`((dpS&D=HY@HU=WT7hl{rG@?x#wA9SvXM_{gl-TA@aav8sCdZ}O?NI{ zcfr6UDw%UG(@0N6(_NlvL`PKD3)kVL<z7Tf4^1!8QTa5Enx2lPLr=Lhf87~PbK9Xa zU5-j-Z#Yb{Xr}X~8+tX@^0XUtW>n;kDm=5a%e_49aM4l4EOcuwG_vwUMYA5H1FNB@ zGDce)&v~FfZd_hkXykj4#*ul)NvEAY31cScdKx)Ao<`x&EULNeT69d~Vq((h9KZ?n zMtHstx^$I@WR-=px|po4pOEifc>BV;Q*Tdg46;>St3yxHvmUFoYsIXp)F+B4Rd?I& zwB0@W`q79X%h&f<UCNYqY))>DFvdYvJ;ca|zDXnWIX}4aR!LY_6VlbNI#XD8Af!96 zdgj-1)m{1P@;4M~)Q-H+e|1M*`#4q5^p3nT;N6ioC7_Z7UD%PACIHY|M6M6Z^F#7{ zfA5aGJODwBDT79K<n=+I=12lc7?hwE;&;Eq$w4vkmuJNqecI}ouZ@H{Pr$N&fmrr> z-<L87{zgtHi<qJzRx!*-hrfn;Aa?besK#->X~X?yBjFa2&|AQtgd#?TJuFHJKfagI z5<TKx=xI=UjQ4Bb$Nidg6k*)+BSn0B<DuuSAZSudN!tL{VUMLv)(^*f#dz8?G3fL? z*}y9)o)R65_3$^~PE%A`DvgmWj+F4!PGl#G=ebag@{93n;B};HcnTdng)hg8mOrNS zjv@}4g!!0=NJXDpY#}D}F)AekUjT*T%baHXdqi!@wCPkjUz(UU{VzC8vJy#oe(@<B ze}-rTd6G=%8wwJ&G%6Tf@S#+k+oeDg^aMiq6yqtk9Hd8%rROx}h<r4i$C<c4Cl7x~ zDN>a?S)4!~+LFZ!<)KG3aF-z9`sTjeiuXM{x8|Vr6`?dgg*1}O{M)~MVI9Re5XbO; zLYn`7@IgFaAaJjXM!F{Xa8zWBirT&6@*1xcO;g){3*{t+htZqS_}l*%ex@|&o}j;4 zhS?ExNeY;K!=xF_9iwuXy>L%53=x%~`La8jW_P<G%A|~@UtXAAcHUf$%40KXPgFCz zLZjYrIu;UD@c}TyScpndHc`nn=dGyhx_y4d2}u!Bx6=_7U%MU^U5kpYdyvH9UJ;RL zG}77d9?gMn*5jPMWnY+|7EGmR3q*FppC<^)6S_N%ZnZCxQ?NP`G32feKGo)iwdEmg zd01QNlRi;u{L%-S`<kF6oY(LuuVKTmDgCf;Yy6j!pH4DkRJdp2QO`sqzxYAH{er-$ zppMP2zjMZSDpF~Bci`=TcZR~1t)a@+O&MF+>FZlRfA9QT7elJDNJ+VGBvM%V`rwmH zoxk!y<Nd~9MYzE7sKByu@S|fN9NRL4TY4U~^Z;O>C2-R3da!hVi7jb_RJC=!zL2U4 zr;cP6ZD-a-atqhJ_q=c2_Vqn66n?JFi)ajYzklcZ{_9~)^&?Gnq_k{J718GHXiZ`5 zfsppVM$xvmGm@R}FaM!+ed^v6n_aagju>+M<v(bP6c#@?a{mY}OJV)J?zOInrSbi_ z_vZfE_5RX(OPfyC(!DnLZtmN;@8oZ1)qf=;vWlOfety05UTGjFtgGG8)dp=l<{rpk z{ldKqZ`oqF*Z)}DU$zjss%M7@Ro3bVoeMSQt+CCx9LFw}+iE6jFWSxCNpv}2Zkskw z)|qQ<j(U>`M|!m#wa44-Hc!3jc<sfh&dEA&I=-1YllQGbz`pu|`DYpQBa{#ZwT(T$ zgE7tPhIwXObHLnCI2%p$voM2<>rR-LCegdlPlAx5AAg5-D*l6Z%4{~<7Hcha$4$0H zc>Y86a|W~cnAiyB4}MS!tyfGtqcSk6&0dM7b&rg94Gu;X6lz&`yoy^#R5nL1t+-9% zs1&tU8oEBhgZk_0SkuzdPP=1QqdEeyP6T!oE~~_2mHKYq>wQdiZLsn~@kbdSWU#9C z)t)Cx?PGNwli$UvPcrh8kCkPCs(0((t`FMZv9QV}M%skyc~>2w{sM9J>;H-|e-|48 zjFXa$Swy6mABFIDw_r|BiEMJn694#kzD^w_xK6<;%Kb*;^M5{sAV6LuRD7Jnr<~)Y zA#NQY<Nfj#WSh_>((_Mz&jmFvi>lEqXu7|9dX((x?YDZ>sAkP^8)sBJGVaZ}*k!(e zTL*WKH4v{I&18)I-C##W_E6)(if0*2nxMa9wm;*%6WdymA#D~+ugGw=v<T)-E*z|m z*eD5inBKtLV|EFqG!1B?bmf-&igVFp%Aiqo(THy}678tM<#8=S9*eV}#X)n^V45MK z8JAb)=dldZhtRuh1r$0B@`gGHWJrTK5~gvN)ahZuh375PF2^(%PoIS3^YG`n02h$E zOwHX3cP`wWx--QTH3aA0zxLiWrv1#rZq{;^%_LWQA1kxhF8_EotSo<|EdOcw`wjoP zfoUCONNc$6{G+<_SUPXB>em?gYZ0aHZpWRDH#!+<J|~vKVO1vNhsAOnXH1DySUr#; zTdctn#FT^*+u~_#aS=?IqxeS6s|~;*ZkA*c%@U|=NmOKxiVj3Y4tSI-*l$K9^Ug*3 z7~}^5X<%WTanf`byhfF{UZ%0J*n@JcuCC@fK{c`^+v9Ab2O!de{!l$Y=cmr{S=Y|4 zUH996A_;T_Uj6XYFC?42KUcB`yIEroQ`);_XNpfVIem<>pON-I-#n4g3iIJVVT7rh zCsLuw_>gQ+$cw!IW4t*+R<R$?xm^kuCOx2V<LZ<%+#$~L{07B~>JR^*(Zw4h9|m=o zOz}#*h0v`$*YQ9fgSsBCu744RD6W`=8`@cox31=>5vKRBd;q2jJ66MJ;Q%TQwHmGs zo4Ej!9JdV`4NY(*(`E1!RrH@89wB>9b&d6KZAIc*T&5306f~DKXb7lLJ*S{Exv>vY z4$xTWQ2@UIe;yJpoOT|3eUQm-{e@wZ`gviv?M$fc4BIyNcQgNC;a3Z+$`)7Eg$+R} z*vA~4V)Cb1^(97r>9JDnTe<t{omc%$Z+wqcmN3$i=Uayq^1)Iu1+4)()_9vp$i7>W zXd8*pWQv-X1enN*l1U1QYHRm&QX=SB<3sr)eT2a1%uQ#}bS7EH%jYRDQuiaN!thAP z?fD65m?CyfkK&S+NWx>MWTUBK#i?Rj#AsRtdO}2Z!-FXu*Rnuz(r7#ir-vaHnW(Ui zh|1=dZh)#yr$<n52DjdWWCSS^8p;T;F|mne#$q{L8-L~G)q}VIdL}X8*2L0HW6kdL z(B}c-d+_I3hYM291^&ZqW;tkKwQjBYjcZ|bX-Hkls>@bSM=Bb^6)mBPmW@-JI=13y zNNQXi0uB6wF@G_eT@zB){GJf2(w?D9M6S83xuap_`C++>k-Pj852o%<1y610*y5Ir zuASn;;o^=^aR*!6xpgdjiVB^g*i%;K!lm$q%b^RG*$Y>glde#)>zPC>OXFlWCOtqV zK_ESiWf#fRDh&CYm8j-Z8(WeYRQOsZ6?g&+IKh~rtbk>OWKvFoA%K@+Fr@x1S`q*Q zaJ<*RwBZXWfgxm!;#3A8Fc2kAFMYZ60@-hh$-X3M`;(Drae9)qjdVeLe88J156>rP z-;$bGaLk<X-@{ZnrI9NY-ziDT*rY}<o))fk#i)2l5EBPm#7T|Ra<nK!W(fNhlWI5$ zV;a0hO`D)V(CF+l&UxHU>3kF*n)a&CEnZt(y0M7YvZ1T6Kvx0F*w9Phc`oW7aH{9m z0x@Efl)i{CNf%)q6Rt3zippl^!T4(u;~0D~rQu$L9tTivKn7N5_q-iOW3pwm*oylU z7*|RWQu+kOh$h~cZh8{lIg>ML=G=%v6*FT`;}n+x;-4d^r*MJsM7r|NZ?7$L)6sCQ zDU@qsbL*ML!R_3^h$h>Y8PVphEAJ`&rvv0?+FGA9qRm>D-;?`u1Ldr?Dy*#!Y3o_7 z*(cj4BCMTZv@;vo9~FO4yy@9G#WwYYn?^%Tqs-ZHhC0tST?jW_3N>9~n`V46m~PxT zv^LJoLc^d%vTN9EQ_zV<g@3)UdG3>oA7A|WBx4@<EPEhg$Xn}pq$>R~gV342Q4lar z{9eEjpqTpeO!fs<agmW;e7>JU69B9$u1E>OuC{AHUnC}eT#E-CoxJ~}2rY3AmOd=R zinv#e47CEd__SbHJOJYvv^26Jc1xEulnAuLae`P<C;HDZH9xX}I5YJ1pdDx`G13~- zP`gZaiF^`Em{h!<<Z<zB8l^@C%7I3XjsfT@5Itb1gfSNAG?7I_Ng4#23e3}DQj*LJ zM`>C_X!FXvJVAY;_t(aa*E|gkkT3rOvm^fqcXzsla0{`-P0+xDXp9Hv_vgWsgfE|4 zL1iQx1Ax|wjazuF<T#Ccf{bMSauVnR7+Ried`YH}>6Bth+z_8a5`>V0!jh6{!Z)6# zQet2QwByUt0(uDgu;jELN<{lnvTyDmLhi}&ab?>V<36Q%%t;j~C*?^cN6wdAn4ZiK zzPXhk6Dr(7pr)48`%0l2<WP(^L&<INDR;ui{YS}RCZ$SN!&D|uFVqx_t5A!XcvVO( znRH=*Qt|18uockyOR)xU5hf+p?SGKAgXPC7zHRhMjJJ)uj3%fpqDjHm-Eip;mBY9d zZMo<L!zk7bdI}bh#=txw9I2ws4VlDEVimTU4S>B3QH(Zu9g?_2?}?+Lj^XzRlQb%} zz?{?-6@mSTn{awS_aXD^D%U||bfAr1H{En+v}R_-HSd7&AbrgNT^&r`VF!t&11-1C zdpa#~zz2Z9gBsaC{ubzDDkehR)W_YMU4d7Ey5QlBvY)jD>w}q0_2KQxR<@h^7U%fS zij5*vas`bRBXH*$72O7YD3Y_{5<qo`<E*Fv6(X3dkBN6PgRg`au&HV{o9L^M35>|` zBwb1`Ai@^m5|z+PH)uCJ#hA6qF^hf$LC|pd8H%fi3&#SZ^;ep$rk|V&N`hTKOM7Qv zL$p!7srwt#Mq%(ZrsCLkS?AwsVr;0lh`{awa~l}WqkTtsj~xy2&rKtv(yL3ZMH&oN z1YL?Q<p@$n+ZX4YCT%qR3YhZ1mJ^i%g<#Kz3GCb=+MDNz$^qFl?1l2sODG*z4j7z7 zm0X%>Y@643oG{t6FE7!seho_jQIS0=x)D_;f-zVIpvMV3G^%JCbQi%f1bR1q9)ac( z@H=sWlccO<;zR#;0RJZZc|HW~{Ou|MHu}#sx?gMbYprkGUhVt5q>9a~zCY*J{fpVP zvj6e&{kc$P^=j`I8iP;rIHxoq`HRIny}qu;d4>MoK-0hK|B2_PEkVy;A9?3BHt&$H zcdhA-{;wrOZq=6(LYwtWPRR9-((@xZMQhf8d?&lsr+BK*@n<7@|8~X1zwduIx7|&# z6%%a!#EyOfCK)^WOREEsoV?YM$J%^<-DldWJ5rzMu}ZU+w)V;oGyF}gs+d~}2IG=n z5>^&HQWgbt?-smW5Ips6>0gybRE1$xc}NA8|DXZtz3XpeKP%p;IJ{}tsp$Ajbu5yp zMm8?L<nHwA(?Z~~p#5i6|I@LJ76ATC)%tWFJP`GzhR{_%(-JxP{uXTQA7Kj*?qna_ zSlP+$SRH<<)vxwrhQUh0nsfc?y{kJ$>)**9Uimu>YqYXC)*X$NxoF?f*kJ@G*M;SI zA$guZ>oa)?ENNq|sd9IeHNo=tYu>BbFtK$VJIW45+VSmIErhxTk0^hf)z>K9)lfk< z#cT$3FqXQ#kJW$>jvHWk6^yv8XaokoqoQfiig-mb9Uny?t5C8T6hF#`af$@}<E==r z1r6D=@NYDUwF&H3Qmb&ImZ10M)A6{_h8QI2=od5k#^?H^j8`vU^pzy*JredO7!@XZ zsQ9SREwdRo+|UchvANU0qJa3g=SQX`=9cdCePCcb6(14F_kocMU^4cB$q>LO_JL6d zV3e=~t%AQyDjh~{TcDQJlsaiCl%$HMa1^Zv?fJ!PRkKfe*zo*P>9v%GuQ>;3@*-kf zE0_yIZD2c)W)w~3_WXDnCjP@{;hrdX<zvxkdgA|~G%tdov^*WLndm*JZ7rpuv@c8< zFF`Ejzri`aFx|kv<2p2`4;}pJsSKzS7V5-68UDmNNuK_@)Crib_S8vPyiSUr=Q^n- zHSu~!UErP{Un`XEC2B-N>0bhBZ@s;P>rM9^>TL^BqC8(*b30tkQ3{w<X03=D(X0}R z=FXzBURZM}WObj^f_j?~4}d)yjTYV6klQ~^{sLVf$Ule6zxTBi<rYw$Qj2K!DY+o$ zl<76kwca5t1=%mI%Y^=kwO$xjf)wPuIOV|iV^WYk3T<NVkJM7xNo7Gt>iHJJ-@($I z9EWZn9NMHfa+1?Q>IC^DhtchVQG!(G3Zy!h%1v4lVeO=n>g1^fvLTSFJa;bjPm{lZ zdO`mAT>e<9_ba!6@|04keF?s$?pycjHcqPZUM!pYNOk^;Q}R+R7@eV6%$^^N&J3{P zni#=V?g4<;_~@*FDv0;3D%5)<Rs_i`!RV}Dlm`<a<9iHup$*x0@sBD<I*yBP+~9G# ziv$!VFGcoCm!g;|=E-|*jG4pd2xH9Rl%*(nAw{{nD1!0%-d~)rr7y&lw+mOQIx<qF zeCZsx&!<XLm4hlgqK9p&?Wi`vz99i<^Ikd=V=&m33HHprKfY$ke4+KlU6vY`yL@Pq zGVY<(_(#!d{BKBWN?bte3|Zi=fVH8fy`_g<rh=CmcO_LxUPFif=3piS9IXWY{sQ>B zx3v`VDL~E;*DFPYyGkGh&ClloEwzYLQB`rNsiLa>nA#YxbeB_Ns+=l>ysB-7_#6be zCU05ARJ8ymg&gx`a@SBb@p_daK9D8u+95Gn>aGO}3aKKYoKPh|k4caNTBqOhgR&bb z6W{x}>!>>D+3I7f0AK)e*HiVWG&oO9Y-9j^Txxh&KY&(%M`v|_yDpx7Z#|arWxgT; zD{2FxtM>-bV-}PrDOYW1^B4Y6Lf91Cbz3M)oDMjCuuJ=vQ)aSauU#@ho9$>kH%n5D z8(#<D)3_U{hEbTu?fp@Wl({7i4K~kP9G_s`bAW1q(gTUeTIz0eo2diI#xYc5Vji0` z2QyCtV27Rp)dFBTzL%1ORsvaQJlC`$!D~Wb*No-gB<N?Fc$`zBBR_!FiC38h@<0w6 zp{|Q!b=?e9H&M+zmmSbc1DBK93~XIIJybJ~8_^MyNe$wF?Nl755mt(TE9eLL!E6}I zRtvyna-p!{zLKmO?cv`NcdCWQzxyB^)X(Vz6#qn4=MrR1D_Hr^55}Vn@zUTqIMqO2 z6Uxc%eHTNbDUti24J_~6Z%UHjp>2Q<r4I5G#qY7Qxeq}rI1GQSWUc#1Be8TF$wZH= zHxjQEp|mG&8H4eBgs`bvMjO?}mvRm`zpG`KsKz~fYfIV!;(M3e?JsJ(WF6Jc*QWdE z3!&=;&>b&?ZV*5pdm*%00NwdQXo~>)xD7@J+>biWbI$=|0qC`kr{>U*2zo3T&jH-p zPPn_M4j#+gSzHH+F=f(vCP9-3C{8t|IZ2)5aXX9%xAkOd3RCq3h<f*_p<b%vyW`kR z&7XpaqlP-V2jkK2P8(o6;O?f5?ZJ5LPr}&b?x8yOVC?*pFgCk;smx@vQEPW9I~|oW zmX}HQX(}fv?w*vmQ^v+|_fh%Da8tc0ai@%p<L-xb(ImEL>hwP0DY*f>N?~o~^W^XD zGs$ffRNp?~DY-@U{84!iP<ctq-M>$GN^U|;{4wPo1br|Bf5X;OaS8t`tYuE6s!?*j z4SHF}zr-3-pNx8I%Jfvq*m$iBQMzQfsr|xJVud_kWa~{9-|}*{V4i0F?j9EC_vC%R z!I$m>MyQc^?Qng-$e*Bpq~sxPT;LwvJxZev#phRNsk5o--%V2F*ljBLXd*sBPK5!j zXjq5hrGk+w%#;pWN!T0LYCR3tBi13fwpsh&+HUQK>(OSVdyMR`4#V@YW{G=zNVhA+ zZrBWfRk_ElC*pa4l?r;#v7}PaNglVJz<DHg`HTtDrepd}Si88qp`TH?&soRdt;-5$ zImnaNvv56SJ%i${<0^tW2T$GB0l4;92VoSZc2h5s2<ixB=H(|&HH^<;OqUo>B%K{o zN30ZOo)VG0tuW_AKlj9tjXX`6+u+#&^9LB&m`7W2)UFG8I<XfT_Vl2g5wYJWjAZuy zcxy*eOvm=agt=w4QoxIR$_l)Yq?)_CC#b{8=0?^De#Bi$jJGC(Ap6D}$$slu@{E-v z2dtyyp!EznWE~c)#o@URxFS-K!`3cx#5zEZS_d055}12r=twP^p^*7WXC#MtSoe7w z$@Ax^=&bY%!5!iS>}PWcsx`4Es266=e0|1dU$74jav+CvIH$3pOqk{GNfpoM0(F5; z&AC;w5ahy$$bAv+i*UCM!73}c6bo^qkc5SUitmDQPg0X{Jg^M~+QP-rb9@OBck&hT z9502Xgw?hW%y<F@<~&oVygj1c{}m)}nwlmDgp%aGWW6*xk;s96A}PL%ak^ku3Upl> zJ<r1=?$1NdfjtgGC&L|=mN;&%UrUbrdFVG%;C`NNu$a6&-41NQuwI%HldphP5%kjD zAK%X0mj)hyo=ncY=V*?})+SDojD!272%!|9J+8fwn*R$Rr^yjv?WWT5ZaZZst=J(L zwh5Ay^%7_S3bgmkkk)D^X{?{PxCtCLf#by1OYRx+JmfVh%nQ<90O>ljC+uRg%smU? z{lakT41^2{Lo5=v!<uMI`MAy;$utS%J0wiW%PF*nL>o$-et4hsj?s&IYx{YgT4ycs zUKH}2N=75)NKggG%(pnLWt-%&TQ6FttTxid?{-P1bID^o4!0AmlO?1C?2(s2n|MHr z%%S}i=hQK)4%kIrW}n52cT(&FF`3wFu@A&l0>UB%OA^>*uqEk=TP-N{9CZcgUmg+h zW0{og%LTA2P}ZybDm1pE9f7jG#47kc>%D@NwS6F_1tpO{Nniy6m*ko|OHh)l)HR^) z`b(B%9$;@kNf!8(M_dwAYx}JL3f2zyS>qFyB>DQMu-1}82+Jz0wMBQXpsWkj;vZ3K zOECI!QyFp1ehszcSWUiycA%Uy#H1G0<V);C*=Mhcu>8q)qX<hUtV!B!5R~o}3XVy? zbWM85OOyxM?c8oC=Q1Uam-8|zCnbmStq97uLaij5C6U7VO%5R}Uvh~3%6Hwpk|-a? zdFnbpB1qZdZjg5B1}@z)Ipe-b&RSid)g9J(xH_#%D#Gf9zh!v4+??UQ^}><wP3o5Q zvH(iK>%YXEoQsc&y;PaixoZWcR`^C96TVr`3*UCH@KN4m<Ku)r6{d8*Dxfdv_?^5$ z-G*}xE_BX;5<(}j{0eX-u^Tp|CoT7@yLLfdeTiL=(+NEM6ZI<JTfi81?><Oat>AuC zD-Tsc7>WAC$XAS{*9E)Mi@`RJ{`F)H298RK2^Z|b#Cz4VT>P%}#8YlOEb&wy3zN8; z+>P=0#FIK6mJE*}9=qat0fmF}9DSynQ4#Hxtt?+Q9|32D;J9RV3Hh2Frs2p2K{vrA zD(77;(l+#}^sLPAA>K@M(lPE<(W}6(*jFNVZVC+v@Zl-JkuMo`JBi$rq4QeZaP{U) zRaES8HEQ$JReCF)3pS!1*T~0Z+^d7P42~k(76Bf)g7DOtj^h_d3I4~N9*n1qe~UXX zU4WhVaP~NX#oU3sWMJPI3Evv1IGh_#>`ep5c6Kz{JH2^`;=SHKab9AD!YjQE-UYlh zd*gZUpeWWP$XnO(YU^t_ciZA*oox|HUcVnXX3mwVM?jGGph5v<S13B{M(_$!jw>Ms zR*uP4D4t`a!kZsUl_V;!_V}?q1l;Zb)FyBB@Dg`;JLWAGJDWp}HFi6S^_C@k${{an zTpBnD4v;uyyP|#gw|@cBG3-6qy@K4nA-{8Y(|*F0tcUYu_dNP`qgULp`=AFjLIN%7 zeVqlDRX8lUdl3vB5B@F05cE7;K-5JyJ!m~G<{d!ZF>ha(aoFLc2InF!<`xe8-eLc| zf}<KSz>|>LgLX!*5<yY0?B7W7nK(G{Pzzz+2=8_!AyBumSaEJy(8+mlNrAn#z@2y8 z0*B`X)r#Z{B+9l3t$-JfdXVipTU~#5JGxz*0(prA#XR2$!ntMvVDK;&Z?m_1;C!Pg z0aj;%91t?jiE1Juh8MX<6$E<Ay5f#;jSDUhIJKDL{i<*^YxEkC;|lOKjGejiPHLu* zubTRpSDFe8kx(SNfa+pEr5q6U?Yawq$5M^~;wmR8OfiN*Cp33O^LZGjaM9!3!6gNz zgRdOKT2o~-&2t5u6gr}5;A{yT>VjhqCwN(=o%9r{XBDUcR5`U4fc478kzWySdaNCK zi_49(^LCGiYm()=`BLsR#6kpoDEDgkK(5^7UVSVeQO<I28rQ77+Pb<#&4TY@a9XAC z$~X<B;<Cd<FZU|q{*8@Z8EAtFqc;P>K#(AnR}GK60HJpk;KljlSS7?_F@Pv&IkH)B z>bUI9hzCX`&~Bq*mm?~%!}<AHU`#YUR{h`?E6!Znfnw6Y2@1HW29G)*S-Z|XAX{Jy z{C!QqC4M}q6h^v&<4JIi0c>4+e+l=ud><V<y;T;odBsj|!I9t}L)u~F(Pw69X&xU1 zhQI{~jEa_pURLmlq-gDn)B%6Q>t7>%PyuN0!L58UbnNV==*N}*S01d~UkTXQ(&lhM z3sca-9y`k&SNiiBlW4q`zJhX=qDJWz*VRQub-+6C5eCgFD!S;EfV<_W$OLNTRe<As zoJg^EOr^ugIkg1$)!w5>!9$%UgdwFz<3yEsObTyfEHR=XWilj6D8YqVoLNj@JVsjz z=gBcK@YujTlL@>u)pnGb&9Q!P4E$jm`xlYRXXI1dZ0<C9TXv&<3Cuvy@mL9P^Wy~f zC0sgla_B@QY7-(rqMniu@s@~^N)-xQGinu==fPFDcc9ibX{)_`1)TP93g`Be?II|X zNvx?S+b<ez%ae7cis!v{A<rq$?OwEbOh{GUt~Bjx9BBB9`?voE9y&y*hfC)2AaPuV z6bXmwV@aFMJ~-kgnp8)IUX+%fGzqDIUReBF)a^z^6($LG-hx{oK9&z{QnU~?#9VE} zPUqtTcwDoL9TdEW8p4}!DS<BEa|#~*ag|srTd%oS<1c%w&hOdLmorsI!c`ris*cUG zpG<r_v32(67sAJeLdS;KV<XI2GJMt=I%{RlUtrH(WUFkS>20|4rvD6~hII*WD@5aw zE1E0Nmrww>3+wNW%CPhFXnIWaql!d>i>7gc7nLIU0e1taVefk}$><Ff{{g!E&*<_Y zy8I1XAZov)zx}Vs@1U6e0f51<Jm&5lPNK@N-?FG|5P4UKrj25kQP?Ffct=Qr4ax_` zRD%8(AyRfHGzmoP28Z-<NE(SA{d2gO4siZ{rAwfz;J_q|$vK}rNPW}D$cx=6a20Gp zuP4zZ16@EPgEOC5yW53)*n6FvONJQ~g?kQ#$GPlqUbe5yFGF7eZXCcrTgK42!SVjV z{^1_vN)ZlM;!~dV5JDvZZ5ag@FWAGP6b&}Pzt+kO9Y7e32tpHg71lLA-a`&!M=-I| zpUB072lBzL`LR<d`UXlPMInN-qXvwlYxq>(2su3(!vkgu=+EZ4tR?ovTi}Oi5quFZ zCk~lL)4*>WP>2s}MrGJtyy>+lo+x=@evU8Gtq74ETz+w`z)&|rpFo!`6rqYKQ8+c~ zS%#ynpoY0eO+t}#0ZLA%#Q;$U8w=+#EZE&q9sh#eYe1H6T8Hv_4do@7pP7dvtMNV? zMMtv)@9{)YvBw=1agHycKgWRDSpIP!`fY?%jwwNy)c7eL9hHNZGyG|WsEWIT7bmFR zs4n)#pRWZeOJA6V*&2A-RN`9<P=%DS2bXesA*z6);qNfOGjcAyJZ+Ea@mXu|s)-~i zmWmHUc>&>QDi~G9;Bfv5+I|C{;speGGtT83PG{`U_B8l}<7v*lV)t-<*f?@M5Q)AT z5zBiP+>znJP0uVjZlMgo2^BYQqR}iRszp8p;UgSQM=Th|R_G%fG@MZ*Xg>nU@SvPw zd<8<vC5<W^{Co*~aKkhBEP`49w`j;496-`%P{Q2U=Lll7CbG!lBQQC%6MWY}`m7kI z@fR|XT*9YD7Ne?|GR9&FN1}1*pu}^?E#1;p;BrPxG~f~Zk)nYeVm(*iAp1yEAqL-( z>r&+68~XsmPE(@10E7E&CpsP<H!e(rh;W97X{ZV8Tos{4&zvyfCrkrEi=3~bc=XK0 z@6HSXMm6!gIcNbfh~7lrC@oY?jL=vC!5I{C6EnTQX{pRb{=+2>V0%<8c!e==RGnZI z@=F#~@oeIbfXA9J*gv$3&KuK0-bXX}`oYzWlq)p)Y)(NXDie!+RD%RC(Kx}0tY>8b z<oQ+%r3ViiqZtSc<>^6$Z~*v_yA+{`Dv`Gw5N}jIREyKtGZwf<#~$;LbaGKK7kW4V z7=}--M|G$GJW||*u=`uiyR8N>9_pJXC!xx4_Uc$PYDC}<0rU=6jT}FqrN`bmVD81q z1FNu*@t_&_4Ff9CBM+rda3}6qR~W{p=Oqyli?Qeu33*&9Ty4C2UXe4Vin&ZNdt>D= zAnya*GuH9AEol&=1QAg!U(wJ#aO}!NAjl&mex=`scI-h~8M_waJOccaUt=B!vN>X* zP)ObKW)b?hh#>wgBC`+1rZ5hBtjk`z%IeBi&pgh^^j-B=1k=MA2X-<JY;;95+Pl~9 zTz})HPx^Zap=bhk?^^xco3Gzw^6G=*A2xr~_CXt~IkwvOL}?KEMt!Qzxx090(SMdz zm#&_EtgZ}9uxc|rWtIlg+05G2-lv9w^`rNW`n|zs)?is36u4GmwY97Lk(}c7Yxk}N z%o}<(=ODN@*5|L+->dg81!-2_3_c2Cu9yac;Cjit+7DiQ4cQQ!SHMWKzsSr7Ck1NF zUFRL=+G#(@s*PcFbx2*!s%w3ceIuSI)sdRIa7`yu(;1w7fAPIV*3#)4c#v^F<44Mn zsxo3e;Gccqy6^hYwUDX~pzpK*yl=oSiGw`R=<j$UO$P%N@0#8=y#wB$EMFQ3b-}Y7 z0-P!ggMVU1n)ije*e8ytwRbPyxx6;vp8+3oVRcPNUBjwPh`fCw5P5axaNRIdH@so^ zsPKcr&9SWtwy8JVG#F|cWSfS4!{8C2?S9*jj)qh<kpnG^$`~-boBwwHJB1;YnIpK) zybFpW_+TretqhF4d;ab7?_6ZHO+d4z<jWjl09+8TI_g*TTW4XkUUjd^pB~V&`pU4r zE~Kwx^$kJ$`_A{A8>cr()^g&J{=~29SI;my>hsZyY_2UddhubCU-UqBU*?|+&}?2^ zIPX9x?*N<EywUYh&j&r5HCxT>!QMxCy#UHcvzT1l7n$f&1K*|UpfajWzB6l{^;`FD zz4dBHRl$`F*r1KG0c7RNOhTOx?jvNHurxm;&G**?+TZPbyAxcLRI?Rb+tQN}sX7)_ z9jJTP^0s9o7oy6!sI0KGG$aKv4(4o24@9K=ds;AUTiW>deP>w>`KyerrZv&J?4E3G z&QG(*9d~U=Tgz(ef?e<Tyw|f)v)RlxbUxB{{wia&kI|5y4^Ob!)?c|E>elS*&U;Se zVT{eG4d+-wITkkOz((0eRUcGsrf=!l=I%#1-M@0J4l~)-FEVq!JF_Zle0^(Uaj)JS zvn)+(YQN{ft^2or^eU^ZN4(1YMvof(t=Dfc`OO>MzbOBt;o}BYb2`!JJ6ZK{Mt(eE zC|vKn*U1_xRtJ%TlD-GS_lJW`Y>}DGvp}o-H3+urbr+M@vFZ5tCx1EkDST+-?CR+! zN<CH`9jy8oBR_^3y7`{@t;W>>_yR{(&U)LMZA|gd!{zOB6XA0+p>s2=VHPsb8P@aP z%x4M@Z%ThL^U3_j^Q>-Q^~`V54SrQPuP&5V$L2KzZ*S)vjp(wO9MjtEaBfW~w}#C% zF}k|2t|_EzVs$MWmv?j>zO+cODO_w06`R@O#*n<=Nru6%2<O#?@@he7gSPFwHjJfa z&Aa}+d*5SoYZzT^Sl1ZRHL|*<jouwyJHk<eJUOy}M_%x>xPp-vfMfP`*F6_wY-V#> ze9DLtoRn{azZ4}9W<2^>U&Ivmvij4E>hx3aXWrmH%4FC06pyoW*Qqxtrs%*%`NmZ? zyUVAD=nBKS@{q2a)l~+r@94}ZyIRNrT)Y@7naaZ(x0%vzMq2c^r2Hq%@3#F_8)Lr6 zOs;I-xb=I2xGCxpf63jxCWy*jF<eVeiy3L*Z_-O5y3spnPjyYcG;j^GcAKeg-N|j; ztY`GSxB?8>>$PvzA~q{oefR3X=haQD{`lM10=kW}A5DBP@zMDow5>I*x87_0i?+9~ ztqw5y<G)en?CJ}0^Py>fp)2yqkYnxs*ZY646j=G`%=-)PfwR^QpXiB3i&#<GwN>>{ z`oGt26*I*ntbUYHjUrE*aU;PSOQ0L&d0}~JNM0H+1e^iYXL9qGN<v@!Oikzv;PfS@ z`i{aU@twn7U5Y~TBL5h8=95>2<qaWuL-5>Z@|G{bJ5CLB7IiJ}m%dlJZED}v4Tnta z_v`%?{_DS(-KzfiwTE3FFRfRsRY36SnUHSyNxJT_t|D-n)iwCi9=D&^D%frxWc5R# z_Q8#oA9MyP0@pt?Sm4c<&gh3w(F;eI(ed!;`OxTj=7OCaonc01nS2MUb~17&WG>6* z^vnH4@Xov~HUFlegVB`*%6D{C0sCjV+AY)<>$BJFKQ{b$EKu;HX*S=?>Mg4SkhZi8 z>W%A1o=J(K8xrVHA_YZ^H22qh@gL{ruitv})^^#!4d*Y&PbNQ}WOE06swbL4%;ytZ zhAmlK@*>$~oO8jTh|R9^DSpj2tj;Y3G$|-Cw88#2`<asNt*dO-SzpFukO|x<k8c&Q zhC!b^-WE@6)v>xENW$Ydx_N=s^+7<n<(JjL>+j!w@AhWbPrv`r!Hi#I%WY7RKKYvY z_i2PS|8a@&f$J|_OzptKbKB!oc>Gdm{1RJY_l-R19P&4Y3mQWOjT<7i0Bo{HLIp>* z3)-2ni8}*Nk92*a`B?K%#2z^lJ~A3QG8#TYJ~~4BP58qEWo$u1xS%;y(7au6kg?i) z1HZ8xTsyqpajzpFdh2-DP#ZGTZX4=0Ykl%3&FyRWlLCP*HoH2U-4M!d*v__W$$g3^ z$rG7;@~4JM4oNWY{gU@eSX0~Pkxx25?qrV+VCF=!tHRl)5O{8^3)i)W>e|7d(B{=} z=WwWVnC%>8v(G~FHk5@8RUt!F;L5v;Z!fadhd1S)s6SS-NBUSpKje{Dfj{FQ->Cnn z@q@;#+Fx2ewXi2iX51DYp9_u8vEweb`6`=t4SZA<R6elWw*(6|E_^ig!PM57aQAqq zdmJ2(G1i%|bs=P3V698+Avas_icb^CGd@t=R|TxWo49iNJ{kUanC%#RcqKe+4-MPd z;aRrP!R9%AnNM>HK!p{SGV;79pvaDf3mZa(4Q!!>Ieccj@Jyt@!tr_IXt<?2)Y8qi z^gdLE`_6~@&a-_N*#cmFq|_WPZ3>k(ZPagmFMRxL==fQNoD7q*A##=_oy;5^p7Vz0 zyv(cLdqxml;%=m=u7LIeC3ZzLgp?S%LENzTEV_Z;GY({e1DWJNCb__AbUPusgl<N= zm{Aq|TA#IUdeh_|37-4#<VS-aKr`<0r9WwCSxaBfyqD>>vsz<VTOHC?Z)<Be521## zS?rVjI<Gq53HELbuz4qd!}-<W{Dx3|1DkL0sUyb*82AX(V7SN<DzXG$VT+o=MXjNt zR<@`uByR_07%8m|d=K2eF^9*X!kDp1X41{Pa+`6#1||3&Umhze&ye^$x}6taL$`VO zk}BM;f$vtht%zSmx02gp-*BYZ%vd@Z^YKlZDLi>+@agke%rgr&nE9KK;+ALtB^(rw zquYS^9J-woU&PpK2wP#XZ}2xtL!`JqT-*`@H^iqmZ$IpN*v_20$Q-k=#go3_Cx<)N zt_F$&njjs@YTi2O%K%-ZEn(Cp&j?Ye0XZP8fv1ApNN(lY5<FEGMT$)R<H7QXrqH(- z(d7BAMbriU<4i$4#5Uvu7yX)Nn)FQ5GYOHA|4j}7okQE}Z49)OLt$mpBW2U$qLK%_ zf7#1av@&h|4-YZc^V>xi?hHZ3XJK)PnRaZCI@S*1X~e6+E|^9HU)eD<g$+kSh9j(@ zJ#07;GMw1T+Ij^V(ZjOO41>(od0%&=w2>(}up!+T-{@tEj)sd)hKf$IMcuxkr}EOU zygDSW4!pK4KlD^y6qZ+n<P{N>ZmlH(pAIvlr_PAVuy!t@%3gadqRL|m>cRyFn1Tbr znTSfiHWX21tu5iFX9h);;`f9iL;2--G3s-``dVD^VBr1$Q+u2(Isuv=^j>bkTeBg& z@Wkem>$B^#Zzf*Z9CCH!NqSDSVBDvPX7}7t{6j%6dO8MU1Z~#Z`L{Hy{ZG=h5qWM{ zUJ{a*`02pmZFybd>tE&m%iHp*r!X-Z{5AR@42(zotv_6VW~?a)YpOz;s(>S`seh!Y z-%y4RbcYUfZ;gi!oOyKMOhlE-yKxJvY96U-f&<}(6QKs^JHib;j~aTOz7Ru+DOeUf z$&@sPOAduf4zVSzZz?0YBJOKy@I@M3!?vzrqcGgm6Kd+&nhiId*={-$NtWuVA=fYe zVQ1J-6Ef7W22i>YrD5&Xw$d0emIsao%t4wdX}&WO$;@W*8@DqXBgV2oeV{mKhl;*4 z^0dU{8+lq-<R1@I|0|n+I9L&MY#jR8wczAt8I#`uO&I*db^l8*2y6F7{?A4jb%#&# zL|G6~Y3`o6b7pPXKe&@?+D0?JtVCGv4{vY7lu)j`tGJ_B>-OjUa5yl`7+SXF2P5*V zu)Ht?TH#b!Zp0sg`Q|W@Z-$EU7j4viBN6AP{U%NIXP15B|7`x5jF9C#Q-I^SwSl!k zMpL%h_Z7&+(bbVJ;lnf~U&|y}X@9RP_H{usQ7G?b-^upf@Qc}uLjTzZ=kA{iv<6SY zpmjs{QT7Mf8#iHs(A2$U|E2R&=fhrRoML+?Sh@9cT`|f=_ly8!Fo(~n3pQ?cJnDC` z*;m8a?ohUy&8Am}A>W+V)nSxxG30B2e2ZQ#-`BQg*rt9~e&%ys5z0vmIT>O(SvKn* zot|d1FNL!&hq5oT*;jB*S;togQBH-BlMZq!c)6T*m2il;ILY=-vGVE9bp<G2HRP*9 z`5I~hw>Qo|I&q2B+r#>~kbaKUyYTo44V7|Gt19k<@iS16TMZNxD*iqPJ|hG?hDi!_ zhr(8aOk<$@-RifigSi{!Z26%L`lIU~T;E)HNV2V?tm^FNIaQEnMjqr@sDM0+o0z6k zThouIMYd=uT(lA@T49TBF!KCJ=_E6?gtUI~E08B{kI-&Ex5IeqTF8Etu}$up_65t{ zuX?X)BYjhk#tQaNoF6;4hM2K)Y$wHLPe1}+29tH|aZcT%`oV`TaA+9LnF{4hu{oEZ zW#<n(9C&z)$vy8=fubzDDT0yVQ+a1Zna?N+K=T&%i=M`xPr-AJ&m2(}Gl~-bZT}(w znAOiR(=*h-3RxCt<-CIR>u+9XN}4u~Z}s7U*<@&Zayxel2K6xe$k0R-g};-CGK!um zw7-*yGjx!vQuDiXNrncZmB!y?NHdHe-%8!@lro4_5z4}_qAa8+3ska-n%`xnWz?d) z^f1kYiBIpFy%9O}SZ<5RvmZlUEZkZ6@yvty`|~^Mx^FUM*=ego->3<NdXJq-o^dz* zPWoErw!DNhRHYFr4Wp@Gm6eRN^7pe!FkXRCi^(|fe}8pBMCd0(-$rw7-+om@Xvg3& z+xqQSZV_QPA^P^~azZ~M`WD)PVHj?=rO;%){YnesjIKJkW<hm-`xQ_-FZz9-h(r7b z&p!u?+B>?gF{SMPC~NN;E0z8&x>ZUU72HhH5V~2U|FJEvYpg~3pK`L$?H`EjuCXI& z|4@*IZvR;VZ=KRldo5j>u5pR<)4s~iu5r2a(=!<;AXED3U<L%}rJoM9!97>{DOs)W z8ZVN4I;K>1jTL2lI$osg8gIxzOLFm_9G)cb_w{Ws`>k?cYA4Y5hDTB8o*!~K%)2bu z&;29`U(f>w&>}LOl()(_1mNBv>BlY5sv`Qi#f606qZZ=PBpYM-O(5`TjZ7{TIE9FR z^aLD9f<poBM`ln`*pjQDB(QZi?o^Wdrn`}_X?eIb!f^s^^8|O3iL}ab8QSn`)ALZH zXjx~^k5sjyy=m|Rr<w^!I4ZOES0?z_3ffFcirli}GVbg0=*cB=5wUHXne<&qJ?u3L zK6Q>i`^}kY_k33#8*Smkudt2eJxJyin=gCQ%x3(hR#-8ecY2%ASE|uR*LhH5ZEOt| zzHPk(yC%`%8QSV~V~M`zaf91@6MT9{3VTqNVGSy_S}VQcoOgSx$5v)%;Zx|yj(yAM zxdm%R@R3+p=!NgTz(&BB(c9=%aCxE4W8NJ6$t^Ap<$|ebhs{QG7f6~(6_wlRIs7G; zsJe@u!&~x3(MRoIWyS7rOhYUVg9SkRsXKW0;IFyj^w>%rmAPs7U`#Zl=jN;vfAk%e ziEp8$Us!FXY4H1h)3prC_C1gxUcW=gr3ewfSc@1%uLJ++BLL$;yOlU=Jm;fVx&U8^ zqR%3F(Z+-gh+ZYWkQ!hl5zed(WmYmxmzc~-Hq*Y^8<8r)(%g_VHzHU0u0%2o;N(A& z+4U@gkmY?t$Yg1t+EjY*h#OKC!Ix!LyPrwZWLb~1D+A-muF7UNqG4NA;40Wx!4x9T zWVDAjRyO7~FFaIi-DcF|pUKbt<6~vkH-s3Hq3G(sD}h-?UH75kqk<0#cH|w8VYC4s zo?X-3RlctLFVAEE_y<oe5dFV3bu|&6HYraYm3-Q9@Z@31zdJ00XY~DERKn?LlL)?R z{#(@IO<B?OX|xzHJxvcl!MSf<HA4(+VgiAhcg?^^6~d9Q;}5FQzeG=ei!N}2A+{|5 z&btuYH(So5C$y@Lm&<XzVUD8A7wrp9$OmoT<aTSIBlS^v$HJ0hW!~9IPXGb%pYZ%S zT%JipBGH#pgGl;y9U)SFL1e)H|48J0lU5=c5q))1WE5pR>jf=+Y*iD<F$G6<a*l3Z z-GQ$=5r(hQq@w1p)KXEwH)@gSjOZ)5R8;zvI!%=Jm0k{$g>Q07M8^PyM<%NJYF?=W aOtpkm<@0V!@;{f#|A$_%3rPzwjsG7>uHz2? literal 0 HcmV?d00001 diff --git a/scripts/add-task b/scripts/add-task new file mode 100755 index 0000000..5a35240 --- /dev/null +++ b/scripts/add-task @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 +""" +add-task + +Appends a new task to Documentation/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" +""" + +import argparse +import importlib.machinery +import importlib.util +import os +import re +import sys +from pathlib import Path + + +def load_update_tasks_module(): + script_dir = Path(__file__).resolve().parent + update_script = script_dir / "update-tasks" + if not update_script.exists(): + raise FileNotFoundError(f"Could not find update-tasks at {update_script}") + loader = importlib.machinery.SourceFileLoader("update_tasks", str(update_script)) + spec = importlib.util.spec_from_loader("update_tasks", loader) + if spec is None: + raise ImportError(f"Could not load spec for {update_script}") + mod = importlib.util.module_from_spec(spec) + loader.exec_module(mod) + return mod + + +def add_task(title, project="shared", subproject=None, task_type="feature", status="triage", description=None, tasks_file=None, epic=None): + update_mod = load_update_tasks_module() + tasks_path = update_mod.find_tasks_file(tasks_file) + + with open(tasks_path, "r", encoding="utf-8") as f: + text = f.read() + + # 1. Parse frontmatter for valid labels and values + fm_match = re.match(r"^---\n(.*?)\n---", text, re.DOTALL) + if not fm_match: + raise ValueError("YAML frontmatter not found in tasks.md") + + fm_data = update_mod.parse_yaml_frontmatter(fm_match.group(1)) + prefix = update_mod.get_task_id_prefix(fm_data) + epic_key, epic_label = update_mod.resolve_epic(epic, fm_data) + + project_list = fm_data.get("projects", []) + subproject_list = fm_data.get("subprojects", []) + status_list = fm_data.get("task-statuses", []) + type_list = fm_data.get("task-types", []) + + project_map = {} + project_value_map = {} + for p in project_list: + val = p.get("value", "").strip() + lbl = p.get("label", "").strip() + if val: + project_map[val.lower()] = lbl or val.capitalize() + project_value_map[val.lower()] = val + if lbl: + project_map[lbl.lower()] = lbl + if val: + project_value_map[lbl.lower()] = val + + subproject_map = {} + subproject_value_map = {} + for sp in subproject_list: + val = sp.get("value", "").strip() + lbl = sp.get("label", "").strip() + if val: + subproject_map[val.lower()] = lbl or val.replace("-", " ").title() + subproject_value_map[val.lower()] = val + if lbl: + subproject_map[lbl.lower()] = lbl + if val: + subproject_value_map[lbl.lower()] = val + + status_map = {s.get("value", "").lower(): s.get("label", "") for s in status_list} + status_value_map = {s.get("value", "").lower(): s.get("value", "") for s in status_list} + for s in status_list: + lbl = s.get("label", "").strip() + val = s.get("value", "").strip() + if lbl: + status_map[lbl.lower()] = lbl + if val: + status_value_map[lbl.lower()] = val + status_map["fixed"] = "Fixed" + status_map["done"] = "Fixed" + status_value_map["fixed"] = "done" + status_value_map["done"] = "done" + + type_map = {t.get("value", "").lower(): t.get("label", "") for t in type_list} + type_value_map = {t.get("value", "").lower(): t.get("value", "") for t in type_list} + for t in type_list: + lbl = t.get("label", "").strip() + val = t.get("value", "").strip() + if lbl: + type_map[lbl.lower()] = lbl + if val: + type_value_map[lbl.lower()] = val + + # Normalize values + proj_input = project.strip().lower() + type_input = task_type.strip().lower() + status_input = status.strip().lower() + + proj_key = project_value_map.get(proj_input, proj_input) + type_key = type_value_map.get(type_input, type_input) + status_key = status_value_map.get(status_input, status_input) + + proj_label = project_map.get(proj_key, project_map.get(proj_input, project.strip().capitalize())) + type_label = type_map.get(type_key, type_map.get(type_input, task_type.strip().capitalize())) + status_label = status_map.get(status_key, status_map.get(status_input, status.strip().capitalize())) + + subproj_key = None + subproj_label = None + if subproject and subproject.strip() and subproject.strip().lower() not in ("", "none", "null"): + sp_in = subproject.strip().lower() + subproj_key = subproject_value_map.get(sp_in, sp_in) + subproj_label = subproject_map.get(subproj_key, subproject_map.get(sp_in, subproject.strip().replace("-", " ").replace("_", " ").title())) + + # 2. Find max task ID + detail_start = text.find("## Detailed Tasks\n") + if detail_start == -1: + raise ValueError("## Detailed Tasks heading not found") + ids = [int(m.group(2)) for m in re.finditer(update_mod.TASK_ID_PATTERN, text[detail_start:], re.IGNORECASE)] + next_num = (max(ids) + 1) if ids else 1 + + # Digit width calculation (minimum 3 digits) + digit_width = max(3, len(str(next_num))) + formatted_id = update_mod.format_task_id(next_num, prefix, digit_width) + formatted_aid = formatted_id.lower() + + # 3. Format description + if description and description.strip(): + desc_lines = description.strip().splitlines() + desc_body = "\n".join(desc_lines) + if not any(line.strip().startswith("- [") for line in desc_lines): + desc_body += "\n- [ ] " + else: + desc_body = "Describe task objectives and implementation requirements here.\n- [ ] " + + if subproj_key: + anchor_attrs = f'id="{formatted_aid}" class="task" data-project="{proj_key}" data-subproject="{subproj_key}" data-status="{status_key}" data-task-type="{type_key}"' + subproj_line = f"**Subproject:** {subproj_label}\n" + else: + anchor_attrs = f'id="{formatted_aid}" class="task" data-project="{proj_key}" data-status="{status_key}" data-task-type="{type_key}"' + subproj_line = "" + + if epic_key: + anchor_attrs += f' data-epic="{epic_key}"' + epic_line = f"**Epic:** {epic_label}\n" if epic_key else "" + + new_task_chunk = ( + f'<a {anchor_attrs}></a>\n' + f"### {title.strip()}\n" + f"**ID:** {formatted_id}\n" + f"**Project:** {proj_label}\n" + f"{subproj_line}" + f"{epic_line}" + f"**Status:** {status_label}\n" + f"**Type:** {type_label}\n\n" + f"**Description:**\n" + f"{desc_body}\n" + ) + + # 4. Insert before Notes marker + notes_marker = "\n---\n\n## Notes\n" + notes_pos = text.find(notes_marker) + if notes_pos == -1: + raise ValueError("Could not find ## Notes marker to insert task before") + + updated_text = text[:notes_pos].rstrip() + "\n\n" + new_task_chunk + "\n---\n\n## Notes\n" + text[notes_pos + len(notes_marker):] + + with open(tasks_path, "w", encoding="utf-8") as f: + f.write(updated_text) + + # 5. Re-run update_tasks_document to sort, standardize, and refresh tables + update_mod.update_tasks_document(tasks_path) + + return formatted_id, title.strip(), proj_label, subproj_label, status_label, type_label, tasks_path + + +def main(): + parser = argparse.ArgumentParser(description="Add a new task to tasks.md and update tables.") + parser.add_argument("title", help="Title of the new task") + parser.add_argument("--epic", default=None, help="Epic value or label from frontmatter (optional)") + parser.add_argument( + "-p", "--project", + default="shared", + help="Project name or value (default: shared)" + ) + parser.add_argument( + "-S", "--subproject", + default=None, + help="Subproject name or category (optional, e.g. actions, expressions, schema)" + ) + parser.add_argument( + "-t", "--type", + default="feature", + help="Task type (default: feature, e.g. chore, bug, feature, lint, docs)" + ) + parser.add_argument( + "-s", "--status", + default="triage", + help="Initial task status (default: triage, e.g. planning, triage, pending, fixed)" + ) + parser.add_argument( + "-d", "--description", + default=None, + help="Task description and requirements" + ) + parser.add_argument( + "-f", "--file", + default=None, + help="Path to tasks.md (optional)" + ) + + args = parser.parse_args() + + try: + new_id, title, proj, subproj, status, ttype, tasks_path = add_task( + title=args.title, + project=args.project, + subproject=args.subproject, + task_type=args.type, + status=args.status, + description=args.description, + tasks_file=args.file, + epic=args.epic, + ) + tag_info = f"{proj} | {subproj} | {status} | {ttype}" if subproj else f"{proj} | {status} | {ttype}" + print(f"Successfully added {new_id}: \"{title}\" [{tag_info}] to {tasks_path}") + except Exception as e: + print(f"Error adding task: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/build-release b/scripts/build-release new file mode 100755 index 0000000..52d7e97 --- /dev/null +++ b/scripts/build-release @@ -0,0 +1,62 @@ +#!/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}" diff --git a/scripts/ci-build.sh b/scripts/ci-build.sh new file mode 100755 index 0000000..420bf45 --- /dev/null +++ b/scripts/ci-build.sh @@ -0,0 +1,173 @@ +#!/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 diff --git a/scripts/env.sh b/scripts/env.sh new file mode 100755 index 0000000..00930f1 --- /dev/null +++ b/scripts/env.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# +# env.sh +# +# Environment configuration and aliases for task management scripts. +# Can be sourced (recommended) into your current shell session or executed directly. +# +# Usage: +# source ./scripts/env.sh +# # or: +# . ./scripts/env.sh +# + +# Resolve directory of this script portably across bash and zsh +if [ -n "${BASH_SOURCE[0]}" ]; then + _SCRIPT_SRC="${BASH_SOURCE[0]}" +elif [ -n "${ZSH_VERSION}" ]; then + _SCRIPT_SRC="${(%):-%x}" +else + _SCRIPT_SRC="$0" +fi + +SCRIPTS_DIR="$(cd "$(dirname "$_SCRIPT_SRC")" && pwd)" +REPO_ROOT="$(cd "$SCRIPTS_DIR/.." && pwd)" + +# Add scripts directory to PATH if not already present +case ":$PATH:" in + *":$SCRIPTS_DIR:"*) ;; + *) export PATH="$SCRIPTS_DIR:$PATH" ;; +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" + +# 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-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" +alias get-tasks="$SCRIPTS_DIR/get-tasks" +alias update-task="$SCRIPTS_DIR/update-task" +alias update-tasks="$SCRIPTS_DIR/update-tasks" +alias triage="$SCRIPTS_DIR/triage" +alias triage-hermes="$SCRIPTS_DIR/triage --hermes" +alias research="$SCRIPTS_DIR/research" +alias research-hermes="$SCRIPTS_DIR/research --hermes" +alias groom="$SCRIPTS_DIR/get-tasks --planning | sed -E 's/ +\\|$/ |/; s/-{10,}/---/'" +alias dp="$SCRIPTS_DIR/get-tasks --planning | sed -E 's/ +\\|$/ |/; s/-{10,}/---/'" +alias work-on-task="$SCRIPTS_DIR/work-on-task" + +# Source optional local uncommitted environment settings if present +if [ -f "$SCRIPTS_DIR/env.sh.local" ]; then + # shellcheck source=/dev/null + . "$SCRIPTS_DIR/env.sh.local" +elif [ -f "$REPO_ROOT/.env" ]; then + # shellcheck source=/dev/null + . "$REPO_ROOT/.env" +fi + +# Detect whether this file is being sourced or executed directly +_IS_SOURCED=0 +if [ -n "${ZSH_VERSION}" ]; then + case "$ZSH_EVAL_CONTEXT" in + *:file*) _IS_SOURCED=1 ;; + esac +elif [ -n "${BASH_VERSION}" ]; then + if [ "${BASH_SOURCE[0]}" != "$0" ]; then + _IS_SOURCED=1 + fi +fi + +if [ "$_IS_SOURCED" -eq 1 ]; then + echo "Task environment initialized. Scripts added to PATH and aliases defined." +else + echo "Task environment script executed." + echo "To load aliases and PATH into your current shell session, run:" + echo " source $SCRIPTS_DIR/env.sh" + echo " # or: . $SCRIPTS_DIR/env.sh" +fi diff --git a/scripts/get-task b/scripts/get-task new file mode 100755 index 0000000..d6016e9 --- /dev/null +++ b/scripts/get-task @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +""" +get-task + +Retrieves detailed information and metadata for a specific task in Documentation/Tasks.md. +Usage: + ./scripts/get-task OT-018 + ./scripts/get-task 18 --json + ./scripts/get-task OT-099 --description-only +""" + +import argparse +import importlib.machinery +import importlib.util +import json +import re +import sys +from pathlib import Path + + +def load_update_tasks_module(): + script_dir = Path(__file__).resolve().parent + update_script = script_dir / "update-tasks" + if not update_script.exists(): + raise FileNotFoundError(f"Could not find update-tasks at {update_script}") + loader = importlib.machinery.SourceFileLoader("update_tasks", str(update_script)) + spec = importlib.util.spec_from_loader("update_tasks", loader) + if spec is None: + raise ImportError(f"Could not load spec for {update_script}") + mod = importlib.util.module_from_spec(spec) + loader.exec_module(mod) + return mod + + +def parse_task_id(id_str): + return load_update_tasks_module().parse_task_id(id_str) + + +def get_task(task_id_input, tasks_file=None): + update_mod = load_update_tasks_module() + tasks_path = update_mod.find_tasks_file(tasks_file) + + with open(tasks_path, "r", encoding="utf-8") as f: + text = f.read() + + fm_data = update_mod.read_frontmatter(text) + prefix = update_mod.get_task_id_prefix(fm_data) + target_num = update_mod.parse_task_id(task_id_input) + + detail_header_marker = "## Detailed Tasks\n" + detail_start = text.find(detail_header_marker) + if detail_start == -1: + raise ValueError("## Detailed Tasks heading not found") + + notes_marker = "\n---\n\n## Notes\n" + notes_start = text.find(notes_marker) + if notes_start == -1: + notes_start = len(text) + + detail_body = text[detail_start + len(detail_header_marker) : notes_start] + task_chunks = re.split(r"(?=<a\s+[^>]*\bid=|<id=)", detail_body) + + for chunk in task_chunks: + chunk_clean = chunk.strip() + if not chunk_clean: + continue + m = re.search(update_mod.TASK_ID_PATTERN, chunk_clean, re.IGNORECASE) + if m and int(m.group(2)) == target_num and update_mod.task_id_matches(task_id_input, m.group(1), prefix): + anchor_match = re.search(update_mod.TASK_ANCHOR_PATTERN, chunk_clean, re.IGNORECASE) + anchor_tag = anchor_match.group(1) if anchor_match else "" + + proj_m = re.search(r'data-project=["\']([^"\']+)["\']', anchor_tag) + subproj_m = re.search(r'data-subproject=["\']([^"\']*)["\']', anchor_tag) + status_m = re.search(r'data-status=["\']([^"\']+)["\']', anchor_tag) + type_m = re.search(r'data-task-type=["\']([^"\']+)["\']', anchor_tag) + epic_m = re.search(r'data-epic=["\']([^"\']*)["\']', anchor_tag) + epic, epic_label = update_mod.resolve_epic(epic_m.group(1) if epic_m else None, fm_data) + + if not subproj_m: + subproj_field_m = re.search(r'^\*\*Subproject:\*\*\s*([^\n]+)$', chunk_clean, re.MULTILINE) + subproject = subproj_field_m.group(1).strip() if subproj_field_m else "" + else: + subproject = subproj_m.group(1).strip() + + title_m = re.search(r'^###\s+(.+)$', chunk_clean, re.MULTILINE) + title = title_m.group(1).strip() if title_m else f"Task {target_num}" + + desc_m = re.search(r'\*\*Description:\*\*\s*\n(.*)$', chunk_clean, re.DOTALL) + description = desc_m.group(1).strip() if desc_m else "" + + digit_width = max(3, len(m.group(1).rsplit("-", 1)[1])) + formatted_id = update_mod.format_task_id(target_num, prefix, digit_width) + formatted_aid = m.group(1) + + return { + "number": target_num, + "id": formatted_id, + "anchor_id": formatted_aid, + "title": title, + "project": proj_m.group(1) if proj_m else "shared", + "subproject": subproject, + "epic": epic, + "epic_label": epic_label, + "status": status_m.group(1) if status_m else "triage", + "type": type_m.group(1) if type_m else "feature", + "description": description, + "raw_markdown": chunk_clean, + "file_path": str(tasks_path), + } + + raise KeyError(f"Task {task_id_input} ({update_mod.format_task_id(target_num, prefix)}) was not found in {tasks_path}") + + +def main(): + parser = argparse.ArgumentParser(description="Get detailed info and metadata for a task in tasks.md") + parser.add_argument("task_id", help="Task number or prefixed ID (e.g. 18, PREFIX-018)") + parser.add_argument("-j", "--json", action="store_true", help="Output details as JSON") + parser.add_argument("-d", "--description", "--description-only", dest="description", action="store_true", help="Output description and checklist") + parser.add_argument("--id", action="store_true", help="Output task ID only") + parser.add_argument("--title", action="store_true", help="Output task title only") + parser.add_argument("--project", action="store_true", help="Output task project only") + parser.add_argument("--subproject", action="store_true", help="Output task subproject only") + parser.add_argument("--epic", action="store_true", help="Output task epic value only") + parser.add_argument("--status", action="store_true", help="Output task status only") + parser.add_argument("--type", action="store_true", help="Output task type only") + parser.add_argument("-m", "--metadata-only", action="store_true", help="Output metadata header attributes only") + parser.add_argument("-f", "--file", default=None, help="Path to tasks.md (optional)") + + args = parser.parse_args() + + try: + task = get_task(args.task_id, tasks_file=args.file) + if args.json: + print(json.dumps(task, indent=2)) + elif args.metadata_only: + print(f"ID: {task['id']}") + print(f"Title: {task['title']}") + print(f"Project: {task['project']}") + if task.get("subproject"): + print(f"Subproject: {task['subproject']}") + if task.get("epic"): + print(f"Epic: {task['epic_label']}") + print(f"Status: {task['status']}") + print(f"Type: {task['type']}") + elif any([args.id, args.title, args.project, args.subproject, args.epic, args.status, args.type, args.description]): + output_parts = [] + if args.id: + output_parts.append(task["id"]) + if args.title: + output_parts.append(task["title"]) + if args.project: + output_parts.append(task["project"]) + if args.subproject: + output_parts.append(task["subproject"]) + if args.epic: + output_parts.append(task["epic"]) + if args.status: + output_parts.append(task["status"]) + if args.type: + output_parts.append(task["type"]) + if args.description: + output_parts.append(task["description"]) + for part in output_parts: + print(part) + else: + print(task["raw_markdown"]) + except Exception as e: + sys.stderr.write(f"Error: {e}\n") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/get-tasks b/scripts/get-tasks new file mode 100755 index 0000000..821f267 --- /dev/null +++ b/scripts/get-tasks @@ -0,0 +1,296 @@ +#!/usr/bin/env bash +# +# get-tasks +# +# Lists tasks from Documentation/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. +# Uses filter-table if available, with a fast built-in fallback. +# + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +TASKS_FILE="${TASKS_FILE:-}" +if [[ -z "$TASKS_FILE" ]]; then +for cand in \ + "$REPO_ROOT/docs/tasks.md" \ + "$REPO_ROOT/docs/Tasks.md"; do + if [[ -f "$cand" ]]; then + TASKS_FILE="$cand" + break + fi +done +fi + +if [[ -z "$TASKS_FILE" ]]; then + TASKS_FILE="$REPO_ROOT/docs/tasks.md" +fi + +ACCEPTS=() +REJECTS=() +MODE="" +SORT_DIR="desc" + +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) + echo "usage: get-tasks [-h] [-p|--planning] [--all] [--accept PREDICATE] [--reject PREDICATE] [--sort {asc,desc}] [-s|--select COLS] [-f|--file PATH]" + echo "" + echo "Lists tasks from docs/tasks.md sorted by ID." + echo "" + echo "options:" + echo " -h, --help Show this help message and exit" + echo " -p, --planning Show tasks in Triage and Planning status" + echo " --all Show all tasks regardless of status" + echo " --accept PREDICATE Include rows matching predicate (e.g. 'Status:Fixed', 'Project:Engine', 'Epic:poc')" + echo " Epic filters accept frontmatter values or labels" + echo " --reject PREDICATE Exclude rows matching predicate (e.g. 'Status:Cancelled')" + echo " --sort [asc|desc] Sort order by task ID (default: desc)" + echo " -s, --select COLS Columns to display (e.g. 'ID,Title,Project,Epic,Status')" + echo " -f, --file PATH Path to tasks.md (overrides TASKS_FILE)" + exit 0 + ;; + -p|--planning) + MODE="planning" + shift + ;; + --all) + MODE="all" + shift + ;; + -f|--file) + if [[ $# -lt 2 ]]; then + echo "Error: $1 requires a file path" >&2 + exit 1 + fi + TASKS_FILE="$2" + shift 2 + ;; + --accept) + if [[ $# -lt 2 ]]; then + echo "Error: --accept requires a predicate argument" >&2 + exit 1 + fi + ACCEPTS+=("$2") + shift 2 + ;; + --accept=*) + ACCEPTS+=("${1#*=}") + shift + ;; + --reject) + if [[ $# -lt 2 ]]; then + echo "Error: $1 requires a predicate argument" >&2 + exit 1 + fi + REJECTS+=("$2") + shift 2 + ;; + --reject=*) + REJECTS+=("${1#*=}") + shift + ;; + -s|--select) + if [[ $# -lt 2 ]]; then + echo "Error: $1 requires a column list argument" >&2 + exit 1 + fi + SELECT_COLS="$2" + shift 2 + ;; + --select=*) + SELECT_COLS="${1#*=}" + shift + ;; + --sort) + if [[ -n "${2:-}" && "$2" != -* ]]; then + SORT_DIR="$2" + shift 2 + else + SORT_DIR="desc" + shift + fi + ;; + --sort=*) + SORT_DIR="${1#*=}" + shift + ;; + *) + echo "Error: Unknown option '$1'" >&2 + echo "Run 'get-tasks --help' for usage." >&2 + exit 1 + ;; + esac +done + +if [ "$SORT_DIR" = "asc" ]; then + SORT_FLAG="ID" +else + SORT_FLAG="-ID" +fi + +# Apply default accept if no explicit accept was provided +if [ ${#ACCEPTS[@]} -eq 0 ]; then + if [ "$MODE" = "planning" ]; then + ACCEPTS+=("Status:Triage,Planning") + elif [ "$MODE" = "all" ]; then + : # No default filter + else + ACCEPTS+=("Status:Triage") + fi +fi + +if [ $((${#ACCEPTS[@]} + ${#REJECTS[@]})) -gt 0 ]; then + ACCEPT_COUNT=${#ACCEPTS[@]} + NORMALIZED_PREDICATES="$(python3 "$SCRIPT_DIR/lib/filter_predicates.py" "$TASKS_FILE" "${ACCEPTS[@]}" "${REJECTS[@]}")" + ACCEPTS=() + REJECTS=() + PREDICATE_INDEX=0 + while IFS= read -r predicate; do + if [ "$PREDICATE_INDEX" -lt "$ACCEPT_COUNT" ]; then + ACCEPTS+=("$predicate") + else + REJECTS+=("$predicate") + fi + PREDICATE_INDEX=$((PREDICATE_INDEX + 1)) + done <<< "$NORMALIZED_PREDICATES" +fi + +if command -v filter-table >/dev/null 2>&1; then + FILTER_ARGS=("$TASKS_FILE") + for acc in "${ACCEPTS[@]}"; do + FILTER_ARGS+=(--accept "$acc") + done + for rej in "${REJECTS[@]}"; do + FILTER_ARGS+=(--reject "$rej") + done + FILTER_ARGS+=(--sort "$SORT_FLAG") + + if [ -n "$SELECT_COLS" ]; then + FILTER_ARGS+=(--select "$SELECT_COLS") + elif [ "$MODE" = "all" ]; then + ALL_COLS="ID,Title,Project" + if grep -q "| Subproject" "$TASKS_FILE"; then + ALL_COLS="$ALL_COLS,Subproject" + fi + if grep -q "| Epic" "$TASKS_FILE"; then + ALL_COLS="$ALL_COLS,Epic" + fi + FILTER_ARGS+=(--select "$ALL_COLS,Status") + else + FILTER_ARGS+=(--select "ID,Status,Title") + fi + + exec filter-table "${FILTER_ARGS[@]}" +else + # Python fallback implementation + python3 - "$TASKS_FILE" "$SORT_DIR" "$MODE" "${SELECT_COLS:-}" "${#ACCEPTS[@]}" "${ACCEPTS[@]}" "${REJECTS[@]}" <<'PY' +import sys, re + +with open(sys.argv[1], 'r', encoding='utf-8') as f: + lines = f.readlines() + +sort_dir, mode, select_cols_str = sys.argv[2:5] +accept_count = int(sys.argv[5]) +accepts = sys.argv[6:6 + accept_count] +rejects = sys.argv[6 + accept_count:] + +def parse_predicates(pred_list): + preds = [] + for p in pred_list: + if ':' in p: + col, vals = p.split(':', 1) + allowed = {v.strip().lower() for v in vals.split(',') if v.strip()} + preds.append((col.strip().lower(), allowed)) + return preds + +parsed_accepts = parse_predicates(list(accepts) if isinstance(accepts, (list, tuple)) else []) +parsed_rejects = parse_predicates(list(rejects) if isinstance(rejects, (list, tuple)) else []) + +in_table = False +headers = [] +header_display = [] +raw_rows = [] + +for line in lines: + if line.startswith('| ID'): + in_table = True + header_display = [h.strip() for h in line.strip().split('|')[1:-1]] + headers = [h.lower() for h in header_display] + continue + if in_table: + if not line.strip() or line.startswith('## Detailed Tasks') or not line.startswith('|'): + break + parts = [p.strip() for p in line.strip().split('|')[1:-1]] + if len(parts) >= len(headers) and not parts[0].startswith(':-'): + row_dict = {headers[i]: parts[i] for i in range(len(headers))} + + # Check accepts + accept_ok = True + for col, allowed in parsed_accepts: + val = row_dict.get(col, '').lower() + if val not in allowed: + accept_ok = False + break + if not accept_ok: + continue + + # Check rejects + reject_hit = False + for col, disallowed in parsed_rejects: + val = row_dict.get(col, '').lower() + if val in disallowed: + reject_hit = True + break + if reject_hit: + continue + + raw_rows.append(row_dict) + +reverse_sort = (sort_dir != 'asc') +raw_rows.sort( + key=lambda r: int(re.search(r'\d+$', r.get('id', '0')).group(0)) if re.search(r'\d+$', r.get('id', '0')) else 0, + reverse=reverse_sort +) + +if select_cols_str: + req_cols = [c.strip() for c in select_cols_str.split(',') if c.strip()] + out_headers = [c for c in req_cols if c.lower() in headers] +else: + if mode == 'all': + out_headers = ['ID', 'Title', 'Project'] + if 'subproject' in headers: + out_headers.append('Subproject') + if 'epic' in headers: + out_headers.append('Epic') + out_headers.append('Status') + else: + out_headers = ['ID', 'Title'] + +col_keys = [c.lower() for c in out_headers] +col_widths = [len(h) for h in out_headers] + +formatted_rows = [] +for r in raw_rows: + row_vals = [] + for k in col_keys: + val = r.get(k, '') + if k == 'title' and not select_cols_str and mode != 'all': + clean_t = val.strip('*').strip('_').strip() + st = r.get('status', '').lower() + val = f'**{clean_t}**' if st == 'pending' else f'*{clean_t}*' if st in ('triage', 'planning') else clean_t + row_vals.append(val) + formatted_rows.append(row_vals) + for idx, v in enumerate(row_vals): + col_widths[idx] = max(col_widths[idx], len(v)) + +hdr_line = '| ' + ' | '.join(f'{out_headers[i]:<{col_widths[i]}}' for i in range(len(out_headers))) + ' |' +sep_line = '| ' + ' | '.join(':' + '-' * (col_widths[i] - 1) for i in range(len(out_headers))) + ' |' +print(hdr_line) +print(sep_line) +for row in formatted_rows: + print('| ' + ' | '.join(f'{row[i]:<{col_widths[i]}}' for i in range(len(out_headers))) + ' |') +PY +fi diff --git a/scripts/lib/__init__.py b/scripts/lib/__init__.py new file mode 100644 index 0000000..3ba972f --- /dev/null +++ b/scripts/lib/__init__.py @@ -0,0 +1 @@ +"""Shared automation library for PlanB task tooling.""" diff --git a/scripts/lib/__pycache__/task_context.cpython-314.pyc b/scripts/lib/__pycache__/task_context.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ab387e014f8b71d15fcb8920197a3b9ed2f81357 GIT binary patch literal 9294 zcmb_idu$s=dS8;u*OH<rTTeftu_W7~EK#xJ*pB>2WXXCYmaL-`SxHpWYjP#7O>vps zrEIb39Z|G^TwEHb7c@HHf>Y1{rvR6pfnI?WxT3}(2OQA;L0WdH+qgIb4N%;Favm_! zzxtb5E-A@6-?iufoSo-4-^_gT`@Z>xhXPdshwz{AJ6HGD;Y%}CaFh~X{TzuLcZrMG zhB@8*+H^b1J4#<(=ULvVJMpy-yN2AlyP39i410#Wx_3y>g(08r8}jRZR?ZJs4F&YT zP_<q?6x4%5HF`}m7jZUl`i@92;%eX`?glOyOxpCF5f5@+n$v3|H4y<hU#YGx;zzEE z)zwD=NLRCTL&Vm~g?9XoPGK};Gx#x4PlR~G(VJQ@_<lL28~k}$(+$ssv@R<tF^Nj= zSy7XEXQh;GcxNR&Dzan^O~$1eF_W~4d`WaQtF>5ZmDtCH7|X_g_1`wSWYdVvFm1O| zM@wa6POGHyJz7eRQ94_~A6u5@Cb3bSzDoCqePb?do3xF2E45b28v9_WV78j$_Hff> zIuXaD&CeaNTl^<%(|)V(SinkEzGF3&w3UkR(>ty7q|Ksqw6r#9`v!`&R=dh~tf7*& zQW1XAUheOhbj+AsH<jz1PvLsIF?L%$E8j_n#c3L&jqR<}SgA?gYSGM1@(?^%wxxd| zCC<q)F_~N-NkxnkEvCw8UF(K8b?KsZEo`_;MOlwkmTl=%GRZheDLR>vQ*mNdlzJ0U zCx)L2B&86YQ{vgiN)L0Gv=k#Vib_J&Dm}r5CmNlRlTtKlxK&A0lJk<`N{cF$a+h=i zf~*-e186X+=mSb76+fq{ifVY}xwN9{N%@N5ofBgTIVGtJMsQros4<B)KaV*{s^QRd z)!;L_oHT+OhK|mt%3PFkf=b^oBVtkvg4+!EU=3(f!zbN92eUZjG`y;mR-&q+=tgyU zislj-t`bF_b#$PQM$ZqS!RYCJRg+Y$TUX|~$5iF26vHxKNXE}fqB=P-(QU0fCezK< zEvBS&>4qLoFBr9yaI`#}$&A_x+Entje?^qzz6jR7H}!)+F?gU5Jg^#UF9y2{!R})4 zSRr_<7(87Fp3V)g)i*DP?$;k$xq83;MDD_xZ&%T`ui)EP^tImgwLS<m=Ue*kMDzWx z=E>x0;9}l$@j+F6v5FL`NU`d0q3ZDM@Mo=el-0A7t5p|s0}p()@0_`Lrs(S^_&Qe3 ze0prvcQ)6*=Jx&hTMq(F`8`K(NApL|<#!FN1_tw<!3V<5cfvQrMIlrWLMv^nLU*oj zjTef1eSxpPrQPFqulWMGm%qYvxEMts3z<AfQ!)8UN0bA0T(Y0|3yzDUY!CMxTf!Ex zO{2yP5qky4Dy5yfta=V`Kh0aYN{i_-o;_#Z*t*++v%>$5o`1v5c1DgH0!?U4W(}7r z=@~T@;#GR~RX2_3zN>T)z`6^9J}2rLolT3{g4pm!qhbo@N7Nzm>LC<S_OvcUIqos% zu$_2BqaQie>F1}7KX3hHFH3&b_L=r?eRppBD$E+FZRimz!!|N>U@Qy&RS%<80e1^J z({{iEUCb@vA{Oy@i4rDk&$dS-U6rMIiO{){IMfl<LFxj@XmV<no_cn`!y%j64q_oY zlSqT&1gi)+REpEAQr(A?Mu$CcfS79_%t(4H!CI<eRInAG$Ocgso>>(Lgu@qj=hV$p zOYX%}tG?rT{<t~cZAIe->-lui*dQ2mZ&=v8*$|$x7*tws6bA@TdA%ci#M#ZDR6PJ< z22WSY2m(%(>ry<bVSh84q#6F0GB+ot;!&Wjq=wLR6w<<EgozAgMy2CoDLe4lASz5a zSVU;WF+@~C0=C*mH2TrRN39=e%k3XrTBq4x=%4ld^8!oWn|gzN^OxhSrMYw)H=be# z6BQm4qNi%iI&Y}3Lc|s+>#ps@#uBTiSqf=r<skPepRtFzjD27S$GzGAwDzO+SD^L0 z+%sI-ndUD({{r}mf5bFBJzOkQH1j3fi7C`Gol5opC2r$(`ElBhIMS|>{SkgUi((UJ zusG5@Xz*q)@PRd=d}QqDvi3~7rkOcP*%&KZ7baJ%44iBY5hv^&*A~9q%m&(kOBmII z!?}%3Vy!Y-cqd(mg(KdIHs8Px)^=;y$6YF0F)&_zt?ZYvH?vvEh^<t}pPil*lX4ue zMDzsIxSlATep8eQU@Vr9G@_&=qRg=BlCTp?*gJ`WUg~vOlZZsMG=bm~=x{x3xXeZd zXV?yAd4jsEpXuEmGC?uMu*>nRhsYX{Q{-S)I9Q?@CfNp}N^fRl7zd>?qATQzM6&j< zFd7mJ2T;dwLeI--!%2@Hw0=@b8T?g6PQd_J(CFyU_p0H%uFAS(2r)4&>zHm<GI+{g z$gMt$#ZwQ{=m;Vs5X-341}U9n!=@Qk*5NkCfVJ`_YE_L2<^f<Ef(dUsL=F1BWCNM= zRtM0C;HQ0!D91glYkGg`-KnL-ihZ^2P_gcCq3-Z%-E+md7YcPRtk#{(jsB+n#A@ve zh4vH6hu^>W?!~`-4awX{zV?NO?z(&Kjt9O#zPfqUN0xi<`S!0hHsyD9tQ@|-t1I6W zUJ>*4&)yt-Sik4}YwunwJb!MrWdQMQ+wymo-&~HZ)coBEWD6|=tM!A6gAY8ucZ8cl z(bHJ)G_D19EgfCvKj>NNTu!X$w}(FYZa#eG)2nOMyNlHa3e^WztK0K|P(Jk1orXK! zU+-LZ*s4ZtUppLL;cE})^}pl!E6=T(zxF-a$$1Wa`=yhs>-qLeKi4{F(<tWrLu20w z{+C>J-)YY#El*Ds#iy_4+3Ng1KzRkIH-YvB0C!f>-A-x~GazSjU{qaK)N3=?X9nBG zj&fGHLajF$Hfj|NG*yh2Of`n=(0ZGJ{b`s^MNK=)ru&%du<dw6qX$CZ``UYbi#Og5 zuRD<cVrTuj6W_00T+^O)H%)oC+J<#6O$nUK`^blsiSImUS%KaG7{33_Q}Nxx5vr?c zh&NKC-HJ_5!~PNI2;4fh*B|A3Ntr(GVS6~-iGa*o5Yc(W0a~s!&~jT{<bHxz7#^VI z`91Bu7{k4B4C@v~5p-)E%6vzhY<$=6q4(tYRcw6!leCW586|h5ir(!a0ko*z$O+1V z+mzLyY{!;%J5g4<rOY|$^m9xT)S<S1OKk(n8n=`+F|D=>HCy@Ey$$_6Pg0n4gXg`I zcErvjd()mt$L1ZK^hBPSbg)`SC)ULZXz7VGZ!N)DBiR@GloY@-6Vr)EVLcTW)j04t z-boVjY@%yMj!Q{dUm$7R*V4FcGHpV7hY9m~gbiO@GVe{WOtX92M-{T6fcBaPg*aJ| z^pGKFnJXxvM>iYjBID&;4>4?l`aJlB9!Sfvtfz~d!x!ZO%}0~s6)BmullH7VBtSoP zb<yI}S<h=qhTxJ)vM$p`R*!>7wwG!zsyWA!V(RRGq7JH(sK1=ibU7wLjinV$))jRj zEC{Wwt<ZcbbQe>AJwg{5?(ZR40f`A&PfFmysnbN(kD}7O5ruARCOf)FpQ)tCI5RhU zh)S)6te28lNK*>Dj&Js$Ih@tHzp`@V0O?Cev1`;})FWiCn++pnMhUiwUPaMvFvGSa zC22jv;V>B=S0+%(l=q03!mi9gU6S^}b0fo}!{NDjs7HWC>>_lfMqVACz}-P2m&4`T zZYE8Q4d!Yf*`JbdIDDBZcC*-A)Hz8P(MasvxVN2ztqP0{mB1n~c_&cob6VIOahRGX zuq4>!B_&C5B$>sar6E$-BIF<uv|8)j%9a?9n>@8ZOc({W8!g!S(!yi+k)aEFTHFVG zy<8f$Um>FxCWsWr7;z<r`#v22D*@Xnm7$K7uy7<ydR2HUbckXG;&WkMQs-sqdTFit z4jsZ?LO{s5)GS;YJ*96qlc#R8u$#2^vI`lQKX)SyyGX+sKxQ0fl9Y-`NDkt*L4`s> zDWx}};5daWMkVc<uB6F1aH^;ngMGVu(`k^N5HqSw4v|Ulh)+ZkV|s-&8JFiWNfF&P zj!gztF`XcNVrpK*;8P>#3G^bSro-)Fjmjh`rT3U3mD0O$&@`|`4qK^mB79>mS>kmZ zju?3cekytc)q2c@9ml@UiC|U=&xSk6*x7OByNYgVjD_zcv#N;emkMW9QYv$DUYZqS z3z*uB1QRbtSJ_+v(|e?W@WrtK($~uN5@JTd2lqI5rND8D$TLJtLzH6Tm868}Jr^by zEW1EsjhXo2`bReg$Et~)_TD%~L4laGP8F)I31=njG`X?>c%YI%@7I>?-3eKx4kg_x zRHdXeFXEWRuyCEsypAbJfRv(QfzT7OEveWsSSax8U6=I)BZi}=h6P;HPXHg(7Z8QK zro}r%3x;X*G9rVgAZqwaco|JX1`V%?gf!`4NuU;7bPSg@>ZyhC%dFp$`CE4-BPZk0 zvdv_+R9{0&X7v0$?sPeBJ-{96{#pCS?YB=CJ9_VS^e!H~RdwI@Owrd?@U`9Z9mo&P zz@c>J?7t-bDRJkm;+cuNXC`i4TJB%jbHD9azV-NRwXm}%|N3<9{MyuXaq4nm>T+J3 zElwrwP9<)&FCG2yi65R=K7YII_WVC~uI_#@zw4zt_B%&^?fTWJJK_A;cp*5EPo?1( z>wNZSLmv;_o-B5ry4!i`R^w9J()<UVOV1Rl+wZjGh91^5Ep;t5uee|h_kWUINx~U( z(!K8DJpRT0zs%;4x9t?xguvqUx4Rabk!$j=`GdEb-oB9c?Ll_0ZOvAd3oI@yUduNf zTIs*t_{nJg&`H#s@~(NS7H8hxvlsx;1{;XlT{+h}$Gbb$Jy@O<{gaLrO5@m!ZEfai z{+fP&CX<)u^Yh<b=eU>b{dRoL*<Z1LLv!b8?t8Wo`Wc0*2cHr9MSQ#$?Y8wDoV_;B z)q+@^ZT)alfX@v)KFWFO);xiHZTIb}mG6CeCBLKhzNdH5^^h0x!9Tmte>XR<=Bvq# zJn4I(#wIh^Pa*M#+;U@OV;qf3A!3g>y6x1xX8KR4%NIgg@{rgK4}^>r8G&APHbZsG z7;Et~oB9%nYi)?ApQOt6y6q8*Ke|yoc&2dh%sR{6Ick<xCS~zNWtmycElfhUaiHOC z0^SUJ$(sRd_RW^<CI|09u;j&X!0E<q=kbWxCLMGU;G3Wq$StAb(>JZEi0e3Aas+tr zx2NZhcp~12aGY+-)7tsa&i_ZWtBM4`Uv(r{899OtEBOrUG8d}Jo{a#!G(Zl(E2B|3 zBz@<H$xKq5HGz;BYeWNVF^$C#tE?8&c=HNp-CbR*)o8ZPvhElWY74+(faTRmvf)se z^qX&7OqsV$^p3w0?HxQfIzjD#8Clg}NY0BgurMiwLk`0oODHh9G{aqb8#DNXluX0I z>w=GB4i$n1H6}%2gi6`n;|aVb#4@I#S>lM84MTRrMIlv5sUj9O>!j5SS+Z>xfU^vx zGY``>Yww!Ly1HP>!iL*TANZ&lQl5RgV!iAEmoI}bqmJ`HFUkf_{e^}f)<_g5fyuv0 z&l>9Ap!I@UT6rmM*hIs}mXO_6HH}IN1!U73v5V56UPvWnJ_(iyYO0Ku?9hCG1e~um zdw#Gt_wsLAyH<CEKkQi&-#_-=u@8H4ujF@xfeA&vp};q+d1}^zyVruXYt?Pb6F+(F z!`D{+bans9eA}t~sjK;_YwKRl+4wc*bh_64oYQA^S`t=H<<Gp4=Ns<v)6DTtNiDfo z{P~lA!b&eyO8KS4Z9ac;3g!RKzxM5yb==Nw_MGxg?O8v+$#FoZ%5bQkP2lN{gYAwg zyuOqM3k;7OC4VkfU1Mm3TRnX#FubGK5iKpoB$Yl&JPn6ZsMiQ0ibM6bnnyJHsIj>3 zxx&8Z)@kM!URL%b29=%owe03z!64kqs6V!Jy36-3b_<;@mEwJ)?C(q4Mk1Sf18x3b z?~13>HuBu2-p+{YiQaHv;{~=nHgKuDl$d)dSoe1DBTSc(&hl!~*IsKbv+!_(9$j1( z#>&1{s{~%x4Yy0HG*Z5sH*f3>blw^^`$O^0T(h+W$61_}xq@W<pSbdX{x&{Y;JNom z*)#IL<roT6$exUt@1*2<LXIWO3o*GaYRpe60gmHjUKUFz40M!I^RlX>s1AiA)&#Wy z!vPQjZdCBfaF%emqvV*ayxf)^W;YDLnudjJL#gdJgLsV0D&WCAagaK5=|gcDmf<@l zW4#kYVO)<T{FQ;=h9J)@P$L)4SllWAv9MAF!;M?9sOzdqHM069ji@ZBr)hMCMlaH+ z7m?wYwP<++HI5>{=`~tS`)Snlu%4ajOVfy&`UuZ7j9FDj8+K%Wi3Cv49az+FO)X`L z?)`V&`yT|GmfBZ?tv8(yyun+ww=zpo(c5;{+eRz8@Y-rsTwmsk-u-vItVi3AJAT-) z8r*-=`9*LadM`Tv%NN|_YRxwd+{42tysyk>`hDL+U$E%gTk!2I`kL?hnm09G3+!K( zescB0tIN}{;Er@6s_NS6c4Z`T=NYJkZ1`IV^K66~P5jzfelZW1AI#NS%-9H2YS=@$ zUD4Q5!_@$#g;#0wq27fnFYXxZZPRpg(swKN=m`Z$|BIqAWHfh;Z5UN$>}ila^D!ql zy9Y4E!`A&{j<IX1D$gnNefy017COQR<Fr0Ra5CF$pL5lpbGtt0>OSYZpL4>0aE-s^ sI#;>Q-*SVi+~AikzpeW5Gk7um#*fFa$1QM+e)GDG+JKJ<@RkkoKP-Vb`Tzg` literal 0 HcmV?d00001 diff --git a/scripts/lib/agents/__init__.py b/scripts/lib/agents/__init__.py new file mode 100644 index 0000000..d91a839 --- /dev/null +++ b/scripts/lib/agents/__init__.py @@ -0,0 +1,52 @@ +import os +from typing import Dict, List, Optional +from .base import BaseAgent +from .antigravity import AntigravityAgent +from .hermes import HermesAgent + +_REGISTERED_AGENTS: List[BaseAgent] = [ + AntigravityAgent(), + HermesAgent(), +] + +_LOOKUP_MAP: Dict[str, BaseAgent] = {} +for agent in _REGISTERED_AGENTS: + _LOOKUP_MAP[agent.name.lower()] = agent + for alias in agent.aliases: + _LOOKUP_MAP[alias.lower()] = agent + + +def list_agents() -> List[BaseAgent]: + """Return list of all registered agent instances.""" + return list(_REGISTERED_AGENTS) + + +def get_agent(name_or_alias: str) -> Optional[BaseAgent]: + """Look up an agent by its primary name or any alias.""" + return _LOOKUP_MAP.get(name_or_alias.strip().lower()) + + +def get_default_agent() -> BaseAgent: + """ + Resolve the default agent to use: + 1. Check TASK_AGENT environment variable. + 2. Fall back to the first available agent CLI found on PATH. + 3. If none installed, return the first registered agent (which will report not found on launch). + """ + env_agent = os.environ.get("TASK_AGENT") + if env_agent: + matched = get_agent(env_agent) + if matched: + return matched + else: + raise ValueError( + f"Unknown agent specified in TASK_AGENT='{env_agent}'. " + f"Available agents: {', '.join([a.name for a in _REGISTERED_AGENTS])}" + ) + + # Auto-detect available agent + for agent in _REGISTERED_AGENTS: + if agent.is_available(): + return agent + + return _REGISTERED_AGENTS[0] diff --git a/scripts/lib/agents/__pycache__/__init__.cpython-314.pyc b/scripts/lib/agents/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..62decdcab3eff9404dbf497a4ffd9a87d99a3273 GIT binary patch literal 3225 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 literal 0 HcmV?d00001 diff --git a/scripts/lib/agents/__pycache__/antigravity.cpython-314.pyc b/scripts/lib/agents/__pycache__/antigravity.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..70cdfe826f3269a39352306ed5d8ad2fe99f6d47 GIT binary patch literal 1886 zcmb7EO>Ep$5Pr}0I^O(kew#v5#haF-tCC%b<iH_9O-s`xs?y+u<v<$C@jAP%9D8~1 zZ6Hw(dq5)el86iGtw(y~z=2zj93!=$;ufV6TzX3tM0#ZAc~>k5A@L;7n>RD>d1k(u zx!m2=PGJ37dUWT30y`d>E7f35=76b_HDb|uB9fg73TI_rnNx*2rwMI@tWisyPtEB< zA0dMzODt`OSgG6kl$yLF+AMm3<n)(#T+Au4cGKq~Hf~jgAB9ekqjCDG!@ZfZ7YaAI zlhDQk<fI5^2VA*L1ht4z*4l)6hD6|)&@4ryEHzJsUL=zge;ycC3TO}fxLwNHq|88B zK!stYMY>2tyQPC$9dXZ0DEu;WwtTTIeb~v(l$@&Yn7I)#^F|bv1J7(RGOyj7H!nHm z?OZ<YaQ?a+s~~eJ_=+Dc$SlDiPD~)vv`&g-O?mfoLP{VVCLhoWwdgYam=rKW%1gA5 zb>@j03n4AqdK3kaFdh8ZbVS@`+iu`+Uh-XG+Z?S&>+zMshumYlAfnB}5{vG5uHc1R zLFuaJu;R)}fxFDFf++CU3l8R(7h0UoSGVI%+jhb*5{~d}J3ikMs?0~eRow+Y<pV%o z*U2fNslqEH_e*j+0XdNwKn52^51(=J4ls4HiZZ#D(EEQZs2wuZ$el$OWKLKbjIe5S zUa1K`h*O_de7Azx;a;%8(R_^c0F66+ZaZ6!A2{oQ_b*|zs%02w;8vYH&kXL}y?6KE z`s2*#Hx~~x`9mYm`th%S!%_!{#Bu?c|H9Hz08vC0ck=d|VBT1*yro)NK?T%PtOL}s zUS-i{RmAPyXM#Dl!^)g>f?_eJ#wr(#X>efZKu%%Z$Y+2wYCy)%dQsX31hc^}1Cbb| zQ`uJp$o=JG+#XiG72jX`Zu`glj6DCaBDFGN7vFC#{Go*I5itJ;x~f`K#~ZJdA%<bH z+8im+u}y$0w5VE2k>=H^R#ckyCGdZk6t$(c#se)|QEA>CA*)7Hu_Oms^OWpa^3l{( zqrgUpg=yAgA?-}G6nE0c<IuF@In(DRbO3aGEi9Q)XfDmH%;k5K34bxCvq5;4VYS4) zvwq+$Mq(E2t}_;~SO>(suoNp1N8WR{ssQQ`UO9(k81EVDerQ)6QHfP|v&1nUV`F2c znN+SGIJMBN#6vCKC9RhzB%rf!2dcIIjjyM_ntqUa+&{K!9A$={_gy%=xcs<p<*;+* zMP}$=;9%>pdu&%d>KJ_5nb}+Ta$$exN$2QqM*Cj+UV8u1PlhS6en@h35yqck+o7|G zjo)tDn^CD2AP*zqlbRDWG#T68@EI3@A9`T~>NeXhMJ_NJTEuNm;KKyx>?{WS76v>B z0}2L;@7j(K%wMkwkAr=0%LsG5Ah7KmO)421`ASmAh&@AsAD;m7`U?pCXKnID$JzS( zPcs9%<@$|N4F=CKPM=d9>iQImTUN9TPu|ZNEQ@a;ez7jLtA1FPJtVsc<|{bO5DRI| y#*xF$Pf#(YlXSetCP9Y&@$*2ARZ8hIl72?Ij!5<wa_)D%k7oC`{~)l*>wf|~1FVGr literal 0 HcmV?d00001 diff --git a/scripts/lib/agents/__pycache__/base.cpython-314.pyc b/scripts/lib/agents/__pycache__/base.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3e1c370205e830aa6b3d1231a6da3ceab4d979ea GIT binary patch literal 2188 zcmb7EOK%)S5bmCton1fH@gsH!1~QJ3#l~b8LB!KItc`??qK%oDNC?@Z-r2Ty!gyxs zZU>yx@(`Smk`u~5z_;Xp0ymBvodY7wC6PGgX0r(ANLA0;URx25mZrM8s(ZS=s{U%S zFW*gI{9gO%iZEct#ooxYmDUs}O|nc}I#1@PqH~5aPT|^^H|H#6&1F>P6d5LC#5GS4 z*IJ_I%=E0X-OM;C*bi}8FcPyoGn@3Ws#JpcYK^OUTm!RpA&@G`U2Lczj#%iYhf`^_ z7p<>AVGzd>QPd^MSk5SOgv1b)vRp%DibUB<*~{c5icy3)*8<spu6<nPma=ZI_S)K; zMQ=#={axCZ)4neBscvoQMvLlkdlZ~jd5tHZbj>iy%d0$6*CxtsI8GI8jvq29ot0QP z<#}iJ!o0(9mn<g4rw|K%I3U>slhkrROwlH}OqPvTza*pv)~CpOv`$@miGD~*bewD& z4{?8D3$E5hRG^7fjpGn9XTwEnOeJ~G(-+qQUwNLy-KU$$Yo+%k7qX<{wbFu!uW(<< z(#5bg!<o3exLA_D2pV7tgK8-avxFFmjg6$&^H>zciYe}S$;dw3VyC?qu93$<JZh3% zVr23=C~nsD)B<~o85JCe9nkf*Y*3m|ICvVyweoD8`yV;Milgd0&6dtvHHetla02NB zkyI=UdCg%_%@LgWbyf{I*qw#);#{$^Nj4!Rl7}l2@0bt+Ad}uedh99-LfqU5q0`<2 zJ6l;D&yG#H@&e&h4wVX{)unnDlz%FfYe2!UXgHJSpM>r0(W$$pYn4oZo)MV8#BPXq zt)Y@`{wc7+V`5c`JUA)^GckcE0`C_{#)2V^q23RoU1E(?45D=i1mc#HAPyJg(l*v* zvwmCsIQ#v^FU7K6|GcQ{q!IfFaBn=mPOo2J>ycmYs8SFqjyw*o@^HiXBv7fdA^2Iz zc@657LMv9p2>iQXiYH(aPosDSgv3iHCfen)L;e4x|KP2q7KAHCQDAKjmF9o3NMNlC zRGh-;UsE{@yAIwq&79vMAa0K9=^zr=r3cmr;N)5hpupP(KBIPYEMUY(MlUb{c3Vfx zumq3sA~sMnp;OmvCt_>-&4f<Fx~oliAZO0=&;go-vp4}KJ9z+p!5aeng9oYIvuT(U zaH2`>4GdmC_wDqJ>0gJ=+#NczH8}a*%eS7rQ@CBYHTpw&>)7;GZu$WDUL(V>H9KZN zc|1Ex1o`lW;ZnS)&5=X=0H1+AgsNE~3b0DDEDWF@NvIYU?LU$LOCe8#c=R>-oB*qP z`aY}dWC^7^Inp<HWBi*B?pmXNbb&#`NgpSz6#B$-cx$)kt;MzV5HkvG@Z)tBw)1<; z^Hu^ORTxA(iou=ryjtu-E-_qj76s-*oJ28(;tYsnprdK70Eyt_Fm!2Xu7U3{r9@C& zS6qS_cJB9apNFC6!GqO&$9%t)(mjViMxl{bKmnvp@|*R-gZ{zh+xJe6Hs5)W8QL{~ z^evOv$G1`2&Vi<FX?xyx?B?KMZ{zFU9fO>Fk@8GmP$^^+lU03z71q!aTWvIgXjRuD wZB05HBzT%l-FtIWX$q&sE8xJ`B|gh-lTv!0jNd0?f7%1|{B`p$0+T-YH;B{K(*OVf literal 0 HcmV?d00001 diff --git a/scripts/lib/agents/__pycache__/hermes.cpython-314.pyc b/scripts/lib/agents/__pycache__/hermes.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..17abea3e4a7dfa7f2b629b6254d55105d6eea44c GIT binary patch literal 1862 zcmb7EO>7%Q6rR~#uh)qaH%XHKZOOW&Nv%*@ZF4{(LZpS_L{*w@gE^39wOMcCt=7Bl z%s7y!2Op3qy(Hp7dm=}A<iLR&;>0mh0F`Y}D#4|<R6(Ri-kY^W;DE$P^XAQ)H#6_& zdvCnAr;EV&t@_>VFBMpE(;2BYb5I7RMHY!gr-?{bDk$ugX{D?RwX6wkge+1^olcc? zp^uP3k|&lnOsv!`eL_v%5owE_AqD*zZf6ThtX=cDh>hzF;YXnp6lk2e>~L>t#S4X- z+(~Hr0di1*y#uD)B7#~(D2r*K4v`296Pl%nl%*D_&@1E;#m@u7N&!6vA9qPvTFMNR z1ymSTMr0~PbXhu>)g5=2Jy!R){C@q^RMlw+kD1F6Gm`^To|spzO&4RmCUw`d$$H+A zHd8PUCk_ZELAwu3Ad|F3Dr8Z4;}b%vAU#QL&>FSqP5L1zVNjH3s2}Uh6HOLEB($X{ z3Lr*0{9kuO++*8r;Ba2`U18fCwNJO=H%sqxkMWX->ZLgr-S%9;OV@+yWzS)i`S}ue znco0W;4hV=iM*5;Tx_hxS=)BPFcOaNY&(7_aZ>tNJbKatU-9EWUbM&|p{a|{kvy!* z<pAWw?f8rv^ckjca1odmSwNW_b7=Fw27rW2v~OE<Mn-}epoH+yd95k@AWnT$^W7T8 zgnPj<NA*#b0~%+2Zab@vA2>^a_fHRwo=6Xd;6jV+<_0(J-oN{xypuco)%<qu!nSdN z_2Y-qiH;*WNM!P6fcdZFSqh{O(}*j1{S{DekE6V$T3ShkG^ZF=TdX%&RBwp5%llX` z$9C8XXWeixTTo+_3&u3qFf^c`Ftnca0%<2*y3UTFv=0a}d>n{OPbQUrPJleTxsS`E z+SlUSE8ndBRGgCgALiw$^w`DsI|DzIS^FNC|C6<bT2aT^udHMd*$bVv0ae+_ZH-n` zOR>}nEou#|qI7OlA@nCnMVm`^4EcMlD4p9QWC1ZD$DC|eon5l#$p;e?ZFG%5+mV6B z8|&i<sIg|vb7Kkg#|oy;O~5^rb~CJ+QE1Lh&6kU@=GGjsuAIL!ThJK-o(%$t`>y(d zHyeqosQoTu5sP)mn-^ANCF00??rH;4iwI+g0CocJ8B2a>HylxmRkvQ{7?sh{QPWJI zs|QXqbZc?{(XvZqGNVb$-5q%Oi^<O?w~U>E!aZXzH@w?_X8W}pJN?#n)_R&7elWCk zd~0>Pw|Gz8>mGcZ&27wlHnX|@WA^m#M%PB>erEIB&xR?}{fHFkEOa@;wnL|m72jps z^{CnmkcVDyr|AUklbmfY`;3dg54|vg<Fsv8BNrGAHR7}r_|T|18%A%>qPK(4l%SLN zuI&iH{H3PwIOz8sX<M!r1h!qE37E0e-cGQL$!AD#@*<EIp8^Pf(Z-&3547HSoIADM z`)cdGLk;@QJ~p329k6|f0GFti{>cXl2Fv4Pn7vpRYYjhKk#!_13Yscd%`g*En~fod v)t}_Vm`vjF7P|y8G?1SHvaeD~pODNG(z8eMzmgMw=>0UmS@@H{BCr1il4qd7 literal 0 HcmV?d00001 diff --git a/scripts/lib/agents/antigravity.py b/scripts/lib/agents/antigravity.py new file mode 100644 index 0000000..14bc991 --- /dev/null +++ b/scripts/lib/agents/antigravity.py @@ -0,0 +1,25 @@ +import os +import shutil +from typing import List, Optional +from .base import BaseAgent + + +class AntigravityAgent(BaseAgent): + """Adapter for Google Antigravity CLI (agy).""" + + name = "antigravity" + aliases = ["agy"] + + def is_available(self) -> bool: + return shutil.which("agy") is not None + + def launch(self, prompt: str, extra_args: Optional[List[str]] = None) -> None: + bin_path = shutil.which("agy") + if not bin_path: + raise FileNotFoundError("Antigravity CLI ('agy') is not found on PATH.") + + cmd = [bin_path, "-i", prompt] + if extra_args: + cmd.extend(extra_args) + + os.execvp(bin_path, cmd) diff --git a/scripts/lib/agents/base.py b/scripts/lib/agents/base.py new file mode 100644 index 0000000..8aa2e9f --- /dev/null +++ b/scripts/lib/agents/base.py @@ -0,0 +1,22 @@ +from abc import ABC, abstractmethod +from typing import List, Optional + + +class BaseAgent(ABC): + """Abstract base class for AI CLI agents.""" + + name: str = "" + aliases: List[str] = [] + + @abstractmethod + def is_available(self) -> bool: + """Check if the agent CLI binary is installed and reachable in PATH.""" + pass + + @abstractmethod + def launch(self, prompt: str, extra_args: Optional[List[str]] = None) -> None: + """Launch the agent interactively with the pre-seeded prompt.""" + pass + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} name={self.name}>" diff --git a/scripts/lib/agents/hermes.py b/scripts/lib/agents/hermes.py new file mode 100644 index 0000000..848f76a --- /dev/null +++ b/scripts/lib/agents/hermes.py @@ -0,0 +1,25 @@ +import os +import shutil +from typing import List, Optional +from .base import BaseAgent + + +class HermesAgent(BaseAgent): + """Adapter for Hermes Agent CLI.""" + + name = "hermes" + aliases = ["hermes-agent"] + + def is_available(self) -> bool: + return shutil.which("hermes") is not None + + def launch(self, prompt: str, extra_args: Optional[List[str]] = None) -> None: + bin_path = shutil.which("hermes") + if not bin_path: + raise FileNotFoundError("Hermes CLI ('hermes') is not found on PATH.") + + cmd = [bin_path, "chat", "-q", prompt] + if extra_args: + cmd.extend(extra_args) + + os.execvp(bin_path, cmd) diff --git a/scripts/lib/filter_predicates.py b/scripts/lib/filter_predicates.py new file mode 100644 index 0000000..24d4480 --- /dev/null +++ b/scripts/lib/filter_predicates.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 + +import runpy +import sys +from pathlib import Path + + +def normalize_predicate(predicate, epics): + column, separator, values = predicate.partition(":") + if not separator or column.strip().lower() != "epic": + return predicate + labels = {} + for epic in epics: + value = epic.get("value", "").strip() + label = epic.get("label", "").strip() or value.replace("-", " ").replace("_", " ").title() + if value: + labels[value.lower()] = label + labels[label.lower()] = label + normalized = [labels.get(value.strip().lower(), value.strip()) for value in values.split(",")] + return f"Epic:{','.join(normalized)}" + + +def main(): + try: + update = runpy.run_path(str(Path(__file__).resolve().parents[1] / "update-tasks")) + text = Path(sys.argv[1]).read_text(encoding="utf-8") + epics = update["read_frontmatter"](text).get("epics", []) + for predicate in sys.argv[2:]: + print(normalize_predicate(predicate, epics)) + except (OSError, ValueError) as error: + print(f"Error normalizing task filters: {error}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/lib/task_context.py b/scripts/lib/task_context.py new file mode 100644 index 0000000..82592ba --- /dev/null +++ b/scripts/lib/task_context.py @@ -0,0 +1,132 @@ +import argparse +import importlib.machinery +import importlib.util +import os +import sys +from pathlib import Path +from typing import Any, Dict, List, Optional +from agents import BaseAgent, get_agent, get_default_agent, list_agents + + +def load_get_task_module(): + """Dynamically load scripts/get-task.""" + repo_root = Path(__file__).resolve().parent.parent.parent + get_task_script = repo_root / "scripts" / "get-task" + if not get_task_script.exists(): + raise FileNotFoundError(f"Could not find get-task script at {get_task_script}") + loader = importlib.machinery.SourceFileLoader("get_task_mod", str(get_task_script)) + spec = importlib.util.spec_from_loader("get_task_mod", loader) + if spec is None: + raise ImportError(f"Could not load spec for {get_task_script}") + mod = importlib.util.module_from_spec(spec) + loader.exec_module(mod) + return mod + + +def fetch_task(task_id: str, tasks_file: Optional[str] = None) -> Dict[str, Any]: + """Retrieve task dictionary using get_task.""" + mod = load_get_task_module() + return mod.get_task(task_id, tasks_file=tasks_file) + + +def assert_task_status(task: Dict[str, Any], allowed_statuses: List[str], command_name: str) -> None: + """Validate that the task status matches one of the allowed statuses, otherwise error and exit.""" + current_status = task.get("status", "").strip().lower() + allowed_lower = [s.lower() for s in allowed_statuses] + + if current_status not in allowed_lower: + if len(allowed_statuses) == 1: + req_str = f"'{allowed_statuses[0]}'" + else: + req_str = " or ".join([f"'{s}'" for s in allowed_statuses]) + sys.stderr.write( + f"Error: Task {task['id']} is in '{task['status']}' status. " + f"{command_name.capitalize()} requires status to be {req_str}.\n" + ) + sys.exit(1) + + +def build_workflow_prompt( + task: Dict[str, Any], + workflow_name: str, + workflow_objective: str, + extra_context: str = "", +) -> str: + """Construct a standardized, high-fidelity prompt for an AI agent.""" + 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. + +### Target Task: +- ID: {task['id']} +- Title: {task['title']} +- Project: {task['project']} +{subproj_line}{epic_line}- Current Status: {task['status']} +- Type: {task['type']} + +### Current Description & Checklist: +{desc} + +### Repository Context & Guidelines: +1. SSoT & Task Management (GEMINI.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). +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" + return prompt + + +def add_common_agent_arguments(parser: argparse.ArgumentParser) -> None: + """Register reusable CLI flags for agent selection and tasks.md path.""" + registered = list_agents() + agent_names = [a.name for a in registered] + + parser.add_argument( + "--agent", + choices=agent_names, + default=None, + help=f"AI agent CLI to use ({', '.join(agent_names)}). Defaults to $TASK_AGENT or first available.", + ) + parser.add_argument("--hermes", action="store_true", help="Shortcut for --agent hermes") + parser.add_argument("--agy", action="store_true", help="Shortcut for --agent antigravity") + parser.add_argument("-f", "--file", default=None, help="Path to tasks.md (optional)") + + +def resolve_selected_agent(args: argparse.Namespace) -> BaseAgent: + """Resolve which agent was requested via flags or environment.""" + if getattr(args, "hermes", False): + agent = get_agent("hermes") + elif getattr(args, "agy", False): + agent = get_agent("antigravity") + elif getattr(args, "agent", None): + agent = get_agent(args.agent) + else: + agent = get_default_agent() + + if not agent: + sys.stderr.write("Error: Could not resolve requested agent.\n") + sys.exit(1) + + if not agent.is_available(): + sys.stderr.write( + f"Error: Selected agent '{agent.name}' is not available on PATH. " + f"Please verify installation.\n" + ) + sys.exit(1) + + return agent diff --git a/scripts/package-release b/scripts/package-release new file mode 100755 index 0000000..75214a3 --- /dev/null +++ b/scripts/package-release @@ -0,0 +1,8 @@ +#!/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 "$@" diff --git a/scripts/research b/scripts/research new file mode 100755 index 0000000..dcb510d --- /dev/null +++ b/scripts/research @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +""" +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, +and launches the agent interactively. + +Usage: + ./scripts/research 037 + ./scripts/research OT-037 --hermes + ./scripts/research 037 --agy + ./scripts/research --help +""" + +import argparse +import sys +from pathlib import Path + +# Ensure scripts/lib is importable +sys.path.insert(0, str(Path(__file__).resolve().parent / "lib")) + +from task_context import ( + fetch_task, + assert_task_status, + build_workflow_prompt, + add_common_agent_arguments, + 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'.""" + + +def main(): + parser = argparse.ArgumentParser( + description="Research a task in Documentation/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) + + args = parser.parse_args() + + agent = resolve_selected_agent(args) + task = fetch_task(args.task_id, tasks_file=args.file) + assert_task_status(task, ["research"], command_name="research") + + prompt = build_workflow_prompt( + task=task, + workflow_name="conducting technical research on", + workflow_objective=RESEARCH_OBJECTIVES.format(task_id=task["id"]), + ) + + print(f"Launching {agent.name.capitalize()} ({agent.name}) to research {task['id']}: {task['title']}...") + agent.launch(prompt) + + +if __name__ == "__main__": + main() diff --git a/scripts/tests/__pycache__/test_task_scripts.cpython-314.pyc b/scripts/tests/__pycache__/test_task_scripts.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5e8b7718426939f31004e5c15dd7486b4be2d8a8 GIT binary patch literal 21767 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 literal 0 HcmV?d00001 diff --git a/scripts/tests/test_task_scripts.py b/scripts/tests/test_task_scripts.py new file mode 100644 index 0000000..a648865 --- /dev/null +++ b/scripts/tests/test_task_scripts.py @@ -0,0 +1,296 @@ +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest +from unittest.mock import patch + + +SCRIPTS = Path(__file__).resolve().parents[1] + + +def tracker(prefix=None, epics=True, number=1): + prefix_line = f"task-id-prefix: '{prefix}'\n" if prefix is not None else "" + epic_frontmatter = """epics: + - value: 'poc' + label: 'Proof of Concept' + description: 'Validate the architecture.' + - value: 'release' + label: "Team's Release" + description: 'Ship the product.' +""" if epics else "" + anchor_prefix = prefix.lower() if prefix else "ot" + return f"""--- +{prefix_line}projects: + - value: 'shared' + label: 'Shared' +subprojects: + - value: 'engine' + label: 'Engine' +{epic_frontmatter}task-statuses: + - value: 'pending' + label: 'Pending' + - value: 'in_progress' + label: 'In Progress' +task-types: + - value: 'feature' + label: 'Feature' +--- +# Tracker + +| ID | Title | Project | Status | Type | +|:---|:------|:--------|:-------|:-----| +| {anchor_prefix.upper()}-{number:03d} | Original | Shared | Pending | Feature | + +--- + +## Detailed Tasks + +<a id="{anchor_prefix}-{number:03d}" class="task" data-project="shared" data-subproject="engine" data-status="pending" data-task-type="feature"></a> +### Original +**ID:** {anchor_prefix.upper()}-{number:03d} +**Project:** Shared +**Subproject:** Engine +**Status:** Pending +**Type:** Feature + +**Description:** +Keep this description. +- [ ] First requirement +- [x] Second requirement + +--- + +## Notes + +See [original](#{anchor_prefix}-{number:03d}). + +#### Projects (Rendered from frontmatter projects) +Placeholder +<!-- Table Sort Injection --> +""" + + +class TaskScriptsTest(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.path = Path(self.temp.name) / "task tracker's file.md" + self.path.write_text(tracker(), encoding="utf-8") + self.env = os.environ.copy() + self.env.pop("TASK_ID_PREFIX", None) + self.env.pop("TASKS_FILE", None) + + def run_cli(self, command, *args, success=True, env=None): + executable = ["bash"] if command == "get-tasks" else [sys.executable] + result = subprocess.run( + [*executable, str(SCRIPTS / command), *args, "-f", str(self.path)], + env=self.env if env is None else env, + text=True, capture_output=True, + ) + if success: + self.assertEqual(result.returncode, 0, result.stderr) + else: + self.assertNotEqual(result.returncode, 0, result.stdout) + return result + + def get_task(self, task_id="1"): + return json.loads(self.run_cli("get-task", task_id, "--json").stdout) + + 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") + self.assertEqual((task["epic"], task["epic_label"]), ("poc", "Proof of Concept")) + self.assertEqual(task["subproject"], "engine") + self.assertIn('data-epic="poc"', task["raw_markdown"]) + self.assertIn("**Epic:** Proof of Concept", task["raw_markdown"]) + self.assertEqual(self.run_cli("get-task", "2", "--epic").stdout.strip(), "poc") + self.assertIn("Epic: Proof of Concept", self.run_cli("get-task", "2", "--metadata-only").stdout) + self.run_cli("update-task", "1", "--epic", "poc", "--status", "in_progress", "--check", "1") + self.run_cli("update-task", "1", "--title", "Renamed") + original = self.get_task() + self.assertEqual(original["epic"], "poc") + self.assertIn("Keep this description.", original["description"]) + self.assertIn("- [x] First requirement", original["description"]) + self.run_cli("update-task", "1", "--epic", "Team's Release", "--no-sync") + self.assertEqual(self.get_task()["epic"], "release") + self.run_cli("update-tasks") + synced = self.path.read_text() + self.run_cli("update-tasks") + self.assertEqual(synced, self.path.read_text()) + self.assertIn("#### Epics (Rendered from frontmatter epics)", synced) + self.run_cli("update-task", "1", "--clear-epic") + self.assertEqual(self.get_task()["epic"], "") + self.assertNotIn("**Epic:**", self.get_task()["raw_markdown"]) + self.run_cli("update-task", "2", "--epic", "") + self.assertEqual(self.get_task("2")["epic"], "") + + def test_invalid_epic_does_not_write(self): + before = self.path.read_text() + for command, args in ( + ("add-task", ("Invalid", "--epic", "missing")), + ("update-task", ("1", "--epic", "missing")), + ): + with self.subTest(command=command): + result = self.run_cli(command, *args, success=False) + self.assertIn("Unknown epic", result.stderr) + self.assertEqual(before, self.path.read_text()) + + def test_frontmatter_prefix_with_digits_and_large_ids(self): + self.path.write_text(tracker(prefix="ab2-work", number=999)) + self.run_cli("add-task", "Thousandth", "--epic", "release") + task = self.get_task("ab2-work-1000") + self.assertEqual(task["number"], 1000) + self.assertEqual(task["id"], "AB2-WORK-1000") + self.assertEqual(task["anchor_id"], "ab2-work-1000") + self.assertEqual(self.get_task("999")["anchor_id"], "ab2-work-0999") + self.assertEqual(self.get_task("999")["id"], "AB2-WORK-0999") + self.assertEqual(self.get_task("AB2-WORK-00999")["number"], 999) + self.run_cli("update-task", "999", "--title", "Still padded", "--no-sync") + self.assertIn("**ID:** AB2-WORK-0999", self.get_task("999")["raw_markdown"]) + self.run_cli("update-task", "AB2-WORK-1000", "--status", "pending") + self.assertEqual(self.get_task("1000")["status"], "pending") + env = {**self.env, "PATH": "/usr/bin:/bin"} + output = self.run_cli("get-tasks", "--all", env=env).stdout + self.assertLess(output.index("AB2-WORK-1000"), output.index("AB2-WORK-0999")) + self.run_cli("get-task", "WRONG-1000", success=False) + for invalid in ("abc", "1junk", "something 1000"): + self.run_cli("get-task", invalid, success=False) + + def test_environment_override_migrates_ids_and_links(self): + self.path.write_text( + tracker(prefix="doc") + + '\n[reference]: #doc-001\n<a href="#doc-001">Task</a>\n' + + '[external](https://example.com/#doc-001)\n' + ) + env = {**self.env, "TASK_ID_PREFIX": "env"} + self.run_cli("update-tasks", env=env) + text = self.path.read_text() + self.assertIn('id="env-001"', text) + self.assertIn("**ID:** ENV-001", text) + self.assertIn("[original](#env-001)", text) + self.assertIn("[reference]: #env-001", text) + self.assertIn('href="#env-001"', text) + self.assertIn("[external](https://example.com/#doc-001)", text) + self.run_cli("add-task", "Next", env=env) + task = json.loads(self.run_cli("get-task", "ENV-002", "--json", env=env).stdout) + self.assertEqual(task["id"], "ENV-002") + + 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()["epic"], "") + self.assertNotIn("| Epic", self.path.read_text()) + env = {**self.env, "TASKS_FILE": str(self.path)} + result = subprocess.run( + [sys.executable, str(SCRIPTS / "get-task"), "1", "--id"], + env=env, text=True, capture_output=True, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout.strip(), "OT-001") + + def test_invalid_prefix_and_duplicate_numbers(self): + for prefix in ("", "1BAD", "BAD SPACE", "BAD/"): + with self.subTest(prefix=prefix): + before = self.path.read_text() + self.run_cli("update-tasks", env={**self.env, "TASK_ID_PREFIX": prefix}, success=False) + self.assertEqual(before, self.path.read_text()) + text = self.path.read_text() + chunk = text.split("## Detailed Tasks\n", 1)[1].split("\n---\n\n## Notes", 1)[0] + self.path.write_text(text.replace("\n---\n\n## Notes", chunk + "\n---\n\n## Notes")) + self.assertIn("Duplicate numeric task IDs", self.run_cli("update-tasks", success=False).stderr) + + def test_summary_table_conflict_handling(self): + text = self.path.read_text() + # Inject conflict markers surrounding and within the summary table + conflicted = text.replace( + "| ID | Title |", + "<<<<<<< HEAD\n| ID | Title |\n=======\n| ID | Title | Project |\n>>>>>>> branch", + ) + self.path.write_text(conflicted) + self.run_cli("update-tasks") + updated = self.path.read_text() + self.assertNotIn("<<<<<<<", updated) + self.assertNotIn("=======", updated) + self.assertNotIn(">>>>>>>", updated) + self.assertIn("OT-001", updated) + self.assertIn("Original", updated) + + + def test_listing_epic_filters_and_selection_fallback(self): + self.run_cli("update-task", "1", "--epic", "release") + self.run_cli("add-task", "Second", "--epic", "poc") + 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")) + 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.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) + 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) + output = self.run_cli("get-tasks", "--all", "--reject", "Epic:poc", env=env).stdout + self.assertNotIn("OT-002", output) + self.assertIn("OT-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) + + def test_listing_external_filter_table_selects_epic(self): + self.run_cli("update-tasks") + stub = Path(self.temp.name) / "filter-table" + stub.write_text("#!/bin/sh\nprintf '%s\\n' \"$@\"\n") + stub.chmod(0o755) + env = {**self.env, "PATH": f"{self.temp.name}:/usr/bin:/bin"} + output = self.run_cli("get-tasks", "--all", "--accept", "Epic:Proof of Concept", env=env).stdout + self.assertIn("ID,Title,Project,Subproject,Epic,Status", output) + self.assertIn("Epic:Proof of Concept", output) + output = self.run_cli( + "get-tasks", "--all", "--accept", "epic:POC,release", "--reject", "Epic:release", + "--accept", "Status:Pending", env=env, + ).stdout + self.assertIn("Epic:Proof of Concept,Team's Release", output) + self.assertIn("Epic:Team's Release", output) + self.assertIn("Status:Pending", output) + self.assertNotIn("Epic:release", output) + + def test_filter_frontmatter_errors_are_reported(self): + self.path.write_text("No frontmatter\n") + env = {**self.env, "PATH": "/usr/bin:/bin"} + result = self.run_cli("get-tasks", "--accept", "Epic:poc", env=env, success=False) + self.assertIn("Error normalizing task filters: YAML frontmatter not found", result.stderr) + + def test_workflow_surfaces(self): + self.run_cli("update-task", "1", "--epic", "poc") + output = self.run_cli("work-on-task", "1", "--start").stdout + self.assertIn("Proof of Concept", output) + self.assertEqual(self.get_task()["status"], "in_progress") + with patch.dict(os.environ, self.env, clear=True): + sys.path.insert(0, str(SCRIPTS / "lib")) + try: + import task_context + task = task_context.fetch_task("1", tasks_file=str(self.path)) + prompt = task_context.build_workflow_prompt(task, "testing", "Verify the task") + finally: + sys.path.pop(0) + self.assertIn("- Epic: Proof of Concept (poc)", prompt) + self.assertIn("--epic=poc", prompt) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/triage b/scripts/triage new file mode 100755 index 0000000..f05e44c --- /dev/null +++ b/scripts/triage @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +""" +triage + +Triages a task from Documentation/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 094 --agy + ./scripts/triage --help +""" + +import argparse +import sys +from pathlib import Path + +# Ensure scripts/lib is importable +sys.path.insert(0, str(Path(__file__).resolve().parent / "lib")) + +from task_context import ( + fetch_task, + assert_task_status, + build_workflow_prompt, + add_common_agent_arguments, + resolve_selected_agent, +) + +TRIAGE_OBJECTIVES = """- Inspect the codebase to understand the current implementation 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 (- [ ]). +- 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.""" + + +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'." + ) + parser.add_argument("task_id", help="Task number or prefixed ID (e.g. 94, PREFIX-094)") + add_common_agent_arguments(parser) + + args = parser.parse_args() + + agent = resolve_selected_agent(args) + task = fetch_task(args.task_id, tasks_file=args.file) + assert_task_status(task, ["planning", "triage"], command_name="triage") + + prompt = build_workflow_prompt( + task=task, + workflow_name="triaging", + workflow_objective=TRIAGE_OBJECTIVES.format(task_id=task["id"]), + ) + + print(f"Launching {agent.name.capitalize()} ({agent.name}) to triage {task['id']}: {task['title']}...") + agent.launch(prompt) + + +if __name__ == "__main__": + main() diff --git a/scripts/update-task b/scripts/update-task new file mode 100755 index 0000000..ddc724f --- /dev/null +++ b/scripts/update-task @@ -0,0 +1,283 @@ +#!/usr/bin/env python3 +""" +update-task + +Modifies metadata, title, and/or description of a specific task in Documentation/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 +""" + +import argparse +import importlib.machinery +import importlib.util +import re +import sys +from pathlib import Path + + +def load_update_tasks_module(): + script_dir = Path(__file__).resolve().parent + update_script = script_dir / "update-tasks" + if not update_script.exists(): + raise FileNotFoundError(f"Could not find update-tasks at {update_script}") + loader = importlib.machinery.SourceFileLoader("update_tasks", str(update_script)) + spec = importlib.util.spec_from_loader("update_tasks", loader) + if spec is None: + raise ImportError(f"Could not load spec for {update_script}") + mod = importlib.util.module_from_spec(spec) + loader.exec_module(mod) + return mod + + +def parse_task_id(id_str): + return load_update_tasks_module().parse_task_id(id_str) + + +def update_task_detail( + task_id_input, + title=None, + project=None, + subproject=None, + clear_subproject=False, + status=None, + task_type=None, + description=None, + append_description=None, + check_items=None, + uncheck_items=None, + check_all=False, + uncheck_all=False, + sync=True, + tasks_file=None, + epic=None, + clear_epic=False, +): + update_mod = load_update_tasks_module() + tasks_path = update_mod.find_tasks_file(tasks_file) + + with open(tasks_path, "r", encoding="utf-8") as f: + text = f.read() + + fm_data = update_mod.read_frontmatter(text) + prefix = update_mod.get_task_id_prefix(fm_data) + target_num = update_mod.parse_task_id(task_id_input) + + detail_header_marker = "## Detailed Tasks\n" + detail_start = text.find(detail_header_marker) + if detail_start == -1: + raise ValueError("## Detailed Tasks heading not found") + + notes_marker = "\n---\n\n## Notes\n" + notes_start = text.find(notes_marker) + if notes_start == -1: + notes_start = len(text) + + detail_body = text[detail_start + len(detail_header_marker) : notes_start] + task_chunks = re.split(r"(?=<a\s+[^>]*\bid=|<id=)", detail_body) + + found = False + new_chunks = [] + + for chunk in task_chunks: + chunk_clean = chunk.strip() + if not chunk_clean: + continue + m = re.search(update_mod.TASK_ID_PATTERN, chunk_clean, re.IGNORECASE) + if m and int(m.group(2)) == target_num and update_mod.task_id_matches(task_id_input, m.group(1), prefix): + found = True + anchor_match = re.search(update_mod.TASK_ANCHOR_PATTERN, chunk_clean, re.IGNORECASE) + if not anchor_match: + raise ValueError(f"Anchor tag missing in task {target_num}") + anchor_tag = anchor_match.group(1) + + # Current metadata + proj_m = re.search(r'data-project=["\']([^"\']+)["\']', anchor_tag) + subproj_m = re.search(r'data-subproject=["\']([^"\']*)["\']', anchor_tag) + status_m = re.search(r'data-status=["\']([^"\']+)["\']', anchor_tag) + type_m = re.search(r'data-task-type=["\']([^"\']+)["\']', anchor_tag) + epic_m = re.search(r'data-epic=["\']([^"\']*)["\']', anchor_tag) + epic_input = epic if epic is not None else (epic_m.group(1) if epic_m else None) + new_epic, epic_label = update_mod.resolve_epic(None if clear_epic else epic_input, fm_data) + + if not subproj_m: + subproj_field_m = re.search(r'^\*\*Subproject:\*\*\s*([^\n]+)$', chunk_clean, re.MULTILINE) + current_subproj = subproj_field_m.group(1).strip() if subproj_field_m else "" + else: + current_subproj = subproj_m.group(1).strip() + + current_proj = proj_m.group(1) if proj_m else "shared" + current_status = status_m.group(1) if status_m else "triage" + current_type = type_m.group(1) if type_m else "feature" + + title_m = re.search(r'^###\s+(.+)$', chunk_clean, re.MULTILINE) + current_title = title_m.group(1).strip() if title_m else f"Task {target_num}" + + desc_m = re.search(r'\*\*Description:\*\*\s*\n(.*)$', chunk_clean, re.DOTALL) + current_desc = desc_m.group(1).strip() if desc_m else "" + + # Apply modifications + new_proj = project.strip().lower() if project else current_proj + new_status = status.strip().lower() if status else current_status + new_type = task_type.strip().lower() if task_type else current_type + new_title = title.strip() if title else current_title + + if clear_subproject or (subproject is not None and subproject.strip().lower() in ("", "none", "null", "clear")): + new_subproj = None + elif subproject is not None: + new_subproj = subproject.strip().lower() + else: + new_subproj = current_subproj if current_subproj else None + + if description is not None: + new_desc = description.strip() + else: + new_desc = current_desc + + if append_description: + if new_desc: + new_desc = new_desc + "\n" + append_description.strip() + else: + new_desc = append_description.strip() + + # Handle checkboxes + if check_all: + new_desc = re.sub(r'-\s*\[\s*\]', '- [x]', new_desc) + elif uncheck_all: + new_desc = re.sub(r'-\s*\[x\]', '- [ ]', new_desc) + + if check_items: + desc_lines = new_desc.splitlines() + cb_count = 0 + for i, line in enumerate(desc_lines): + if re.match(r'^\s*-\s*\[[\s x]\]', line): + cb_count += 1 + for ci in check_items: + if str(ci).isdigit() and int(ci) == cb_count: + desc_lines[i] = re.sub(r'-\s*\[[\s x]\]', '- [x]', line, count=1) + elif isinstance(ci, str) and ci.lower() in line.lower(): + desc_lines[i] = re.sub(r'-\s*\[[\s x]\]', '- [x]', line, count=1) + new_desc = "\n".join(desc_lines) + + if uncheck_items: + desc_lines = new_desc.splitlines() + cb_count = 0 + for i, line in enumerate(desc_lines): + if re.match(r'^\s*-\s*\[[\s x]\]', line): + cb_count += 1 + for ui in uncheck_items: + if str(ui).isdigit() and int(ui) == cb_count: + desc_lines[i] = re.sub(r'-\s*\[[\s x]\]', '- [ ]', line, count=1) + elif isinstance(ui, str) and ui.lower() in line.lower(): + desc_lines[i] = re.sub(r'-\s*\[[\s x]\]', '- [ ]', line, count=1) + new_desc = "\n".join(desc_lines) + + digit_width = max(3, len(m.group(1).rsplit("-", 1)[1])) + normalized_id = update_mod.format_task_id(target_num, prefix, digit_width) + normalized_aid = m.group(1) + + # Assemble modified chunk (update-tasks will format headers/labels upon sync) + if new_subproj: + anchor_tag_str = f'<a id="{normalized_aid}" class="task" data-project="{new_proj}" data-subproject="{new_subproj}" data-status="{new_status}" data-task-type="{new_type}"></a>' + subproj_line = f"**Subproject:** {new_subproj.capitalize()}\n" + else: + anchor_tag_str = f'<a id="{normalized_aid}" class="task" data-project="{new_proj}" data-status="{new_status}" data-task-type="{new_type}"></a>' + subproj_line = "" + + if new_epic: + anchor_tag_str = anchor_tag_str.replace('></a>', f' data-epic="{new_epic}"></a>') + epic_line = f"**Epic:** {epic_label}\n" if new_epic else "" + + updated_chunk = ( + f'{anchor_tag_str}\n' + f"### {new_title}\n" + f"**ID:** {normalized_id}\n" + f"**Project:** {new_proj.capitalize()}\n" + f"{subproj_line}" + f"{epic_line}" + f"**Status:** {new_status.capitalize()}\n" + f"**Type:** {new_type.capitalize()}\n\n" + f"**Description:**\n" + f"{new_desc}" + ) + new_chunks.append(updated_chunk) + else: + new_chunks.append(chunk_clean) + + if not found: + raise KeyError(f"Task {task_id_input} ({update_mod.format_task_id(target_num, prefix)}) was not found in {tasks_path}") + + # Reconstruct document + new_detail_body = "\n\n".join(new_chunks) + "\n" + updated_text = text[: detail_start + len(detail_header_marker)] + new_detail_body + text[notes_start:] + + with open(tasks_path, "w", encoding="utf-8") as f: + f.write(updated_text) + + if sync: + update_mod.update_tasks_document(tasks_path) + + return update_mod.format_task_id(target_num, prefix, digit_width), tasks_path + + +def main(): + parser = argparse.ArgumentParser(description="Modify task details and synchronize tasks.md") + parser.add_argument("task_id", help="Task number or prefixed ID (e.g. 18, PREFIX-018)") + parser.add_argument("--epic", default=None, help="Update epic using a frontmatter value or label") + parser.add_argument("--clear-epic", action="store_true", help="Remove epic from task") + parser.add_argument("-T", "--title", default=None, help="Update task title") + parser.add_argument("-p", "--project", default=None, help="Update project (e.g. android, editor, shared, story-engine)") + parser.add_argument("-S", "--subproject", default=None, help="Update subproject (e.g. actions, expressions, schema)") + parser.add_argument("--clear-subproject", action="store_true", help="Remove subproject from task") + parser.add_argument("-s", "--status", default=None, help="Update status (e.g. triage, pending, in_progress, done, blocked, cancelled)") + parser.add_argument("-t", "--type", default=None, help="Update task type (e.g. feature, bug, lint, chore)") + parser.add_argument("-d", "--description", default=None, help="Replace entire description body") + parser.add_argument("--desc-file", default=None, help="File containing new description body") + parser.add_argument("-a", "--append-description", default=None, help="Append text to existing description") + parser.add_argument("--check", action="append", default=[], help="Checkbox index (1-based) or substring to check [x]") + parser.add_argument("--uncheck", action="append", default=[], help="Checkbox index (1-based) or substring to uncheck [ ]") + parser.add_argument("--check-all", action="store_true", help="Mark all checkboxes in this task as checked [x]") + parser.add_argument("--uncheck-all", action="store_true", help="Mark all checkboxes in this task as unchecked [ ]") + parser.add_argument("--no-sync", action="store_true", help="Do not run update-tasks table sync") + parser.add_argument("-f", "--file", default=None, help="Path to tasks.md (optional)") + + args = parser.parse_args() + + desc_content = args.description + if args.desc_file: + with open(args.desc_file, "r", encoding="utf-8") as df: + desc_content = df.read() + + try: + formatted_id, path = update_task_detail( + task_id_input=args.task_id, + title=args.title, + project=args.project, + subproject=args.subproject, + clear_subproject=args.clear_subproject, + status=args.status, + task_type=args.type, + description=desc_content, + append_description=args.append_description, + check_items=args.check, + uncheck_items=args.uncheck, + check_all=args.check_all, + uncheck_all=args.uncheck_all, + sync=not args.no_sync, + tasks_file=args.file, + epic=args.epic, + clear_epic=args.clear_epic, + ) + print(f"Successfully updated task {formatted_id} in {path}") + except Exception as e: + sys.stderr.write(f"Error: {e}\n") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/update-tasks b/scripts/update-tasks new file mode 100755 index 0000000..0020529 --- /dev/null +++ b/scripts/update-tasks @@ -0,0 +1,574 @@ +#!/usr/bin/env python3 +""" +update-tasks + +Synchronizes and regenerates Documentation/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). + - 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)) +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) + - Task Types (Rendered from frontmatter task-types) +6. Assembles and writes the updated document. +""" + +import sys +import os +import re +from pathlib import Path + + +def find_tasks_file(explicit_path=None): + explicit_path = explicit_path or os.environ.get("TASKS_FILE") + if explicit_path: + p = Path(explicit_path) + if p.is_file(): + return p + raise FileNotFoundError(f"Specified file not found: {explicit_path}") + + # Search upwards or relative to script / cwd + script_dir = Path(__file__).resolve().parent + candidates = [ + Path.cwd() / "docs" / "tasks.md", + Path.cwd() / "docs" / "Tasks.md", + Path.cwd() / "Documentation" / "tasks.md", + Path.cwd() / "Documentation" / "Tasks.md", + Path.cwd() / "tasks.md", + Path.cwd() / "Tasks.md", + script_dir.parent / "docs" / "tasks.md", + script_dir.parent / "docs" / "Tasks.md", + script_dir.parent / "Documentation" / "tasks.md", + script_dir.parent / "Documentation" / "Tasks.md", + script_dir / "docs" / "tasks.md", + script_dir / "docs" / "Tasks.md", + script_dir / "Documentation" / "tasks.md", + script_dir / "Documentation" / "Tasks.md", + ] + for c in candidates: + if c.is_file(): + return c + + raise FileNotFoundError("Could not locate docs/tasks.md or docs/Tasks.md") + + +def parse_yaml_frontmatter(fm_text): + sections = {} + current_section = None + current_item = None + + for line in fm_text.splitlines(): + if line and not line.startswith(" ") and ":" in line and not line.endswith(":"): + key, value = line.split(":", 1) + sections[key.strip()] = value.strip().strip("\"'") + current_section = None + current_item = None + elif line and not line.startswith(" ") and line.endswith(":"): + current_section = line[:-1].strip() + sections[current_section] = [] + current_item = None + elif current_section is not None: + stripped = line.strip() + if not stripped: + continue + if line.startswith(" - "): + current_item = {} + sections[current_section].append(current_item) + kv = line[4:].strip() + if ":" in kv: + k, v = kv.split(":", 1) + current_item[k.strip()] = v.strip().strip("\"'") + elif line.startswith(" ") and current_item is not None: + kv = line.strip() + if ":" in kv: + k, v = kv.split(":", 1) + current_item[k.strip()] = v.strip().strip("\"'") + + return sections + + +TASK_ID_PATTERN = r'\bid=["\']([a-z][a-z0-9_-]*-(\d+))["\']' +TASK_ANCHOR_PATTERN = r'<(?:a\s+)?([^>]*' + TASK_ID_PATTERN + r'[^>]*)>' + + +def read_frontmatter(text): + match = re.match(r"^---\n(.*?)\n---", text, re.DOTALL) + if not match: + raise ValueError("YAML frontmatter not found in tasks.md") + return parse_yaml_frontmatter(match.group(1)) + + +def get_task_id_prefix(fm_data): + prefix = os.environ.get("TASK_ID_PREFIX", fm_data.get("task-id-prefix", "OT")) + 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() + + +def format_task_id(number, prefix, digit_width=3): + return f"{prefix}-{number:0{max(3, digit_width, len(str(number)))}d}" + + +def parse_task_id(id_str): + match = re.fullmatch(r"(?:[a-z][a-z0-9_-]*-)?(\d+)", str(id_str).strip(), re.IGNORECASE) + if not match: + raise ValueError(f"Invalid task ID '{id_str}'; expected a task number or PREFIX-number") + return int(match.group(1)) + + +def task_id_matches(id_str, anchor_id, prefix): + number = parse_task_id(id_str) + task_id = str(id_str).strip() + anchor_prefix, anchor_number = anchor_id.rsplit("-", 1) + return number == int(anchor_number) and ( + task_id.isdigit() or task_id.rsplit("-", 1)[0].lower() in (anchor_prefix.lower(), prefix.lower()) + ) + + +def resolve_epic(epic, fm_data): + if epic is None or epic.strip().lower() in ("", "none", "null", "clear"): + return "", "" + for item in fm_data.get("epics", []): + value = item.get("value", "").strip() + label = item.get("label", "").strip() or value.replace("-", " ").replace("_", " ").title() + if value and epic.strip().lower() in (value.lower(), label.lower()): + return value, label + raise ValueError(f"Unknown epic '{epic}'; use a value or label from frontmatter epics") + + +def build_markdown_table(headers, rows, alignments=None): + if not rows: + return "" + + num_cols = len(headers) + widths = [len(h) for h in headers] + for row in rows: + for i, val in enumerate(row): + widths[i] = max(widths[i], len(str(val))) + + if alignments is None: + alignments = [":---"] * num_cols + + header_line = "| " + " | ".join(f"{h:<{widths[i]}}" for i, h in enumerate(headers)) + " |\n" + separator_parts = [] + for i, a in enumerate(alignments): + w = max(widths[i], 3) + if a.startswith(":") and a.endswith(":"): + separator_parts.append(":" + "-" * (w - 2) + ":") + elif a.endswith(":"): + separator_parts.append("-" * (w - 1) + ":") + else: + separator_parts.append(":" + "-" * (w - 1)) + separator_line = "|" + "|".join(separator_parts) + "|\n" + + row_lines = [] + for row in rows: + line = "| " + " | ".join(f"{str(row[i]):<{widths[i]}}" for i in range(num_cols)) + " |\n" + row_lines.append(line) + + return header_line + separator_line + "".join(row_lines) + + +def update_tasks_document(tasks_path): + tasks_path = Path(tasks_path) + with open(tasks_path, "r", encoding="utf-8") as f: + text = f.read() + + # 1. Parse frontmatter + fm_match = re.match(r"^---\n(.*?)\n---", text, re.DOTALL) + if not fm_match: + raise ValueError("YAML frontmatter not found in tasks.md") + + fm_text = fm_match.group(1) + fm_data = parse_yaml_frontmatter(fm_text) + prefix = get_task_id_prefix(fm_data) + + project_list = fm_data.get("projects", []) + epic_list = fm_data.get("epics", []) + subproject_list = fm_data.get("subprojects", []) + status_list = fm_data.get("task-statuses", []) + type_list = fm_data.get("task-types", []) + + # Maps for lookup (both value and label matching) + project_map = {} + project_value_map = {} + for p in project_list: + val = p.get("value", "").strip() + lbl = p.get("label", "").strip() + if val: + project_map[val.lower()] = lbl or val.capitalize() + project_value_map[val.lower()] = val + if lbl: + project_map[lbl.lower()] = lbl + if val: + project_value_map[lbl.lower()] = val + + subproject_map = {} + subproject_value_map = {} + for sp in subproject_list: + val = sp.get("value", "").strip() + lbl = sp.get("label", "").strip() + if val: + subproject_map[val.lower()] = lbl or val.replace("-", " ").title() + subproject_value_map[val.lower()] = val + if lbl: + subproject_map[lbl.lower()] = lbl + if val: + subproject_value_map[lbl.lower()] = val + + status_map = {s.get("value", "").lower(): s.get("label", "") for s in status_list} + status_value_map = {s.get("value", "").lower(): s.get("value", "") for s in status_list} + for s in status_list: + lbl = s.get("label", "").strip() + val = s.get("value", "").strip() + if lbl: + status_map[lbl.lower()] = lbl + if val: + status_value_map[lbl.lower()] = val + # Accept 'done' or 'fixed' fallbacks if not defined in frontmatter + status_map.setdefault("fixed", status_map.get("done", "Done")) + status_map.setdefault("done", status_map.get("fixed", "Done")) + status_value_map.setdefault("fixed", "done") + status_value_map.setdefault("done", "done") + + type_map = {t.get("value", "").lower(): t.get("label", "") for t in type_list} + type_value_map = {t.get("value", "").lower(): t.get("value", "") for t in type_list} + for t in type_list: + lbl = t.get("label", "").strip() + val = t.get("value", "").strip() + if lbl: + type_map[lbl.lower()] = lbl + if val: + type_value_map[lbl.lower()] = val + + # 2. Locate boundaries + m_detail = re.search(r"^## Detailed Tasks\s*$", text, re.MULTILINE) + if not m_detail: + raise ValueError("## Detailed Tasks heading not found") + detail_start = m_detail.start() + detail_header_len = len(m_detail.group(0)) + + m_notes = re.search(r"(?:\n\s*---\s*)?\n\s*## Notes\s*$", text, re.MULTILINE) + if not m_notes: + raise ValueError("## Notes section marker not found") + notes_start = m_notes.start() + + # Determine part_a (everything before the summary table) + tasks_list_m = re.search(r'<a\s+[^>]*\bid=["\']tasks-list["\'][^>]*></a>', text, re.IGNORECASE) + progress_m = re.search(r'^## Overall Progress\s*$', text, re.MULTILINE) + + if tasks_list_m and tasks_list_m.start() < detail_start: + part_a = text[:tasks_list_m.end()].rstrip() + "\n\n" + table_search_start = tasks_list_m.end() + elif progress_m and progress_m.start() < detail_start: + part_a = text[:progress_m.end()].rstrip() + "\n\n" + table_search_start = progress_m.end() + else: + table_start_marker = "| ID" + table_start = text.find(table_start_marker) + if table_start == -1 or table_start >= detail_start: + raise ValueError("Overall Progress Table (| ID) not found") + # Strip any leading git conflict markers right before | ID from part_a + raw_part_a = text[:table_start] + raw_part_a = re.sub(r'<{7}\s+[^\n]*\n*$', '', raw_part_a) + part_a = raw_part_a.rstrip() + "\n\n" + table_search_start = table_start + + # Determine part_c (the transition into ## Detailed Tasks) + # Check if <a id="task-details"></a> was present between table and ## Detailed Tasks + between_table_and_detail = text[table_search_start:detail_start] + has_task_details_anchor = bool(re.search(r'<a\s+[^>]*\bid=["\']task-details["\'][^>]*>', between_table_and_detail, re.IGNORECASE)) + + if has_task_details_anchor: + part_c = "\n\n---\n\n<a id=\"task-details\"></a>\n\n## Detailed Tasks\n" + else: + part_c = "\n\n---\n\n## Detailed Tasks\n" + + detail_body = text[detail_start + detail_header_len : notes_start] + + # 3. Extract and Sort Task Chunks by Numeric ID + task_chunks = re.split(r"(?=<a\s+[^>]*\bid=|<id=)", detail_body) + task_chunks = [c.strip() for c in task_chunks if c.strip()] + + if not task_chunks: + raise ValueError("No task chunks found in Detailed Tasks") + + extracted_chunks = [] + for chunk in task_chunks: + m = re.search(TASK_ID_PATTERN, chunk, re.IGNORECASE) + if not m: + raise ValueError(f"Could not find task ID in chunk:\n{chunk[:150]}") + num = int(m.group(2)) + extracted_chunks.append((num, chunk)) + + seen_nums = {} + for num, chunk in extracted_chunks: + seen_nums.setdefault(num, []).append(chunk) + + duplicates = [num for num, chunks in seen_nums.items() if len(chunks) > 1] + if duplicates: + prefix = get_task_id_prefix(fm_data) + dup_ids = [format_task_id(num, prefix) for num in sorted(duplicates)] + raise ValueError(f"Duplicate numeric task IDs found in Detailed Tasks: {', '.join(dup_ids)}") + + # Sort task detail section chunks by numeric ID + extracted_chunks.sort(key=lambda x: x[0]) + + max_id_num = extracted_chunks[-1][0] if extracted_chunks else 1 + digit_width = max(3, len(str(max_id_num))) + + # 4. Update the Task Detail Section (First) + updated_detailed_chunks = [] + updated_task_records = [] + + header_block_pattern = r'<(?:a\s+[^>]*\bid=|\bid=)["\'][a-zA-Z][a-zA-Z0-9_-]*-\d+["\'][^>]*></a>\s*\n###\s+[^\n]+\n\*\*ID:\*\*[^\n]+\n\*\*Project:\*\*[^\n]+(?:\n\*\*(?:Subproject|Epic):\*\*[^\n]*)*\n\*\*Status:\*\*[^\n]+\n\*\*Type:\*\*[^\n]+' + anchor_replacements = {} + + for num, chunk in extracted_chunks: + anchor_match = re.search(TASK_ANCHOR_PATTERN, chunk, re.IGNORECASE) + if not anchor_match: + raise ValueError(f"Anchor tag missing in task chunk {num}") + anchor_tag = anchor_match.group(1) + + proj_m = re.search(r'data-project=["\']([^"\']+)["\']', anchor_tag) + subproj_m = re.search(r'data-subproject=["\']([^"\']*)["\']', anchor_tag) + status_m = re.search(r'data-status=["\']([^"\']+)["\']', anchor_tag) + type_m = re.search(r'data-task-type=["\']([^"\']+)["\']', anchor_tag) + epic_m = re.search(r'data-epic=["\']([^"\']*)["\']', anchor_tag) + epic_key, expected_epic = resolve_epic(epic_m.group(1) if epic_m else None, fm_data) + + # Also check for **Subproject:** in chunk if anchor tag didn't have data-subproject + if not subproj_m: + subproj_field_m = re.search(r'^\*\*Subproject:\*\*\s*([^\n]+)$', chunk, re.MULTILINE) + subproj_val = subproj_field_m.group(1).strip() if subproj_field_m else None + else: + subproj_val = subproj_m.group(1).strip() if subproj_m.group(1).strip() else None + + proj_raw = proj_m.group(1).strip() if proj_m else "shared" + status_raw = status_m.group(1).strip() if status_m else "triage" + type_raw = type_m.group(1).strip() if type_m else "feature" + + proj_key = project_value_map.get(proj_raw.lower(), proj_raw.lower()) + status_key = status_value_map.get(status_raw.lower(), status_raw.lower()) + type_key = type_value_map.get(type_raw.lower(), type_raw.lower()) + + expected_proj = project_map.get(proj_key, project_map.get(proj_raw.lower(), proj_raw.capitalize())) + expected_status = status_map.get(status_key, status_map.get(status_raw.lower(), status_raw.capitalize())) + expected_type = type_map.get(type_key, type_map.get(type_raw.lower(), type_raw.capitalize())) + + subproj_key = None + expected_subproj = None + if subproj_val and subproj_val.lower() not in ("", "none", "null"): + subproj_key = subproject_value_map.get(subproj_val.lower(), subproj_val.lower()) + expected_subproj = subproject_map.get(subproj_key, subproject_map.get(subproj_val.lower(), subproj_val.replace("-", " ").replace("_", " ").title())) + + title_m = re.search(r'^###\s+(.+)$', chunk, re.MULTILINE) + title = title_m.group(1).strip() if title_m else f"Task {num}" + + normalized_id = format_task_id(num, prefix, digit_width) + normalized_aid = normalized_id.lower() + anchor_replacements[anchor_match.group(2).lower()] = normalized_aid + + if subproj_key: + anchor_tag_str = f'<a id="{normalized_aid}" class="task" data-project="{proj_key}" data-subproject="{subproj_key}" data-status="{status_key}" data-task-type="{type_key}"></a>' + subproj_line = f"**Subproject:** {expected_subproj}\n" + else: + anchor_tag_str = f'<a id="{normalized_aid}" class="task" data-project="{proj_key}" data-status="{status_key}" data-task-type="{type_key}"></a>' + subproj_line = "" + + if epic_key: + anchor_tag_str = anchor_tag_str.replace('></a>', f' data-epic="{epic_key}"></a>') + epic_line = f"**Epic:** {expected_epic}\n" if epic_key else "" + + new_header_block = ( + f'{anchor_tag_str}\n' + f"### {title}\n" + f"**ID:** {normalized_id}\n" + f"**Project:** {expected_proj}\n" + f"{subproj_line}" + f"{epic_line}" + f"**Status:** {expected_status}\n" + f"**Type:** {expected_type}" + ) + + if re.search(header_block_pattern, chunk): + updated_chunk = re.sub(header_block_pattern, new_header_block, chunk, count=1) + else: + # Fallback if header spacing or fields differed slightly + rest = chunk.split("**Type:**", 1)[1].lstrip().split("\n", 1)[1] if "**Type:**" in chunk else "" + updated_chunk = f"{new_header_block}\n\n{rest}".rstrip() + + updated_detailed_chunks.append(updated_chunk) + + updated_task_records.append({ + "num": num, + "id": normalized_id, + "aid": normalized_aid, + "title": title, + "project": expected_proj, + "subproject": expected_subproj or "", + "epic": expected_epic, + "status": expected_status, + "type": expected_type, + }) + + # Assemble sorted, updated Detailed Tasks section + sorted_detailed_tasks = "\n\n".join(updated_detailed_chunks) + + # 5. Rebuild Summary Table (Rendered from the sorted, updated task details) + has_subprojects = any(bool(t.get("subproject")) for t in updated_task_records) + if has_subprojects: + summary_headers = ["ID", "Title", "Project", "Subproject", "Status", "Type"] + alignments = [":---", ":---", ":---", ":---", ":---", ":---"] + else: + summary_headers = ["ID", "Title", "Project", "Status", "Type"] + alignments = [":---", ":---", ":---", ":---", ":---"] + has_epics = bool(epic_list) + if has_epics: + summary_headers.insert(-2, "Epic") + alignments.insert(-2, ":---") + + summary_rows = [] + + for task in updated_task_records: + clean_title = task["title"] + while (clean_title.startswith("**") and clean_title.endswith("**")) or ( + clean_title.startswith("*") and clean_title.endswith("*") + ): + if clean_title.startswith("**") and clean_title.endswith("**"): + clean_title = clean_title[2:-2].strip() + elif clean_title.startswith("*") and clean_title.endswith("*"): + clean_title = clean_title[1:-1].strip() + + st_lower = task["status"].lower() + if st_lower == "pending": + formatted_title = f"**{clean_title}**" + elif st_lower in ("triage", "planning"): + formatted_title = f"*{clean_title}*" + else: + formatted_title = clean_title + + formatted_type = f"[{task['type']}](#{task['aid']})" + row = [task["id"], formatted_title, task["project"]] + if has_subprojects: + row.append(task["subproject"]) + if has_epics: + row.append(task["epic"]) + row.extend([task["status"], formatted_type]) + summary_rows.append(row) + + new_summary_table = build_markdown_table( + summary_headers, summary_rows, alignments=alignments + ).rstrip("\n") + + # 8. Re-render metadata derived enum tables at end of document + end_text = text[notes_start:] + + # Projects table: Value | Label | Prefix | Path + project_headers = ["Value", "Label", "Prefix", "Path"] + project_rows = [ + [p.get("value", ""), p.get("label", ""), p.get("prefix", ""), p.get("path", "")] + for p in project_list + ] + projects_table = build_markdown_table(project_headers, project_rows, [":---", ":---", ":---", ":---"]).strip() + + # Statuses table: Value | Label | Description + status_headers = ["Value", "Label", "Description"] + status_rows = [ + [s.get("value", ""), s.get("label", ""), s.get("description", "")] + for s in status_list + ] + statuses_table = build_markdown_table(status_headers, status_rows, [":---", ":---", ":---"]).strip() + + # Types table: Value | Label | Prefix | Description + type_headers = ["Value", "Label", "Prefix", "Description"] + type_rows = [ + [t.get("value", ""), t.get("label", ""), t.get("prefix", ""), t.get("description", "")] + for t in type_list + ] + types_table = build_markdown_table(type_headers, type_rows, [":---", ":---", ":---", ":---"]).strip() + + # Subprojects table if present in frontmatter + subproject_section = "" + if subproject_list: + subproject_headers = ["Value", "Label", "Description"] + subproject_rows = [ + [sp.get("value", ""), sp.get("label", ""), sp.get("description", "")] + for sp in subproject_list + ] + subprojects_table = build_markdown_table(subproject_headers, subproject_rows, [":---", ":---", ":---"]).strip() + subproject_section = f"\n\n#### Subprojects (Rendered from frontmatter subprojects)\n{subprojects_table}" + + epic_section = "" + if epic_list: + epic_rows = [[e.get("value", ""), e.get("label", ""), e.get("description", "")] for e in epic_list] + epics_table = build_markdown_table(["Value", "Label", "Description"], epic_rows).strip() + epic_section = f"\n\n#### Epics (Rendered from frontmatter epics)\n{epics_table}" + + # Replace enum tables in end_text + enum_tables_pattern = r"(#### Projects \(Rendered from frontmatter projects\)\n).*?(<!-- Table Sort Injection -->)" + new_enum_tables = ( + "#### Projects (Rendered from frontmatter projects)\n" + f"{projects_table}\n\n" + "#### Task Statuses (Rendered from frontmatter task-statuses)\n" + f"{statuses_table}\n\n" + "#### Task Types (Rendered from frontmatter task-types)\n" + f"{types_table}" + f"{subproject_section}{epic_section}\n\n" + r"\2" + ) + + updated_end_text = re.sub(enum_tables_pattern, new_enum_tables, end_text, flags=re.DOTALL) + + # 9. Assemble full document + new_full_text = ( + part_a + + new_summary_table + + part_c + + "\n" + + sorted_detailed_tasks + + "\n\n" + + updated_end_text.lstrip("\n") + ) + new_full_text = re.sub( + r'(\]\(|href=["\']|^\[[^\]\n]+\]:[ \t]*)#([a-z][a-z0-9_-]*-\d+)(?=[\s)"\']|$)', + lambda m: m.group(1) + "#" + anchor_replacements.get(m.group(2).lower(), m.group(2)), + new_full_text, + flags=re.IGNORECASE | re.MULTILINE, + ) + + with open(tasks_path, "w", encoding="utf-8") as f: + f.write(new_full_text) + + return len(updated_task_records) + + +def main(): + import argparse + parser = argparse.ArgumentParser(description="Synchronize and update Documentation/tasks.md") + parser.add_argument( + "-f", "--file", + dest="file", + default=None, + help="Path to tasks.md (defaults to searching workspace)" + ) + args = parser.parse_args() + + try: + tasks_file = find_tasks_file(args.file) + count = update_tasks_document(tasks_file) + print(f"Successfully synchronized and updated {count} tasks in {tasks_file}") + except Exception as e: + print(f"Error updating tasks: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/work-on-task b/scripts/work-on-task new file mode 100755 index 0000000..390dedf --- /dev/null +++ b/scripts/work-on-task @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +""" +work-on-task + +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 +""" + +import argparse +import sys +from pathlib import Path + +# Add scripts/lib to Python path +sys.path.insert(0, str(Path(__file__).resolve().parent / "lib")) +from task_context import fetch_task, load_get_task_module + + +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("-f", "--file", dest="file", default=None, help="Path to tasks.md") + args = parser.parse_args() + + try: + task = fetch_task(args.task_id, tasks_file=args.file) + except Exception as e: + sys.stderr.write(f"Error fetching task {args.task_id}: {e}\n") + sys.exit(1) + + status = task.get("status", "").strip().lower() + valid_work_statuses = ["pending", "in_progress"] + invalid_statuses = ["planning", "triage", "research", "done", "cancelled", "blocked"] + + 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" + f"Tasks in '{task['status']}' are usually not ready to be worked on directly.\n" + f"To bypass this check, pass --force.\n" + ) + sys.exit(2) + + print(f"=== Task {task['id']}: {task['title']} ===") + print(f"Project: {task['project']}") + if task.get("epic"): + print(f"Epic: {task['epic_label']}") + print(f"Status: {task['status']}") + print(f"Type: {task['type']}") + if task.get("description"): + print("\nRequirements & Description:") + print(task["description"]) + + if args.start and status != "in_progress": + mod = load_get_task_module() + update_mod = mod.load_update_tasks_module() + script_dir = Path(__file__).resolve().parent + import importlib.machinery + import importlib.util + ut_script = script_dir / "update-task" + loader = importlib.machinery.SourceFileLoader("update_task_mod", str(ut_script)) + spec = importlib.util.spec_from_loader("update_task_mod", loader) + ut_mod = importlib.util.module_from_spec(spec) + loader.exec_module(ut_mod) + + ut_mod.update_task_detail(task["id"], status="in_progress", tasks_file=args.file) + print(f"\n[Status updated to 'in_progress']") + + +if __name__ == "__main__": + main()