Skip to content

Commit 126bcb8

Browse files
committed
Add OpenCode/Claude compat: generate CLAUDE.md, symlink skills, merge fix-workflow-first
1 parent 3ac8d26 commit 126bcb8

File tree

13 files changed

+253
-44
lines changed

13 files changed

+253
-44
lines changed

.cursor/rules/fix-workflow-first.mdc

Lines changed: 0 additions & 22 deletions
This file was deleted.

.cursor/rules/workflow-halt-on-error.mdc

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,48 @@ alwaysApply: true
55

66
<rules description="Non-negotiable constraints.">
77

8+
<rule id="skill-script-path-resolution">When a skill mentions a script path, resolve it under `~/.cursor/skills/<skill>/scripts/` unless the skill explicitly specifies an absolute path elsewhere. Do not assume repo-relative `scripts/` paths without verifying the skill directory contents.</rule>
9+
810
<rule id="halt-on-error">When ANY shell command fails (non-zero exit code) during a workflow:
911
1. **STOP** — do not retry, work around, substitute, or continue the workflow.
1012
2. **Report** — show the user the exact command, exit code, and error output.
1113
3. **Diagnose** — classify the failure: missing tool (`command not found`), wrong path, permissions, or logic error.
12-
4. **Evaluate workflow** — if the failure reveals a gap in a command/skill definition, read `~/.cursor/rules/fix-workflow-first.mdc` and follow it.
14+
4. **Evaluate workflow** — if the failure reveals a gap in a skill definition, follow the fix-workflow-first rules below.
1315
5. **Wait** — do not resume until the user responds.
1416
</rule>
1517

18+
<rule id="fix-workflow-first">When a workflow gap is discovered in a skill definition:
19+
1. **Stop immediately** — do not continue the current task or apply any workaround.
20+
2. **Identify the root cause** in the skill (`.cursor/skills/*/SKILL.md`) definition.
21+
3. **Propose the fix** to the user and wait for approval before proceeding.
22+
4. **Fix the skill** using `/author` after approval.
23+
5. **Resume the original task** only after the skill is updated.
24+
25+
Fixing the skill takes **absolute priority** over all other actions — including workarounds, continuing the original task, or applying temporary fixes. Do NOT apply workarounds or manual fixes before proposing the skill update. The correct sequence is: identify gap → propose fix → get approval → apply fix → then resume original task. This applies to all workflow issues — missed steps, incorrect output, wrong tool usage, shell failures, formatting problems, etc. The skill is the source of truth; patching around it creates drift.
26+
</rule>
27+
28+
<rule id="auto-fix-verification-failures">Exception to `halt-on-error`: For verification/code-quality failures where diagnostics are explicit and local, continue automatically with bounded remediation.
29+
30+
Allowed auto-fix scope:
31+
- TypeScript/compiler failures (`tsc`) with clear file/line diagnostics
32+
- Lint failures (`eslint`) with clear file/line diagnostics
33+
- Test failures (`jest`/`yarn test`) when stack traces or assertion output identify failing test files
34+
- `verify-repo.sh` code-step failures that resolve to one of the above
35+
36+
Required behavior:
37+
1. Briefly log rationale: failure type, affected files, and why scope is unambiguous.
38+
2. Apply the minimal fix in the failing repo.
39+
3. Re-run the failing verification step.
40+
4. Limit to 2 remediation attempts; if still failing or scope expands, fall back to `halt-on-error`.
41+
42+
Never auto-fix:
43+
- Missing tools/auth (`command not found`, `PROMPT_GH_AUTH`)
44+
- Wrong path/permissions
45+
- Companion script contract/usage failures
46+
- Unexpected exit codes from orchestrator scripts
47+
- Any failure requiring destructive operations or workflow bypasses
48+
</rule>
49+
1650
<rule id="no-silent-substitution">Do NOT silently substitute an alternative tool or approach when a command fails. If `rg` is not found, do not fall back to `grep`. If a script exits non-zero, do not manually replicate what the script does. The failure is the signal — report it.</rule>
1751

1852
</rules>

.cursor/skills/convention-sync/SKILL.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@ metadata:
66
author: j0ntz
77
---
88

9-
<goal>Sync cursor files between `~/.cursor/` and the `edge-conventions` repo, commit, push, and update PR description from README.</goal>
9+
<goal>Sync cursor files between `~/.cursor/` and the `edge-conventions` repo, commit, push, and update PR description from README. Also maintains cross-tool compatibility: symlinks `~/.claude/skills``~/.cursor/skills` and generates `~/.claude/CLAUDE.md` from always-apply rules.</goal>
1010

