Skip to content

Commit 15cfa35

Browse files
committed
Sync commands, rules: add push-env-key.sh, workflow-halt-on-error rule, update pr-address, pr-create, fix-workflow-first, answer-questions-first
1 parent f171bd1 commit 15cfa35

File tree

8 files changed

+159
-18
lines changed

8 files changed

+159
-18
lines changed

.cursor/commands/pr-address.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,33 @@
1212
<rule id="resolution-source-of-truth">Only explicitly resolved threads (`isResolved: true`) or `<!-- addressed:... -->` markers count as resolved. Recency (commits after a comment) does NOT mean resolved.</rule>
1313
</rules>
1414

15-
<step id="1" name="Fetch all unresolved feedback">
16-
Always fetch live from GitHub. The script returns all unresolved feedback — no recency filtering.
15+
<step id="1" name="Fetch all unresolved feedback and PR body">
16+
Always fetch live from GitHub. Run both in parallel:
1717

1818
```bash
19+
# Fetch unresolved feedback
1920
~/.cursor/commands/pr-address.sh fetch --owner <OWNER> --repo <REPO> --pr <NUMBER>
21+
22+
# Populate /tmp/pr-body.md from the live PR body (source of truth)
23+
~/.cursor/commands/pr-address.sh fetch-pr-body --owner <OWNER> --repo <REPO> --pr <NUMBER>
2024
```
2125

22-
If the script exits code 2 with `PROMPT_GH_AUTH`, prompt: "`gh` CLI is not authenticated. Please run: `gh auth login`"
26+
If either script exits code 2 with `PROMPT_GH_AUTH`, prompt: "`gh` CLI is not authenticated. Please run: `gh auth login`"
2327

24-
The output contains:
28+
The `fetch` output contains:
2529
- **prAuthor**: The PR author's GitHub username
2630
- **currentUser**: Your GitHub username (the authenticated `gh` user)
2731
- **hasHumanReviewers**: `true` if any external human reviewer (not `currentUser`, not bots) has commented — used for autosquash decision
2832
- **humanReviewers**: List of external human reviewer usernames
2933
- **threads**: All unresolved inline review threads (includes comments from `currentUser` for context)
3034
- **reviewBodies**: Latest review body per non-author reviewer (excludes `prAuthor` and bots)
3135
- **topLevel**: Top-level comments (excludes `prAuthor` and bots)
36+
37+
The `fetch-pr-body` call writes the current PR body to `/tmp/pr-body.md`. This file is available for editing throughout the session. If you need to update the PR body (e.g. to revise the description after addressing feedback), edit `/tmp/pr-body.md` via the Write tool and push it back:
38+
39+
```bash
40+
gh pr edit <NUMBER> --body-file /tmp/pr-body.md
41+
```
3242
</step>
3343

3444
<step id="2" name="Process all unresolved feedback">

.cursor/commands/pr-address.sh

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
# mark-addressed --owner <o> --repo <r> --pr <n> --type <review|comment> --target-id <id> --body <text>
1111
# resolve-id --owner <o> --repo <r> --pr <n> --node-id <id>
1212
# headline --owner <o> --repo <r> --sha <sha>
13+
# fetch-pr-body --owner <o> --repo <r> --pr <n> Fetch current PR body → /tmp/pr-body.md
1314
# autosquash Rebase --autosquash from merge-base
1415
#
1516
# Exit codes: 0 = success, 1 = error, 2 = needs user input (e.g. gh not authenticated)
@@ -249,6 +250,16 @@ case "$CMD" in
249250
gh api "repos/$OWNER/$REPO/commits/$SHA" --jq '.commit.message | split("\n") | .[0]'
250251
;;
251252

