cursordoctrine 0.5.4 → 0.5.5

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/bin/cli.mjs CHANGED
@@ -334,6 +334,25 @@ function verify() {
334
334
  JSON.stringify({ role: 'user', message: { content: `<user_query>${q}</user_query>` } }) + '\n',
335
335
  'utf8');
336
336
 
337
+ // --- Case 0: no .scope.json + NO transcript -> WRITE scaffold anyway -------
338
+ // This is the 0.5.3 regression: creation was gated on $hasQuery, so when
339
+ // Cursor didn't surface transcript_path in the first postToolUse fire, the
340
+ // scope never appeared. Now creation is unconditional on a real root.
341
+ runHook(hook('intent-anchor'), { conversation_id: anchorCid, cwd: repoDir });
342
+ let d0 = drainedOf(anchorCid);
343
+ if (!existsSync(scopePath)) {
344
+ cleanup(); return { ok: false, detail: 'scaffold NOT created without transcript (0.5.3 regression)' };
345
+ }
346
+ let scope0;
347
+ try { scope0 = JSON.parse(readFileSync(scopePath, 'utf8')); }
348
+ catch { cleanup(); return { ok: false, detail: '.scope.json (no-transcript) is not valid JSON' }; }
349
+ if (!scope0.intent || !scope0.intent.includes('TODO')) {
350
+ cleanup(); return { ok: false, detail: `no-transcript scaffold should have intent <TODO>, got: ${scope0.intent}` };
351
+ }
352
+ // Clear the latch + scope so Case A starts fresh (with a real query this time).
353
+ runHook(hook('final-review'), { conversation_id: anchorCid, status: 'completed' });
354
+ cleanup();
355
+
337
356
  // --- Case A: no .scope.json + prompt q1 -> WRITE scaffold with q1 as intent -
338
357
  writeTranscript(q1);
339
358
  runHook(hook('intent-anchor'), { conversation_id: anchorCid, cwd: repoDir, transcript_path: transcriptPath });
@@ -122,5 +122,18 @@ Determinism / purity:
122
122
  - In-place mutation of shared state (arr.push, obj.prop =) when a caller holds
123
123
  a reference -> return new structures ([...arr, x], .map/.filter).
124
124
 
125
+ Logic & structure:
126
+ - Arrow code: >2 levels of nested if/for -> flatten with guard clauses
127
+ (early returns). Code reads top-to-bottom, no deep indent.
128
+ - Switch/if-else bloat: a switch or 5+ if/else branches -> Map/dispatch
129
+ (Record<State, fn>) or the Command pattern.
130
+ - Mixed abstraction (SLAP): a function mixing DB calls + string validation +
131
+ date formatting -> one level of abstraction per function; extract helpers.
132
+ - Primitive obsession: a primitive with business rules (email, userId, chainId)
133
+ passed as a bare string/number across functions -> a named type/value object.
134
+ - Imperative transforms: a `for` loop building an array when the language has
135
+ .map/.filter/.reduce -> use the declarative form; reserve `for` for cases
136
+ map/reduce cannot express.
137
+
125
138
  You do NOT need to run a tool for these — read the diff and apply the named fix.
126
139
  If none apply, say so in one line.
@@ -58,6 +58,16 @@ pending_dir="$(hooks_pending_dir)"
58
58
  latch="$pending_dir/intent-injected-$cid.flag"
59
59
  hash_file="$pending_dir/last-query-$cid.hash"
60
60
 
61
+ # Stale-latch defense: if a previous session died mid-turn without hitting
62
+ # stop (Cursor crash, force-quit), the latch can persist and silence this hook
63
+ # for the whole next session -> scope never gets created. If the latch is older
64
+ # than 2 hours, treat it as orphaned and clear it. Normal clears happen at
65
+ # every stop (final-review.sh); this is the backstop for abnormal terminations.
66
+ if [ -f "$latch" ]; then
67
+ age_hours=$(( ($(date +%s) - $(stat -c %Y "$latch" 2>/dev/null || stat -f %m "$latch" 2>/dev/null || echo 0)) / 3600 ))
68
+ [ "$age_hours" -ge 2 ] && rm -f "$latch" 2>/dev/null
69
+ fi
70
+
61
71
  # Already injected this turn -> quiet. Latch cleared at every stop.
