cursordoctrine 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/cli.mjs +1 -1
- package/linux/declared-editing.md +30 -0
- package/linux/hooks/subagent-stop-review.sh +103 -103
- package/linux/inject-doctrine.sh +1 -1
- package/package.json +1 -1
- package/windows/declared-editing.md +30 -0
- package/windows/inject-doctrine.ps1 +59 -58
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Thin self-review hooks for Cursor. Five hook events, one message bus. The model
|
|
|
6
6
|
|
|
7
7
|
A small set of Cursor hooks that make the agent review its own work without bolting a static-analysis pipeline onto every keystroke. There is no regex army and no scoring engine. The hooks do three jobs:
|
|
8
8
|
|
|
9
|
-
1. **Inject the doctrine** at session start, so every chat begins with the same short governing text (`doctrine.md` + `USER-RULES.md`).
|
|
9
|
+
1. **Inject the doctrine** at session start, so every chat begins with the same short governing text (`doctrine.md` + `USER-RULES.md` + `declared-editing.md` — the YAGNI ultra ladder that prevents over-building before a single line is written).
|
|
10
10
|
2. **Hand the model its own edits back.** After each agent edit, a self-review prompt (plus minimal-edit, semantic-density, and anti-slop advisories when they trip) is stashed and delivered on the next turn. The model reads its own diff, fixes real bugs, and stays quiet otherwise.
|
|
11
11
|
3. **Gate blast radius.** One permission gate denies a short, explicit list of dangerous commands (`rm -rf /`, `curl | sh`, force-push, `npm publish`, ...). Everything else is allowed.
|
|
12
12
|
|
package/bin/cli.mjs
CHANGED
|
@@ -40,7 +40,7 @@ const pendingDir = join(cursorDst, '.hooks-pending');
|
|
|
40
40
|
const hooksJsonDst = join(cursorDst, 'hooks.json');
|
|
41
41
|
|
|
42
42
|
const injectName = platform === 'windows' ? 'inject-doctrine.ps1' : 'inject-doctrine.sh';
|
|
43
|
-
const doctrineFiles = [injectName, 'doctrine.md', 'USER-RULES.md'];
|
|
43
|
+
const doctrineFiles = [injectName, 'doctrine.md', 'USER-RULES.md', 'declared-editing.md'];
|
|
44
44
|
|
|
45
45
|
function payloadHookFiles() {
|
|
46
46
|
return readdirSync(join(payload, 'hooks'));
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Declared-editing — YAGNI ultra
|
|
2
|
+
|
|
3
|
+
ACTIVE EVERY RESPONSE. No drift back to over-building. Still active if unsure.
|
|
4
|
+
|
|
5
|
+
Before writing any code, stop at the first rung that holds:
|
|
6
|
+
|
|
7
|
+
1. Does this need to exist at all? (YAGNI) If no — say so, don't build it.
|
|
8
|
+
2. Does the stdlib already do this? Use it.
|
|
9
|
+
3. Does a native platform feature cover it? Use it.
|
|
10
|
+
4. Does an already-installed dependency solve it? Use it.
|
|
11
|
+
5. Can this be one line? Make it one line.
|
|
12
|
+
6. Only then: write the minimum code that works.
|
|
13
|
+
|
|
14
|
+
Ultra means:
|
|
15
|
+
|
|
16
|
+
- Deletion before addition. If you can remove code to solve the problem, remove it.
|
|
17
|
+
- Ship the one-liner and challenge the rest of the requirement in the same breath.
|
|
18
|
+
- A hand-rolled abstraction is a bug farm with a hit rate. Say so.
|
|
19
|
+
- Question complex requests: "Do you actually need X, or does Y cover it?"
|
|
20
|
+
|
|
21
|
+
Mark intentional simplifications with a `declared:` comment naming the ceiling
|
|
22
|
+
and the upgrade path: `// declared: O(n^2) scan, fine <10k rows; index at 50k`.
|
|
23
|
+
|
|
24
|
+
Not lazy about: input validation at trust boundaries, error handling that
|
|
25
|
+
prevents data loss, security, accessibility, anything explicitly requested.
|
|
26
|
+
Non-trivial logic leaves ONE runnable check behind (an assert or one small
|
|
27
|
+
test, no framework, no fixtures). Trivial one-liners need none.
|
|
28
|
+
|
|
29
|
+
Output format when you skipped building something:
|
|
30
|
+
-> skipped: [X], add when [Y]
|
|
@@ -1,103 +1,103 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# subagent-stop-review.sh - subagentStop for Cursor (Linux).
|
|
3
|
-
#
|
|
4
|
-
# Counterpart of final-review.sh for delegated work. afterFileEdit DOES fire
|
|
5
|
-
# inside subagents (verified: a subagent run left its edits in
|
|
6
|
-
# session-edits-<subagent-cid>.txt), but subagents get no `stop` event, so
|
|
7
|
-
# that marker is never drained and the five-axis review never fires for
|
|
8
|
-
# delegated implementations. This hook closes the loop: when a subagent
|
|
9
|
-
# finishes and ITS conversation has a session-edits marker, return ONE
|
|
10
|
-
# followup_message so the subagent audits its own implementation before the
|
|
11
|
-
# result goes back to the parent.
|
|
12
|
-
#
|
|
13
|
-
# Same bounding pattern as final-review.sh:
|
|
14
|
-
# - marker-gated: no edits in the subagent run -> no review, no noise,
|
|
15
|
-
# - reviewed-<cid>.flag one-shot brake: the stop AFTER the review pass
|
|
16
|
-
# clears flag + marker and ends the loop (one review per implementation;
|
|
17
|
-
# resumed subagents with a second implementation get a second review),
|
|
18
|
-
# - loop_limit in hooks.json caps runaway follow-ups harness-side,
|
|
19
|
-
# - only on status == 'completed' when a status field is present.
|
|
20
|
-
#
|
|
21
|
-
# If subagentStop's stdin carries a conversation_id that doesn't match the
|
|
22
|
-
# id afterFileEdit used, the marker lookup misses and this emits {} - the
|
|
23
|
-
# marker fold in post-tool-use.sh / final-review.sh still routes the
|
|
24
|
-
# subagent's edits into the parent's stop review as the backstop.
|
|
25
|
-
#
|
|
26
|
-
# Always emits valid JSON ({} = no follow-up). Review body reuses
|
|
27
|
-
# final-review.md (embedded fallback if missing).
|
|
28
|
-
# Disable: HOOKS_ENFORCE=0 or SUBAGENT_REVIEW_ENFORCE=0.
|
|
29
|
-
|
|
30
|
-
set +e
|
|
31
|
-
. "$(dirname "$0")/hook-common.sh"
|
|
32
|
-
|
|
33
|
-
emit_none() { printf '{}'; exit 0; }
|
|
34
|
-
|
|
35
|
-
[ "${HOOKS_ENFORCE:-}" = "0" ] && emit_none
|
|
36
|
-
[ "${SUBAGENT_REVIEW_ENFORCE:-}" = "0" ] && emit_none
|
|
37
|
-
|
|
38
|
-
input="$(read_hook_stdin)"
|
|
39
|
-
[ -n "$input" ] || emit_none
|
|
40
|
-
|
|
41
|
-
status="$(json_get "$input" status)"
|
|
42
|
-
cid="$(safe_conversation_id "$input")"
|
|
43
|
-
|
|
44
|
-
pending_dir="$(hooks_pending_dir)"
|
|
45
|
-
marker="$pending_dir/session-edits-$cid.txt"
|
|
46
|
-
flag="$pending_dir/reviewed-$cid.flag"
|
|
47
|
-
|
|
48
|
-
# One-shot brake: the previous subagentStop for this id emitted the review.
|
|
49
|
-
if [ -f "$flag" ]; then
|
|
50
|
-
rm -f "$flag" "$marker" 2>/dev/null
|
|
51
|
-
emit_none
|
|
52
|
-
fi
|
|
53
|
-
|
|
54
|
-
# Review only a clean completion; otherwise clear the marker and stop.
|
|
55
|
-
if [ -n "$status" ] && [ "$status" != "completed" ]; then
|
|
56
|
-
rm -f "$marker" 2>/dev/null
|
|
57
|
-
emit_none
|
|
58
|
-
fi
|
|
59
|
-
|
|
60
|
-
# No edits this run -> nothing to review.
|
|
61
|
-
[ -f "$marker" ] || emit_none
|
|
62
|
-
edited="$(grep -vE '^[[:space:]]*$' "$marker" 2>/dev/null | sort -u)"
|
|
63
|
-
rm -f "$marker" 2>/dev/null
|
|
64
|
-
[ -n "$edited" ] || emit_none
|
|
65
|
-
|
|
66
|
-
# Compose the follow-up review prompt (md preferred, embedded fallback).
|
|
67
|
-
prompt_file="$HOME/.agents/hooks/final-review.md"
|
|
68
|
-
body=""
|
|
69
|
-
[ -f "$prompt_file" ] && body="$(cat "$prompt_file")"
|
|
70
|
-
if [ -z "$body" ]; then
|
|
71
|
-
body='Audit everything you changed in this run and FIX what fails (do NOT revert the
|
|
72
|
-
behaviour the task asked for):
|
|
73
|
-
1. Correctness - logic, edge cases (null/empty/zero/boundary), language traps, security.
|
|
74
|
-
2. Reliability - error paths handled, no swallowed errors, resources released.
|
|
75
|
-
3. Coverage - behaviour-bearing changes have real tests; RUN the suite if present.
|
|
76
|
-
4. Anti-slop - if ~/.cursor/skills/anti-slop/scripts/scan_slop.py exists, run
|
|
77
|
-
`python ~/.cursor/skills/anti-slop/scripts/scan_slop.py --all`; otherwise
|
|
78
|
-
apply ~/.agents/hooks/anti-slop.md to the session diff.
|
|
79
|
-
If an axis is clean, say so in one line. Then stop.'
|
|
80
|
-
fi
|
|
81
|
-
body="$(expand_agent_paths "$body")"
|
|
82
|
-
|
|
83
|
-
file_list=""
|
|
84
|
-
while IFS= read -r p; do
|
|
85
|
-
[ -n "$p" ] || continue
|
|
86
|
-
rp="$(resolve_agent_path "$p")"
|
|
87
|
-
file_list="${file_list} ${rp}"$'\n'
|
|
88
|
-
done <<EOF
|
|
89
|
-
$edited
|
|
90
|
-
EOF
|
|
91
|
-
file_list="$(printf '%s' "$file_list" | head -n 30)"
|
|
92
|
-
msg="SUBAGENT FINAL REVIEW - you just finished delegated implementation work. Before your result returns to the parent agent, audit it.
|
|
93
|
-
|
|
94
|
-
Files you changed this run:
|
|
95
|
-
$file_list
|
|
96
|
-
|
|
97
|
-
$body"
|
|
98
|
-
|
|
99
|
-
# Arm the one-shot brake BEFORE emitting, so a crash after emit can't re-fire.
|
|
100
|
-
touch "$flag" 2>/dev/null
|
|
101
|
-
|
|
102
|
-
emit_json followup_message "$msg"
|
|
103
|
-
exit 0
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# subagent-stop-review.sh - subagentStop for Cursor (Linux).
|
|
3
|
+
#
|
|
4
|
+
# Counterpart of final-review.sh for delegated work. afterFileEdit DOES fire
|
|
5
|
+
# inside subagents (verified: a subagent run left its edits in
|
|
6
|
+
# session-edits-<subagent-cid>.txt), but subagents get no `stop` event, so
|
|
7
|
+
# that marker is never drained and the five-axis review never fires for
|
|
8
|
+
# delegated implementations. This hook closes the loop: when a subagent
|
|
9
|
+
# finishes and ITS conversation has a session-edits marker, return ONE
|
|
10
|
+
# followup_message so the subagent audits its own implementation before the
|
|
11
|
+
# result goes back to the parent.
|
|
12
|
+
#
|
|
13
|
+
# Same bounding pattern as final-review.sh:
|
|
14
|
+
# - marker-gated: no edits in the subagent run -> no review, no noise,
|
|
15
|
+
# - reviewed-<cid>.flag one-shot brake: the stop AFTER the review pass
|
|
16
|
+
# clears flag + marker and ends the loop (one review per implementation;
|
|
17
|
+
# resumed subagents with a second implementation get a second review),
|
|
18
|
+
# - loop_limit in hooks.json caps runaway follow-ups harness-side,
|
|
19
|
+
# - only on status == 'completed' when a status field is present.
|
|
20
|
+
#
|
|
21
|
+
# If subagentStop's stdin carries a conversation_id that doesn't match the
|
|
22
|
+
# id afterFileEdit used, the marker lookup misses and this emits {} - the
|
|
23
|
+
# marker fold in post-tool-use.sh / final-review.sh still routes the
|
|
24
|
+
# subagent's edits into the parent's stop review as the backstop.
|
|
25
|
+
#
|
|
26
|
+
# Always emits valid JSON ({} = no follow-up). Review body reuses
|
|
27
|
+
# final-review.md (embedded fallback if missing).
|
|
28
|
+
# Disable: HOOKS_ENFORCE=0 or SUBAGENT_REVIEW_ENFORCE=0.
|
|
29
|
+
|
|
30
|
+
set +e
|
|
31
|
+
. "$(dirname "$0")/hook-common.sh"
|
|
32
|
+
|
|
33
|
+
emit_none() { printf '{}'; exit 0; }
|
|
34
|
+
|
|
35
|
+
[ "${HOOKS_ENFORCE:-}" = "0" ] && emit_none
|
|
36
|
+
[ "${SUBAGENT_REVIEW_ENFORCE:-}" = "0" ] && emit_none
|
|
37
|
+
|
|
38
|
+
input="$(read_hook_stdin)"
|
|
39
|
+
[ -n "$input" ] || emit_none
|
|
40
|
+
|
|
41
|
+
status="$(json_get "$input" status)"
|
|
42
|
+
cid="$(safe_conversation_id "$input")"
|
|
43
|
+
|
|
44
|
+
pending_dir="$(hooks_pending_dir)"
|
|
45
|
+
marker="$pending_dir/session-edits-$cid.txt"
|
|
46
|
+
flag="$pending_dir/reviewed-$cid.flag"
|
|
47
|
+
|
|
48
|
+
# One-shot brake: the previous subagentStop for this id emitted the review.
|
|
49
|
+
if [ -f "$flag" ]; then
|
|
50
|
+
rm -f "$flag" "$marker" 2>/dev/null
|
|
51
|
+
emit_none
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# Review only a clean completion; otherwise clear the marker and stop.
|
|
55
|
+
if [ -n "$status" ] && [ "$status" != "completed" ]; then
|
|
56
|
+
rm -f "$marker" 2>/dev/null
|
|
57
|
+
emit_none
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# No edits this run -> nothing to review.
|
|
61
|
+
[ -f "$marker" ] || emit_none
|
|
62
|
+
edited="$(grep -vE '^[[:space:]]*$' "$marker" 2>/dev/null | sort -u)"
|
|
63
|
+
rm -f "$marker" 2>/dev/null
|
|
64
|
+
[ -n "$edited" ] || emit_none
|
|
65
|
+
|
|
66
|
+
# Compose the follow-up review prompt (md preferred, embedded fallback).
|
|
67
|
+
prompt_file="$HOME/.agents/hooks/final-review.md"
|
|
68
|
+
body=""
|
|
69
|
+
[ -f "$prompt_file" ] && body="$(cat "$prompt_file")"
|
|
70
|
+
if [ -z "$body" ]; then
|
|
71
|
+
body='Audit everything you changed in this run and FIX what fails (do NOT revert the
|
|
72
|
+
behaviour the task asked for):
|
|
73
|
+
1. Correctness - logic, edge cases (null/empty/zero/boundary), language traps, security.
|
|
74
|
+
2. Reliability - error paths handled, no swallowed errors, resources released.
|
|
75
|
+
3. Coverage - behaviour-bearing changes have real tests; RUN the suite if present.
|
|
76
|
+
4. Anti-slop - if ~/.cursor/skills/anti-slop/scripts/scan_slop.py exists, run
|
|
77
|
+
`python ~/.cursor/skills/anti-slop/scripts/scan_slop.py --all`; otherwise
|
|
78
|
+
apply ~/.agents/hooks/anti-slop.md to the session diff.
|
|
79
|
+
If an axis is clean, say so in one line. Then stop.'
|
|
80
|
+
fi
|
|
81
|
+
body="$(expand_agent_paths "$body")"
|
|
82
|
+
|
|
83
|
+
file_list=""
|
|
84
|
+
while IFS= read -r p; do
|
|
85
|
+
[ -n "$p" ] || continue
|
|
86
|
+
rp="$(resolve_agent_path "$p")"
|
|
87
|
+
file_list="${file_list} ${rp}"$'\n'
|
|
88
|
+
done <<EOF
|
|
89
|
+
$edited
|
|
90
|
+
EOF
|
|
91
|
+
file_list="$(printf '%s' "$file_list" | head -n 30)"
|
|
92
|
+
msg="SUBAGENT FINAL REVIEW - you just finished delegated implementation work. Before your result returns to the parent agent, audit it.
|
|
93
|
+
|
|
94
|
+
Files you changed this run:
|
|
95
|
+
$file_list
|
|
96
|
+
|
|
97
|
+
$body"
|
|
98
|
+
|
|
99
|
+
# Arm the one-shot brake BEFORE emitting, so a crash after emit can't re-fire.
|
|
100
|
+
touch "$flag" 2>/dev/null
|
|
101
|
+
|
|
102
|
+
emit_json followup_message "$msg"
|
|
103
|
+
exit 0
|
package/linux/inject-doctrine.sh
CHANGED
|
@@ -16,7 +16,7 @@ set +e
|
|
|
16
16
|
cat >/dev/null
|
|
17
17
|
|
|
18
18
|
context=""
|
|
19
|
-
for p in "$HOME/.cursor/doctrine.md" "$HOME/.cursor/USER-RULES.md"; do
|
|
19
|
+
for p in "$HOME/.cursor/doctrine.md" "$HOME/.cursor/USER-RULES.md" "$HOME/.cursor/declared-editing.md"; do
|
|
20
20
|
if [ -f "$p" ]; then
|
|
21
21
|
part="$(cat "$p")"
|
|
22
22
|
if [ -n "$context" ]; then context="$context
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursordoctrine",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Thin self-review hooks for Cursor — the model is the auditor. Intent-trace final review (Tier 0), unified 13-item anti-slop checklist, operational slop detection.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"cursordoctrine": "bin/cli.mjs"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Declared-editing — YAGNI ultra
|
|
2
|
+
|
|
3
|
+
ACTIVE EVERY RESPONSE. No drift back to over-building. Still active if unsure.
|
|
4
|
+
|
|
5
|
+
Before writing any code, stop at the first rung that holds:
|
|
6
|
+
|
|
7
|
+
1. Does this need to exist at all? (YAGNI) If no — say so, don't build it.
|
|
8
|
+
2. Does the stdlib already do this? Use it.
|
|
9
|
+
3. Does a native platform feature cover it? Use it.
|
|
10
|
+
4. Does an already-installed dependency solve it? Use it.
|
|
11
|
+
5. Can this be one line? Make it one line.
|
|
12
|
+
6. Only then: write the minimum code that works.
|
|
13
|
+
|
|
14
|
+
Ultra means:
|
|
15
|
+
|
|
16
|
+
- Deletion before addition. If you can remove code to solve the problem, remove it.
|
|
17
|
+
- Ship the one-liner and challenge the rest of the requirement in the same breath.
|
|
18
|
+
- A hand-rolled abstraction is a bug farm with a hit rate. Say so.
|
|
19
|
+
- Question complex requests: "Do you actually need X, or does Y cover it?"
|
|
20
|
+
|
|
21
|
+
Mark intentional simplifications with a `declared:` comment naming the ceiling
|
|
22
|
+
and the upgrade path: `// declared: O(n^2) scan, fine <10k rows; index at 50k`.
|
|
23
|
+
|
|
24
|
+
Not lazy about: input validation at trust boundaries, error handling that
|
|
25
|
+
prevents data loss, security, accessibility, anything explicitly requested.
|
|
26
|
+
Non-trivial logic leaves ONE runnable check behind (an assert or one small
|
|
27
|
+
test, no framework, no fixtures). Trivial one-liners need none.
|
|
28
|
+
|
|
29
|
+
Output format when you skipped building something:
|
|
30
|
+
-> skipped: [X], add when [Y]
|
|
@@ -1,58 +1,59 @@
|
|
|
1
|
-
# inject-doctrine.ps1 - Cursor sessionStart injection.
|
|
2
|
-
#
|
|
3
|
-
# Emits {"additional_context": "<doctrine + USER-RULES>"} as PURE-ASCII JSON.
|
|
4
|
-
#
|
|
5
|
-
# Why pure ASCII: the doctrine contains multi-byte UTF-8 characters (em dash,
|
|
6
|
-
# section sign, <=, arrows). Written as UTF-8, their continuation bytes
|
|
7
|
-
# (0x80-0x9F) get decoded by Cursor's JSON reader as C1 control characters ->
|
|
8
|
-
# "Bad control character in string literal in JSON at position N". Escaping every
|
|
9
|
-
# non-ASCII char to \uXXXX makes the output byte-identical under EVERY encoding,
|
|
10
|
-
# so it cannot be mangled; JSON.parse turns § back into the real char. We
|
|
11
|
-
# also write the bytes straight to stdout to bypass [Console]::OutputEncoding.
|
|
12
|
-
#
|
|
13
|
-
# Fail open: missing files or any error -> "{}" (valid, empty). Never block or
|
|
14
|
-
# crash session start.
|
|
15
|
-
|
|
16
|
-
$ErrorActionPreference = 'SilentlyContinue'
|
|
17
|
-
|
|
18
|
-
# Drain stdin (Cursor sends session metadata) so the pipe never blocks.
|
|
19
|
-
$null = [Console]::In.ReadToEnd()
|
|
20
|
-
|
|
21
|
-
function Write-StdoutAscii([string]$s) {
|
|
22
|
-
# Write exact ASCII bytes to stdout, immune to whatever [Console]::OutputEncoding is.
|
|
23
|
-
$bytes = [System.Text.Encoding]::ASCII.GetBytes($s)
|
|
24
|
-
$stdout = [Console]::OpenStandardOutput()
|
|
25
|
-
$stdout.Write($bytes, 0, $bytes.Length)
|
|
26
|
-
$stdout.Flush()
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
$paths = @(
|
|
31
|
-
(Join-Path $PSScriptRoot 'doctrine.md'),
|
|
32
|
-
(Join-Path $PSScriptRoot 'USER-RULES.md')
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
#
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
1
|
+
# inject-doctrine.ps1 - Cursor sessionStart injection.
|
|
2
|
+
#
|
|
3
|
+
# Emits {"additional_context": "<doctrine + USER-RULES>"} as PURE-ASCII JSON.
|
|
4
|
+
#
|
|
5
|
+
# Why pure ASCII: the doctrine contains multi-byte UTF-8 characters (em dash,
|
|
6
|
+
# section sign, <=, arrows). Written as UTF-8, their continuation bytes
|
|
7
|
+
# (0x80-0x9F) get decoded by Cursor's JSON reader as C1 control characters ->
|
|
8
|
+
# "Bad control character in string literal in JSON at position N". Escaping every
|
|
9
|
+
# non-ASCII char to \uXXXX makes the output byte-identical under EVERY encoding,
|
|
10
|
+
# so it cannot be mangled; JSON.parse turns § back into the real char. We
|
|
11
|
+
# also write the bytes straight to stdout to bypass [Console]::OutputEncoding.
|
|
12
|
+
#
|
|
13
|
+
# Fail open: missing files or any error -> "{}" (valid, empty). Never block or
|
|
14
|
+
# crash session start.
|
|
15
|
+
|
|
16
|
+
$ErrorActionPreference = 'SilentlyContinue'
|
|
17
|
+
|
|
18
|
+
# Drain stdin (Cursor sends session metadata) so the pipe never blocks.
|
|
19
|
+
$null = [Console]::In.ReadToEnd()
|
|
20
|
+
|
|
21
|
+
function Write-StdoutAscii([string]$s) {
|
|
22
|
+
# Write exact ASCII bytes to stdout, immune to whatever [Console]::OutputEncoding is.
|
|
23
|
+
$bytes = [System.Text.Encoding]::ASCII.GetBytes($s)
|
|
24
|
+
$stdout = [Console]::OpenStandardOutput()
|
|
25
|
+
$stdout.Write($bytes, 0, $bytes.Length)
|
|
26
|
+
$stdout.Flush()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
$paths = @(
|
|
31
|
+
(Join-Path $PSScriptRoot 'doctrine.md'),
|
|
32
|
+
(Join-Path $PSScriptRoot 'USER-RULES.md'),
|
|
33
|
+
(Join-Path $PSScriptRoot 'declared-editing.md')
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
$parts = foreach ($p in $paths) {
|
|
37
|
+
if (Test-Path -LiteralPath $p) { Get-Content -Raw -LiteralPath $p }
|
|
38
|
+
}
|
|
39
|
+
$context = ($parts -join "`n`n").Trim()
|
|
40
|
+
|
|
41
|
+
if (-not $context) { Write-StdoutAscii '{}'; exit 0 }
|
|
42
|
+
|
|
43
|
+
$json = @{ additional_context = $context } | ConvertTo-Json -Compress
|
|
44
|
+
|
|
45
|
+
# Escape every non-ASCII (and any stray control) char to \uXXXX -> pure ASCII.
|
|
46
|
+
# ConvertTo-Json's structural chars and \n / \" escapes are ASCII and pass through.
|
|
47
|
+
$sb = [System.Text.StringBuilder]::new($json.Length + 64)
|
|
48
|
+
foreach ($ch in $json.ToCharArray()) {
|
|
49
|
+
$code = [int][char]$ch
|
|
50
|
+
if ($code -lt 32 -or $code -gt 126) { [void]$sb.AppendFormat('\u{0:x4}', $code) }
|
|
51
|
+
else { [void]$sb.Append($ch) }
|
|
52
|
+
}
|
|
53
|
+
Write-StdoutAscii $sb.ToString()
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
Write-StdoutAscii '{}'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
exit 0
|