mister-wolf 2.14.0 → 2.15.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 (68) hide show
  1. package/README.md +2 -0
  2. package/README.ru.md +2 -0
  3. package/dist/adapters/cli/commands/memory-doctor.d.ts.map +1 -1
  4. package/dist/adapters/cli/commands/memory-doctor.js +32 -1
  5. package/dist/adapters/cli/commands/memory-doctor.js.map +1 -1
  6. package/dist/adapters/cli/commands/memory-scaffold.d.ts.map +1 -1
  7. package/dist/adapters/cli/commands/memory-scaffold.js +15 -0
  8. package/dist/adapters/cli/commands/memory-scaffold.js.map +1 -1
  9. package/dist/adapters/cli/commands/memory-sync.d.ts.map +1 -1
  10. package/dist/adapters/cli/commands/memory-sync.js +8 -0
  11. package/dist/adapters/cli/commands/memory-sync.js.map +1 -1
  12. package/dist/adapters/fs/artifact-fs.d.ts +4 -0
  13. package/dist/adapters/fs/artifact-fs.d.ts.map +1 -0
  14. package/dist/adapters/fs/artifact-fs.js +29 -0
  15. package/dist/adapters/fs/artifact-fs.js.map +1 -0
  16. package/dist/adapters/platforms/opencode-adapter.d.ts +8 -1
  17. package/dist/adapters/platforms/opencode-adapter.d.ts.map +1 -1
  18. package/dist/adapters/platforms/opencode-adapter.js +33 -0
  19. package/dist/adapters/platforms/opencode-adapter.js.map +1 -1
  20. package/dist/app/use-cases/artifact-front-matter.d.ts +5 -0
  21. package/dist/app/use-cases/artifact-front-matter.d.ts.map +1 -0
  22. package/dist/app/use-cases/artifact-front-matter.js +16 -0
  23. package/dist/app/use-cases/artifact-front-matter.js.map +1 -0
  24. package/dist/app/use-cases/init-project.d.ts +3 -1
  25. package/dist/app/use-cases/init-project.d.ts.map +1 -1
  26. package/dist/app/use-cases/init-project.js +36 -16
  27. package/dist/app/use-cases/init-project.js.map +1 -1
  28. package/dist/app/use-cases/lint-artifacts.d.ts +18 -0
  29. package/dist/app/use-cases/lint-artifacts.d.ts.map +1 -0
  30. package/dist/app/use-cases/lint-artifacts.js +249 -0
  31. package/dist/app/use-cases/lint-artifacts.js.map +1 -0
  32. package/dist/app/use-cases/scaffold-agent.d.ts +1 -1
  33. package/dist/app/use-cases/scaffold-agent.d.ts.map +1 -1
  34. package/dist/app/use-cases/scaffold-agent.js +1 -1
  35. package/dist/app/use-cases/scaffold-agent.js.map +1 -1
  36. package/dist/app/use-cases/scaffold-artifact.d.ts +22 -0
  37. package/dist/app/use-cases/scaffold-artifact.d.ts.map +1 -0
  38. package/dist/app/use-cases/scaffold-artifact.js +153 -0
  39. package/dist/app/use-cases/scaffold-artifact.js.map +1 -0
  40. package/dist/app/use-cases/sync-artifact-index.d.ts +20 -0
  41. package/dist/app/use-cases/sync-artifact-index.d.ts.map +1 -0
  42. package/dist/app/use-cases/sync-artifact-index.js +68 -0
  43. package/dist/app/use-cases/sync-artifact-index.js.map +1 -0
  44. package/dist/domain/memory-types.d.ts +12 -0
  45. package/dist/domain/memory-types.d.ts.map +1 -1
  46. package/dist/domain/memory-types.js +4 -0
  47. package/dist/domain/memory-types.js.map +1 -1
  48. package/dist/domain/schemas/relation-schema.d.ts +1 -1
  49. package/dist/ports/platform-adapter.port.d.ts +15 -0
  50. package/dist/ports/platform-adapter.port.d.ts.map +1 -1
  51. package/package.json +1 -1
  52. package/templates/.wolf/router.log +1 -0
  53. package/templates/base/skills/finishing-a-development-branch/SKILL.md +32 -13
  54. package/templates/base/skills/receiving-code-review/SKILL.md +39 -0
  55. package/templates/base/skills/test-driven-development/SKILL.md +59 -37
  56. package/templates/base/skills/using-git-worktrees/SKILL.md +25 -15
  57. package/templates/base/skills/verification-before-completion/SKILL.md +39 -25
  58. package/templates/base/skills/wolf-brainstorm/SKILL.md +51 -36
  59. package/templates/base/skills/wolf-design/SKILL.md +45 -0
  60. package/templates/base/skills/wolf-execute/SKILL.md +30 -13
  61. package/templates/base/skills/wolf-plan/SKILL.md +38 -21
  62. package/templates/base/skills/wolf-review/SKILL.md +33 -14
  63. package/templates/base/skills/wolf-sdd/SKILL.md +38 -16
  64. package/templates/base/skills/wolf-skill-intake/SKILL.md +66 -0
  65. package/templates/base/skills/wolf-testplan/SKILL.md +37 -0
  66. package/templates/base/skills/writing-skills/SKILL.md +40 -0
  67. package/templates/opencode/plugins/wolf-router.ts +110 -52
  68. package/templates/opencode/plugins/wolf-session-start.js +88 -40