253+
fetch-pr-body)
254+
require_gh
255+
if [[ -z "$OWNER" || -z "$REPO" || -z "$PR" ]]; then
256+
echo "Error: --owner, --repo, --pr required" >&2; exit 1
257+
fi
258+
BODY=$(gh api "repos/$OWNER/$REPO/pulls/$PR" --jq '.body // ""')
259+
echo "$BODY" > /tmp/pr-body.md
260+
echo ">> Wrote PR body to /tmp/pr-body.md ($(wc -c < /tmp/pr-body.md | tr -d ' ') bytes)"
261+
;;
262+
252263
autosquash)
253264
DEFAULT_UPSTREAM=$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null \
254265
|| echo "origin/$(git remote show origin | sed -n '/HEAD branch/s/.*: //p')")
@@ -258,7 +269,7 @@ case "$CMD" in
258269
;;
259270

260271
*)
261-
echo "Usage: pr-address.sh {fetch|reply|resolve-thread|mark-addressed|resolve-id|headline|autosquash} [args]" >&2
272+
echo "Usage: pr-address.sh {fetch|reply|resolve-thread|mark-addressed|resolve-id|headline|fetch-pr-body|autosquash} [args]" >&2
262273
exit 1
263274
;;
264275
esac

.cursor/commands/pr-create.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ Run full verification before creating the PR:
7272

7373
Where `<upstream-ref>` is `origin/develop` for `edge-react-gui` or `origin/master` for other repos. Set `block_until_ms: 120000`.
7474

75-
**CHANGELOG check:** Before running verification, confirm a CHANGELOG entry exists on this branch: `git diff origin/$DEFAULT_BRANCH..HEAD -- CHANGELOG.md`. If empty, add one now (see `im.md` CHANGELOG placement rules).
75+
**CHANGELOG check:** Before running verification, read the top ~50 lines of `CHANGELOG.md` and confirm that entries exist which reflect the branch's changes. If no matching entries exist, add them now (see `im.md` CHANGELOG placement rules).
7676

7777
If verification fails, fix the issue, amend or fixup the relevant commit, push again, then continue.
7878
</step>
@@ -86,9 +86,6 @@ DEFAULT_BRANCH=$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/de
8686

8787
# All commit messages on this branch
8888
git log origin/$DEFAULT_BRANCH..HEAD --format=%B---
89-
90-
# CHANGELOG diff
91-
git diff origin/$DEFAULT_BRANCH..HEAD -- CHANGELOG.md
9289
```
9390

9491
<sub-step name="PR title">
@@ -142,14 +139,14 @@ If Asana context was fetched:
142139

143140
- **Title**: Align the PR title with the task name if it's descriptive.
144141
- **Dependencies**: Cross-reference with linked PRs mentioned in task comments.
145-
- **Context subsection**: Add a `#### Context` subsection at the **beginning** of the Description section — what the task is, why it matters, key decisions from comments. Wrap file paths, function names, and code references in backticks.
142+
- **Context subsection**: Add a `#### Context` subsection at the **beginning** of the Description section — what the task is, why it matters, key decisions from comments. Wrap file paths, function names, and code references in backticks. The Asana link itself is injected by `pr-create.sh` via `--asana-task` — do not duplicate it here.
146143

147144
Example:
148145

149146
```markdown
150147
#### Context
151148

152-
Asana: "gui: Token list not updating after add" (P2). The `useTokenList` hook
149+
"gui: Token list not updating after add" (P2). The `useTokenList` hook
153150
caches stale data because `useSyncEffect` doesn't re-trigger on `currencyConfig` changes.
154151

155152
#### Changes
@@ -167,13 +164,16 @@ caches stale data because `useSyncEffect` doesn't re-trigger on `currencyConfig`
167164
<step id="7" name="Create PR">
168165
Create the PR immediately — do not ask for confirmation.
169166

170-
1. **Write the body to a temp file** using the Write tool (NOT a shell command):
167+
1. **Write the body to a temp file** using the **Write tool** (NOT ApplyPatch, NOT a shell command):
171168
- Path: `/tmp/pr-body.md`
172169
- Content: the full PR body built in step 6
170+
- 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.**
173171
2. **Run the script**:
174172
```bash
175-
~/.cursor/commands/pr-create.sh --title "<title>" --body-file /tmp/pr-body.md
173+
~/.cursor/commands/pr-create.sh --title "<title>" --body-file /tmp/pr-body.md --asana-task <task_gid>
176174
```
175+
- 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.
176+
- 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`.
177177

