@nitra/cursor 12.15.0 → 12.16.0

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.
Files changed (77) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/bin/n-cursor.js +2 -11
  3. package/lib/docs/index.md +9 -6
  4. package/lib/docs/pi-agent-fix.md +28 -0
  5. package/lib/docs/pi-agent-skill.md +36 -0
  6. package/lib/docs/pi-model-tiers.md +46 -0
  7. package/lib/docs/pi-one-shot.md +34 -0
  8. package/lib/docs/pi-telemetry-store.md +33 -0
  9. package/lib/docs/pi-trace.md +27 -0
  10. package/lib/docs/pi-write-guard.md +32 -0
  11. package/lib/pi-agent-fix.mjs +253 -0
  12. package/lib/pi-agent-skill.mjs +181 -0
  13. package/lib/pi-model-tiers.mjs +109 -0
  14. package/lib/pi-one-shot.mjs +129 -0
  15. package/lib/pi-telemetry-store.mjs +0 -0
  16. package/lib/pi-trace.mjs +40 -0
  17. package/lib/pi-write-guard.mjs +147 -0
  18. package/package.json +5 -1
  19. package/rules/bun/docs/main.md +7 -6
  20. package/rules/doc-files/js/docgen-files-batch.mjs +20 -5
  21. package/rules/doc-files/js/docgen-gen.mjs +42 -25
  22. package/rules/doc-files/js/docgen-judge-measure.mjs +16 -13
  23. package/rules/doc-files/js/docgen-judge.mjs +11 -9
  24. package/rules/doc-files/js/docs/docgen-files-batch.md +3 -20
  25. package/rules/doc-files/js/docs/docgen-gen.md +3 -20
  26. package/rules/doc-files/js/docs/docgen-judge-measure.md +3 -18
  27. package/rules/doc-files/js/docs/docgen-judge.md +3 -22
  28. package/rules/npm-module/js/docs/skill_meta.md +22 -15
  29. package/rules/npm-module/js/skill_meta.mjs +5 -1
  30. package/rules/python/docs/main.md +11 -11
  31. package/rules/rust/docs/main.md +5 -5
  32. package/rules/text/js/cspell-fix.mjs +15 -16
  33. package/rules/text/js/docs/cspell-fix.md +16 -9
  34. package/rules/text/main.mjs +4 -4
  35. package/schemas/skill-meta.json +8 -0
  36. package/scripts/docs/skills-cli.md +21 -25
  37. package/scripts/docs/update-blue-oak.md +8 -8
  38. package/scripts/lib/adr/docs/normalize-cli.md +3 -20
  39. package/scripts/lib/adr/docs/normalize-pipeline.md +3 -33
  40. package/scripts/lib/adr/normalize-cli.mjs +2 -2
  41. package/scripts/lib/adr/normalize-pipeline.mjs +78 -44
  42. package/scripts/lib/docs/discover-checkable-rules.md +6 -6
  43. package/scripts/lib/docs/inline-template-links.md +8 -6
  44. package/scripts/lib/docs/list-project-rules-mdc.md +5 -3
  45. package/scripts/lib/docs/root-notice.md +13 -16
  46. package/scripts/lib/docs/run-lint.md +10 -8
  47. package/scripts/lib/docs/skill-meta.md +29 -10
  48. package/scripts/lib/fix/docs/discover-t0-patterns.md +10 -13
  49. package/scripts/lib/fix/docs/escalation-log.md +10 -9
  50. package/scripts/lib/fix/docs/index.md +0 -1
  51. package/scripts/lib/fix/docs/orchestrator.md +15 -13
  52. package/scripts/lib/fix/escalation-log.mjs +1 -1
  53. package/scripts/lib/fix/orchestrator.mjs +67 -32
  54. package/scripts/lib/run-lint.mjs +2 -10
  55. package/scripts/lib/skill-meta.mjs +22 -0
  56. package/scripts/skills-cli.mjs +52 -14
  57. package/scripts/utils/ast-extract.mjs +105 -0
  58. package/scripts/utils/docs/ast-extract.md +30 -0
  59. package/scripts/utils/docs/walkDir.md +17 -20
  60. package/lib/docs/llm.md +0 -33
  61. package/lib/docs/models.md +0 -48
  62. package/lib/docs/omlx-trace.md +0 -49
  63. package/lib/docs/omlx.md +0 -41
  64. package/lib/llm.mjs +0 -215
  65. package/lib/models.mjs +0 -75
  66. package/lib/omlx-trace.mjs +0 -158
  67. package/lib/omlx.mjs +0 -220
  68. package/scripts/lib/fix/analyze-escalation.mjs +0 -353
  69. package/scripts/lib/fix/docs/analyze-escalation.md +0 -44
  70. package/scripts/lib/fix/docs/llm-fix-apply.md +0 -31
  71. package/scripts/lib/fix/docs/llm-lint-fix.md +0 -31
  72. package/scripts/lib/fix/docs/llm-worker.md +0 -33
  73. package/scripts/lib/fix/docs/verbose-block.md +0 -27
  74. package/scripts/lib/fix/llm-fix-apply.mjs +0 -113
  75. package/scripts/lib/fix/llm-lint-fix.mjs +0 -82
  76. package/scripts/lib/fix/llm-worker.mjs +0 -332
  77. package/scripts/lib/fix/verbose-block.mjs +0 -82
@@ -3,29 +3,31 @@ type: JS Module
3
3
  title: orchestrator.mjs
4
4
  resource: npm/scripts/lib/fix/orchestrator.mjs
5
5
  docgen:
6
- crc: c34d0630
6
+ crc: 49146418
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  ---
10
10
 
11
- Модуль відповідає за управління процесом вирішення порушень. Він будує послідовність тирів ескалації за допомогою `buildLadder`. Функція `parseOrchestratorArgs` визначає бюджет LLM та фільтр правил. Далі, `runOrchestratorCli` виконує процес виправлення правил, послідовно застосовуючи `escalateRule` по тирах до досягнення першого успішного вирішення.
11
+ ## Огляд
12
+
13
+ Цей модуль керує процесом виправлення порушень. Він виконує функцію `parseOrchestratorArgs` для визначення обсягу роботи з хмарними моделями та фільтрами правил. Модуль забезпечує класифікацію помилок через `classifyFixError` та ескалацію правил за допомогою `escalateRule`. Крім того, він створює структуру для застосування правил за допомогою `buildLadder` і запускає основний клієнтський процес за допомогою `runOrchestratorCli`.
12
14
 
13
15
  ## Поведінка
14
16
 
15
- buildLadder будує послідовність тирів ескалації для вирішення порушень.
16
- escalateRule проходить по послідовності тирів, намагаючись вирішити одне правило до першого успішного перевірки.
17
- parseOrchestratorArgs парсить аргументи командного рядка для визначення максимального бюджету LLM та фільтра правил.
18
- runOrchestratorCli виконує повний процес виправлення правил, використовуючи послідовність тирів та бюджет LLM.
17
+ Поведінка:
18
+ classifyFixError визначає тип помилки, виниклої при спробі виправлення.
19
+ buildLadder конструює послідовність моделей для багаторівневої спроби усунення порушень.
20
+ escalateRule ітерує по моделях драбини, намагаючись виправити одне порушення, і звітує про результат.
21
+ parseOrchestratorArgs виділяє максимальну кількість викликів хмарної моделі та фільтр правил із вхідних аргументів.
22
+ runOrchestratorCli виконує повний цикл фіксації порушень, включаючи початкову перевірку, детерміноване виправлення та ітерації з LLM.
19
23
 
20
24
  ## Публічний API
21
25
 
22
- buildLadderСтворює послідовність моделей для ескалації, виходячи з доступних рівнів. Недоступні рівні ігноруються.
23
-
24
- escalateRule — Виконує один етап перевірки за драбиною ескалації. Для кожного кроку викликається модель, відбувається повторна перевірка правила, і результат фіксується в лозі. Процес може зупинитися достроково при певних умовах або після вичерпання ліміту.
25
-
26
- parseOrchestratorArgsВитягує максимальне значення для середнього бюджету з аргументів командного рядка та збирає список фільтрів правил.
27
-
28
- runOrchestratorCli — Запускає основний процес оркестрації, обробляючи аргументи та керуючи виконанням правил.
26
+ classifyFixErrorВизначає тип помилки, яку виявив pi-agent-fix: системна, транспортна чи якісна.
27
+ buildLadder — Створює послідовність моделей для усунення помилок, починаючи з найменш ресурсомістких локальних спроб і рухаючись до хмарних моделей, враховуючи попередній досвід.
28
+ escalateRule — Застосовує одне правило за визначеною драбиною, повторюючи перевірку після кожного кроку та фіксуючи результати у лозі.
29
+ parseOrchestratorArgs — Зчитує аргументи командного рядка, зокрема максимальний бюджет середнього рівня, та витягує фільтр правил.
30
+ runOrchestratorCliВиконує основну логіку оркестрації, керуючи циклом виявлення, класифікації та ескалації виправлень.
29
31
 
30
32
  ## Гарантії поведінки
31
33
 
@@ -5,7 +5,7 @@
5
5
  * правило стало зеленим після цього рунга (`recheckOk` = «чи допомогло»),
6
6
  * залишковий violation і `diagnosis` (само-аналіз моделі «чому не вдалося»).
7
7
  *
8
- * Це доповнення до always-on wire-trace (`lib/omlx-trace.mjs`): trace знає
8
+ * Це доповнення до always-on wire-trace (`lib/pi-trace.mjs`): trace знає
9
9
  * `messages`/`reasoning`/`usage` кожного виклику, але **не** знає результату
10
10
  * re-check — а саме «чи допомогло» й потрібне для пост-аналізу драбини. Join із
11
11
  * trace — за полем `caller` (`fix:<rule>:<rung>`), яке цей модуль і формує.
@@ -4,10 +4,9 @@ import { env } from 'node:process'
4
4
  import { runConformanceCheck } from './run-conformance-check.mjs'
5
5
  import { runT0AutoCli } from './t0.mjs'
6
6
  import { logEscalation } from './escalation-log.mjs'
7
- import { runLlmWorker } from './llm-worker.mjs'
8
- import { printVerboseBlock } from './verbose-block.mjs'
9
- import { classifyOmlxError } from '../../../lib/llm.mjs'
10
- import { CLOUD_AVG, CLOUD_MIN, LOCAL_MIN } from '../../../lib/models.mjs'
7
+ import { runPiAgentFix } from '../../../lib/pi-agent-fix.mjs'
8
+ import { recordFixTelemetry } from '../../../lib/pi-telemetry-store.mjs'
9
+ import { CLOUD_AVG, CLOUD_MIN, LOCAL_MIN } from '../../../lib/pi-model-tiers.mjs'
11
10
 
