@nitra/cursor 12.15.1 → 12.16.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/CHANGELOG.md +16 -0
- package/bin/n-cursor.js +1 -1
- package/lib/docs/index.md +9 -6
- package/lib/docs/pi-agent-fix.md +28 -0
- package/lib/docs/pi-agent-skill.md +36 -0
- package/lib/docs/pi-model-tiers.md +46 -0
- package/lib/docs/pi-one-shot.md +34 -0
- package/lib/docs/pi-telemetry-store.md +33 -0
- package/lib/docs/pi-trace.md +27 -0
- package/lib/docs/pi-write-guard.md +32 -0
- package/lib/pi-agent-fix.mjs +253 -0
- package/lib/pi-agent-skill.mjs +181 -0
- package/lib/pi-model-tiers.mjs +109 -0
- package/lib/pi-one-shot.mjs +129 -0
- package/lib/pi-telemetry-store.mjs +0 -0
- package/lib/pi-trace.mjs +40 -0
- package/lib/pi-write-guard.mjs +147 -0
- package/package.json +5 -1
- package/rules/doc-files/js/docgen-files-batch.mjs +20 -5
- package/rules/doc-files/js/docgen-gen.mjs +42 -25
- package/rules/doc-files/js/docgen-judge-measure.mjs +16 -13
- package/rules/doc-files/js/docgen-judge.mjs +11 -9
- package/rules/doc-files/js/docs/docgen-files-batch.md +3 -20
- package/rules/doc-files/js/docs/docgen-gen.md +3 -20
- package/rules/doc-files/js/docs/docgen-judge-measure.md +3 -18
- package/rules/doc-files/js/docs/docgen-judge.md +3 -22
- package/rules/npm-module/js/docs/skill_meta.md +22 -15
- package/rules/npm-module/js/skill_meta.mjs +5 -1
- package/rules/text/js/cspell-fix.mjs +15 -16
- package/rules/text/js/docs/cspell-fix.md +16 -9
- package/rules/text/main.mjs +4 -4
- package/schemas/skill-meta.json +8 -0
- package/scripts/docs/skills-cli.md +21 -25
- package/scripts/lib/adr/docs/normalize-cli.md +3 -20
- package/scripts/lib/adr/docs/normalize-pipeline.md +3 -33
- package/scripts/lib/adr/normalize-cli.mjs +2 -2
- package/scripts/lib/adr/normalize-pipeline.mjs +78 -44
- package/scripts/lib/docs/skill-meta.md +27 -10
- package/scripts/lib/fix/docs/escalation-log.md +10 -9
- package/scripts/lib/fix/docs/orchestrator.md +13 -20
- package/scripts/lib/fix/escalation-log.mjs +1 -1
- package/scripts/lib/fix/orchestrator.mjs +65 -31
- package/scripts/lib/skill-meta.mjs +22 -0
- package/scripts/skills-cli.mjs +52 -14
- package/scripts/utils/ast-extract.mjs +105 -0
- package/scripts/utils/docs/ast-extract.md +30 -0
- package/lib/docs/llm.md +0 -33
- package/lib/docs/models.md +0 -48
- package/lib/docs/omlx-trace.md +0 -49
- package/lib/docs/omlx.md +0 -41
- package/lib/llm.mjs +0 -215
- package/lib/models.mjs +0 -75
- package/lib/omlx-trace.mjs +0 -158
- package/lib/omlx.mjs +0 -220
- package/scripts/lib/fix/docs/llm-fix-apply.md +0 -31
- package/scripts/lib/fix/docs/llm-lint-fix.md +0 -31
- package/scripts/lib/fix/docs/llm-worker.md +0 -28
- package/scripts/lib/fix/docs/verbose-block.md +0 -27
- package/scripts/lib/fix/llm-fix-apply.mjs +0 -113
- package/scripts/lib/fix/llm-lint-fix.mjs +0 -82
- package/scripts/lib/fix/llm-worker.mjs +0 -346
- package/scripts/lib/fix/verbose-block.mjs +0 -82
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/** @see ./docs/pi-agent-skill.md */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agentic skill-runner поверх pi `createAgentSession`
|
|
5
|
+
* (спека docs/specs/2026-06-26-skills-cli-pi-runner-design.md §5).
|
|
6
|
+
*
|
|
7
|
+
* Виконує один скіл як pi-агента: готовий `buildSkillPrompt`-рядок → `session.prompt`.
|
|
8
|
+
* Повний user-trust набір вбудованих tools (`read/grep/find/edit/write/ls/bash`), БЕЗ
|
|
9
|
+
* write-guard — скіл є явною user-invocation (паритет із `claude -p`, який теж без
|
|
10
|
+
* обмежень). Модель — з ОДНОГО тиру (`main.json.tier`, дефолт `max`), без escalation-
|
|
11
|
+
* сходів fix-engine. Runaway-backstop: turn-ceiling + per-call timeout. Асистентський
|
|
12
|
+
* текст стрімиться у stdout (паритет із `claude -p`). Телеметрія `kind:"skill"` у
|
|
13
|
+
* глобальний trace ([pi-trace]).
|
|
14
|
+
*
|
|
15
|
+
* Pi вантажиться lazy (тверда межа CI). Логіка інжектована через `deps` для unit-тестів.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { env, stdout } from 'node:process'
|
|
19
|
+
import { getRegistry, resolveModel, resolveModelSpec } from './pi-model-tiers.mjs'
|
|
20
|
+
import { writeTrace } from './pi-trace.mjs'
|
|
21
|
+
|
|
22
|
+
/** Аварійна стеля turns на сесію скіла (runaway-backstop). Override: `N_CURSOR_SKILL_TURN_CEILING`. */
|
|
23
|
+
const TURN_CEILING = Number(env.N_CURSOR_SKILL_TURN_CEILING) || 80
|
|
24
|
+
|
|
25
|
+
/** Дефолтний timeout одного скіл-виклику (скіли довгі). Override: `N_CURSOR_SKILL_TIMEOUT_MS`. */
|
|
26
|
+
const DEFAULT_TIMEOUT_MS = Number(env.N_CURSOR_SKILL_TIMEOUT_MS) || 600_000
|
|
27
|
+
|
|
28
|
+
/** Повний user-trust набір вбудованих tools. `bash` — обовʼязковий (taze→bun, coverage→тести). */
|
|
29
|
+
const SKILL_TOOLS = ['read', 'grep', 'find', 'edit', 'write', 'ls', 'bash']
|
|
30
|
+
|
|
31
|
+
/** Тира скіла → pi `thinkingLevel` (одна тира на виклик, без rung-сходів). */
|
|
32
|
+
const THINKING_BY_TIER = { min: 'low', avg: 'medium', max: 'high' }
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Дефолтна фабрика pi-сесії: повний tool-set із `bash`, БЕЗ custom-tools і write-guard.
|
|
36
|
+
* @returns {Promise<object>} pi AgentSession
|
|
37
|
+
*/
|
|
38
|
+
async function defaultCreateSession({ registry, model, cwd, thinkingLevel }) {
|
|
39
|
+
const { createAgentSession, SessionManager } = await import('@earendil-works/pi-coding-agent')
|
|
40
|
+
const { session } = await createAgentSession({
|
|
41
|
+
modelRegistry: registry,
|
|
42
|
+
model,
|
|
43
|
+
tools: SKILL_TOOLS,
|
|
44
|
+
thinkingLevel: thinkingLevel ?? 'medium',
|
|
45
|
+
cwd: cwd ?? process.cwd(),
|
|
46
|
+
sessionManager: SessionManager.inMemory()
|
|
47
|
+
})
|
|
48
|
+
return session
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Гонка з таймаутом; на таймаут кличе `onTimeout` (abort) і реджектить. */
|
|
52
|
+
function withTimeout(promise, ms, onTimeout) {
|
|
53
|
+
if (!ms || ms <= 0) return promise
|
|
54
|
+
let timer
|
|
55
|
+
const timeout = new Promise((_, reject) => {
|
|
56
|
+
timer = setTimeout(() => {
|
|
57
|
+
onTimeout?.()
|
|
58
|
+
reject(new Error(`skill timeout ${ms}ms`))
|
|
59
|
+
}, ms)
|
|
60
|
+
})
|
|
61
|
+
return Promise.race([Promise.resolve(promise).finally(() => clearTimeout(timer)), timeout])
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Виконує ОДИН скіл агентно через pi.
|
|
66
|
+
* @param {string} prompt готовий промпт (`buildSkillPrompt`)
|
|
67
|
+
* @param {{
|
|
68
|
+
* skillId?: string,
|
|
69
|
+
* tier?: 'min'|'avg'|'max',
|
|
70
|
+
* modelSpec?: string,
|
|
71
|
+
* cwd?: string,
|
|
72
|
+
* thinkingLevel?: 'off'|'minimal'|'low'|'medium'|'high'|'xhigh',
|
|
73
|
+
* timeoutMs?: number,
|
|
74
|
+
* caller?: string,
|
|
75
|
+
* deps?: { createSession?: Function, getRegistry?: Function, registry?: object, trace?: Function, clock?: () => number, out?: (s: string) => void }
|
|
76
|
+
* }} [opts]
|
|
77
|
+
* @returns {Promise<{ ok: boolean, telemetry: object|null, error: string|null }>}
|
|
78
|
+
*/
|
|
79
|
+
export async function runPiAgentSkill(prompt, opts = {}) {
|
|
80
|
+
const {
|
|
81
|
+
skillId = 'skill',
|
|
82
|
+
tier = 'max',
|
|
83
|
+
modelSpec,
|
|
84
|
+
cwd = process.cwd(),
|
|
85
|
+
thinkingLevel,
|
|
86
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
87
|
+
caller = `skill:${skillId}`,
|
|
88
|
+
deps = {}
|
|
89
|
+
} = opts
|
|
90
|
+
const createSession = deps.createSession ?? defaultCreateSession
|
|
91
|
+
const getReg = deps.getRegistry ?? getRegistry
|
|
92
|
+
const trace = deps.trace ?? writeTrace
|
|
93
|
+
const clock = deps.clock ?? (() => Date.now())
|
|
94
|
+
const out = deps.out ?? (s => stdout.write(s))
|
|
95
|
+
|
|
96
|
+
const fail = (error, model) => {
|
|
97
|
+
trace({ caller, backend: 'pi-ai', kind: 'skill', skill: skillId, tier, model: model ?? null, cwd, error })
|
|
98
|
+
return { ok: false, telemetry: null, error }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let registry
|
|
102
|
+
let spec
|
|
103
|
+
let model
|
|
104
|
+
try {
|
|
105
|
+
registry = deps.registry ?? (await getReg())
|
|
106
|
+
spec = modelSpec ?? resolveModel(tier) // '' допустимо → дефолт провайдера pi
|
|
107
|
+
model = spec ? resolveModelSpec(registry, spec) : null
|
|
108
|
+
if (spec && !model) return fail(`модель не знайдена: ${spec}`, spec)
|
|
109
|
+
} catch (e) {
|
|
110
|
+
return fail(`registry: ${e.message}`, null)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let session
|
|
114
|
+
try {
|
|
115
|
+
session = await createSession({
|
|
116
|
+
registry,
|
|
117
|
+
model,
|
|
118
|
+
cwd,
|
|
119
|
+
thinkingLevel: thinkingLevel ?? THINKING_BY_TIER[tier] ?? 'medium'
|
|
120
|
+
})
|
|
121
|
+
} catch (e) {
|
|
122
|
+
return fail(`session: ${e.message}`, spec)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let turnCount = 0
|
|
126
|
+
let toolCallCount = 0
|
|
127
|
+
let backstopHit = false
|
|
128
|
+
session.subscribe(event => {
|
|
129
|
+
switch (event.type) {
|
|
130
|
+
case 'turn_start':
|
|
131
|
+
turnCount++
|
|
132
|
+
if (turnCount > TURN_CEILING) {
|
|
133
|
+
backstopHit = true
|
|
134
|
+
session.abort?.()
|
|
135
|
+
}
|
|
136
|
+
break
|
|
137
|
+
case 'tool_execution_start':
|
|
138
|
+
toolCallCount++
|
|
139
|
+
break
|
|
140
|
+
case 'message_update':
|
|
141
|
+
if (event.assistantMessageEvent?.type === 'text_delta') {
|
|
142
|
+
out(event.assistantMessageEvent.delta ?? '')
|
|
143
|
+
}
|
|
144
|
+
break
|
|
145
|
+
default:
|
|
146
|
+
break
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
const startedAt = clock()
|
|
151
|
+
let error = null
|
|
152
|
+
try {
|
|
153
|
+
await withTimeout(session.prompt(prompt), timeoutMs, () => session.abort?.())
|
|
154
|
+
} catch (e) {
|
|
155
|
+
error = e.message
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const telemetry = {
|
|
159
|
+
skill: skillId,
|
|
160
|
+
tier,
|
|
161
|
+
model: spec || null,
|
|
162
|
+
turnCount,
|
|
163
|
+
toolCallCount,
|
|
164
|
+
backstopHit,
|
|
165
|
+
wallMs: clock() - startedAt
|
|
166
|
+
}
|
|
167
|
+
trace({
|
|
168
|
+
caller,
|
|
169
|
+
backend: 'pi-ai',
|
|
170
|
+
kind: 'skill',
|
|
171
|
+
skill: skillId,
|
|
172
|
+
tier,
|
|
173
|
+
model: spec || null,
|
|
174
|
+
cwd,
|
|
175
|
+
turnCount,
|
|
176
|
+
toolCallCount,
|
|
177
|
+
backstopHit,
|
|
178
|
+
error
|
|
179
|
+
})
|
|
180
|
+
return { ok: !error && !backstopHit, telemetry, error }
|
|
181
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/** @see ./docs/pi-model-tiers.md */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Тир-конфіг моделей для pi-embed fix-engine і shared LLM consumers.
|
|
5
|
+
*
|
|
6
|
+
* Замінює `models.mjs` після міграції на pi-SDK: тири лишаються політикою n-cursor
|
|
7
|
+
* (які env-моделі = який тир), але резолвінг тепер через pi `ModelRegistry.find`,
|
|
8
|
+
* а не ручний routing на omlx/pi-CLI.
|
|
9
|
+
*
|
|
10
|
+
* Формат значень env — `"provider/model-id"` (pi-формат), напр.:
|
|
11
|
+
* N_LOCAL_MIN_MODEL=omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
12
|
+
* N_CLOUD_MIN_MODEL=openai/gpt-5.4-mini
|
|
13
|
+
* N_CLOUD_AVG_MODEL=openai/gpt-5.4
|
|
14
|
+
*
|
|
15
|
+
* Pi вантажиться ВИКЛЮЧНО у `getRegistry()` (lazy dynamic import) — модуль сам по собі
|
|
16
|
+
* pi-free, тому pure-функції (`parseModelId`, `resolveModelSpec`, `thinkingLevelForTier`,
|
|
17
|
+
* `resolveModel`) юніт-тестуються без pi (registry інжектується).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { env } from 'node:process'
|
|
21
|
+
|
|
22
|
+
// ── Тири (env-політика) ──────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
/** Швидкий локальний inference. Напр.: omlx/gemma-4-e4b-it-OptiQ-4bit */
|
|
25
|
+
export const LOCAL_MIN = env.N_LOCAL_MIN_MODEL ?? ''
|
|
26
|
+
/** Середній локальний. */
|
|
27
|
+
export const LOCAL_AVG = env.N_LOCAL_AVG_MODEL ?? ''
|
|
28
|
+
/** Максимальний локальний. */
|
|
29
|
+
export const LOCAL_MAX = env.N_LOCAL_MAX_MODEL ?? ''
|
|
30
|
+
/** Мінімальний хмарний (потрібен ключ у pi auth). Напр.: openai/gpt-5.4-mini */
|
|
31
|
+
export const CLOUD_MIN = env.N_CLOUD_MIN_MODEL ?? ''
|
|
32
|
+
/** Середній хмарний. Напр.: openai/gpt-5.4 */
|
|
33
|
+
export const CLOUD_AVG = env.N_CLOUD_AVG_MODEL ?? ''
|
|
34
|
+
/** Максимальний хмарний. Напр.: openai/gpt-5.5 */
|
|
35
|
+
export const CLOUD_MAX = env.N_CLOUD_MAX_MODEL ?? ''
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Каскадне розв'язання абстрактного тиру в `"provider/model-id"` (контракт із
|
|
39
|
+
* `models.mjs`, збережений для shared one-shot consumers):
|
|
40
|
+
* 'min' → LOCAL_MIN → LOCAL_AVG → LOCAL_MAX → CLOUD_MIN
|
|
41
|
+
* 'avg' → LOCAL_AVG → LOCAL_MAX → CLOUD_AVG
|
|
42
|
+
* 'max' → LOCAL_MAX → CLOUD_MAX
|
|
43
|
+
* @param {'min'|'avg'|'max'} tier тир
|
|
44
|
+
* @returns {string} `"provider/model-id"` або `''` (pi-дефолт провайдера)
|
|
45
|
+
* @throws {TypeError} якщо tier невідомий
|
|
46
|
+
*/
|
|
47
|
+
export function resolveModel(tier) {
|
|
48
|
+
if (tier === 'min') return LOCAL_MIN || LOCAL_AVG || LOCAL_MAX || CLOUD_MIN
|
|
49
|
+
if (tier === 'avg') return LOCAL_AVG || LOCAL_MAX || CLOUD_AVG
|
|
50
|
+
if (tier === 'max') return LOCAL_MAX || CLOUD_MAX
|
|
51
|
+
throw new TypeError(`resolveModel: unknown tier "${tier}". Use 'min', 'avg', or 'max'.`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Escalation-rung → pi thinkingLevel (§3) ──────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* pi `thinkingLevel` за rung-тиром fix-драбини: слабка локальна — `low`,
|
|
58
|
+
* cloud-min — `medium`, cloud-avg (найдорожча, найскладніше) — `high`. Замінює
|
|
59
|
+
* ручний числовий `thinkingBudget`-плюмбінг старого omlx-каналу.
|
|
60
|
+
* @param {string} tier rung-тир (`local-min` | `local-min-retry` | `cloud-min` | `cloud-avg`)
|
|
61
|
+
* @returns {'off'|'minimal'|'low'|'medium'|'high'|'xhigh'} дискретний рівень
|
|
62
|
+
*/
|
|
63
|
+
export function thinkingLevelForTier(tier) {
|
|
64
|
+
if (tier === 'cloud-avg') return 'high'
|
|
65
|
+
if (tier === 'cloud-min') return 'medium'
|
|
66
|
+
return 'low' // local-min, local-min-retry
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Парсинг і резолвінг через pi ─────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Розбирає `"provider/model-id"` у пару. Перший `/` — роздільник (model-id може
|
|
73
|
+
* містити власні `/`). Порожній провайдер чи id → `null` (malformed).
|
|
74
|
+
* @param {string} spec `"provider/model-id"`
|
|
75
|
+
* @returns {{ provider: string, id: string } | null} пара або null
|
|
76
|
+
*/
|
|
77
|
+
export function parseModelId(spec) {
|
|
78
|
+
if (typeof spec !== 'string') return null
|
|
79
|
+
const i = spec.indexOf('/')
|
|
80
|
+
if (i < 1 || i === spec.length - 1) return null
|
|
81
|
+
return { provider: spec.slice(0, i), id: spec.slice(i + 1) }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Резолвить `"provider/model-id"` у pi Model-обʼєкт через інжектований registry.
|
|
86
|
+
* `createAgentSession` чекає саме Model-обʼєкт (НЕ рядок) — див. Спайк 2.
|
|
87
|
+
* @param {{ find: (provider: string, id: string) => object|null|undefined }} registry pi ModelRegistry
|
|
88
|
+
* @param {string} spec `"provider/model-id"`
|
|
89
|
+
* @returns {object|null} pi Model або null (malformed/не знайдено)
|
|
90
|
+
*/
|
|
91
|
+
export function resolveModelSpec(registry, spec) {
|
|
92
|
+
const parsed = parseModelId(spec)
|
|
93
|
+
if (!parsed) return null
|
|
94
|
+
return registry.find(parsed.provider, parsed.id) ?? null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Lazy singleton pi `ModelRegistry` (вантажить `~/.pi/agent/models.json` + `auth.json`).
|
|
99
|
+
* **Єдина** точка, де модуль торкається pi — dynamic import тримає `--read-only`
|
|
100
|
+
* шлях pi-free (тверда межа CI). Кешується на процес.
|
|
101
|
+
* @returns {Promise<object>} pi ModelRegistry
|
|
102
|
+
*/
|
|
103
|
+
let _registry = null
|
|
104
|
+
export async function getRegistry() {
|
|
105
|
+
if (_registry) return _registry
|
|
106
|
+
const { ModelRegistry, AuthStorage } = await import('@earendil-works/pi-coding-agent')
|
|
107
|
+
_registry = ModelRegistry.create(AuthStorage.create())
|
|
108
|
+
return _registry
|
|
109
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/** @see ./docs/pi-one-shot.md */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bounded one-shot LLM-виклик поверх pi (§3а спеки pi-migration).
|
|
5
|
+
*
|
|
6
|
+
* Для shared не-agent consumers (`doc-files` generation/judge, `text/cspell`
|
|
7
|
+
* класифікація, ADR-normalize): «messages → текст», без write-tool, без агентного
|
|
8
|
+
* циклу. Реалізовано як `createAgentSession({ noTools: 'all' })` + `session.prompt`
|
|
9
|
+
* (агент без tools = plain completion) — перевикористовує верифікований pi-embed
|
|
10
|
+
* (той самий ModelRegistry/AuthStorage, що й agent-fix), замість окремого
|
|
11
|
+
* raw-pi-ai streaming-плюмбінгу.
|
|
12
|
+
*
|
|
13
|
+
* Замінює `callLlm`/прямий omlx-канал для не-agent задач. Pi вантажиться lazy
|
|
14
|
+
* (тверда межа CI). Повертає structured `{ content, usage, error, model, caller }`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { getRegistry, resolveModel, resolveModelSpec } from './pi-model-tiers.mjs'
|
|
18
|
+
import { writeTrace } from './pi-trace.mjs'
|
|
19
|
+
|
|
20
|
+
/** Дефолтний timeout одного one-shot виклику. */
|
|
21
|
+
const DEFAULT_TIMEOUT_MS = 120_000
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Дефолтна фабрика сесії (lazy pi). `noTools:'all'` → чистий completion.
|
|
25
|
+
* Інструкції НЕ йдуть через `replaceInstructions`: слабкі локальні моделі (gemma-4b)
|
|
26
|
+
* трактують system-prompt-інструкції як «правила для підтвердження» й мета-рамблять
|
|
27
|
+
* замість виконувати. Тому system-повідомлення зливаються у prompt (див. runOneShot) —
|
|
28
|
+
* перевірено: інлайн-інструкції модель ВИКОНУЄ, у system-промпті — переказує.
|
|
29
|
+
* @returns {Promise<object>} pi AgentSession
|
|
30
|
+
*/
|
|
31
|
+
async function defaultCreateSession({ registry, model, cwd, thinkingLevel }) {
|
|
32
|
+
const { createAgentSession, SessionManager } = await import('@earendil-works/pi-coding-agent')
|
|
33
|
+
const { session } = await createAgentSession({
|
|
34
|
+
modelRegistry: registry,
|
|
35
|
+
model,
|
|
36
|
+
noTools: 'all',
|
|
37
|
+
thinkingLevel: thinkingLevel ?? 'off',
|
|
38
|
+
cwd: cwd ?? process.cwd(),
|
|
39
|
+
sessionManager: SessionManager.inMemory()
|
|
40
|
+
})
|
|
41
|
+
return session
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Гонка проміса з таймаутом (мс ≤ 0 → без таймауту). */
|
|
45
|
+
function withTimeout(promise, ms) {
|
|
46
|
+
if (!ms || ms <= 0) return promise
|
|
47
|
+
let timer
|
|
48
|
+
const timeout = new Promise((_, reject) => {
|
|
49
|
+
timer = setTimeout(() => reject(new Error(`one-shot timeout ${ms}ms`)), ms)
|
|
50
|
+
})
|
|
51
|
+
return Promise.race([Promise.resolve(promise).finally(() => clearTimeout(timer)), timeout])
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Виконує bounded one-shot LLM-виклик.
|
|
56
|
+
* @param {{
|
|
57
|
+
* messages: Array<{role: string, content: string}>,
|
|
58
|
+
* modelTier?: 'min'|'avg'|'max',
|
|
59
|
+
* modelSpec?: string,
|
|
60
|
+
* thinkingLevel?: 'off'|'minimal'|'low'|'medium'|'high'|'xhigh',
|
|
61
|
+
* timeoutMs?: number,
|
|
62
|
+
* caller?: string,
|
|
63
|
+
* cwd?: string,
|
|
64
|
+
* deps?: { createSession?: Function, getRegistry?: Function, registry?: object, trace?: Function }
|
|
65
|
+
* }} args параметри
|
|
66
|
+
* @returns {Promise<{ content: string, usage: object|null, error: string|null, model: string|null, caller: string }>} результат
|
|
67
|
+
*/
|
|
68
|
+
export async function runOneShot({
|
|
69
|
+
messages,
|
|
70
|
+
modelTier = 'min',
|
|
71
|
+
modelSpec,
|
|
72
|
+
thinkingLevel,
|
|
73
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
74
|
+
caller = 'one-shot',
|
|
75
|
+
cwd,
|
|
76
|
+
deps = {}
|
|
77
|
+
} = {}) {
|
|
78
|
+
const createSession = deps.createSession ?? defaultCreateSession
|
|
79
|
+
const getReg = deps.getRegistry ?? getRegistry
|
|
80
|
+
const trace = deps.trace ?? writeTrace
|
|
81
|
+
|
|
82
|
+
// Усі повідомлення (system+user) зливаються в один prompt — інлайн-інструкції
|
|
83
|
+
// слабка локальна модель ВИКОНУЄ, а в system-промпті (replaceInstructions) — переказує.
|
|
84
|
+
const userText = messages.map(m => m.content).join('\n\n')
|
|
85
|
+
|
|
86
|
+
const fail = (error, model) => {
|
|
87
|
+
trace({ caller, backend: 'pi-ai', kind: 'one-shot', model: model ?? null, cwd: cwd ?? null, error })
|
|
88
|
+
return { content: '', usage: null, error, model: model ?? null, caller }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let registry
|
|
92
|
+
let spec
|
|
93
|
+
let model
|
|
94
|
+
try {
|
|
95
|
+
registry = deps.registry ?? (await getReg())
|
|
96
|
+
spec = modelSpec ?? resolveModel(modelTier)
|
|
97
|
+
model = spec ? resolveModelSpec(registry, spec) : null
|
|
98
|
+
if (spec && !model) return fail(`модель не знайдена: ${spec}`, spec)
|
|
99
|
+
} catch (e) {
|
|
100
|
+
return fail(`registry: ${e.message}`, null)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let session
|
|
104
|
+
try {
|
|
105
|
+
session = await createSession({ registry, model, cwd, thinkingLevel })
|
|
106
|
+
} catch (e) {
|
|
107
|
+
return fail(`session: ${e.message}`, spec)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let text = ''
|
|
111
|
+
let usage = null
|
|
112
|
+
session.subscribe(event => {
|
|
113
|
+
if (event.type === 'message_update' && event.assistantMessageEvent?.type === 'text_delta') {
|
|
114
|
+
text += event.assistantMessageEvent.delta ?? ''
|
|
115
|
+
} else if (event.type === 'message_end' && event.message?.usage) {
|
|
116
|
+
usage = event.message.usage
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
let error = null
|
|
121
|
+
try {
|
|
122
|
+
await withTimeout(session.prompt(userText), timeoutMs)
|
|
123
|
+
} catch (e) {
|
|
124
|
+
error = e.message
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
trace({ caller, backend: 'pi-ai', kind: 'one-shot', model: spec, cwd: cwd ?? null, usage, error })
|
|
128
|
+
return { content: text.trim(), usage, error, model: spec, caller }
|
|
129
|
+
}
|
|
Binary file
|
package/lib/pi-trace.mjs
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** @see ./docs/pi-trace.md */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Глобальний append-only LLM wire-trace (§7 спеки pi-migration).
|
|
5
|
+
*
|
|
6
|
+
* Замінює project-local `omlx-trace.mjs`: єдиний trace живе глобально
|
|
7
|
+
* (`~/.n-cursor/llm-trace.jsonl`), щоб (1) прибрати службовий шум із consumer-репо,
|
|
8
|
+
* (2) лишити cross-project телеметрію mineable в одному місці. Старі project-local
|
|
9
|
+
* `<cwd>/.n-cursor/llm-trace.jsonl` більше не створюються.
|
|
10
|
+
*
|
|
11
|
+
* Записувач **best-effort**: будь-яка IO-помилка ковтається — трасування ніколи не
|
|
12
|
+
* валить виклик LLM. Шлях перевизначається `N_CURSOR_TRACE_PATH` (для тестів/CI).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { appendFileSync, mkdirSync } from 'node:fs'
|
|
16
|
+
import { homedir } from 'node:os'
|
|
17
|
+
import { dirname, join } from 'node:path'
|
|
18
|
+
import { env } from 'node:process'
|
|
19
|
+
|
|
20
|
+
/** Шлях глобального trace (env-override `N_CURSOR_TRACE_PATH`). @returns {string} */
|
|
21
|
+
export function tracePath() {
|
|
22
|
+
return env.N_CURSOR_TRACE_PATH || join(homedir(), '.n-cursor', 'llm-trace.jsonl')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Дописує один trace-запис (JSONL). Поля за §7: `caller`, `rule`, `rung`, `model`,
|
|
27
|
+
* `backend:"pi-ai"`, `kind:"agent"|"one-shot"`, `cwd`, плюс довільна корисна навантага.
|
|
28
|
+
* Ніколи не кидає.
|
|
29
|
+
* @param {object} record запис трасування
|
|
30
|
+
* @param {string} [path] шлях (за замовч. `tracePath()`)
|
|
31
|
+
* @returns {void}
|
|
32
|
+
*/
|
|
33
|
+
export function writeTrace(record, path = tracePath()) {
|
|
34
|
+
try {
|
|
35
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
36
|
+
appendFileSync(path, `${JSON.stringify({ ts: new Date().toISOString(), ...record })}\n`)
|
|
37
|
+
} catch {
|
|
38
|
+
// best-effort: трасування не повинно валити виклик
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/** @see ./docs/pi-write-guard.md */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Write-safety guard для pi-agent fix-engine (§12 спеки pi-migration).
|
|
5
|
+
*
|
|
6
|
+
* Рішення §1 «патч застосовує агент» забирає dry-run, тому контроль над записом
|
|
7
|
+
* виносимо у три незалежні під-механізми, що спираються на одну git-precondition:
|
|
8
|
+
* - **Scope/Denylist** — превентивний veto через `pi.on('tool_call')`: блок запису
|
|
9
|
+
* поза git-root, під `.git/`, або в будь-що, що матчить `git check-ignore`.
|
|
10
|
+
* - **Snapshot** — per-file pre-image (наявний вміст або позначка NEW) на перший
|
|
11
|
+
* дотик; покриває abort посеред запису.
|
|
12
|
+
* - **Rollback** — відновлення pre-image (або видалення NEW-файлів) на провал verdict-а.
|
|
13
|
+
*
|
|
14
|
+
* ⚠️ Розводка: фабрику передавати ВИКЛЮЧНО через `new DefaultResourceLoader({
|
|
15
|
+
* extensionFactories: [factory] })` → `resourceLoader`. Top-level
|
|
16
|
+
* `createAgentSession({ extensionFactories })` **мовчки ігнорується** (Спайк 3,
|
|
17
|
+
* fail-open). Тому caller ОБОВ'ЯЗКОВО перевіряє `state.attached` (fail-closed canary)
|
|
18
|
+
* перед `session.prompt` і скасовує fix, якщо guard не приєднався.
|
|
19
|
+
*
|
|
20
|
+
* Pi-free: фабрика лише shape-сумісна з ExtensionAPI (`pi.on`), імпортів pi нема.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { spawnSync } from 'node:child_process'
|
|
24
|
+
import { existsSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
|
|
25
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
26
|
+
|
|
27
|
+
/** Sentinel pre-image для файлу, якого до запису не існувало (rollback = видалити). */
|
|
28
|
+
export const NEW_FILE = Symbol('new-file')
|
|
29
|
+
|
|
30
|
+
/** Tool-и, що пишуть на диск і підлягають veto. */
|
|
31
|
+
const WRITE_TOOLS = new Set(['edit', 'write'])
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* git-root для cwd (`git rev-parse --show-toplevel`) або null, якщо не git-репо.
|
|
35
|
+
* Fix-шлях вимагає git → caller на null **пропускає fix** (§12 precondition).
|
|
36
|
+
* @param {string} cwd робоча директорія
|
|
37
|
+
* @returns {string|null} абсолютний git-root або null
|
|
38
|
+
*/
|
|
39
|
+
export function gitRoot(cwd) {
|
|
40
|
+
const r = spawnSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8' })
|
|
41
|
+
if (r.status !== 0) return null
|
|
42
|
+
return r.stdout.trim() || null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* realpath шляху з найкращих зусиль: для наявного — повний realpath; для ще-неіснуючого
|
|
47
|
+
* (NEW-файл) — realpath батьківської теки + basename; інакше — як є. Знімає розбіжність
|
|
48
|
+
* symlink-шляхів (macOS `/tmp` → `/private/tmp`), через яку tracked-файл хибно блокувався.
|
|
49
|
+
* @param {string} p шлях
|
|
50
|
+
* @returns {string} нормалізований абсолютний шлях
|
|
51
|
+
*/
|
|
52
|
+
function realpathBestEffort(p) {
|
|
53
|
+
try {
|
|
54
|
+
return realpathSync(p)
|
|
55
|
+
} catch {
|
|
56
|
+
try {
|
|
57
|
+
return join(realpathSync(dirname(p)), basename(p))
|
|
58
|
+
} catch {
|
|
59
|
+
return p
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Чи `abs` під `root` (захист від `..`-escape). @param {string} root @param {string} abs */
|
|
65
|
+
function isUnder(root, abs) {
|
|
66
|
+
const rel = relative(root, abs)
|
|
67
|
+
return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Чи `abs` ігнорований git'ом (`git check-ignore -q`, exit 0 = ignored). */
|
|
71
|
+
function isIgnored(root, abs) {
|
|
72
|
+
return spawnSync('git', ['check-ignore', '-q', '--', abs], { cwd: root }).status === 0
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Створює write-guard для однієї fix-сесії.
|
|
77
|
+
* @param {{ cwd: string, root?: string|null, checkIgnore?: (root: string, abs: string) => boolean }} opts
|
|
78
|
+
* cwd — робоча директорія; root — git-root (за替замовч. обчислюється); checkIgnore — інжекція для тестів
|
|
79
|
+
* @returns {{
|
|
80
|
+
* factory: (pi: { on: Function }) => void,
|
|
81
|
+
* state: { attached: boolean, root: string|null, preImages: Map<string, string|symbol>, blocks: Array<{path:string,reason:string}> },
|
|
82
|
+
* rollback: () => void,
|
|
83
|
+
* touchedFiles: () => string[]
|
|
84
|
+
* }} guard
|
|
85
|
+
*/
|
|
86
|
+
export function createWriteGuard({ cwd, root, checkIgnore = isIgnored }) {
|
|
87
|
+
const rawRoot = root === undefined ? gitRoot(cwd) : root
|
|
88
|
+
const gitRootDir = rawRoot ? realpathBestEffort(rawRoot) : rawRoot
|
|
89
|
+
// editLog — повні правки агента (oldText/newText / content) для телеметрії §7.
|
|
90
|
+
const state = { attached: false, root: gitRootDir, preImages: new Map(), blocks: [], editLog: [] }
|
|
91
|
+
|
|
92
|
+
const factory = pi => {
|
|
93
|
+
state.attached = true
|
|
94
|
+
// Синхронний хендлер: уся робота (spawnSync/fs) синхронна; pi коректно
|
|
95
|
+
// awaitить і не-Promise return (Спайк 3). Так veto детерміновано тестується.
|
|
96
|
+
pi.on('tool_call', event => {
|
|
97
|
+
if (!WRITE_TOOLS.has(event?.toolName)) return undefined
|
|
98
|
+
const raw = event?.input?.path
|
|
99
|
+
if (typeof raw !== 'string' || raw === '') return undefined // не резолвимо — хай pi сам
|
|
100
|
+
|
|
101
|
+
// realpath-нормалізація: edit-шлях агента може бути symlink-варіантом root'а.
|
|
102
|
+
const abs = realpathBestEffort(isAbsolute(raw) ? raw : resolve(cwd, raw))
|
|
103
|
+
const block = reason => {
|
|
104
|
+
state.blocks.push({ path: abs, reason })
|
|
105
|
+
return { block: true, reason }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 1. Scope: під git-root.
|
|
109
|
+
if (!gitRootDir || !isUnder(gitRootDir, abs)) return block(`запис поза git-root: ${raw}`)
|
|
110
|
+
// 2. .git/ (поза моделлю ignore).
|
|
111
|
+
const rel = relative(gitRootDir, abs)
|
|
112
|
+
if (rel === '.git' || rel.startsWith(`.git${sep}`)) return block(`запис у .git/ заблоковано: ${raw}`)
|
|
113
|
+
// 3. Denylist = git-ignored (build-артефакти, node_modules, .env, .worktrees…).
|
|
114
|
+
if (checkIgnore(gitRootDir, abs)) return block(`запис у git-ignored заблоковано: ${raw}`)
|
|
115
|
+
|
|
116
|
+
// 4. Allow + Snapshot pre-image на перший дотик + лог правки (телеметрія §7).
|
|
117
|
+
if (!state.preImages.has(abs)) {
|
|
118
|
+
state.preImages.set(abs, existsSync(abs) ? readFileSync(abs, 'utf8') : NEW_FILE)
|
|
119
|
+
}
|
|
120
|
+
state.editLog.push({
|
|
121
|
+
path: abs,
|
|
122
|
+
tool: event.toolName,
|
|
123
|
+
edits: event.input?.edits ?? null, // edit: [{oldText,newText}]
|
|
124
|
+
content: event.input?.content ?? null // write: повний вміст
|
|
125
|
+
})
|
|
126
|
+
return undefined
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Відновлює pre-image усіх зачеплених файлів (NEW → видалити). */
|
|
131
|
+
function rollback() {
|
|
132
|
+
for (const [abs, pre] of state.preImages) {
|
|
133
|
+
if (pre === NEW_FILE) {
|
|
134
|
+
if (existsSync(abs)) rmSync(abs)
|
|
135
|
+
} else {
|
|
136
|
+
writeFileSync(abs, pre)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Список абсолютних шляхів, яких агент торкнувся (для scoped re-check verdict §4+5). */
|
|
142
|
+
function touchedFiles() {
|
|
143
|
+
return [...state.preImages.keys()]
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { factory, state, rollback, touchedFiles }
|
|
147
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nitra/cursor",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.16.1",
|
|
4
4
|
"description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -62,6 +62,10 @@
|
|
|
62
62
|
"yaml": "^2.9.0",
|
|
63
63
|
"zod": "^4.4.3"
|
|
64
64
|
},
|
|
65
|
+
"optionalDependencies": {
|
|
66
|
+
"@earendil-works/pi-ai": "0.80.2",
|
|
67
|
+
"@earendil-works/pi-coding-agent": "0.80.2"
|
|
68
|
+
},
|
|
65
69
|
"engines": {
|
|
66
70
|
"bun": ">=1.3",
|
|
67
71
|
"node": ">=25"
|
|
@@ -12,8 +12,24 @@ import {
|
|
|
12
12
|
import { basename, dirname, join, relative } from 'node:path'
|
|
13
13
|
|
|
14
14
|
import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
|
|
15
|
-
import { classifyOmlxError, preflightLocalModel } from '../../../lib/llm.mjs'
|
|
16
15
|
import { generateDoc, DEFAULT_LOCAL_MODEL } from './docgen-gen.mjs'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Класифікує помилку генерації для batch-логіки (замінює `classifyOmlxError` після
|
|
19
|
+
* pi-міграції — помилки приходять як винятки з generateDoc/pi-one-shot):
|
|
20
|
+
* - `permanent` — pre-send guard «Prompt too long» → skip (не ретраїти);
|
|
21
|
+
* - `systemic` — модель/сервер/registry/RAM упали → circuit-breaker abort;
|
|
22
|
+
* - `transient` — таймаут (можна було б ретраїти);
|
|
23
|
+
* - `infra` — інше (рахуємо як помилку, але без abort).
|
|
24
|
+
* @param {string} msg повідомлення помилки
|
|
25
|
+
* @returns {'permanent'|'systemic'|'transient'|'infra'} клас
|
|
26
|
+
*/
|
|
27
|
+
function classifyDocgenError(msg) {
|
|
28
|
+
if (/prompt too long|pre-send guard|too long/i.test(msg)) return 'permanent'
|
|
29
|
+
if (/registry:|session:|не знайдена|memory|enomem|connection refused|econnrefused/i.test(msg)) return 'systemic'
|
|
30
|
+
if (/timeout|etimedout/i.test(msg)) return 'transient'
|
|
31
|
+
return 'infra'
|
|
32
|
+
}
|
|
17
33
|
import { crc32, stampDoc, readDocQuality, readDocModel, QUALITY_THRESHOLD } from './docgen-crc.mjs'
|
|
18
34
|
import { resolveRoot, scanForDocFiles, scanOrphanedDocs } from './docgen-scan.mjs'
|
|
19
35
|
|
|
@@ -134,7 +150,7 @@ async function generateOne(file, root, progress, stats) {
|
|
|
134
150
|
}
|
|
135
151
|
return 'ok'
|
|
136
152
|
} catch (error) {
|
|
137
|
-
const cls =
|
|
153
|
+
const cls = classifyDocgenError(error.message)
|
|
138
154
|
if (cls === 'permanent') {
|
|
139
155
|
stats.skipped.push(file.sourcePath)
|
|
140
156
|
process.stdout.write(`⊘ skip (permanent): ${error.message}\n`)
|
|
@@ -307,9 +323,8 @@ export async function runDocFilesGenCli(argv) {
|
|
|
307
323
|
* @returns {Promise<number>} 0 — без помилок; 1 — фейл preflight або є помилки; 2 — systemic-abort
|
|
308
324
|
*/
|
|
309
325
|
export async function runGenerationBatch(targets, root, { headline } = {}) {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
console.error(`✗ fix-doc-files: ${problem}`)
|
|
326
|
+
if (!DEFAULT_LOCAL_MODEL) {
|
|
327
|
+
console.error('✗ fix-doc-files: локальну модель не задано (N_LOCAL_MIN_MODEL)')
|
|
313
328
|
return 1
|
|
314
329
|
}
|
|
315
330
|
|