178178
Using `--body-file` avoids shell escaping issues with multi-line content. Do NOT use `--body` with inline content.
179179

.cursor/commands/pr-create.sh

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ const args = process.argv.slice(2);
1414
let title = null;
1515
let bodyFile = null;
1616
let draft = false;
17+
let asanaTask = null;
1718

1819
for (let i = 0; i < args.length; i++) {
1920
if (args[i] === "--title" && args[i + 1]) title = args[++i];
2021
else if (args[i] === "--body-file" && args[i + 1]) bodyFile = args[++i];
22+
else if (args[i] === "--asana-task" && args[i + 1]) asanaTask = args[++i];
2123
else if (args[i] === "--draft") draft = true;
2224
}
2325

@@ -139,6 +141,23 @@ if (!body) {
139141
}
140142
}
141143

144+
// Inject Asana link if provided and not already present
145+
if (asanaTask) {
146+
const asanaUrl = `https://app.asana.com/0/0/${asanaTask}/f`;
147+
const asanaRegex = new RegExp(`https://app\\.asana\\.com/\\d+/\\d+/(?:task/)?${asanaTask}`, "i");
148+
if (!asanaRegex.test(body)) {
149+
const link = `[Asana task](${asanaUrl})`;
150+
const descIdx = body.indexOf("### Description\n");
151+
if (descIdx !== -1) {
152+
const afterHeader = descIdx + "### Description\n".length;
153+
const rest = body.slice(afterHeader).replace(/^\n*/, "");
154+
body = body.slice(0, afterHeader) + `\n${link}\n\n` + rest;
155+
} else {
156+
body = `${link}\n\n` + body;
157+
}
158+
}
159+
}
160+
142161
// Create PR via gh CLI — write body to a temp file to avoid arg length issues
143162
const tmpBody = path.join(os.tmpdir(), `pr-body-${process.pid}.md`);
144163
fs.writeFileSync(tmpBody, body, "utf8");
@@ -147,6 +166,11 @@ if (draft) ghArgs.push("--draft");
147166