12
11
  /**
13
12
  * Дефолтний кеп на виклики хмарної avg-моделі за один прогін (щоб драбина на N
@@ -16,22 +15,41 @@ import { CLOUD_AVG, CLOUD_MIN, LOCAL_MIN } from '../../../lib/models.mjs'
16
15
  const DEFAULT_MAX_AVG = 3
17
16
 
18
17
  /**
19
- * Timeout одного LLM-виклику за тиром. Локальні рунги **fail-fast**: не палити
20
- * стіну 120s на повільному 4b (curl exit 28) швидше абортнути й ескалувати.
21
- * Хмарні повний. Перевизначення: `N_LOCAL_FIX_TIMEOUT_MS` / `N_CLOUD_FIX_TIMEOUT_MS`.
18
+ * Timeout усієї агентної сесії за тиром. Агентний фікс багатоходовий
19
+ * (read→edit→self_check, кожен turn = окремий API-раунд), тож на правилах із багатьма
20
+ * порушеннями навіть швидкий cloud не вкладається у пару хвилин (вимір: gpt-5.4-mini
21
+ * timeout на test/doc-files/npm-module за 120s). Тому cloud — теж 5 хв, як local;
22
+ * основний backstop усе одно turn-ceiling (~50) у pi-agent-fix.
23
+ * Перевизначення: `N_LOCAL_FIX_TIMEOUT_MS` / `N_CLOUD_FIX_TIMEOUT_MS`.
22
24
  */
23
- const LOCAL_TIMEOUT_MS = Number(env.N_LOCAL_FIX_TIMEOUT_MS) || 45_000
24
- const CLOUD_TIMEOUT_MS = Number(env.N_CLOUD_FIX_TIMEOUT_MS) || 120_000
25
+ const LOCAL_TIMEOUT_MS = Number(env.N_LOCAL_FIX_TIMEOUT_MS) || 300_000
26
+ const CLOUD_TIMEOUT_MS = Number(env.N_CLOUD_FIX_TIMEOUT_MS) || 300_000
25
27
 
26
- /** Маркер дружнього повідомлення про відсутній API-ключ `llm-worker.callModel`). */
27
- const NO_KEY_RE = /немає ключа|api key/i
28
+ /** Реальний транспорт-збій провайдера (мережа/сокет) НЕ наш агентний backstop-timeout. */
29
+ const TRANSPORT_RE = /etimedout|timed out|econnrefused|connection refused/i
28
30
 
29
31
  /**
30
- * Хмарний транспорт (pi) упав на рівні процесу: таймаут/spawn-помилка. Стіна часу
31
- * однакова для всіх cloud-рунгів (та сама pi-транспортна стіна), а cloud-avg — інша
32
- * модель, не більший timeout. Ескалація на неї лише спалить avg-бюджет → обрив.
32
+ * Systemic повтор тієї ж моделі марний: нема git, fail-closed guard, відсутня модель,
33
+ * registry/session/auth. Quality модель видала поганий фікс (retry/escalate може допомогти).
33
34
  */
34
- const CLOUD_TRANSPORT_RE = /etimedout|timed out|pi error/i
35
+ const SYSTEMIC_RE = /не git-репо|fail-closed|write-guard|модель не знайдена|registry:|session:|немає ключа|api key/i
36
+
37
+ /**
38
+ * Класифікує помилку pi-agent-fix: systemic | transport | quality (замінює
39
+ * `classifyOmlxError` після pi-міграції — помилки приходять як винятки з `session.prompt`).
40
+ * @param {string|null|undefined} error повідомлення помилки
41
+ * @returns {'systemic'|'transport'|'quality'|null} клас
42
+ */
43
+ export function classifyFixError(error) {
44
+ if (!error) return null
45
+ // Наш агентний backstop-timeout — НЕ транспорт-збій провайдера: модель працювала, просто
46
+ // не встигла. Тому quality (а не transport-break) → драбина падає на наступний (сильніший)
47
+ // rung, замість обриву; для cloud-min це означає шанс cloud-avg.
48
+ if (/^fix timeout /i.test(error)) return 'quality'
49
+ if (SYSTEMIC_RE.test(error)) return 'systemic'
50
+ if (TRANSPORT_RE.test(error)) return 'transport'
51
+ return 'quality'
52
+ }
35
53
 
36
54
  /**
37
55
  * Будує драбину ескалації за наявними тирами (спека 2026-06-19-fix-escalation-cascade):
@@ -72,9 +90,9 @@ export function buildLadder({ localMin, cloudMin, cloudAvg }) {
72
90
  */
73
91
  function decideAfterFailure(rung, error) {
74
92
  if (!error) return null
75
- if (NO_KEY_RE.test(error)) return 'break'
76
- if (rung.local && classifyOmlxError(error) === 'systemic') return 'skip-model'
77
- if (!rung.local && CLOUD_TRANSPORT_RE.test(error)) return 'break'
93
+ const kind = classifyFixError(error)
94
+ if (kind === 'systemic') return rung.local ? 'skip-model' : 'break'
95
+ if (!rung.local && kind === 'transport') return 'break'
78
96
  return null
79
97
  }
80
98
 