62
72
  [ -f "$latch" ] && exit 0
63
73
 
@@ -133,30 +143,38 @@ EOF
133
143
  fi
134
144
  fi
135
145
 
136
- # --- auto-create / regenerate .scope.json (the 0.4.4 behavior, fixed) --------
137
- # The user wants: every NEW prompt -> a fresh .scope.json the agent works from.
138
- # So we WRITE the scaffold when (a) there is no valid contract, OR (b) the
139
- # contract on disk is stale (its _intent_hash != current query hash). Fixed vs
140
- # 0.4.4: never writes to $HOME (bail above if no real root) -> no ghost files;
141
- # regenerates on prompt CHANGE not just absence -> "each prompt, new file".
146
+ # --- auto-create / regenerate .scope.json -----------------------------------
147
+ # CREATION does NOT require the query: if there's a root and no scope yet,
148
+ # scaffold it NOW with intent=<TODO> (the agent fills it from the chat it's
149
+ # already responding to). This was the 0.5.3 bug - creation was gated on
150
+ # $hasQuery, so when Cursor didn't surface transcript_path in the first
151
+ # postToolUse fire, the scope never got created.
152
+ # REGENERATION does require the query: we can only detect a prompt change if
153
+ # we can hash the current request. Without a query we leave an existing scope
154
+ # alone (re-inject it) rather than blank it.
142
155
  regenerated=0
143
- should_write=0
144
- if [ "$has_query" = "1" ]; then
145
- if [ "$scope_exists" != "1" ] || [ "$scope_stale" = "1" ]; then
146
- should_write=1
147
- fi
156
+ should_create=0
157
+ should_regen=0
158
+ [ "$scope_exists" != "1" ] && should_create=1
159
+ if [ "$has_query" = "1" ] && [ "$scope_exists" = "1" ] && [ "$scope_stale" = "1" ]; then
160
+ should_regen=1
148
161
  fi
149
162
 
150
- if [ "$should_write" = "1" ]; then
151
- # jq preferred; python3 fallback. Write intent from the query, TODO
152
- # placeholders for files/acceptance, and record _intent_hash so staleness
153
- # is self-contained in the file (survives cross-session hash sweeps).
163
+ if [ "$should_create" = "1" ] || [ "$should_regen" = "1" ]; then
164
+ # intent from the query when available, else a TODO for the agent to fill.
165
+ if [ "$has_query" = "1" ]; then
166
+ intent_val="$current_query"
167
+ else
168
+ intent_val="<TODO: state the operational objective - what is strictly necessary>"
169
+ fi
170
+ # jq preferred; python3 fallback. Write intent, empty files[], TODO
171
+ # acceptance, and record _intent_hash so staleness is self-contained.
154
172
  if have_jq; then
155
- jq -n --arg intent "$current_query" --arg hash "$current_hash" \
173
+ jq -n --arg intent "$intent_val" --arg hash "$current_hash" \
156
174
  '{intent:$intent, files:[], acceptance:"<TODO: the one deterministic check that decides done>", allow_growth:false, _intent_hash:$hash, _generated_by:"intent-anchor hook"}' \
157
175
  > "$scope_path" 2>/dev/null && regenerated=1
158
176
  elif have_py; then
159
- if I_FILE="$scope_path" I_INTENT="$current_query" I_HASH="$current_hash" python3 -c '
177
+ if I_FILE="$scope_path" I_INTENT="$intent_val" I_HASH="$current_hash" python3 -c '
160
178
  import json, os