148167
const result = spawnSync("gh", ghArgs, { encoding: "utf8" });
149168
try { fs.unlinkSync(tmpBody); } catch {}
169+
if (bodyFile && bodyFile.startsWith(os.tmpdir())) {
170+
try {
171+
fs.unlinkSync(bodyFile);
172+
} catch {}
173+
}
150174
if (result.status !== 0) {
151175
console.error("ERROR:", (result.stderr || "").trim());
152176
process.exit(1);

.cursor/commands/push-env-key.sh

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#!/usr/bin/env bash
2+
# push-env-key.sh — Update a single key in the server's env.json and push
3+
#
4+
# Usage: push-env-key.sh <KEY> <VALUE> [-m "commit message"]
5+
#
6+
# Examples:
7+
# push-env-key.sh EDGE_API_KEY abc123
8+
# push-env-key.sh EDGE_API_KEY abc123 -m "Rotate Edge API key"
9+
10+
set -euo pipefail
11+
12+
SERVER="jack"
13+
REMOTE_REPO="/home/jon/jenkins-files/master"
14+
15+
KEY=""
16+
VALUE=""
17+
COMMIT_MSG=""
18+
19+
while [[ $# -gt 0 ]]; do
20+
case "$1" in
21+
-m) COMMIT_MSG="$2"; shift 2 ;;
22+
*)
23+
if [[ -z "$KEY" ]]; then KEY="$1"
24+
elif [[ -z "$VALUE" ]]; then VALUE="$1"
25+
else echo "Unexpected argument: $1" >&2; exit 1
26+
fi
27+
shift ;;
28+
esac
29+
done
30+
31+
if [[ -z "$KEY" || -z "$VALUE" ]]; then
32+
echo "Usage: push-env-key.sh <KEY> <VALUE> [-m \"commit message\"]" >&2
33+
exit 1
34+
fi
35+
36+
if [[ -z "$COMMIT_MSG" ]]; then
37+
COMMIT_MSG="Update $KEY in env.json"
38+
fi
39+
40+
ssh "$SERVER" bash -s -- "$KEY" "$VALUE" "$COMMIT_MSG" "$REMOTE_REPO" <<'REMOTE'
41+
set -euo pipefail
42+
KEY="$1"
43+
VALUE="$2"
44+
MSG="$3"
45+
REPO="$4"
46+
47+
cd "$REPO"
48+
git pull --ff-only
49+
50+
CURRENT=$(jq -r --arg k "$KEY" '.[$k] // empty' env.json)
51+
if [[ "$CURRENT" == "$VALUE" ]]; then
52+
echo "No change: $KEY is already set to that value."
53+
exit 0
54+
fi
55+
56+
jq --arg k "$KEY" --arg v "$VALUE" '.[$k] = $v' env.json > env.json.tmp
57+
mv env.json.tmp env.json
58+
59+
git add env.json
60+
git commit -m "$MSG"
61+
git push
62+
echo "Done: $KEY updated and pushed."
63+
REMOTE

.cursor/rules/answer-questions-first.mdc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ alwaysApply: true
55

66
# Answer Questions Before Acting
77

8-
Before using any code editing tools, scan the user's message for `?` characters.
8+
Before using any code editing tools, scan the user's message for `?` characters and determine if it's a question.
99

10-
- **Ignore** `?` inside URLs or query parameters (e.g. `?param=x`, `?key=value`)
11-
- **Treat all other `?`** as question statements
10+
- **Ignore** `?` inside code, URLs or query parameters (e.g. `?param=x`, `?key=value` , `const x = ifTrue ? 'yes' : 'no'`)
11+
- **Treat all other `?`** as question statements, if they appear to be questions.
1212

1313
If questions are detected:
1414

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
description: When a workflow issue is identified in a command or skill, fix the command/skill definition first before fixing the downstream misbehavior.
3-
alwaysApply: true
3+
alwaysApply: false
44
---
55

66
<goal>Ensure workflow definitions are fixed at the source before any workarounds are applied.</goal>
@@ -19,4 +19,4 @@ alwaysApply: true
1919
5. **Resume the original task** only after the command/skill is updated.
2020
</sequence>
2121

22-
<scope>This applies to all workflow issues — missed steps, incorrect output, wrong tool usage, formatting problems, etc. The command/skill is the source of truth; patching around it creates drift.</scope>
22+
<scope>This applies to all workflow issues — missed steps, incorrect output, wrong tool usage, shell failures, formatting problems, etc. The command/skill is the source of truth; patching around it creates drift.</scope>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
description: Halt on workflow errors and detect slash-command invocations in user messages
3+
alwaysApply: true
4+
---
5+
6+
<rules description="Non-negotiable constraints.">
7+
8+
<rule id="halt-on-error">When ANY shell command fails (non-zero exit code) during a workflow:
9+
1. **STOP** — do not retry, work around, substitute, or continue the workflow.
10+
2. **Report** — show the user the exact command, exit code, and error output.
11+
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.
13+
5. **Wait** — do not resume until the user responds.
14+
</rule>
15+
16+
<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>
17+
18+
</rules>
19+
20+
<slash-command-detection description="Detect /command invocations in user messages, analogous to answer-questions-first.mdc for '?' characters.">
21+
22+
Scan the user's message for `/word` tokens. A token is a **command invocation** when ALL of:
23+
- `/word` is preceded by whitespace, a newline, or is at the start of the message
24+
- `word` contains only lowercase letters and hyphens (e.g., `/im`, `/pr-create`, `/author`)
25+
- `/word` is NOT inside a file path, URL, or code block
26+
27+
When detected:
28+
1. Read `~/.cursor/commands/<word>.md` and follow it immediately.
29+
2. If the file does not exist, inform the user: "Command `/<word>` not found in `~/.cursor/commands/`."
30+
31+
**Ignore `/`** in: file paths (`/Users/...`, `~/...`), URLs (`https://...`), mid-word (`and/or`), backticks/code blocks.
32+
33+
</slash-command-detection>

0 commit comments

Comments
 (0)