@@ -115,61 +115,119 @@ async function resolvePlaybook(agentId: string): Promise<{ id: string; body: str
115
115
  return found;
116
116
  }
117
117
 
118
- export const WolfPlaybookPlugin = async () => {
119
- // 4.C: один WOLF_SESSION на процесс opencode — спавны волка и bash-вызовы
120
- // наследуют ключ. Известный потолок: несколько чатов одного процесса
121
- // opencode шарят ключ — консервативная недо-дедупликация.
118
+ // B3: метрика вызовов скиллов. Форма входа v1: input.tool — имя тула,
119
+ // output.args — объект аргументов; v2: event.tool + event.input (name | skill —
120
+ // фолбэк). Отдельный JSONL-файл — инвариант: enum событий SignalEventSchema
121
+ // закрытый, в общий router.log эти события класть нельзя. WOLF_SKILL_LOG —
122
+ // тест-шов (читается на каждый вызов: env ставится тестами уже после импорта).
123
+ function recordSkillMetric(tool: unknown, args: unknown): void {
124
+ try {
125
+ if (tool !== 'skill') return;
126
+ const a = (args ?? {}) as { name?: string; skill?: string } | null;
127
+ const skill = a?.name ?? a?.skill;
128
+ if (!skill) return;
129
+ const file =
130
+ process.env.WOLF_SKILL_LOG ?? path.join(PROJECT_ROOT, '.wolf', 'metrics', 'skill-invocations.jsonl');
131
+ fs.mkdirSync(path.dirname(file), { recursive: true });
132
+ const entry: { ts: string; skill: string; agent?: string } = { ts: new Date().toISOString(), skill };
133
+ if (lastAgentId) entry.agent = lastAgentId;
134
+ fs.appendFileSync(file, `${JSON.stringify(entry)}\n`);
135
+ } catch {
136
+ /* fail-safe: падение метрики не роняет сессию */
137
+ }
138
+ }
139
+
140
+ // Общая доставка playbook (v1 system.transform / v2 context): joined system-текст
141
+ // → маркер agent-id → гвард идемпотентности INJECT_HEADER → resolvePlaybook →
142
+ // push(текст). Fail-safe: не роняем сессию ни на одном шаге.
143
+ async function deliverPlaybook(joined: string, push: (text: string) => void): Promise<void> {
144
+ try {
145
+ const m = joined.match(AGENT_ID_RE);
146
+ if (!m) return; // рамка без маркера — не наша забота
147
+ if (joined.includes(INJECT_HEADER)) return; // идемпотентность: не вставляем дважды
148
+ const agentId = m[1];
149
+ lastAgentId = agentId; // агент известен хуку skill-метрики (B3)
150
+ const startedAt = Date.now();
151
+ const resolved = await resolvePlaybook(agentId);
152
+ const ms = Date.now() - startedAt;
153
+ if (resolved) {
154
+ push(`\n\n${INJECT_HEADER}\n\n${resolved.body}`);
155
+ logRoute(
156
+ `agent-id=${agentId} playbook=hit name=${resolved.id} variant=canonical injected=yes` +
157
+ ` ms=${ms} bytes=${Buffer.byteLength(resolved.body)}`
158
+ );
159
+ } else {
160
+ // T012: miss канона → универсальный fallback; инъекция для любого agent-id
161
+ push(`\n\n${INJECT_HEADER}\n\n${FALLBACK_PLAYBOOK}`);
162
+ logRoute(
163
+ `agent-id=${agentId} playbook=hit name=fallback variant=fallback injected=yes` +
164
+ ` ms=${ms} bytes=${Buffer.byteLength(FALLBACK_PLAYBOOK)}`
165
+ );
166
+ }
167
+ } catch {
168
+ /* fail-safe: не роняем сессию */
169
+ }
170
+ }
171
+
172
+ // 4.C: один WOLF_SESSION на процесс opencode — спавны волка и bash-вызовы
173
+ // наследуют ключ. Известный потолок: несколько чатов одного процесса opencode
174
+ // шарят ключ — консервативная недо-дедупликация, не ошибка.
175
+ function ensureSession(): void {
122
176
  if (!process.env.WOLF_SESSION) process.env.WOLF_SESSION = 'opc-' + randomUUID();
177
+ }
178
+
179
+ // v1-фабрика: loader v1 вызывает `server()` и получает hook-объект.
180
+ async function server() {
181
+ ensureSession();
123
182
  return {
124
- 'experimental.chat.system.transform': async (_input, output) => {
125
- try {
126
- const joined = output.system.join('\n');
127
- const m = joined.match(AGENT_ID_RE);
128
- if (!m) return; // рамка без маркера — не наша забота
129
- if (joined.includes(INJECT_HEADER)) return; // идемпотентность: не вставляем дважды
130
- const agentId = m[1];
131
- lastAgentId = agentId; // агент известен хуку skill-метрики (B3)
132
- const startedAt = Date.now();
133
- const resolved = await resolvePlaybook(agentId);
134
- const ms = Date.now() - startedAt;
135
- if (resolved) {
136
- output.system.push(`\n\n${INJECT_HEADER}\n\n${resolved.body}`);
137
- logRoute(
138
- `agent-id=${agentId} playbook=hit name=${resolved.id} variant=canonical injected=yes` +
139
- ` ms=${ms} bytes=${Buffer.byteLength(resolved.body)}`
140
- );
141
- } else {
142
- // T012: miss канона → универсальный fallback; инъекция для любого agent-id
143
- output.system.push(`\n\n${INJECT_HEADER}\n\n${FALLBACK_PLAYBOOK}`);
144
- logRoute(
145
- `agent-id=${agentId} playbook=hit name=fallback variant=fallback injected=yes` +
146
- ` ms=${ms} bytes=${Buffer.byteLength(FALLBACK_PLAYBOOK)}`
147
- );
148
- }
149
- } catch {
150
- /* fail-safe: не роняем сессию */
151
- }
183
+ 'experimental.chat.system.transform': async (_input: unknown, output: { system: string[] }) => {
184
+ await deliverPlaybook((output?.system ?? []).join('\n'), (text) => output.system.push(text));
152
185
  },
153
- // B3: метрика вызовов скиллов. Форма входа по докам opencode: input.tool —
154
- // имя тула, output.args — объект аргументов (name | skill — фолбэк).
155
- // Отдельный JSONL-файл — инвариант: enum событий SignalEventSchema закрытый,
156
- // в общий router.log эти события класть нельзя. WOLF_SKILL_LOG — тест-шов
157
- // (читается на каждый вызов: env ставится тестами уже после импорта модуля).
158
186
  'tool.execute.before': async (input: unknown, output: unknown) => {
159
- try {
160
- if ((input as { tool?: string } | null)?.tool !== 'skill') return;
161
- const args = (output as { args?: { name?: string; skill?: string } } | null)?.args;
162
- const skill = args?.name ?? args?.skill;
163
- if (!skill) return;
164
- const file =
165
- process.env.WOLF_SKILL_LOG ?? path.join(PROJECT_ROOT, '.wolf', 'metrics', 'skill-invocations.jsonl');
166
- fs.mkdirSync(path.dirname(file), { recursive: true });
167
- const entry: { ts: string; skill: string; agent?: string } = { ts: new Date().toISOString(), skill };
168
- if (lastAgentId) entry.agent = lastAgentId;
169
- fs.appendFileSync(file, `${JSON.stringify(entry)}\n`);
170
- } catch {
171
- /* fail-safe: падение метрики не роняет сессию */
172
- }
187
+ recordSkillMetric(
188
+ (input as { tool?: string } | null)?.tool,
189
+ (output as { args?: { name?: string; skill?: string } } | null)?.args
190
+ );
173
191
  },
174
192
  };
175
- };
193
+ }
194
+
195
+ // v2-контракт: loader v2 вызывает `setup(ctx)`; инъекция — текст-блок в
196
+ // event.system (мутация доходит до модельного запроса), метрика — tool-hook.
197
+ interface V2ContextEvent {
198
+ system: Array<{ type: 'text'; text: string }>;
199
+ messages: Array<{ id: string; role: string; content: Array<{ type: 'text'; text: string }>; metadata: unknown }>;
200
+ agent: string;
201
+ }
202
+ interface V2ToolEvent {
203
+ tool: string;
204
+ input: unknown;
205
+ id: string;
206
+ }
207
+ interface V2Ctx {
208
+ session: {
209
+ hook(name: 'context', fn: (event: V2ContextEvent) => Promise<void>): Promise<{ dispose: () => Promise<void> }>;
210
+ };
211
+ tool: {
212
+ hook(
213
+ name: 'execute.before',
214
+ fn: (event: V2ToolEvent) => Promise<void>
215
+ ): Promise<{ dispose: () => Promise<void> }>;
216
+ };
217
+ }
218
+
219
+ async function setup(ctx: V2Ctx) {
220
+ ensureSession();
221
+ await ctx.session.hook('context', async (event) => {
222
+ await deliverPlaybook(
223
+ (event?.system ?? []).map((s) => s.text).join('\n'),
224
+ (text) => event.system.push({ type: 'text', text })
225
+ );
226
+ });
227
+ await ctx.tool.hook('execute.before', async (event) => {
228
+ recordSkillMetric(event.tool, event.input);
229
+ });
230
+ }
231
+
232
+ // РОВНО ОДИН export на файл (контракт loader'а opencode): dual v1/v2 entry.
233
+ export default { id: 'wolf-router', setup, server };
@@ -56,16 +56,15 @@ const WORKER_BODY = `## using-skills: пассивный режим воркер
56
56
  /**
57
57
  * Чистая функция инъекции (юнит-тестируемая): по маркеру в транскрипте и
58
58
  * agent-id решает, какое тело доставить. null = инъекция не нужна.
59
- * Fail-safe (m13): битый вход → null, никогда не бросает.
59
+ * Shape-agnostic (v1 parts / v2 content): части сообщения — `m.parts` ИЛИ
60
+ * `m.content`. Fail-safe (m13): битый вход → null, никогда не бросает.
60
61
  */
61
62
  function computeInjection(messages, agentId) {
62
63
  try {
63
64
  if (!Array.isArray(messages)) return null;
64
65
  // H3: идемпотентность — маркер в ЛЮБОМ сообщении транскрипта → не инъекцируем.
65
- const seen = messages.some(
66
- (m) =>
67
- Array.isArray(m?.parts) &&
68
- m.parts.some((p) => p?.type === 'text' && String(p.text ?? '').includes(MARKER))
66
+ const seen = messages.some((m) =>
67
+ blockList(m).some((p) => p?.type === 'text' && String(p.text ?? '').includes(MARKER))
69
68
  );
70
69
  if (seen) return null;
71
70
  // MAJ-4: worker-* → усечённое тело (без диспетчерского контура using-skills).
@@ -95,9 +94,13 @@ const runWolf = (args) => {
95
94
  .catch(() => null);
96
95
  };
97
96
 
97
+ // Блоки контента сообщения: v1 транскрипт — m.parts, v2 context — m.content.
98
+ const blockList = (message) =>
99
+ Array.isArray(message?.parts) ? message.parts : Array.isArray(message?.content) ? message.content : [];
100
+
98
101
  // Topic = первые слова первого пользовательского сообщения, для `call --for`.
99
102
  const topicOf = (message) =>
100
- (message?.parts ?? [])
103
+ blockList(message)
101
104
  .filter((p) => p.type === 'text' && p.text)
102
105
  .map((p) => p.text)
103
106
  .join(' ')
@@ -106,49 +109,94 @@ const topicOf = (message) =>
106
109
  .slice(0, 10)
107
110
  .join(' ');
108
111
 
112
+ /** MAJ-1: БЕЗ кэша на процесс — каждый запуск без маркера пересчитывает
113
+ * recap/call заново (маркер исчез после /clear или compact → снова).
114
+ * L2 (worker-*) секции recap/call не трогает вообще. */
115
+ async function buildSections(body, firstUser, agentId) {
116
+ const sections = [body];
117
+ if (!(agentId ?? '').startsWith('worker-')) {
118
+ const topic = topicOf(firstUser);
119
+ const [recap, call] = await Promise.all([
120
+ runWolf(['recap']),
121
+ topic ? runWolf(['call', '--for', topic, '--compact', '600']) : null,
122
+ ]);
123
+ if (call?.trim()) sections.unshift(`## Active injections\n${call.trim()}`);
124
+ if (recap?.trim()) sections.unshift(`## Recap\n${recap.trim()}`);
125
+ }
126
+ return sections;
127
+ }
128
+
129
+ const wrap = (sections) =>
130
+ `<session_context>\n${MARKER}\n\n${sections.join('\n\n')}\n</session_context>`;
131
+
109
132
  // MAJ-4: уровень сессии читаем из system-промпта (маркер agent-id в теле