161
179
  obj = {
162
180
  "intent": os.environ["I_INTENT"],
@@ -173,7 +191,7 @@ with open(os.environ["I_FILE"], "w", encoding="utf-8") as f:
173
191
  fi
174
192
  fi
175
193
  if [ "$regenerated" = "1" ]; then
176
- scope_intent="$current_query"
194
+ scope_intent="$intent_val"
177
195
  scope_acceptance="<TODO: the one deterministic check that decides done>"
178
196
  scope_files="(auto-tracked - the scope hook records every file you edit)"
179
197
  scope_exists=1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursordoctrine",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "description": "Thin self-review hooks for Cursor — the model is the auditor. Pruned + deduplicated: intent-anchor (auto-scaffolded .scope.json per prompt + per-turn re-injection against Salience Dilution), intent-trace final review, unified anti-slop checklist as single source of truth.",
5
5
  "bin": {
6
6
  "cursordoctrine": "bin/cli.mjs"
@@ -122,5 +122,18 @@ Determinism / purity:
122
122
  - In-place mutation of shared state (arr.push, obj.prop =) when a caller holds
123
123
  a reference -> return new structures ([...arr, x], .map/.filter).
124
124
 
125
+ Logic & structure:
126
+ - Arrow code: >2 levels of nested if/for -> flatten with guard clauses
127
+ (early returns). Code reads top-to-bottom, no deep indent.
128
+ - Switch/if-else bloat: a switch or 5+ if/else branches -> Map/dispatch
129
+ (Record<State, fn>) or the Command pattern.
130
+ - Mixed abstraction (SLAP): a function mixing DB calls + string validation +
131
+ date formatting -> one level of abstraction per function; extract helpers.
132
+ - Primitive obsession: a primitive with business rules (email, userId, chainId)
133
+ passed as a bare string/number across functions -> a named type/value object.
134
+ - Imperative transforms: a `for` loop building an array when the language has
135
+ .map/.filter/.reduce -> use the declarative form; reserve `for` for cases
136
+ map/reduce cannot express.
137
+
125
138
  You do NOT need to run a tool for these — read the diff and apply the named fix.
126
139
  If none apply, say so in one line.
@@ -14,17 +14,20 @@
14
14
  # at the START of each turn's work, before edits pile up and dilute the
15
15
  # original intent. Works UNCONDITIONALLY - no transcript needed.
16
16
  #
17
- # 2. AUTO-CREATE / REGENERATE .scope.json: when the current <user_query>
18
- # differs from the contract on disk (no contract yet, OR _intent_hash
19
- # mismatch), the hook WRITES a scaffold to the REPO ROOT: intent locked
20
- # from the prompt, files as an EMPTY array (scope-gate-audit.ps1 fills it
21
- # mechanically as the agent edits - the agent never maintains files[] by
22
- # hand), acceptance as a TODO the agent sets. This is the user-requested
23
- # behavior: every new prompt -> a fresh .scope.json the agent works from.
24
- # Fixed vs the broken 0.4.4 build: never writes to $HOME (bails if no real
25
- # root resolves -> no ghost files), regenerates on prompt CHANGE not just
26
- # on absence.
27
- # 3. RE-INJECT on same-prompt turns: when the query is unchanged (contract
17
+ # 2. AUTO-CREATE .scope.json (UNCONDITIONAL on a real root): if no valid
18
+ # contract exists in the repo root, WRITE one now - intent locked from the
19
+ # query when available, otherwise `intent: <TODO>` for the agent to fill.
20
+ # Creation does NOT require transcript_path; only regeneration does. This
21
+ # was the 0.5.3 bug: creation was gated on $hasQuery, so when Cursor didn't
22
+ # surface the transcript on the first postToolUse fire, the scope never
23
+ # appeared and the agent had no contract to work from.
24
+ # 3. REGENERATE on prompt CHANGE: when the current <user_query> hash differs
25
+ # from the contract's _intent_hash, overwrite the scaffold with the new
26
+ # intent + empty files + TODO acceptance. Requires $hasQuery (you can only
27
+ # detect a change if you can read the request). Fixed vs the broken 0.4.4
28
+ # build: never writes to $HOME (bails if no real root resolves -> no
29
+ # ghost files).
30
+ # 4. RE-INJECT on same-prompt turns: when the query is unchanged (contract
28
31
  # already current), the hook re-injects the existing contract into the
29
32
  # feedback bus so it stays in the model's attentional focus each turn.
30
33
  #
@@ -56,6 +59,16 @@ $pendingDir = Get-HooksPendingDir
56
59
  $latch = Join-Path $pendingDir "intent-injected-$cid.flag"
57
60
  $hashFile = Join-Path $pendingDir "last-query-$cid.hash"
58
61
 
62
+ # Stale-latch defense: if a previous session died mid-turn without hitting
63
+ # stop (Cursor crash, force-quit), the latch can persist and silence this hook
64
+ # for the whole next session -> scope never gets created. If the latch is older
65
+ # than 2 hours, treat it as orphaned and clear it. Normal clears happen at
66
+ # every stop (final-review.ps1); this is the backstop for abnormal terminations.
67
+ if (Test-Path $latch) {
68
+ $age = (Get-Date) - (Get-Item $latch).LastWriteTime
69
+ if ($age.TotalHours -ge 2) { Remove-Item $latch -Force -ErrorAction SilentlyContinue }
70
+ }
71
+
59
72
  # Already injected this turn -> quiet. Latch cleared at every stop.
60
73
  if (Test-Path $latch) { exit 0 }
61
74
 
@@ -115,21 +128,24 @@ if (Test-Path -LiteralPath $scopePath) {
115
128
  } catch { $scopeExists = $false } # malformed JSON -> treat as missing
116
129
  }
117
130
 
118
- # --- auto-create / regenerate .scope.json (the 0.4.4 behavior, fixed) --------
119
- # The user wants: every NEW prompt -> a fresh .scope.json the agent works from.
120
- # So we WRITE the scaffold when (a) there is no valid contract, OR (b) the
121
- # contract on disk is stale (its _intent_hash != current query hash). Intent is
122
- # locked from the current <user_query>; files starts EMPTY (scope-gate-audit
123
- # auto-records edits into it); acceptance is a TODO the agent sets. Fixed vs 0.4.4:
124
- # - NEVER writes to $HOME (bail above if no real root) -> no ghost files.
125
- # - Regenerates on prompt CHANGE, not just on absence -> "each prompt, new file".
126
- # - Records _intent_hash so staleness is self-contained in the file.
131
+ # --- auto-create / regenerate .scope.json -----------------------------------
132
+ # CREATION does NOT require the query: if there's a root and no scope yet,
133
+ # scaffold it NOW with intent=<TODO> (the agent fills it from the chat it's
134
+ # already responding to). This was the 0.5.3 bug - creation was gated on
135
+ # $hasQuery, so when Cursor didn't surface transcript_path in the first
136
+ # postToolUse fire, the scope never got created. The agent never had a
137
+ # contract to work from.
138
+ # REGENERATION does require the query: we can only detect a prompt change if
139
+ # we can hash the current request. Without a query we leave an existing scope
140
+ # alone (re-inject it) rather than blank it.
127
141
  $regenerated = $false
128
- $shouldWrite = $hasQuery -and (-not $scopeExists -or $scopeStale)
129
- if ($shouldWrite) {
142
+ $shouldCreate = -not $scopeExists
143
+ $shouldRegen = $hasQuery -and $scopeExists -and $scopeStale
144
+ if ($shouldCreate -or $shouldRegen) {
130
145
  try {
146
+ $intentVal = if ($hasQuery) { $currentQuery } else { '<TODO: state the operational objective - what is strictly necessary>' }
131
147
  $scaffold = [ordered]@{
132
- intent = $currentQuery
148
+ intent = $intentVal
133
149
  files = @()
134
150
  acceptance = '<TODO: the one deterministic check that decides done>'
135
151
  allow_growth = $false
@@ -138,7 +154,7 @@ if ($shouldWrite) {
138
154
  }
139
155
  $json = $scaffold | ConvertTo-Json -Depth 5
140
156
  [System.IO.File]::WriteAllText($scopePath, $json, [System.Text.UTF8Encoding]::new($false))
141
- $scopeIntent = $currentQuery
157
+ $scopeIntent = $intentVal
142
158
  $scopeAcceptance = '<TODO: the one deterministic check that decides done>'
143
159
  $scopeFiles = '(auto-tracked - the scope hook records every file you edit)'
144
160
  $scopeExists = $true