1111
<rules>
1212
<rule id="use-companion-script">Use `scripts/convention-sync.sh` for diffing and syncing. Do NOT manually diff or copy files.</rule>
1313
<rule id="dry-run-first">Always run without `--stage` first to show the summary. Only stage/commit after user confirms.</rule>
1414
<rule id="no-script-bypass">If the script fails, report the error and STOP.</rule>
1515
<rule id="readme-is-source">`.cursor/README.md` is the source of truth for documentation. The script mirrors it to the PR description automatically.</rule>
16+
<rule id="claude-compat">Every run ensures `~/.claude/skills` symlinks to `~/.cursor/skills` and regenerates `~/.claude/CLAUDE.md` from `alwaysApply: true` rules. This enables OpenCode and Claude Code to discover skills and rules without separate config.</rule>
1617
</rules>
1718

1819
<step id="1" name="Detect changes and PR status">

.cursor/skills/convention-sync/scripts/convention-sync.sh

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,26 @@ done
113113

114114
total=$(echo "$new_json $mod_json $del_json" | jq -s '.[0] + .[1] + .[2] | length')
115115

116+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
117+
118+
# Ensure ~/.claude/skills symlink points to ~/.cursor/skills
119+
CLAUDE_SKILLS="$HOME/.claude/skills"
120+
if [[ -L "$CLAUDE_SKILLS" ]]; then
121+
link_target="$(readlink "$CLAUDE_SKILLS")"
122+
if [[ "$link_target" != "$USER_DIR/skills" ]]; then
123+
rm "$CLAUDE_SKILLS"
124+
ln -s "$USER_DIR/skills" "$CLAUDE_SKILLS"
125+
fi
126+
elif [[ ! -e "$CLAUDE_SKILLS" ]]; then
127+
mkdir -p "$(dirname "$CLAUDE_SKILLS")"
128+
ln -s "$USER_DIR/skills" "$CLAUDE_SKILLS"
129+
fi
130+
131+
# Regenerate ~/.claude/CLAUDE.md from alwaysApply rules
132+
if [[ -x "$SCRIPT_DIR/generate-claude-md.sh" ]]; then
133+
"$SCRIPT_DIR/generate-claude-md.sh" >/dev/null
134+
fi
135+
116136
if [[ "$DO_STAGE" == true && "$total" -gt 0 ]]; then
117137
all_copy=$(echo "$new_json $mod_json" | jq -sr '.[0] + .[1] | .[]')
118138
all_del=$(echo "$del_json" | jq -r '.[]')
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#!/usr/bin/env bash
2+
# generate-claude-md.sh — Generate ~/.claude/CLAUDE.md from alwaysApply .mdc rules.
3+
# Usage: ./generate-claude-md.sh [--dry-run]
4+
#
5+
# Reads all .mdc files in ~/.cursor/rules/ that have alwaysApply: true,
6+
# strips YAML frontmatter, and concatenates them into ~/.claude/CLAUDE.md.
7+
8+
set -euo pipefail
9+
10+
RULES_DIR="$HOME/.cursor/rules"
11+
OUTPUT="$HOME/.claude/CLAUDE.md"
12+
DRY_RUN=false
13+
14+
[[ "${1:-}" == "--dry-run" ]] && DRY_RUN=true
15+
16+
if [[ ! -d "$RULES_DIR" ]]; then
17+
echo "ERROR: $RULES_DIR does not exist" >&2
18+
exit 1
19+
fi
20+
21+
mkdir -p "$(dirname "$OUTPUT")"
22+
23+
collected=()
24+
skipped=()
25+
26+
for mdc in "$RULES_DIR"/*.mdc; do
27+
[[ -f "$mdc" ]] || continue
28+
basename="$(basename "$mdc")"
29+
30+
if head -20 "$mdc" | grep -q '^alwaysApply: true'; then
31+
collected+=("$basename")
32+
else
33+
skipped+=("$basename")
34+
fi
35+
done
36+
37+
if [[ ${#collected[@]} -eq 0 ]]; then
38+
echo '{"collected":[],"skipped":[],"output":"","dry_run":true}'
39+
exit 0
40+
fi
41+
42+
content="# Global Rules\n\n"
43+
content+="# Auto-generated from ~/.cursor/rules/ (alwaysApply: true files only).\n"
44+
content+="# Do not edit manually. Re-generate via convention-sync.\n\n"
45+
46+
for basename in "${collected[@]}"; do
47+
mdc="$RULES_DIR/$basename"
48+
name="${basename%.mdc}"
49+
50+
# Strip YAML frontmatter (everything between first --- and second ---)
51+
body=$(awk '
52+
BEGIN { in_front=0; past_front=0 }
53+
/^---$/ {
54+
if (!past_front) {
55+
if (in_front) { past_front=1; next }
56+
else { in_front=1; next }
57+
}
58+
}
59+
past_front { print }
60+
' "$mdc")
61+
62+
# Trim leading blank lines
63+
body=$(echo "$body" | sed '/./,$!d')
64+
65+
content+="---\n\n"
66+
content+="## $name\n\n"
67+
content+="$body\n\n"
68+
done
69+
70+
if [[ "$DRY_RUN" == true ]]; then
71+
echo -e "$content" > /dev/null
72+
else
73+
echo -e "$content" > "$OUTPUT"
74+
fi
75+
76+
# Output JSON summary
77+
collected_json=$(printf '%s\n' "${collected[@]}" | jq -R . | jq -s .)
78+
skipped_json=$(printf '%s\n' "${skipped[@]}" | jq -R . | jq -s .)
79+
80+
jq -n \
81+
--argjson collected "$collected_json" \
82+
--argjson skipped "$skipped_json" \
83+
--arg output "$OUTPUT" \
84+
--arg dry_run "$DRY_RUN" \
85+
'{collected: $collected, skipped: $skipped, output: $output, dry_run: ($dry_run == "true")}'

.cursor/skills/dep-pr/SKILL.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ metadata:
1313
<rule id="check-existence">Always check if a dependent task already exists before creating one. The script handles this — respect the `CREATED: false` output.</rule>
1414
<rule id="script-timeouts">Asana scripts can take up to 90s. Always set `block_until_ms: 120000`.</rule>
1515
<rule id="no-impl-before-task">Do NOT begin implementation until the dependent task is created and linked.</rule>
16-
<rule id="same-project">The dependent task MUST be created in the same non-version project(s) as the parent task. The script handles this automatically — it copies all non-version project memberships from the parent.</rule>
16+
<rule id="same-project">The dependent task MUST be created in the same project(s) as the parent task, including release-version project tags (for example `4.46.0`). The script handles this automatically by copying all parent project memberships.</rule>
1717
<rule id="initial-assignee">The dependent task is automatically assigned to the current user (resolved via `asana-whoami.sh`). Do NOT hardcode a user GID — omit `--assignee` to let the script auto-resolve.</rule>
1818
</rules>
1919

@@ -66,16 +66,16 @@ Derive the dependent task name from the parent: `<target-prefix>: <parent task n
6666
If the parent task name already has a prefix (e.g. `gui: Some feature`), strip it and replace with the target prefix. If no prefix, prepend the target prefix.
6767

6868
```bash
69-
scripts/asana-create-dep-task.sh \
69+
~/.cursor/skills/dep-pr/scripts/asana-create-dep-task.sh \
7070
--parent <parent_gid> \
7171
--name "<prefix>: <task name>" \
7272
--notes "<description referencing parent task>"
7373
```
7474

7575
The script:
7676
- Checks if a matching dependency already exists (by name) — if so, outputs `CREATED: false` and the existing GID
77-
- Creates the task in the parent's non-version project(s)
78-
- Copies priority and status from the parent
77+
- Creates the task in all parent project memberships (including release-version tags)
78+
- Copies priority, status, and `Planned` from the parent
7979
- Assigns to the current user (auto-resolved via `asana-whoami.sh`)
8080
- Sets the new task as a blocking dependency of the parent
8181

.cursor/skills/dep-pr/scripts/asana-create-dep-task.sh

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
# TASK_URL: <url>
1717
# CREATED: true|false (false if task already existed)
1818
# ASSIGNED_TO: <user_gid>
19-
# FIELDS_SET: priority=<val>, status=<val>, reviewer=<name>, implementor=<name>
19+
# FIELDS_SET: priority=<val>, status=<val>, planned=<val>, reviewer=<name>, implementor=<name>
2020
# DEPENDENCY_SET: <new_gid> blocks <parent_gid>
2121
#
2222
# Exit codes: 0 = success, 1 = error
@@ -84,22 +84,23 @@ fi
8484
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,custom_fields.people_value.gid,custom_fields.people_value.name" \
8585
-H "$AUTH")
8686

87-
read -r WORKSPACE_GID PROJECT_GIDS PRIORITY_INFO STATUS_INFO REVIEWER_INFO < <(echo "$parent_info" | python3 -c "
87+
read -r WORKSPACE_GID PROJECT_GIDS PRIORITY_INFO STATUS_INFO PLANNED_INFO REVIEWER_INFO < <(echo "$parent_info" | python3 -c "
8888
import sys, json, re
8989
data = json.load(sys.stdin)['data']
9090
ws = data.get('workspace', {}).get('gid', '')
9191
92-
# Collect all non-version projects (board/backlog projects, not release milestones)
92+
# Collect all parent projects (including release-version projects like 4.46.0)
9393
projects = []
9494
for m in data.get('memberships', []):
9595
p = m.get('project', {})
96-
if not re.match(r'^\d+\.\d+\.\d+$', p.get('name', '')):
97-
projects.append(p.get('gid', ''))
96+
gid = p.get('gid', '')
97+
if gid:
98+
projects.append(gid)
9899
if not projects and data.get('memberships'):
99100
projects.append(data['memberships'][0]['project']['gid'])
100101
proj_str = ','.join(projects)
101102
102-
# Field GIDs
103+
# Field GIDs (stable known fields)
103104
ENUM_FIELDS = {
104105
'795866930204488': 'priority',
105106
'1190660107346181': 'status',
@@ -116,6 +117,13 @@ for f in data.get('custom_fields', []):
116117
if fgid in ENUM_FIELDS and f.get('enum_value'):
117118
label = ENUM_FIELDS[fgid]
118119
enum_results[label] = (fgid, f['enum_value']['gid'], f['enum_value'].get('name', ''))
120+
# "Planned" is workspace-specific, so detect by field name:
121+
if f.get('name') == 'Planned' and f.get('enum_value'):
122+
enum_results['planned'] = (
123+
fgid,
124+
f['enum_value']['gid'],
125+
f['enum_value'].get('name', '')
126+
)
119127
if fgid in PEOPLE_FIELDS:
120128
label = PEOPLE_FIELDS[fgid]
121129
pv = f.get('people_value', [])
@@ -132,7 +140,7 @@ def fmt_people(key):
132140
return ':'.join(people_results[key])
133141
return '::'
134142
135-
print(f\"{ws} {proj_str} {fmt_enum('priority')} {fmt_enum('status')} {fmt_people('reviewer')}\")
143+
print(f\"{ws} {proj_str} {fmt_enum('priority')} {fmt_enum('status')} {fmt_enum('planned')} {fmt_people('reviewer')}\")
136144
")
137145

138146
PRIORITY_FIELD=$(echo "$PRIORITY_INFO" | cut -d: -f1)
@@ -141,6 +149,9 @@ PRIORITY_NAME=$(echo "$PRIORITY_INFO" | cut -d: -f3)
141149
STATUS_FIELD=$(echo "$STATUS_INFO" | cut -d: -f1)
142150
STATUS_ENUM=$(echo "$STATUS_INFO" | cut -d: -f2)
143151
STATUS_NAME=$(echo "$STATUS_INFO" | cut -d: -f3)
152+
PLANNED_FIELD=$(echo "$PLANNED_INFO" | cut -d: -f1)
153+
PLANNED_ENUM=$(echo "$PLANNED_INFO" | cut -d: -f2)
154+
PLANNED_NAME=$(echo "$PLANNED_INFO" | cut -d: -f3)
144155
REVIEWER_FIELD=$(echo "$REVIEWER_INFO" | cut -d: -f1)
145156
REVIEWER_GID=$(echo "$REVIEWER_INFO" | cut -d: -f2)
146157
REVIEWER_NAME=$(echo "$REVIEWER_INFO" | cut -d: -f3)
@@ -159,8 +170,10 @@ import json
159170
cf = {}
160171
pf, pe = '$PRIORITY_FIELD', '$PRIORITY_ENUM'
161172
sf, se = '$STATUS_FIELD', '$STATUS_ENUM'
173+
plf, ple = '$PLANNED_FIELD', '$PLANNED_ENUM'
162174
if pf and pe: cf[pf] = pe
163175
if sf and se: cf[sf] = se
176+
if plf and ple: cf[plf] = ple
164177
print(json.dumps(cf))
165178
")
166179

@@ -241,6 +254,7 @@ echo "DEPENDENCY_SET: $NEW_GID blocks $PARENT_GID"
241254
fields_msg=""
242255
[[ -n "$PRIORITY_NAME" ]] && fields_msg="priority=$PRIORITY_NAME"
243256
[[ -n "$STATUS_NAME" ]] && fields_msg="${fields_msg:+$fields_msg, }status=$STATUS_NAME"
257+
[[ -n "$PLANNED_NAME" ]] && fields_msg="${fields_msg:+$fields_msg, }planned=$PLANNED_NAME"
244258
[[ -n "$REVIEWER_NAME" ]] && fields_msg="${fields_msg:+$fields_msg, }reviewer=$REVIEWER_NAME"
245259
[[ -n "$IMPLEMENTOR_GID" ]] && fields_msg="${fields_msg:+$fields_msg, }implementor=$IMPLEMENTOR_NAME"
246260
[[ -n "$fields_msg" ]] && echo "FIELDS_SET: $fields_msg"

.cursor/skills/pr-create/SKILL.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ metadata:
99
<goal>End-to-end flow: resolve an Asana task, implement it (or continue from a prior `/im` run), then create a PR with Asana linking and assignment.</goal>
1010

1111
<rules description="Non-negotiable constraints.">
12-
<rule id="use-companion-script">Do NOT call `gh` or `curl` directly. Use `scripts/pr-create.sh` for PR creation (it uses `gh` internally).</rule>
12+
<rule id="use-companion-script">Do NOT call `gh` or `curl` directly. Use `~/.cursor/skills/pr-create/scripts/pr-create.sh` for PR creation (it uses `gh` internally).</rule>
1313
<rule id="no-script-bypass">If a companion script fails, report the error and STOP. Do NOT fall back to raw `gh`, `curl`, or other workarounds.</rule>
1414
<rule id="gh-auth-required">If any script exits code 2 with `PROMPT_GH_AUTH`, prompt the user to run `gh auth login` and STOP.</rule>
1515
<rule id="commit-script">Always commit using `~/.cursor/skills/lint-commit.sh -m "message" [files...]`. Never use raw `git add` + `git commit`.</rule>
@@ -178,7 +178,7 @@ Create the PR immediately — do not ask for confirmation.
178178
- The Write tool **overwrites** the file. ApplyPatch `Add File` may append to an existing file, causing stale content from a prior PR to bleed through. **Always use Write.**
179179
2. **Run the script**:
180180
```bash
181-
scripts/pr-create.sh --title "<title>" --body-file /tmp/pr-body.md --asana-task <task_gid>
181+
~/.cursor/skills/pr-create/scripts/pr-create.sh --title "<title>" --body-file /tmp/pr-body.md --asana-task <task_gid>
182182
```
183183
- Pass `--asana-task <task_gid>` when an Asana task is available. The script injects a clickable Asana link into the PR body if one isn't already present. This is **required** for downstream `/pr-land` to extract the task GID.
184184
- The script cleans up `/tmp/pr-body.md` after use to prevent cross-PR contamination. It will be re-populated from GitHub if needed during `/pr-address`.
@@ -192,7 +192,7 @@ If the script exits code 2 with `PROMPT_GH_AUTH`, prompt the user to run `gh aut
192192
If no Asana link was provided, skip silently.
193193

194194
```bash
195-
scripts/asana-attach-pr.sh \
195+
~/.cursor/skills/pr-create/scripts/asana-attach-pr.sh \
196196
--task <task_gid> \
197197
--pr-url <pr_url> \
198198
--pr-title "<title>" \
@@ -211,7 +211,7 @@ By default, the script only attaches — no assignment or status change.
211211
With `--assign`, the script exits code 2 and outputs `PROMPT_REVIEWER` if the Reviewer field is empty. Ask the user who to assign using the team roster, then re-run with the override:
212212

213213
```bash
214-
scripts/asana-attach-pr.sh \
214+
~/.cursor/skills/pr-create/scripts/asana-attach-pr.sh \
215215
--task <task_gid> --pr-url <pr_url> --pr-title "<title>" --pr-number <number> \
216216
--assign --reviewer <user_gid>
217217
```

0 commit comments

Comments
 (0)