110
133
  // рамки агента). Порядок хуков на первый запрос — на совести рантайма;
111
134
  // пока agent-id не виден, дефолт — полный набор (L0/L1, primary-сессии).
112
135
  let currentAgentId = null;
113
136
 
114
- export const WolfSessionStartPlugin = async () => ({
115
- 'experimental.chat.system.transform': async (_input, output) => {
116
- try {
117
- const m = (output?.system ?? []).join('\n').match(AGENT_ID_RE);
118
- currentAgentId = m ? m[1] : null;
119
- } catch {
120
- /* fail-safe */
121
- }
122
- },
123
- 'experimental.chat.messages.transform': async (_input, output) => {
137
+ // 4.C/P204: один WOLF_SESSION на процесс opencode — и v1-, и v2-фабрика
138
+ // ставят ключ только при отсутствии (спавны наследуют process.env).
139
+ function ensureSession() {
140
+ if (!process.env.WOLF_SESSION) process.env.WOLF_SESSION = 'opc-' + randomUUID();
141
+ }
142
+
143
+ // v1-фабрика: loader v1 вызывает `server()` и получает hook-объект.
144
+ async function server() {
145
+ ensureSession();
146
+ return {
147
+ 'experimental.chat.system.transform': async (_input, output) => {
148
+ try {
149
+ const m = (output?.system ?? []).join('\n').match(AGENT_ID_RE);
150
+ currentAgentId = m ? m[1] : null;
151
+ } catch {
152
+ /* fail-safe */
153
+ }
154
+ },
155
+ 'experimental.chat.messages.transform': async (_input, output) => {
156
+ try {
157
+ const messages = output?.messages;
158
+ const body = computeInjection(messages, currentAgentId);
159
+ if (!body) return;
160
+ const firstUser = messages.find((m) => m.info?.role === 'user');
161
+ if (!firstUser?.parts?.length) return;
162
+
163
+ const sections = await buildSections(body, firstUser, currentAgentId);
164
+ const ref = firstUser.parts[0];
165
+ firstUser.parts.unshift({
166
+ ...ref,
167
+ type: 'text',
168
+ text: wrap(sections),
169
+ });
170
+ } catch {
171
+ // Fail-safe (m13): ошибка плагина не имеет права уронить сессию.
172
+ }
173
+ },
174
+ };
175
+ }
176
+
177
+ // v2-контракт: loader v2 вызывает `setup(ctx)`. agent-id — из текстов
178
+ // event.system (маркер рамки), первый user — по m.role, инъекция — text-блок
179
+ // в начало content (мутация доходит до модельного запроса).
180
+ async function setup(ctx) {
181
+ ensureSession();
182
+ await ctx.session.hook('context', async (event) => {
124
183
  try {
125
- const messages = output?.messages;
184
+ const messages = event?.messages;
185
+ const systemText = (event?.system ?? []).map((s) => s?.text ?? '').join('\n');
186
+ const m = systemText.match(AGENT_ID_RE);
187
+ currentAgentId = m ? m[1] : typeof event?.agent === 'string' ? event.agent : null;
126
188
  const body = computeInjection(messages, currentAgentId);
127
189
  if (!body) return;
128
- const firstUser = messages.find((m) => m.info?.role === 'user');
129
- if (!firstUser?.parts?.length) return;
130
-
131
- const sections = [body];
132
- // MAJ-1: БЕЗ кэша на процесс — каждый запуск без маркера пересчитывает
133
- // recap/call заново (маркер исчез после /clear или compact → снова).
134
- if (!(currentAgentId ?? '').startsWith('worker-')) {
135
- const topic = topicOf(firstUser);
136
- const [recap, call] = await Promise.all([
137
- runWolf(['recap']),
138
- topic ? runWolf(['call', '--for', topic, '--compact', '600']) : null,
139
- ]);
140
- if (call?.trim()) sections.unshift(`## Active injections\n${call.trim()}`);
141
- if (recap?.trim()) sections.unshift(`## Recap\n${recap.trim()}`);
142
- }
190
+ const firstUser = Array.isArray(messages) ? messages.find((mm) => mm?.role === 'user') : null;
191
+ if (!Array.isArray(firstUser?.content) || firstUser.content.length === 0) return;
143
192
 
144
- const ref = firstUser.parts[0];
145
- firstUser.parts.unshift({
146
- ...ref,
147
- type: 'text',
148
- text: `<session_context>\n${MARKER}\n\n${sections.join('\n\n')}\n</session_context>`,
149
- });
193
+ const sections = await buildSections(body, firstUser, currentAgentId);
194
+ firstUser.content.unshift({ type: 'text', text: wrap(sections) });
150
195
  } catch {
151
196
  // Fail-safe (m13): ошибка плагина не имеет права уронить сессию.
152
197
  }
153
- },
154
- });
198
+ });
199
+ }
200
+
201
+ // РОВНО ОДИН export на файл (контракт loader'а opencode): dual v1/v2 entry.
202
+ export default { id: 'wolf-session-start', setup, server };