@nitra/cursor 1.13.67 → 1.13.69
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.
|
@@ -165,6 +165,8 @@ PROMPT_HEADER=$(cat <<'EOF'
|
|
|
165
165
|
]
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
Принцип вибору операції: уникай дрібних дублів. Перш ніж обрати `rewrite`, звір тему драфта з clean-списком і з рештою драфтів цього батча. Якщо рішення по суті вже зафіксоване — у наявному clean-ADR або в драфті, який ти переписуєш через `rewrite`, — і драфт лише уточнює / доповнює / виправляє / продовжує його, обери `merge-into`, а не `rewrite`. `rewrite` (новий файл) виправданий лише для справді самостійного, нового рішення. Краще один повний наскрізний ADR, ніж кілька майже однакових файлів.
|
|
169
|
+
|
|
168
170
|
Правила:
|
|
169
171
|
|
|
170
172
|
1. `delete` — драфт тривіальний / повністю покритий іншим існуючим clean-ADR-ом / порожній. Поясни короткою причиною українською.
|
|
@@ -180,14 +182,17 @@ PROMPT_HEADER=$(cat <<'EOF'
|
|
|
180
182
|
- У `## More Information` перенеси файли, команди, публічні API, конфіги й transcript facts. Якщо нема — `Додаткової інформації в transcript не зафіксовано.`
|
|
181
183
|
- `slug` — kebab-case українською (наприклад `ланцюжок-запуску-abie`, `npm-publish-flow`). Без розширення `.md`. Літери малі, дозволено цифри, дефіс, кирилиця. Якщо тема технічна англійською (назва пакету, ключове слово) — лиши англійською без транслітерації.
|
|
182
184
|
|
|
183
|
-
3. `merge-into` —
|
|
185
|
+
3. `merge-into` — рішення драфта НЕ самостійне: воно лише уточнює, доповнює, виправляє або продовжує рішення, яке вже зафіксоване або (а) в clean-файлі зі списку нижче, або (б) у драфті цього ж батча, який ти переписуєш через `rewrite`. `target`:
|
|
186
|
+
- для (а) — точна назва clean-файлу зі списку (з `.md`);
|
|
187
|
+
- для (б) — `<slug>.md`, де `<slug>` дорівнює полю `slug` тієї `rewrite`-операції (timestamp-префікс скрипт додасть сам — не дописуй його).
|
|
188
|
+
`additions` — лише новий зміст, який варто дописати в кінець target-файлу під підзаголовком `## Update YYYY-MM-DD` (date з `captured` драфта). Якщо нічого нового додати — використовуй `delete`.
|
|
184
189
|
|
|
185
190
|
Жорсткі обмеження:
|
|
186
191
|
|
|
187
192
|
- Поверни валідний JSON, нічого крім нього. Жодних code-fence, жодних коментарів.
|
|
188
193
|
- Кожен файл з вхідного списку має зʼявитися у `operations` рівно один раз.
|
|
189
194
|
- Слаги не повторювати між операціями того самого батча. Якщо дві чернетки про одну тему — одна `rewrite`, інша `merge-into target: <slug>.md` з тим самим slug-ом.
|
|
190
|
-
-
|
|
195
|
+
- `target` у `merge-into` — це або файл зі списку clean-файлів, або `<slug>.md` rewrite-операції цього ж батча. Іншого target не вигадуй.
|
|
191
196
|
- Не вигадуй альтернативи, decision drivers, наслідки, людей або зовнішній контекст. Якщо даних бракує — явно напиши, що transcript цього не містить.
|
|
192
197
|
|
|
193
198
|
Вхідні драфти і clean-список — нижче.
|
|
@@ -278,7 +283,22 @@ resolve_unique_slug_path() {
|
|
|
278
283
|
APPLIED=0
|
|
279
284
|
SKIPPED=0
|
|
280
285
|
|
|
281
|
-
|
|
286
|
+
# Apply operations in two ordered groups — delete/rewrite first, merge-into
|
|
287
|
+
# last — so a merge-into can target a clean file that a rewrite of the same
|
|
288
|
+
# batch only just created. Looping over a file (not a pipe) keeps the loop in
|
|
289
|
+
# the main shell, so APPLIED/SKIPPED survive to the final summary line.
|
|
290
|
+
OPS_FILE="$TMP_DIR/ops.jsonl"
|
|
291
|
+
{
|
|
292
|
+
jq -c '.operations[] | select(.op != "merge-into")' "$RESPONSE_CLEAN_FILE"
|
|
293
|
+
jq -c '.operations[] | select(.op == "merge-into")' "$RESPONSE_CLEAN_FILE"
|
|
294
|
+
} > "$OPS_FILE"
|
|
295
|
+
|
|
296
|
+
# slug → created clean-file path: written by rewrite ops, read by merge-into
|
|
297
|
+
# ops (one tab-separated "slug<TAB>path" line per rewrite).
|
|
298
|
+
SLUG_MAP="$TMP_DIR/slug-map.txt"
|
|
299
|
+
: > "$SLUG_MAP"
|
|
300
|
+
|
|
301
|
+
while IFS= read -r op_json; do
|
|
282
302
|
OP=$(printf '%s' "$op_json" | jq -r '.op // empty')
|
|
283
303
|
FILE=$(printf '%s' "$op_json" | jq -r '.file // empty')
|
|
284
304
|
SRC_PATH="$ADR_DIR/$FILE"
|
|
@@ -349,6 +369,9 @@ jq -c '.operations[]' "$RESPONSE_CLEAN_FILE" | while IFS= read -r op_json; do
|
|
|
349
369
|
DEST_PATH=$(resolve_unique_slug_path "$DEST_SLUG")
|
|
350
370
|
printf '%s\n' "$CONTENT" > "$DEST_PATH"
|
|
351
371
|
rm -- "$SRC_PATH"
|
|
372
|
+
# Record bare slug → final path so a same-batch merge-into can target
|
|
373
|
+
# this freshly created file by `<slug>.md` despite the timestamp prefix.
|
|
374
|
+
printf '%s\t%s\n' "$SLUG" "$DEST_PATH" >> "$SLUG_MAP"
|
|
352
375
|
log "rewrite: $FILE → $(basename "$DEST_PATH")"
|
|
353
376
|
APPLIED=$(( APPLIED + 1 ))
|
|
354
377
|
;;
|
|
@@ -367,7 +390,32 @@ jq -c '.operations[]' "$RESPONSE_CLEAN_FILE" | while IFS= read -r op_json; do
|
|
|
367
390
|
continue
|
|
368
391
|
;;
|
|
369
392
|
esac
|
|
393
|
+
# Resolve the target clean file. The LLM gives a bare `<slug>.md`, but the
|
|
394
|
+
# real file usually carries a `YYYYMMDD-HHMMSS-` prefix. Try, in order:
|
|
395
|
+
# 1. exact name in docs/adr/,
|
|
396
|
+
# 2. a rewrite of this batch that produced that slug (SLUG_MAP),
|
|
397
|
+
# 3. a unique existing clean file whose name ends with `-<slug>.md`.
|
|
370
398
|
TARGET_PATH="$ADR_DIR/$TARGET"
|
|
399
|
+
if [ ! -f "$TARGET_PATH" ]; then
|
|
400
|
+
TSLUG="${TARGET%.md}"
|
|
401
|
+
MAPPED=$(awk -F'\t' -v s="$TSLUG" '$1 == s { print $2; exit }' "$SLUG_MAP")
|
|
402
|
+
if [ -z "$MAPPED" ]; then
|
|
403
|
+
SUFFIX_HITS=0
|
|
404
|
+
for cf in "$ADR_DIR"/*-"$TSLUG".md; do
|
|
405
|
+
[ -f "$cf" ] || continue
|
|
406
|
+
MAPPED="$cf"
|
|
407
|
+
SUFFIX_HITS=$(( SUFFIX_HITS + 1 ))
|
|
408
|
+
done
|
|
409
|
+
if [ "$SUFFIX_HITS" -gt 1 ]; then
|
|
410
|
+
log "skip merge-into: target '$TARGET' ambiguous ($SUFFIX_HITS matches)"
|
|
411
|
+
SKIPPED=$(( SKIPPED + 1 ))
|
|
412
|
+
continue
|
|
413
|
+
fi
|
|
414
|
+
fi
|
|
415
|
+
if [ -n "$MAPPED" ]; then
|
|
416
|
+
TARGET_PATH="$MAPPED"
|
|
417
|
+
fi
|
|
418
|
+
fi
|
|
371
419
|
if [ ! -f "$TARGET_PATH" ]; then
|
|
372
420
|
log "skip merge-into: target '$TARGET' missing"
|
|
373
421
|
SKIPPED=$(( SKIPPED + 1 ))
|
|
@@ -380,7 +428,7 @@ jq -c '.operations[]' "$RESPONSE_CLEAN_FILE" | while IFS= read -r op_json; do
|
|
|
380
428
|
fi
|
|
381
429
|
printf '\n%s\n' "$ADDITIONS" >> "$TARGET_PATH"
|
|
382
430
|
rm -- "$SRC_PATH"
|
|
383
|
-
log "merge-into: $FILE → $
|
|
431
|
+
log "merge-into: $FILE → $(basename "$TARGET_PATH")"
|
|
384
432
|
APPLIED=$(( APPLIED + 1 ))
|
|
385
433
|
;;
|
|
386
434
|
*)
|
|
@@ -388,6 +436,6 @@ jq -c '.operations[]' "$RESPONSE_CLEAN_FILE" | while IFS= read -r op_json; do
|
|
|
388
436
|
SKIPPED=$(( SKIPPED + 1 ))
|
|
389
437
|
;;
|
|
390
438
|
esac
|
|
391
|
-
done
|
|
439
|
+
done < "$OPS_FILE"
|
|
392
440
|
|
|
393
|
-
log "done"
|
|
441
|
+
log "done (applied $APPLIED, skipped $SKIPPED)"
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@
|
|
|
4
4
|
|
|
5
5
|
Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
|
|
6
6
|
|
|
7
|
+
## [1.13.69] - 2026-05-21
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **CLI `check` без аргументів** більше не парсить `AGENTS.md` — список правил для прогону будується з **`*.mdc` у `.cursor/rules/`** (той самий дисковий індекс, що для `AGENTS.md` / `CLAUDE.md`): `n-bun.mdc` → `check bun`, ручні `conftest.mdc` тощо — за наявності programmatic check у пакеті. Явний `check bun ga` без змін. Зачеплено: [discover-check-rules-from-cursor.mjs](scripts/utils/discover-check-rules-from-cursor.mjs), [n-cursor.js](bin/n-cursor.js), [fix/SKILL.md](skills/fix/SKILL.md).
|
|
12
|
+
|
|
13
|
+
## [1.13.68] - 2026-05-21
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- ADR-хук **`normalize-decisions.sh`**: нормалізація тепер активніше повторно використовує наявні ADR замість створення нових файлів. У промпт додано принцип вибору операції — перш ніж `rewrite` (новий файл), агент звіряє тему драфта з clean-списком і рештою драфтів батча; якщо рішення по суті вже зафіксоване і драфт лише уточнює/доповнює/виправляє його — обирає `merge-into`. Правило `merge-into` тепер явно дозволяє `target` двох видів: clean-файл зі списку або `<slug>.md` `rewrite`-операції цього ж батча; суперечливе обмеження «не вигадуй target поза clean-списком» узгоджено з цим. Зачеплено: [normalize-decisions.sh](.claude-template/hooks/normalize-decisions.sh).
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- ADR-хук **`normalize-decisions.sh`**: `merge-into` більше не падає в `skip … target missing`, коли драфт треба влити в clean-ADR, який створює `rewrite` того самого батча, або в наявний clean-ADR, на який LLM послався голим `<slug>.md` без timestamp-префікса. Операції тепер застосовуються двома впорядкованими групами (спершу `delete`/`rewrite`, потім `merge-into`), а `target` резолвиться за трьома кроками: точна назва → slug-мапа rewrite-ів цього батча → єдиний наявний clean-файл із суфіксом `-<slug>.md`. Цикл застосування переведено з pipe на читання з файлу — лічильники `applied`/`skipped` виживають і потрапляють у фінальний рядок логу `done (applied N, skipped M)`. Зачеплено: [normalize-decisions.sh](.claude-template/hooks/normalize-decisions.sh).
|
|
22
|
+
|
|
7
23
|
## [1.13.67] - 2026-05-21
|
|
8
24
|
|
|
9
25
|
### Changed
|
package/bin/n-cursor.js
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Використання:
|
|
7
7
|
* `npx \@nitra/cursor` — завантажити cursor-правила
|
|
8
|
-
* `npx \@nitra/cursor check` — перевірити
|
|
8
|
+
* `npx \@nitra/cursor check` — перевірити правила з `.cursor/rules/*.mdc`, для яких у пакеті є check/policy;
|
|
9
9
|
* якщо в корені вже є `.n-cursor.json`, спочатку зчитується конфіг і за потреби дописується `$schema`
|
|
10
|
-
* `npx \@nitra/cursor check bun` — перевірити лише вказані правила (ігнорує
|
|
10
|
+
* `npx \@nitra/cursor check bun` — перевірити лише вказані правила (ігнорує `.cursor/rules/`)
|
|
11
11
|
* `npx \@nitra/cursor rename-yaml-extensions` — k8s `*.yml` → `*.yaml`, `.github` `*.yaml` → `*.yml` (опції: `--dry-run`, `--root=…`; див. bin/rename-yaml-extensions.mjs)
|
|
12
12
|
* `npx \@nitra/cursor stop-hook` — точка входу Stop hook Claude Code (читає stdin, виходить 0 при `stop_hook_active`,
|
|
13
13
|
* інакше викликає `check`); прописується автоматично в `.claude/settings.json`
|
|
@@ -78,6 +78,7 @@ import {
|
|
|
78
78
|
} from '../scripts/auto-rules.mjs'
|
|
79
79
|
import { detectAutoSkills } from '../scripts/auto-skills.mjs'
|
|
80
80
|
import { runStopHookCli } from '../scripts/claude-stop-hook.mjs'
|
|
81
|
+
import { discoverCheckRulesFromCursorRules } from '../scripts/utils/discover-check-rules-from-cursor.mjs'
|
|
81
82
|
import { discoverCheckableRules } from '../scripts/utils/discover-checkable-rules.mjs'
|
|
82
83
|
import { ensureNitraCursorInRootDevDependencies } from '../scripts/ensure-nitra-cursor-dev-dependencies.mjs'
|
|
83
84
|
import { runLintDocker } from '../rules/docker/lint/lint.mjs'
|
|
@@ -978,57 +979,10 @@ function discoverCheckScripts() {
|
|
|
978
979
|
}
|
|
979
980
|
|
|
980
981
|
/**
|
|
981
|
-
*
|
|
982
|
-
* @param {string} mdcBasename наприклад n-bun.mdc або script.mdc
|
|
983
|
-
* @returns {string} id без суфікса .mdc та без префікса n- для керованих правил
|
|
984
|
-
*/
|
|
985
|
-
function mdcBasenameToCheckId(mdcBasename) {
|
|
986
|
-
const base = basename(mdcBasename)
|
|
987
|
-
const withoutExt = base.endsWith('.mdc') ? base.slice(0, -'.mdc'.length) : base
|
|
988
|
-
return withoutExt.startsWith(RULE_PREFIX) ? withoutExt.slice(RULE_PREFIX.length) : withoutExt
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
/**
|
|
992
|
-
* Зчитує AGENTS.md і повертає унікальні id перевірок у порядку згадування, лише ті що є в available
|
|
993
|
-
* @param {string[]} available імена з discoverCheckScripts()
|
|
994
|
-
* @returns {Promise<string[]>} унікальні id перевірок у порядку згадування в AGENTS.md
|
|
995
|
-
*/
|
|
996
|
-
async function discoverCheckRulesFromAgentsMd(available) {
|
|
997
|
-
const agentsPath = join(cwd(), AGENTS_FILE)
|
|
998
|
-
if (!existsSync(agentsPath)) {
|
|
999
|
-
throw new Error(
|
|
1000
|
-
`Немає ${AGENTS_FILE}. Запустіть \`npx ${PACKAGE_NAME}\` або вкажіть правила: \`npx ${PACKAGE_NAME} check bun ga\``
|
|
1001
|
-
)
|
|
1002
|
-
}
|
|
1003
|
-
const text = await readFile(agentsPath, 'utf8')
|
|
1004
|
-
const re = /\.cursor\/rules\/([^\s#`>]+\.mdc)/g
|
|
1005
|
-
const raw = []
|
|
1006
|
-
let m
|
|
1007
|
-
while ((m = re.exec(text)) !== null) {
|
|
1008
|
-
raw.push(m[1])
|
|
1009
|
-
}
|
|
1010
|
-
if (raw.length === 0) {
|
|
1011
|
-
throw new Error(
|
|
1012
|
-
`У ${AGENTS_FILE} немає посилань \`.cursor/rules/….mdc\`. Оновіть файл (\`npx ${PACKAGE_NAME}\`) або передайте правила явно.`
|
|
1013
|
-
)
|
|
1014
|
-
}
|
|
1015
|
-
const seen = new Set()
|
|
1016
|
-
const ordered = []
|
|
1017
|
-
for (const pathFragment of raw) {
|
|
1018
|
-
const id = mdcBasenameToCheckId(pathFragment)
|
|
1019
|
-
if (available.includes(id) && !seen.has(id)) {
|
|
1020
|
-
seen.add(id)
|
|
1021
|
-
ordered.push(id)
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
return ordered
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
/**
|
|
1028
|
-
* Запускає перевірки: без аргументів — за списком у AGENTS.md; з аргументами — лише вказані правила.
|
|
982
|
+
* Запускає перевірки: без аргументів — за `*.mdc` у `.cursor/rules/`; з аргументами — лише вказані правила.
|
|
1029
983
|
* Делегує оркестрацію concern-ів (JS-checks + policy через `runConftestBatch`) у `runRule`;
|
|
1030
984
|
* сам `runChecks` відповідає лише за фільтр id, агрегацію exit-кодів і shared walk-cache на прогон.
|
|
1031
|
-
* @param {string[]} requestedRules імена правил; порожній масив — брати з
|
|
985
|
+
* @param {string[]} requestedRules імена правил; порожній масив — брати з `.cursor/rules/*.mdc`
|
|
1032
986
|
* @returns {Promise<void>}
|
|
1033
987
|
*/
|
|
1034
988
|
async function runChecks(requestedRules) {
|
|
@@ -1054,10 +1008,16 @@ async function runChecks(requestedRules) {
|
|
|
1054
1008
|
if (requestedRules.length > 0) {
|
|
1055
1009
|
idsToCheck = requestedRules
|
|
1056
1010
|
} else {
|
|
1057
|
-
|
|
1011
|
+
const mdcFiles = await listProjectRulesMdcFiles()
|
|
1012
|
+
if (mdcFiles.length === 0) {
|
|
1013
|
+
throw new Error(
|
|
1014
|
+
`Немає файлів *.mdc у ${RULES_DIR}/. Запустіть \`npx ${PACKAGE_NAME}\` або вкажіть правила: \`npx ${PACKAGE_NAME} check bun ga\``
|
|
1015
|
+
)
|
|
1016
|
+
}
|
|
1017
|
+
idsToCheck = discoverCheckRulesFromCursorRules(available, mdcFiles)
|
|
1058
1018
|
if (idsToCheck.length === 0) {
|
|
1059
1019
|
console.log(
|
|
1060
|
-
`\n🔍 ${PACKAGE_NAME} check — у ${
|
|
1020
|
+
`\n🔍 ${PACKAGE_NAME} check — у ${RULES_DIR}/ немає правил з programmatic перевіркою ` +
|
|
1061
1021
|
`(відповідного check-*.mjs або policy/<name>/target.json у пакеті). Нічого не запущено.\n`
|
|
1062
1022
|
)
|
|
1063
1023
|
return
|
package/package.json
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Визначає список id правил для `npx @nitra/cursor check` без аргументів:
|
|
3
|
+
* зчитує базові імена `*.mdc` у `.cursor/rules/` і залишає лише ті id,
|
|
4
|
+
* для яких у пакеті є programmatic перевірка (JS-концерн або policy з target.json).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Префікс керованих правил пакета у `.cursor/rules/`. */
|
|
8
|
+
export const MANAGED_RULE_FILE_PREFIX = 'n-'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Перетворює базове ім'я `.mdc` у id правила для `check <id>`.
|
|
12
|
+
* @param {string} mdcBasename наприклад `n-bun.mdc` або `my-rule.mdc`
|
|
13
|
+
* @returns {string} id без `.mdc`; для `n-*` — без префікса `n-`
|
|
14
|
+
*/
|
|
15
|
+
export function mdcBasenameToCheckId(mdcBasename) {
|
|
16
|
+
const base = mdcBasename.includes('/') ? mdcBasename.slice(mdcBasename.lastIndexOf('/') + 1) : mdcBasename
|
|
17
|
+
const withoutExt = base.endsWith('.mdc') ? base.slice(0, -'.mdc'.length) : base
|
|
18
|
+
return withoutExt.startsWith(MANAGED_RULE_FILE_PREFIX)
|
|
19
|
+
? withoutExt.slice(MANAGED_RULE_FILE_PREFIX.length)
|
|
20
|
+
: withoutExt
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Будує впорядкований список id перевірок за файлами правил на диску.
|
|
25
|
+
* @param {string[]} available id з `discoverCheckableRules` (алфавітний порядок пакета)
|
|
26
|
+
* @param {string[]} mdcBasenames відсортовані імена `*.mdc` з `.cursor/rules/`
|
|
27
|
+
* @returns {string[]} унікальні id у порядку `mdcBasenames`, лише присутні в `available`
|
|
28
|
+
*/
|
|
29
|
+
export function discoverCheckRulesFromCursorRules(available, mdcBasenames) {
|
|
30
|
+
const seen = new Set()
|
|
31
|
+
const ordered = []
|
|
32
|
+
for (const basename of mdcBasenames) {
|
|
33
|
+
const id = mdcBasenameToCheckId(basename)
|
|
34
|
+
if (available.includes(id) && !seen.has(id)) {
|
|
35
|
+
seen.add(id)
|
|
36
|
+
ordered.push(id)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return ordered
|
|
40
|
+
}
|
package/skills/fix/SKILL.md
CHANGED
|
@@ -12,7 +12,7 @@ description: >-
|
|
|
12
12
|
|
|
13
13
|
## Workflow
|
|
14
14
|
|
|
15
|
-
1. **Діагностика** — запусти перевірку (за замовчуванням лише правила з
|
|
15
|
+
1. **Діагностика** — запусти перевірку (за замовчуванням лише правила з `.cursor/rules/*.mdc`, для яких у пакеті є programmatic check; повний набір — явні аргументи: `npx @nitra/cursor check bun ga …`):
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
npx @nitra/cursor check
|