Skip to content

Commit eb91e47

Browse files
author
Jon Tzeng
committed
Add Cursor agent workflow: commands, scripts, rules, and skills
Complete agent-assisted development workflow for Edge repositories. Includes slash commands with companion scripts, coding standards rules, review standards extracted from PR history, and the author skill for creating/maintaining commands and skills.
1 parent 2a91c51 commit eb91e47

40 files changed

Lines changed: 6052 additions & 0 deletions
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
#!/usr/bin/env bash
2+
# asana-attach-pr.sh
3+
# Attach a GitHub PR to an Asana task. Optionally assign to reviewer and set status.
4+
#
5+
# Usage:
6+
# asana-attach-pr.sh --task <task_gid> --pr-url <pr_url> --pr-title <title> --pr-number <number> \
7+
# [--assign] [--reviewer <user_gid>] [--implementor <user_gid>]
8+
#
9+
# Requires env vars: ASANA_TOKEN, ASANA_GITHUB_SECRET
10+
#
11+
# Without --assign: only attaches the PR to the task (no assignment or status change).
12+
# With --assign: also assigns to reviewer, sets status to "Review Needed", and
13+
# auto-populates Est. Review Hrs if empty.
14+
#
15+
# If --assign is used and --reviewer or --implementor are provided, they override
16+
# what's on the task. If the task's Reviewer field is empty and no override is
17+
# given, the script outputs PROMPT_REVIEWER so the calling agent can ask the user
18+
# and re-run with the override. If the Implementor field is empty and no override
19+
# is given, it auto-resolves to the current user via asana-whoami.sh.
20+
#
21+
# Output: One-line summary per action (success/failure/prompt)
22+
set -euo pipefail
23+
24+
TASK_GID=""
25+
PR_URL=""
26+
PR_TITLE=""
27+
PR_NUMBER=""
28+
REVIEWER_OVERRIDE=""
29+
IMPLEMENTOR_OVERRIDE=""
30+
DO_ASSIGN=false
31+
32+
while [[ $# -gt 0 ]]; do
33+
case "$1" in
34+
--task) TASK_GID="$2"; shift 2 ;;
35+
--pr-url) PR_URL="$2"; shift 2 ;;
36+
--pr-title) PR_TITLE="$2"; shift 2 ;;
37+
--pr-number) PR_NUMBER="$2"; shift 2 ;;
38+
--assign) DO_ASSIGN=true; shift ;;
39+
--reviewer) REVIEWER_OVERRIDE="$2"; shift 2 ;;
40+
--implementor) IMPLEMENTOR_OVERRIDE="$2"; shift 2 ;;
41+
*) echo "Unknown arg: $1" >&2; exit 1 ;;
42+
esac
43+
done
44+
45+
if [[ -z "$TASK_GID" || -z "$PR_URL" || -z "$PR_TITLE" || -z "$PR_NUMBER" ]]; then
46+
echo "Error: --task, --pr-url, --pr-title, and --pr-number are all required" >&2
47+
exit 1
48+
fi
49+
if [[ -z "${ASANA_TOKEN:-}" ]]; then
50+
echo "Error: ASANA_TOKEN not set" >&2; exit 1
51+
fi
52+
if [[ -z "${ASANA_GITHUB_SECRET:-}" ]]; then
53+
echo "Error: ASANA_GITHUB_SECRET not set" >&2; exit 1
54+
fi
55+
56+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
57+
58+
# Known field GIDs (airbitz.co workspace)
59+
STATUS_FIELD="1190660107346181"
60+
REVIEW_NEEDED_OPTION="1190660107348334"
61+
REVIEWER_FIELD="1203334388004673"
62+
IMPLEMENTOR_FIELD="1203334386796983"
63+
SPENT_DEV_HRS_FIELD="1202996660964169"
64+
EST_REVIEW_HRS_FIELD="1203002792997295"
65+
66+
# --- Step 1: Attach PR via GitHub integration ---
67+
ATTACH_RESULT=$(curl -s -X POST "https://github.integrations.asana.plus/custom/v1/actions/widget" \
68+
-H "Authorization: Bearer $ASANA_GITHUB_SECRET" \
69+
-H "Content-Type: application/json" \
70+
-d "{
71+
\"allowedProjects\": [],
72+
\"blockedProjects\": [],
73+
\"pullRequestDescription\": \"https://app.asana.com/0/0/$TASK_GID\",
74+
\"pullRequestName\": $(python3 -c "import json,sys; print(json.dumps(sys.argv[1]))" "$PR_TITLE"),
75+
\"pullRequestNumber\": $PR_NUMBER,
76+
\"pullRequestURL\": \"$PR_URL\"
77+
}" 2>&1)
78+
79+
ATTACH_STATUS=$(echo "$ATTACH_RESULT" | python3 -c "import sys,json; r=json.load(sys.stdin); print(r[0].get('result','unknown'))" 2>/dev/null || echo "error: $ATTACH_RESULT")
80+
echo ">> PR attach: $ATTACH_STATUS"
81+
82+
# Without --assign, stop after attaching
83+
if ! $DO_ASSIGN; then
84+
exit 0
85+
fi
86+
87+
# --- Step 2: Read task fields (Reviewer + Implementor) ---
88+
TASK_FIELDS=$(curl -s "https://app.asana.com/api/1.0/tasks/$TASK_GID?opt_fields=custom_fields.gid,custom_fields.people_value,custom_fields.number_value" \
89+
-H "Authorization: Bearer $ASANA_TOKEN")
90+
91+
read_people_field() {
92+
local field_gid="$1"
93+
echo "$TASK_FIELDS" | python3 -c "
94+
import sys, json
95+
data = json.load(sys.stdin)['data']
96+
for f in data['custom_fields']:
97+
if f['gid'] == '$field_gid':
98+
pv = f.get('people_value', [])
99+
print(pv[0]['gid'] if pv else '')
100+
break
101+
" 2>/dev/null || echo ""
102+
}
103+
104+
REVIEWER_GID="${REVIEWER_OVERRIDE:-$(read_people_field "$REVIEWER_FIELD")}"
105+
IMPLEMENTOR_GID="${IMPLEMENTOR_OVERRIDE:-$(read_people_field "$IMPLEMENTOR_FIELD")}"
106+
107+
# Auto-resolve implementor to current user if empty
108+
if [[ -z "$IMPLEMENTOR_GID" ]]; then
109+
IMPLEMENTOR_GID=$("$SCRIPT_DIR/asana-whoami.sh" 2>/dev/null || true)
110+
if [[ -n "$IMPLEMENTOR_GID" ]]; then
111+
IMPLEMENTOR_OVERRIDE="$IMPLEMENTOR_GID"
112+
echo ">> Implementor: auto-resolved to current user ($IMPLEMENTOR_GID)"
113+
fi
114+
fi
115+
116+
if [[ -z "$REVIEWER_GID" ]]; then
117+
echo ">> PROMPT_REVIEWER"
118+
exit 2
119+
fi
120+
121+
if [[ -z "$IMPLEMENTOR_GID" ]]; then
122+
echo ">> PROMPT_IMPLEMENTOR"
123+
exit 2
124+
fi
125+
126+
# --- Step 3: Set Implementor if override was provided ---
127+
if [[ -n "$IMPLEMENTOR_OVERRIDE" ]]; then
128+
curl -s -X PUT "https://app.asana.com/api/1.0/tasks/$TASK_GID" \
129+
-H "Authorization: Bearer $ASANA_TOKEN" \
130+
-H "Content-Type: application/json" \
131+
-d "{\"data\":{\"custom_fields\":{\"$IMPLEMENTOR_FIELD\":{\"people_value\":[\"$IMPLEMENTOR_OVERRIDE\"]}}}}" > /dev/null 2>&1 || true
132+
echo ">> Implementor: set"
133+
fi
134+
135+
# --- Step 4: Assign to reviewer and set status to Review Needed ---
136+
UPDATE_RESULT=$(curl -s -X PUT "https://app.asana.com/api/1.0/tasks/$TASK_GID" \
137+
-H "Authorization: Bearer $ASANA_TOKEN" \
138+
-H "Content-Type: application/json" \
139+
-d "{
140+
\"data\": {
141+
\"assignee\": \"$REVIEWER_GID\",
142+
\"custom_fields\": {
143+
\"$STATUS_FIELD\": \"$REVIEW_NEEDED_OPTION\"
144+
}
145+
}
146+
}" 2>&1)
147+
148+
ASSIGNEE_NAME=$(echo "$UPDATE_RESULT" | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['assignee']['name'])" 2>/dev/null || echo "unknown")
149+
echo ">> Assigned to: $ASSIGNEE_NAME"
150+
echo ">> Status: Review Needed"
151+
152+
# --- Step 5: Auto-populate Est. Review Hrs if empty (non-blocking) ---
153+
python3 -c "
154+
import sys, json
155+
data = json.loads('''$TASK_FIELDS''')['data']
156+
spent_dev = None
157+
est_review = None
158+
for f in data['custom_fields']:
159+
if f['gid'] == '$SPENT_DEV_HRS_FIELD':
160+
spent_dev = f.get('number_value')
161+
elif f['gid'] == '$EST_REVIEW_HRS_FIELD':
162+
est_review = f.get('number_value')
163+
if est_review is not None:
164+
print('>> Est. Review Hrs: already set (' + str(est_review) + ')')
165+
elif spent_dev is None:
166+
print('>> Est. Review Hrs: skipped (no Spent Dev Hrs)')
167+
else:
168+
val = max(round(spent_dev * 0.1, 1), 0.1)
169+
print(f'SET_EST_REVIEW={val}')
170+
" 2>/dev/null | while IFS= read -r line; do
171+
if [[ "$line" == SET_EST_REVIEW=* ]]; then
172+
VAL="${line#SET_EST_REVIEW=}"
173+
curl -s -X PUT "https://app.asana.com/api/1.0/tasks/$TASK_GID" \
174+
-H "Authorization: Bearer $ASANA_TOKEN" \
175+
-H "Content-Type: application/json" \
176+
-d "{\"data\":{\"custom_fields\":{\"$EST_REVIEW_HRS_FIELD\":$VAL}}}" > /dev/null 2>&1 || true
177+
echo ">> Est. Review Hrs: set to $VAL (10% of Spent Dev Hrs)"
178+
else
179+
echo "$line"
180+
fi
181+
done || true
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
#!/usr/bin/env bash
2+
# asana-create-dep-task.sh
3+
# Create a dependent Asana task that blocks a parent task.
4+
# Checks for existing dependencies first to avoid duplicates.
5+
#
6+
# Usage:
7+
# asana-create-dep-task.sh --parent <parent_gid> --name "task name" [--notes "description"] [--assignee <user_gid>]
8+
#
9+
# If --assignee is omitted, the task is assigned to the current user
10+
# (resolved via asana-whoami.sh).
11+
#
12+
# Requires env var: ASANA_TOKEN
13+
#
14+
# Output:
15+
# TASK_GID: <gid>
16+
# TASK_URL: <url>
17+
# CREATED: true|false (false if task already existed)
18+
# ASSIGNED_TO: <user_gid>
19+
# FIELDS_SET: priority=<val>, status=<val>
20+
# DEPENDENCY_SET: <new_gid> blocks <parent_gid>
21+
#
22+
# Exit codes: 0 = success, 1 = error
23+
set -euo pipefail
24+
25+
PARENT_GID=""
26+
TASK_NAME=""
27+
TASK_NOTES=""
28+
ASSIGNEE_GID=""
29+
30+
while [[ $# -gt 0 ]]; do
31+
case "$1" in
32+
--parent) PARENT_GID="$2"; shift 2 ;;
33+
--name) TASK_NAME="$2"; shift 2 ;;
34+
--notes) TASK_NOTES="$2"; shift 2 ;;
35+
--assignee) ASSIGNEE_GID="$2"; shift 2 ;;
36+
*) echo "Unknown flag: $1" >&2; exit 1 ;;
37+
esac
38+
done
39+
40+
if [[ -z "$PARENT_GID" || -z "$TASK_NAME" ]]; then
41+
echo "Usage: asana-create-dep-task.sh --parent <gid> --name <name> [--notes <desc>] [--assignee <gid>]" >&2
42+
exit 1
43+
fi
44+
45+
if [[ -z "${ASANA_TOKEN:-}" ]]; then
46+
echo "Error: ASANA_TOKEN not set" >&2
47+
exit 1
48+
fi
49+
50+
API="https://app.asana.com/api/1.0"
51+
AUTH="Authorization: Bearer $ASANA_TOKEN"
52+
53+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
54+
55+
# Auto-resolve assignee to current user if not provided
56+
if [[ -z "$ASSIGNEE_GID" ]]; then
57+
ASSIGNEE_GID=$("$SCRIPT_DIR/asana-whoami.sh" 2>/dev/null || true)
58+
fi
59+
60+
# Phase 1: Check if a dependency with a matching name already exists
61+
existing=$(curl -s "$API/tasks/$PARENT_GID/dependencies?opt_fields=name&limit=100" \
62+
-H "$AUTH" | python3 -c "
63+
import sys, json
64+
data = json.load(sys.stdin).get('data', [])
65+
target = '''$TASK_NAME'''
66+
for dep in data:
67+
if dep.get('name', '').strip().lower() == target.strip().lower():
68+
print(dep['gid'])
69+
sys.exit(0)
70+
print('')
71+
")
72+
73+
if [[ -n "$existing" ]]; then
74+
echo "TASK_GID: $existing"
75+
echo "TASK_URL: https://app.asana.com/0/0/$existing"
76+
echo "CREATED: false"
77+
exit 0
78+
fi
79+
80+
# Phase 2: Get parent task's project and custom fields to copy
81+
parent_info=$(curl -s "$API/tasks/$PARENT_GID?opt_fields=workspace.gid,memberships.project.gid,memberships.project.name,custom_fields.gid,custom_fields.enum_value.gid,custom_fields.enum_value.name" \
82+
-H "$AUTH")
83+
84+
read -r WORKSPACE_GID PROJECT_GIDS PRIORITY_GID PRIORITY_VAL STATUS_GID STATUS_VAL < <(echo "$parent_info" | python3 -c "
85+
import sys, json, re
86+
data = json.load(sys.stdin)['data']
87+
ws = data.get('workspace', {}).get('gid', '')
88+
89+
# Collect all non-version projects (board/backlog projects, not release milestones)
90+
projects = []
91+
for m in data.get('memberships', []):
92+
p = m.get('project', {})
93+
if not re.match(r'^\d+\.\d+\.\d+$', p.get('name', '')):
94+
projects.append(p.get('gid', ''))
95+
if not projects and data.get('memberships'):
96+
projects.append(data['memberships'][0]['project']['gid'])
97+
proj_str = ','.join(projects)
98+
99+
# Custom fields to copy
100+
FIELD_MAP = {
101+
'795866930204488': 'priority',
102+
'1190660107346181': 'status',
103+
}
104+
fields = {}
105+
for f in data.get('custom_fields', []):
106+
label = FIELD_MAP.get(f['gid'])
107+
if label and f.get('enum_value'):
108+
fields[label + '_gid'] = f['gid']
109+
fields[label + '_val'] = f['enum_value']['gid']
110+
fields[label + '_name'] = f['enum_value']['name']
111+
112+
pri_gid = fields.get('priority_gid', '')
113+
pri_val = fields.get('priority_val', '')
114+
sta_gid = fields.get('status_gid', '')
115+
sta_val = fields.get('status_val', '')
116+
117+
print(f'{ws} {proj_str} {pri_gid}:{pri_val}:{fields.get(\"priority_name\",\"\")} {sta_gid}:{sta_val}:{fields.get(\"status_name\",\"\")}')
118+
")
119+
120+
PRIORITY_FIELD=$(echo "$PRIORITY_VAL" | cut -d: -f1)
121+
PRIORITY_ENUM=$(echo "$PRIORITY_VAL" | cut -d: -f2)
122+
PRIORITY_NAME=$(echo "$PRIORITY_VAL" | cut -d: -f3)
123+
STATUS_FIELD=$(echo "$STATUS_VAL" | cut -d: -f1)
124+
STATUS_ENUM=$(echo "$STATUS_VAL" | cut -d: -f2)
125+
STATUS_NAME=$(echo "$STATUS_VAL" | cut -d: -f3)
126+
127+
# Phase 3: Create the task
128+
NOTES_JSON=$(python3 -c "import json; print(json.dumps('''$TASK_NOTES'''))")
129+
130+
custom_fields_json="{}"
131+
if [[ -n "$PRIORITY_FIELD" && -n "$PRIORITY_ENUM" ]]; then
132+
custom_fields_json=$(python3 -c "
133+
import json
134+
cf = {}
135+
pf = '$PRIORITY_FIELD'
136+
pe = '$PRIORITY_ENUM'
137+
sf = '$STATUS_FIELD'
138+
se = '$STATUS_ENUM'
139+
if pf and pe: cf[pf] = pe
140+
if sf and se: cf[sf] = se
141+
print(json.dumps(cf))
142+
")
143+
fi
144+
145+
# Build projects list from comma-separated GIDs
146+
IFS=',' read -ra PROJECT_ARR <<< "$PROJECT_GIDS"
147+
148+
new_task=$(curl -s "$API/tasks" \
149+
-H "$AUTH" \
150+
-H "Content-Type: application/json" \
151+
-d "$(python3 -c "
152+
import json
153+
projects = '''$PROJECT_GIDS'''.split(',')
154+
assignee = '''$ASSIGNEE_GID''' or None
155+
data = {
156+
'data': {
157+
'name': '''$TASK_NAME''',
158+
'notes': $NOTES_JSON,
159+
'projects': [p for p in projects if p],
160+
'workspace': '$WORKSPACE_GID',
161+
'custom_fields': $custom_fields_json
162+
}
163+
}
164+
if assignee:
165+
data['data']['assignee'] = assignee
166+
print(json.dumps(data))
167+
")")
168+
169+
NEW_GID=$(echo "$new_task" | python3 -c "
170+
import sys, json
171+
data = json.load(sys.stdin)
172+
if 'errors' in data:
173+
print('ERROR: ' + json.dumps(data['errors']), file=sys.stderr)
174+
sys.exit(1)
175+
print(data['data']['gid'])
176+
")
177+
178+
if [[ -z "$NEW_GID" || "$NEW_GID" == "ERROR"* ]]; then
179+
echo "Error creating task" >&2
180+
exit 1
181+
fi
182+
183+
FIRST_PROJECT=$(echo "$PROJECT_GIDS" | cut -d, -f1)
184+
echo "TASK_GID: $NEW_GID"
185+
echo "TASK_URL: https://app.asana.com/0/$FIRST_PROJECT/$NEW_GID"
186+
echo "CREATED: true"
187+
[[ -n "$ASSIGNEE_GID" ]] && echo "ASSIGNED_TO: $ASSIGNEE_GID"
188+
189+
# Phase 4: Set as blocking dependency
190+
curl -s -X POST "$API/tasks/$PARENT_GID/addDependencies" \
191+
-H "$AUTH" \
192+
-H "Content-Type: application/json" \
193+
-d "{\"data\": {\"dependencies\": [\"$NEW_GID\"]}}" > /dev/null
194+
195+
echo "DEPENDENCY_SET: $NEW_GID blocks $PARENT_GID"
196+
197+
if [[ -n "$PRIORITY_NAME" || -n "$STATUS_NAME" ]]; then
198+
fields_msg=""
199+
[[ -n "$PRIORITY_NAME" ]] && fields_msg="priority=$PRIORITY_NAME"
200+
[[ -n "$STATUS_NAME" ]] && fields_msg="${fields_msg:+$fields_msg, }status=$STATUS_NAME"
201+
echo "FIELDS_SET: $fields_msg"
202+
fi

0 commit comments

Comments
 (0)