@@ -87,7 +105,7 @@ function decideAfterFailure(rung, error) {
87
105
  * @param {string} cwd корінь проєкту
88
106
  * @param {{
89
107
  * ladder: Array<{tier:string,model:string,feedback:boolean,local:boolean,isAvg:boolean}>,
90
- * worker: { runLlmWorker: (ruleId: string, violation: string, cwd: string, opts: object) => object },
108
+ * worker: { runFix: (ruleId: string, violation: string, cwd: string, opts: object) => Promise<object> },
91
109
  * check: (rules: string[], cwd: string) => Promise<{rules: Array<{ruleId:string,ok:boolean,output:string}>}>,
92
110
  * avgBudget: number,
93
111
  * clock?: () => number,
@@ -125,42 +143,59 @@ export async function escalateRule(rule, cwd, deps) {
125
143
  }
126
144
 
127
145
  const startedAt = clock()
128
- const res = worker.runLlmWorker(rule.ruleId, currentViolation, cwd, {
146
+ // self_check (advisory §4+5) — той самий verdict-helper, що й зовнішній re-check.
147
+ const selfCheck = async () => {
148
+ const r = await check([rule.ruleId], cwd)
149
+ return { ok: r.rules.every(x => x.ok), output: r.rules.find(x => !x.ok)?.output ?? 'ok' }
150
+ }
151
+ const res = await worker.runFix(rule.ruleId, currentViolation, cwd, {
129
152
  model: rung.model,
153
+ tier: rung.tier,
130
154
  feedback: rung.feedback ? feedback : null,
131
155
  caller: `fix:${rule.ruleId}:${rung.tier}`,
132
- timeoutMs: rung.timeoutMs
156
+ timeoutMs: rung.timeoutMs,
157
+ deps: { selfCheck }
133
158
  })
134
159
  if (rung.isAvg) avgUsed++
135
160
 
161
+ // Зовнішній canonical re-check = джерело правди (§4+5).
136
162
  const recheck = await check([rule.ruleId], cwd)
137
163
  const recheckOk = recheck.rules.every(r => r.ok)
138
164
  const remaining = recheckOk ? '' : (recheck.rules.find(r => !r.ok)?.output ?? '')
165
+
166
+ // Distillation-стор §13: persist кожну спробу (повні edits + verdict).
167
+ if (res.telemetry) {
168
+ recordFixTelemetry({
169
+ ...res.telemetry,
170
+ violationSignature: currentViolation,
171
+ recheck: { external: recheckOk ? 'pass' : 'fail' },
172
+ escalated: !recheckOk
173
+ })
174
+ }
175
+
139
176
  record({
140
177
  ...common,
141
- callOk: res.ok,
178
+ callOk: !res.error,
142
179
  callError: res.error ?? null,
143
180
  recheckOk,
144
181
  remainingViolation: remaining,
145
- diagnosis: res.diagnosis ?? null,
182
+ diagnosis: res.telemetry ? `turns=${res.telemetry.turnCount} tools=${res.telemetry.toolCallCount}` : null,
146
183
  ms: clock() - startedAt
147
184
  })
148
185
 
149
186
  if (recheckOk) {
150
187
  log(` ✅ ${rung.tier} (${rung.model || 'pi'}): ${rule.ruleId}`)
151
- } else {
152
- const hint = res.error ? ` ❌ ${res.error.slice(0, 120)}` : ' ❌ досі порушено'
153
- log(` ⚡ ${rung.tier} (${rung.model || 'pi'}): ${rule.ruleId}${hint}`)
188
+ return { resolved: true, avgUsed }
154
189
  }
155
190
 
156
- if (env.N_CURSOR_FIX_VERBOSE !== 'off' && res.promptSummary) {
157
- printVerboseBlock(rule.ruleId, res.promptSummary, res.reasoning ?? null, res.reasoningSource ?? null)
158
- }
191
+ const hint = res.error ? ` ${res.error.slice(0, 120)}` : ' ❌ досі порушено'
192
+ log(` ⚡ ${rung.tier} (${rung.model || 'pi'}): ${rule.ruleId}${hint}`)
159
193
 
160
- if (recheckOk) return { resolved: true, avgUsed }
194
+ // Recheck провалився clean-slate per rung: відкотити правки цього рунга (§12).
195
+ res.rollback?.()
161
196
 
162
197
  // Feedback для наступного рунга + оновлений violation.
163
- feedback = { previousModel: rung.model, previousChanges: res.changes ?? [], previousError: res.error ?? null }
198
+ feedback = { previousModel: rung.model, previousError: res.error ?? null }
164
199
  currentViolation = remaining || currentViolation
165
200
 
166
201
  const action = decideAfterFailure(rung, res.error)
@@ -210,7 +245,7 @@ async function runT0Step(cwd, ruleFilter, failed) {
210
245
  * @returns {Promise<number>} 0 = all clean, 1 = unresolved
211
246
  */
212
247
  export async function runOrchestratorCli(args, cwd) {
213
- const worker = { runLlmWorker }
248
+ const worker = { runFix: runPiAgentFix }
214
249
  const { maxAvg, ruleFilter } = parseOrchestratorArgs(args)
215
250
  const ladder = buildLadder({ localMin: LOCAL_MIN, cloudMin: CLOUD_MIN, cloudAvg: CLOUD_AVG })
216
251
 
@@ -166,22 +166,14 @@ async function runPerFileRules(ids, ctx) {
166
166
  }
167
167
 
168
168
  /**
169
- * Конформність-фаза `--full` (поглинула `fix`): escalation-аналітику обрамляє зсувом логу
170
- * (записи саме цього прогону), у fix-режимі по конформності викликає аналіз.
169
+ * Конформність-фаза `--full`.
171
170
  * @param {string} cwd корінь
172
171
  * @param {boolean} readOnly лише детект
173
172
  * @param {(s: string) => void} log логер
174
173
  * @returns {Promise<number>} код конформності
175
174
  */
176
175
  async function runFullConformancePhase(cwd, readOnly, log) {
177
- const { escalationLogSize, maybeAnalyzeEscalation, reportRunStats } = await import('./fix/analyze-escalation.mjs')
178
- const escOffset = readOnly ? 0 : escalationLogSize()
179
- const conformanceCode = await runConformance(cwd, readOnly, log)
180
- if (!readOnly) {
181
- reportRunStats(escOffset, log) // резюме викликів моделей (локальна / cloud-min / cloud-avg)
182
- maybeAnalyzeEscalation(cwd, escOffset, log)
183
- }
184
- return conformanceCode
176
+ return runConformance(cwd, readOnly, log)
185
177
  }
186
178
 
187
179
  /**
@@ -19,6 +19,15 @@ import { join } from 'node:path'
19
19
  /** Літерал безумовної автоактивації (українською, як у `auto-skills.mjs`). */
20
20
  export const SKILL_ALWAYS = 'завжди'
21
21
 
22
+ /** Допустимі тири моделі для агентного виконання скіла (`pi`-runner). */
23
+ export const SKILL_TIERS = ['min', 'avg', 'max']
24
+
25
+ /**
26
+ * Дефолтна тира за відсутності `main.json.tier`: скіли відкриті й агентні, слабка
27
+ * локальна модель іде в мета-рамблінг, тож безпечніше дефолтити в найсильніший тир.
28
+ */
29
+ export const DEFAULT_SKILL_TIER = 'max'
30
+
22
31
  /**
23
32
  * @typedef {{ always: true } | { rules: string[] }} SkillAutoSpec
24
33
  */
@@ -50,6 +59,19 @@ export function skillRequiresRoot(meta) {
50
59
  return meta?.worktree === true || meta?.requireRoot === true
51
60
  }
52
61
 
62
+ /**
63
+ * Тира моделі для агентного виконання скіла (`pi`-runner). Повертає `main.json.tier`,
64
+ * якщо це валідний тир, інакше — `DEFAULT_SKILL_TIER` (`max`).
65
+ * @param {Record<string, unknown> | null} meta розпарсений `main.json` (або null)
66
+ * @returns {'min'|'avg'|'max'} тира
67
+ */
68
+ export function skillTier(meta) {
69
+ const tier = meta?.tier
70
+ return typeof tier === 'string' && SKILL_TIERS.includes(tier)
71
+ ? /** @type {'min'|'avg'|'max'} */ (tier)
72
+ : DEFAULT_SKILL_TIER
73
+ }
74
+
53
75
  /**
54
76
  * Читає й парсить `main.json` одного скіла.
55
77
  * @param {string} skillDir абсолютний шлях до каталогу скіла
@@ -3,14 +3,16 @@
3
3
  *
4
4
  * Скіли читаються з `npm/skills/<id>/SKILL.md` установленого пакета (або кешу `npx`).
5
5
  * Промпт збирає інструкцію скілу + контекст поточного CWD (`package.json`, `tsconfig.json`,
6
- * `.n-cursor.json`) — далі stdout або делегування в `cursor-agent` / `claude`.
6
+ * `.n-cursor.json`) — далі stdout або виконання через вбудований pi-агент
7
+ * (рекомендовано), чи deprecated-делегування в `cursor-agent` / `claude`.
7
8
  *
8
9
  * Підтримувані формати:
9
10
  * `npx \@nitra/cursor skill list`
10
11
  * `npx \@nitra/cursor skill taze`
11
- * `npx \@nitra/cursor skill cursor taze`
12
- * `npx \@nitra/cursor skill cursor taze "онови залежності"`
13
- * `npx \@nitra/cursor skill claude taze` — те саме через Claude Code CLI
12
+ * `npx \@nitra/cursor skill pi taze` — виконати через вбудований pi-агент (рекомендовано)
13
+ * `npx \@nitra/cursor skill pi taze "онови залежності"`
14
+ * `npx \@nitra/cursor skill cursor taze` — deprecated: зовнішній Cursor CLI
15
+ * `npx \@nitra/cursor skill claude taze` — deprecated: зовнішній Claude Code CLI
14
16
  */
15
17
 
16
18
  import { spawnSync } from 'node:child_process'
@@ -19,14 +21,18 @@ import { dirname, join } from 'node:path'
19
21
  import { cwd } from 'node:process'
20
22
  import { fileURLToPath } from 'node:url'
21
23
 
22
- const RUNNERS = new Set(['cursor', 'claude'])
24
+ import { readSkillMetaRaw, skillTier } from './lib/skill-meta.mjs'
25
+
26
+ /** Виконавці скіла. `pi` — вбудований (рекомендований); `cursor`/`claude` — deprecated зовнішні CLI. */
27
+ const RUNNERS = new Set(['pi', 'cursor', 'claude'])
23
28
 
24
29
  const USAGE_LINES = [
25
30
  'Usage:',
26
31
  ' npx @nitra/cursor skill list',
27
32
  ' npx @nitra/cursor skill <skill-id> ["task"]',
28
- ' npx @nitra/cursor skill cursor <skill-id> ["task"]',
29
- ' npx @nitra/cursor skill claude <skill-id> ["task"]',
33
+ ' npx @nitra/cursor skill pi <skill-id> ["task"] # вбудований pi-агент (рекомендовано)',
34
+ ' npx @nitra/cursor skill cursor <skill-id> ["task"] # deprecated',
35
+ ' npx @nitra/cursor skill claude <skill-id> ["task"] # deprecated',
30
36
  '',
31
37
  'Skill id: каталог у пакеті (lint, taze, …) або з префіксом n- (n-lint → lint).'
32
38
  ]
@@ -124,15 +130,43 @@ export function buildSkillPrompt(skillsRoot, rawSkillName, task, projectDir = cw
124
130
  }
125
131
 
126
132
  /**
133
+ * Виконує скіл через вбудований pi-агент (рекомендований шлях). Модель — з тиру скіла
134
+ * (`main.json.tier`, дефолт `max`); `cwd` = каталог виклику (worktree, за потреби,
135
+ * створює сам скіл за SKILL.md-preflight). Pi вантажиться lazy.
136
+ * @param {string} prompt готовий `buildSkillPrompt`
137
+ * @param {string} rawSkillName ім'я скілу з CLI (можливо з префіксом n-)
138
+ * @param {string} skillsRoot абсолютний шлях до `skills/` пакета
139
+ * @param {string} projectDir робочий каталог сесії (= каталог виклику)
140
+ * @param {(line: string) => void} logError вивід помилок
141
+ * @param {{ runPiAgentSkill?: Function }} deps інжекти для тестів
142
+ * @returns {Promise<number>} exit code (0 — ok)
143
+ */
144
+ async function runPiRunner(prompt, rawSkillName, skillsRoot, projectDir, logError, deps = {}) {
145
+ const skillId = normalizeSkillId(rawSkillName)
146
+ const tier = skillTier(readSkillMetaRaw(join(skillsRoot, skillId)))
147
+ const runPiAgentSkill = deps.runPiAgentSkill ?? (await import('../lib/pi-agent-skill.mjs')).runPiAgentSkill
148
+ const result = await runPiAgentSkill(prompt, { skillId, tier, cwd: projectDir })
149
+ if (result.error) {
150
+ logError(result.error)
151
+ }
152
+ return result.ok ? 0 : 1
153
+ }
154
+
155
+ /**
156
+ * Deprecated: делегує у зовнішній `claude -p` / `cursor-agent -p`. Лишається як fallback
157
+ * для тих, у кого pi-модель ще не налаштована; буде прибрано (мігруй на `skill pi`).
127
158
  * @param {'claude' | 'cursor'} kind який LLM CLI запускати
128
159
  * @param {string} prompt промпт для передачі у stdin
129
160
  * @param {string} projectDir робочий каталог дочірнього процесу
161
+ * @param {(line: string) => void} logError вивід попередження/помилок
130
162
  * @returns {number} exit code дочірнього процесу
131
163
  */
132
- function runLlmCli(kind, prompt, projectDir) {
164
+ function runLlmCli(kind, prompt, projectDir, logError) {
165
+ logError(`[deprecated] skill ${kind} → use 'skill pi'; зовнішній CLI буде прибрано`)
166
+
133
167
  if (kind === 'claude') {
134
168
  if (!isBinaryInPath('claude')) {
135
- throw new Error('`claude` not found in PATH. Install Claude Code CLI or use `skill cursor`.')
169
+ throw new Error('`claude` not found in PATH. Install Claude Code CLI or use `skill pi`.')
136
170
  }
137
171
 
138
172
  const result = spawnSync('claude', ['-p'], {
@@ -145,7 +179,7 @@ function runLlmCli(kind, prompt, projectDir) {
145
179
  }
146
180
 
147
181
  if (!isBinaryInPath('cursor-agent')) {
148
- throw new Error('`cursor-agent` not found in PATH. Install Cursor CLI or use `skill claude`.')
182
+ throw new Error('`cursor-agent` not found in PATH. Install Cursor CLI or use `skill pi`.')
149
183
  }
150
184
 
151
185
  const result = spawnSync('cursor-agent', ['-p'], {
@@ -168,15 +202,16 @@ export function resolveBundledPackageRoot(fromModuleUrl = import.meta.url) {
168
202
 
169
203
  /**
170
204
  * @param {string[]} argv аргументи після `skill` у `n-cursor`
171
- * @param {{ packageRoot?: string, projectDir?: string, log?: (line: string) => void, logError?: (line: string) => void }} [options] перевизначення кореня пакета, каталогу проєкту та функцій виводу (для тестів)
172
- * @returns {number} exit code
205
+ * @param {{ packageRoot?: string, projectDir?: string, log?: (line: string) => void, logError?: (line: string) => void, deps?: { runPiAgentSkill?: Function } }} [options] перевизначення кореня пакета, каталогу проєкту, функцій виводу та інжектів (для тестів)
206
+ * @returns {Promise<number>} exit code
173
207
  */
174
- export function runSkillsCli(argv, options = {}) {
208
+ export async function runSkillsCli(argv, options = {}) {
175
209
  const log = options.log ?? (line => console.log(line))
176
210
  const logError = options.logError ?? (line => console.error(line))
177
211
  const packageRoot = options.packageRoot ?? resolveBundledPackageRoot()
178
212
  const skillsRoot = join(packageRoot, 'skills')
179
213
  const projectDir = options.projectDir ?? cwd()
214
+ const deps = options.deps ?? {}
180
215
 
181
216
  const [first, second, ...rest] = argv
182
217
  const skillIds = listSkillIds(skillsRoot)
@@ -201,7 +236,10 @@ export function runSkillsCli(argv, options = {}) {
201
236
  }
202
237
  const task = rest.join(' ')
203
238
  const prompt = buildSkillPrompt(skillsRoot, second, task, projectDir)
204
- return runLlmCli(/** @type {'claude' | 'cursor'} */ (first), prompt, projectDir)
239
+ if (first === 'pi') {
240
+ return await runPiRunner(prompt, second, skillsRoot, projectDir, logError, deps)
241
+ }
242
+ return runLlmCli(/** @type {'claude' | 'cursor'} */ (first), prompt, projectDir, logError)
205
243
  }
206
244
 
207
245
  if (skillIds.includes(normalizeSkillId(first))) {
@@ -0,0 +1,105 @@
1
+ /** @see ./docs/ast-extract.md */
2
+
3
+ /**
4
+ * Generic AST-facts extractor для `ast_facts`-tool fix-engine (§3б спеки pi-migration).
5
+ *
6
+ * Дає агенту структурований зріз файлу (`imports` / `exports` / `topLevelFunctions`)
7
+ * замість сирого вмісту — скорочує redundant read-turns на слабкій локальній моделі.
8
+ * Використовується як **generic fallback**, коли правило не має власного
9
+ * `rules/<id>/js/_ast-context.mjs`.
10
+ *
11
+ * Парсинг — через спільний `parseProgramOrNull` (oxc). Будь-яка помилка (read/parse)
12
+ * деградує до `{ error, imports:[], exports:[], topLevelFunctions:[] }`, щоб агент
13
+ * продовжив із тим, що має (контракт §3б: fallback до голого вмісту).
14
+ */
15
+
16
+ import { readFileSync } from 'node:fs'
17
+ import { parseProgramOrNull } from './ast-scan-utils.mjs'
18
+
19
+ /** Порожній результат із причиною (read/parse fail). @param {string} error причина */
20
+ function empty(error) {
21
+ return { error, imports: [], exports: [], topLevelFunctions: [] }
22
+ }
23
+
24
+ /** Чи init-вираз — функція (для `export const f = () => …`). @param {unknown} node init */
25
+ function isFunctionInit(node) {
26
+ return !!node && (node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression')
27
+ }
28
+
29
+ /**
30
+ * Витягає AST-факти з вихідного коду (без файлового IO — для тестів і reuse).
31
+ * @param {string} content вихідний код
32
+ * @param {string} virtualPath шлях (визначає мову js/ts/jsx/tsx)
33
+ * @returns {{ imports: Array<{source: string, names: string[]}>, exports: string[], topLevelFunctions: string[], error?: string }} факти
34
+ */
35
+ export function extractContextFromSource(content, virtualPath) {
36
+ const program = parseProgramOrNull(content, virtualPath)
37
+ if (!program) return empty('parse failed')
38
+
39
+ const imports = []
40
+ const exports = []
41
+ const topLevelFunctions = []
42
+
43
+ for (const node of program.body ?? []) {
44
+ switch (node?.type) {
45
+ case 'ImportDeclaration':
46
+ imports.push({
47
+ source: node.source?.value ?? '',
48
+ names: (node.specifiers ?? []).map(s => s.local?.name).filter(Boolean)
49
+ })
50
+ break
51
+
52
+ case 'FunctionDeclaration':
53
+ if (node.id?.name) topLevelFunctions.push(node.id.name)
54
+ break
55
+
56
+ case 'ExportNamedDeclaration': {
57
+ const d = node.declaration
58
+ if (d?.type === 'FunctionDeclaration' && d.id?.name) {
59
+ exports.push(d.id.name)
60
+ topLevelFunctions.push(d.id.name)
61
+ } else if ((d?.type === 'ClassDeclaration' || d?.type === 'TSInterfaceDeclaration') && d.id?.name) {
62
+ exports.push(d.id.name)
63
+ } else if (d?.type === 'VariableDeclaration') {
64
+ for (const decl of d.declarations ?? []) {
65
+ if (!decl.id?.name) continue
66
+ exports.push(decl.id.name)
67
+ if (isFunctionInit(decl.init)) topLevelFunctions.push(decl.id.name)
68
+ }
69
+ }
70
+ for (const spec of node.specifiers ?? []) {
71
+ if (spec.exported?.name) exports.push(spec.exported.name)
72
+ }
73
+ break
74
+ }
75
+
76
+ case 'ExportDefaultDeclaration':
77
+ exports.push('default')
78
+ break
79
+
80
+ case 'ExportAllDeclaration':
81
+ exports.push(node.exported?.name ?? '*')
82
+ break
83
+
84
+ default:
85
+ break
86
+ }
87
+ }
88
+
89
+ return { imports, exports, topLevelFunctions }
90
+ }
91
+
92
+ /**
93
+ * Читає файл і витягає AST-факти. Read-помилка → `{ error, … }` (не кидає).
94
+ * @param {string} filePath абсолютний/відносний шлях до файлу
95
+ * @returns {{ imports: Array<{source: string, names: string[]}>, exports: string[], topLevelFunctions: string[], error?: string }} факти
96
+ */
97
+ export function extractContext(filePath) {
98
+ let content
99
+ try {
100
+ content = readFileSync(filePath, 'utf8')
101
+ } catch (e) {
102
+ return empty(`read failed: ${e.message}`)
103
+ }
104
+ return extractContextFromSource(content, filePath)
105
+ }
@@ -0,0 +1,30 @@
1
+ ---
2
+ type: JS Module
3
+ title: ast-extract.mjs
4
+ resource: npm/scripts/utils/ast-extract.mjs
5
+ docgen:
6
+ crc: 61e8c7bc
7
+ model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
+ score: 100
9
+ ---
10
+
11
+ ## Огляд
12
+
13
+ Цей модуль аналізує вихідний код, витягуючи ключові структурні елементи. Він дозволяє отримати інформацію про імпорти, експорти та назви верхнерівневих функцій, використовуючи функції `f`, `extractContextFromSource` та `extractContext`. Функціонал працює з кодом, наданим безпосередньо або шляхом до файлу. Процес аналізу реалізований з механізмом перехоплення помилок (fail-safe), гарантуючи, що винятки не будуть викинуті назовні.
14
+
15
+ ## Поведінка
16
+
17
+ f: витягує структурований зріз коду, включаючи імпорти, експорти та назви верхнерівневих функцій.
18
+ extractContextFromSource: витягує структурований зріз коду з наданого вмісту, визначуючи імпорти, експорти та назви верхнерівневих функцій.
19
+ extractContext: читає вміст файлу за вказаним шляхом і витягує з нього структуровані факти коду, обробляючи помилки під час читання.
20
+
21
+ ## Публічний API
22
+
23
+ - f — основна логіка модуля
24
+ - extractContextFromSource — аналізує структуру коду, наданого як текст, для отримання його граматичного представлення, не звертаючись до файлової системи
25
+ - extractContext — читає вміст файлу та аналізує його структуру, повертаючи помилку читання замість помилки виконання
26
+
27
+ ## Гарантії поведінки
28
+
29
+ - Read-only: не виконує операцій запису (ФС/БД).
30
+ - Перехоплює помилки і не пропускає винятків назовні (fail-safe).
@@ -3,34 +3,31 @@ type: JS Module
3
3
  title: walkDir.mjs
4
4
  resource: npm/scripts/utils/walkDir.mjs
5
5
  docgen:
6
- crc: 5e5fec27
6
+ crc: 73503ca2
7
+ model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
+ score: 100
7
9
  ---
8
10
 
9
- Файл `n-cursor.js` забезпечує рекурсивний обхід каталогів для скриптів перевірки, дозволяючи виконувати callback-функцію для кожного звичайного файлу. Він обходить дерево каталогів від заданого кореня, ігноруючи певні директорії (наприклад, `.git`, `node_modules`) та шляхи, вказані в `ignorePaths`. Це дозволяє автоматизувати процес аналізу конфігураційних файлів та інших ресурсів у проєкті.
11
+ ## Огляд
12
+
13
+ Публічна функція `walkDir` здійснює рекурсивний пошук файлів у вказаному каталозі. Вона обходить файлову систему, ігноруючи каталоги `.git` та `node_modules`. Знаходячи кожен файл, вона передає його повний шлях у колбек для подальшої обробки. Компонент є read-only, тобто не виконує записів у файлову систему чи бази даних. При роботі з файловою системою він перехоплює помилки, забезпечуючи безпечну роботу без викидання винятків.
10
14
 
11
15
  ## Поведінка
12
16
 
13
- 1. Починається з заданого кореневого каталогу.
14
- 2. Для кожного підкаталогу в кореневому каталозі:
15
- 2.1. Перевіряє, чи є підкаталог у списку `ignorePaths`. Якщо так, пропускає обхід цього підкаталогу та його вмісту.
16
- 2.2. Якщо підкаталог не в списку `ignorePaths`, обробляє вміст підкаталогу:
17
- 2.2.1. Для кожного файлу в підкаталозі, викликає функцію `onFile` з шляхом до файлу.
18
- 2.2.2. Для кожного підкаталогу в підкаталозі, рекурсивно викликає функцію `walkDir` для обходу цього підкаталогу. Під час рекурсивного виклику, також перевіряє, чи є підкаталог у списку `ignorePaths`.
19
- 3. Не обробляє каталоги `node_modules`, `.git`, `dist`, `coverage`, `.turbo` та `.next`.
20
- 4. Якщо при спробі `readdir` для каталогу виникає помилка, обхід цього каталогу припиняється.
21
- 5. Обхід завершується, коли всі підкаталоги та файли в заданому дереві були оброблені.
17
+ 1. Викликається функція walkDir для початку рекурсивного обходу каталогу.
18
+ 2. Система розшифровує вхідний шлях до кореня обходу.
19
+ 3. Система формує додаткові правила ігнорування на основі наданих шляхів.
20
+ 4. Система завжди ігнорує каталоги .git та node_modules.
21
+ 5. Система виконує пошук усіх файлів у каталозі, застосовуючи правила ігнорування, включаючи ті, що визначені в .gitignore.
22
+ 6. У разі виникнення помилки під час пошуку, процес обходу припиняється без генерації винятку.
23
+ 7. Для кожного знайденого файлу система викликає наданий колбек, передаючи йому повний абсолютний шлях до цього файлу.
22
24
 
23
25
  ## Публічний API
24
26
 
25
- - walkDir — Обходить каталог рекурсивно, ігноруючи вказані шляхи.
27
+ walkDir — рекурсивно переглядає файли та папки, ігноруючи ті, що вказані у файлі `.gitignore`.
26
28
 
27
29
  ## Гарантії поведінки
28
30
 
29
- - Функція рекурсивно обходить дерево каталогів, починаючи з заданого кореня.
30
- - Для кожного звичайного файлу викликається переданий callback.
31
- - Не обходить каталоги `node_modules`, `.git`, `dist`, `coverage`, `.turbo`, `.next`.
32
- - Може пропустити каталоги, вказані в `ignorePaths`.
33
- - У разі невдачі `readdir` для каталогу, функція тихо виходить без викидання помилок.
34
- - Функція не викидає винятки назовні.
35
- - У разі невдачі повертає `false` або `null`.
36
- - Не використовує кешування.
31
+ - Read-only: не виконує операцій запису (ФС/БД).
32
+ - Перехоплює помилки і не пропускає винятків назовні (fail-safe).
33
+ - Свідомо пропускає шляхи: `.git`, `node_modules`.