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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 75 additions & 13 deletions .github/workflows/hats-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,26 @@ on:
required: false
type: string
default: ""
category:
description: "Playground category (default: inferred from task type)"
required: false
type: string
default: ""
genre:
description: "Playground genre/type bucket"
required: false
type: string
default: ""
project:
description: "Playground project slug"
required: false
type: string
default: ""
workspace_root:
description: "Sandbox workspace root on the runner"
required: false
type: string
default: "/tmp/hats-playground"

permissions:
contents: read
Expand Down Expand Up @@ -90,17 +110,31 @@ jobs:
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
# workflow_dispatch inputs are controlled by the UI schema — safe
echo "task=${{ inputs.task }}" >> "$GITHUB_OUTPUT"
echo "hats=${{ inputs.hats }}" >> "$GITHUB_OUTPUT"
echo "callback_repo=${{ inputs.callback_repo }}" >> "$GITHUB_OUTPUT"
echo "callback_pr=${{ inputs.callback_pr }}" >> "$GITHUB_OUTPUT"
echo "callback_issue=${{ inputs.callback_issue }}" >> "$GITHUB_OUTPUT"
# Prompt may contain special chars — pass via env to avoid code injection
INPUT_PROMPT="${{ inputs.prompt }}" python3 -c "
# String inputs may contain newlines — sanitize before writing outputs
INPUT_PROMPT="${{ inputs.prompt }}" \
INPUT_HATS="${{ inputs.hats }}" \
INPUT_CALLBACK_REPO="${{ inputs.callback_repo }}" \
INPUT_CALLBACK_PR="${{ inputs.callback_pr }}" \
INPUT_CALLBACK_ISSUE="${{ inputs.callback_issue }}" \
INPUT_CATEGORY="${{ inputs.category }}" \
INPUT_GENRE="${{ inputs.genre }}" \
INPUT_PROJECT="${{ inputs.project }}" \
INPUT_WORKSPACE_ROOT="${{ inputs.workspace_root }}" \
python3 -c "
import os, re
val = os.environ.get('INPUT_PROMPT', '')
sanitized = re.sub(r'[\r\n]', ' ', val).strip()
def sanitize(value):
text = str(value or '')
return re.sub(r'[\r\n]', ' ', text).strip()
with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
f.write(f'prompt={sanitized}\n')
f.write(f\"prompt={sanitize(os.environ.get('INPUT_PROMPT', ''))}\n\")
f.write(f\"hats={sanitize(os.environ.get('INPUT_HATS', ''))}\n\")
f.write(f\"callback_repo={sanitize(os.environ.get('INPUT_CALLBACK_REPO', ''))}\n\")
f.write(f\"callback_pr={sanitize(os.environ.get('INPUT_CALLBACK_PR', ''))}\n\")
f.write(f\"callback_issue={sanitize(os.environ.get('INPUT_CALLBACK_ISSUE', ''))}\n\")
f.write(f\"category={sanitize(os.environ.get('INPUT_CATEGORY', ''))}\n\")
f.write(f\"genre={sanitize(os.environ.get('INPUT_GENRE', ''))}\n\")
f.write(f\"project={sanitize(os.environ.get('INPUT_PROJECT', ''))}\n\")
f.write(f\"workspace_root={sanitize(os.environ.get('INPUT_WORKSPACE_ROOT', ''))}\n\")
"
else
# repository_dispatch — extract from client_payload with sanitization
Expand All @@ -126,6 +160,10 @@ jobs:
f.write(f\"callback_repo={sanitize(payload.get('callback_repo', ''))}\n\")
f.write(f\"callback_pr={sanitize(payload.get('callback_pr', ''))}\n\")
f.write(f\"callback_issue={sanitize(payload.get('callback_issue', ''))}\n\")
f.write(f\"category={sanitize(payload.get('category', ''))}\n\")
f.write(f\"genre={sanitize(payload.get('genre', ''))}\n\")
f.write(f\"project={sanitize(payload.get('project', ''))}\n\")
f.write(f\"workspace_root={sanitize(payload.get('workspace_root', '/tmp/hats-playground'))}\n\")
f.write(f\"context={sanitize(payload.get('context', ''))}\n\")
"
fi
Expand Down Expand Up @@ -175,14 +213,29 @@ jobs:
--task "${{ steps.params.outputs.task }}"
--prompt "${{ steps.params.outputs.prompt }}"
--config scripts/hat_configs.yml
--output /tmp/hats-task-output
--json-file /tmp/hats-task-result.json
--workspace-root "${{ steps.params.outputs.workspace_root }}"
--source-repo "${{ steps.params.outputs.callback_repo }}"
--source-pr "${{ steps.params.outputs.callback_pr }}"
--source-issue "${{ steps.params.outputs.callback_issue }}"
)

if [ -n "${{ steps.params.outputs.hats }}" ]; then
ARGS+=(--hats "${{ steps.params.outputs.hats }}")
fi

if [ -n "${{ steps.params.outputs.category }}" ]; then
ARGS+=(--category "${{ steps.params.outputs.category }}")
fi

if [ -n "${{ steps.params.outputs.genre }}" ]; then
ARGS+=(--genre "${{ steps.params.outputs.genre }}")
fi

if [ -n "${{ steps.params.outputs.project }}" ]; then
ARGS+=(--project "${{ steps.params.outputs.project }}")
fi

if [ -d "/tmp/hats-context" ] && [ "$(ls -A /tmp/hats-context 2>/dev/null)" ]; then
ARGS+=(--context-dir /tmp/hats-context)
fi
Expand Down Expand Up @@ -211,15 +264,16 @@ jobs:
CALLBACK_PR="${{ steps.params.outputs.callback_pr }}"
CALLBACK_ISSUE="${{ steps.params.outputs.callback_issue }}"
TARGET="${CALLBACK_PR:-$CALLBACK_ISSUE}"
OUTPUT_DIR="${{ steps.run.outputs.output_dir }}"

if [ -z "$GH_TOKEN" ] || [ -z "$TARGET" ]; then
echo "⚠️ Cannot post results — missing token or target"
exit 0
fi

# Build comment from summary
if [ -f /tmp/hats-task-output/HATS_TASK_SUMMARY.md ]; then
REPORT_BODY=$(cat /tmp/hats-task-output/HATS_TASK_SUMMARY.md)
if [ -n "$OUTPUT_DIR" ] && [ -f "$OUTPUT_DIR/HATS_TASK_SUMMARY.md" ]; then
REPORT_BODY=$(cat "$OUTPUT_DIR/HATS_TASK_SUMMARY.md")
else
REPORT_BODY="🎩 Hats Task completed. Files generated: ${{ steps.run.outputs.files_generated }}"
fi
Expand All @@ -241,7 +295,15 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: hats-task-output
path: /tmp/hats-task-output/
path: ${{ steps.run.outputs.output_dir }}/
retention-days: 30

- name: Upload playground workspace
if: always() && steps.run.outputs.workspace_root != ''
uses: actions/upload-artifact@v4
with:
name: hats-playground
path: ${{ steps.run.outputs.workspace_root }}/
retention-days: 30

- name: Upload JSON result
Expand Down
25 changes: 22 additions & 3 deletions FORK_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,15 +179,15 @@ export HAT_STACK_REPO="YOUR_USERNAME/hat_stack"
```bash
# Generate a new code module — results posted as a PR comment
hat task generate_code "Build a FastAPI auth module with JWT and refresh tokens" \
--repo myorg/myapp --pr 42
--repo myorg/myapp --pr 42 --category code --genre api --project auth-service

# Write documentation for an endpoint
hat task generate_docs "Write API documentation for the /users endpoints" \
--repo myorg/myapp --issue 10

# Plan a migration
hat task plan "Plan a migration from REST to GraphQL for the orders service" \
--repo myorg/myapp
--repo myorg/myapp --category plans --genre migration --project orders-service

# Generate tests for a module
hat task test "Write unit tests for auth.py covering edge cases and error paths" \
Expand All @@ -210,7 +210,26 @@ hat status
3. The task runner selects the right hats and models for the job
4. Primary hat generates the deliverable, supporting hats review/enhance it
5. Gold Hat does final QA
6. Results are posted back to your project's PR/issue as a comment
6. Results are written into a sandboxed playground tree on the runner using `category/genre/project/run-id`
7. Results are posted back to your project's PR/issue as a comment and uploaded as artifacts

**Playground layout:**

```text
/tmp/hats-playground/
└── <category>/
└── <genre>/
└── <project>/
└── <run-id>/
├── generated files...
├── HATS_TASK_SUMMARY.md
├── hats_task_result.json
└── PLAYGROUND_MANIFEST.json
```

If you do not pass `--category`, `--genre`, or `--project`, Hat Stack infers sensible defaults and creates the folders automatically.

If the first Ollama model fails during task mode, Hat Stack automatically retries comparable configured fallback models before giving up.

**For Copilot in VS Code:** Your Copilot agent can shell out to `hat task ...` commands. The `gh` CLI handles auth, and hat_stack handles execution. Your Copilot agent gives the instruction, hat_stack's model pool does the heavy lifting, results come back to the PR.

Expand Down
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,8 @@ cp scripts/hat /usr/local/bin/hat # or add scripts/ to PATH
export HAT_STACK_REPO="YOUR_USERNAME/hat_stack"

# Generate code
hat task generate_code "Build a FastAPI auth module with JWT" --repo myorg/app --pr 42
hat task generate_code "Build a FastAPI auth module with JWT" \
--repo myorg/app --pr 42 --category code --genre api --project auth-service

# Write documentation
hat task generate_docs "Write API docs for /users endpoints" --repo myorg/app --issue 10
Expand All @@ -208,6 +209,16 @@ hat task analyze "Security audit of payment processing" --repo myorg/payments
git diff main | hat review - --repo myorg/app --pr 123
```

Task runs now support a structured playground sandbox on the runner:

- Workflow default workspace root: `/tmp/hats-playground`
- Layout: `<workspace>/<category>/<genre>/<project>/<run-id>/`
- Contents: generated files, `HATS_TASK_SUMMARY.md`, `hats_task_result.json`, `PLAYGROUND_MANIFEST.json`
- Persistence: both the run output and the full playground tree are uploaded as workflow artifacts
- Resilience: if the first Ollama model fails, task mode retries comparable configured fallback models automatically

If no workspace root is provided outside the workflow, task mode falls back to a unique temporary output directory under `/tmp`.

Or dispatch directly via `gh` CLI (what your Copilot agent would call):

```bash
Expand Down Expand Up @@ -275,4 +286,3 @@ hat_stack/
## License

MIT — See [LICENSE](LICENSE).

20 changes: 16 additions & 4 deletions scripts/hat
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
#
# Usage:
# hat review <diff_file> [--repo owner/repo] [--pr 42]
# hat task <task_type> "<prompt>" [--repo owner/repo] [--pr 42] [--hats black,green]
# hat task <task_type> "<prompt>" [--repo owner/repo] [--pr 42] [--hats black,green] \
# [--category code] [--genre api] [--project auth-service]
# hat status [run_id]
# hat list-tasks
#
Expand All @@ -19,7 +20,7 @@
#
# # Generate a new module
# hat task generate_code "Build a FastAPI auth module with JWT and refresh tokens" \
# --repo myorg/myapp --pr 123
# --repo myorg/myapp --pr 123 --category code --genre api --project auth-service
#
# # Write docs for an existing module
# hat task generate_docs "Write API documentation for the /users endpoints" \
Expand Down Expand Up @@ -174,6 +175,7 @@ cmd_task() {
local task_type="$1"; shift
local prompt="$1"; shift
local callback_repo="" callback_pr="" callback_issue="" hats="" context=""
local category="" genre="" project=""

while [[ $# -gt 0 ]]; do
case "$1" in
Expand All @@ -182,6 +184,9 @@ cmd_task() {
--issue) callback_issue="$2"; shift 2 ;;
--hats) hats="$2"; shift 2 ;;
--context) context="$2"; shift 2 ;;
--category) category="$2"; shift 2 ;;
--genre) genre="$2"; shift 2 ;;
--project) project="$2"; shift 2 ;;
*) die "Unknown option: $1" ;;
esac
done
Expand All @@ -199,6 +204,7 @@ cmd_task() {
payload=$(HAT_TASK="$task_type" HAT_PROMPT="$prompt" \
HAT_CB_REPO="$callback_repo" HAT_CB_PR="$callback_pr" \
HAT_CB_ISSUE="$callback_issue" HAT_HATS="$hats" HAT_CTX="$context" \
HAT_CATEGORY="$category" HAT_GENRE="$genre" HAT_PROJECT="$project" \
python3 -c "
import json, os
payload = {
Expand All @@ -209,6 +215,9 @@ payload = {
'callback_issue': os.environ.get('HAT_CB_ISSUE', ''),
'hats': os.environ.get('HAT_HATS', ''),
'context': os.environ.get('HAT_CTX', ''),
'category': os.environ.get('HAT_CATEGORY', ''),
'genre': os.environ.get('HAT_GENRE', ''),
'project': os.environ.get('HAT_PROJECT', ''),
}
payload = {k: v for k, v in payload.items() if v}
print(json.dumps(payload))
Expand Down Expand Up @@ -250,7 +259,7 @@ cmd_list_tasks() {
echo ""
echo -e "${BOLD}Usage:${NC}"
echo ' hat task generate_code "Build a user auth module" --repo myorg/myapp --pr 42'
echo ' hat task plan "Plan migration to microservices" --repo myorg/monolith'
echo ' hat task plan "Plan migration to microservices" --repo myorg/monolith --category plans --genre migration --project orders'
}

cmd_help() {
Expand All @@ -269,10 +278,13 @@ cmd_help() {
echo " --issue <number> Issue to post results to"
echo " --hats <hat1,hat2> Specific hats to use"
echo " --context <text> Additional context"
echo " --category <name> Playground category label"
echo " --genre <name> Playground genre/type label"
echo " --project <name> Playground project label"
echo ""
echo -e "${BOLD}Examples:${NC}"
echo ' git diff main | hat review - --repo myorg/app --pr 42'
echo ' hat task generate_code "Build JWT auth module" --repo myorg/app --pr 42'
echo ' hat task generate_code "Build JWT auth module" --repo myorg/app --pr 42 --category code --genre api --project auth'
echo ' hat task generate_docs "Write API docs for /users" --repo myorg/app --issue 10'
echo ' hat task plan "Plan GraphQL migration" --repo myorg/app'
echo ' hat task analyze "Security audit of payments" --repo myorg/payments'
Expand Down
Loading
Loading