@maintainabilityai/research-runner 0.1.19 → 0.1.22

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.
@@ -0,0 +1,792 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.SKILLS = void 0;
37
+ exports.isSkillName = isSkillName;
38
+ exports.runSkill = runSkill;
39
+ exports.readStdin = readStdin;
40
+ /**
41
+ * skills — CLI subcommand backends for the agentic-SDLC Skills surface
42
+ * declared in `vscode-extension/code-templates/skills/<name>/SKILL.md`.
43
+ *
44
+ * Each skill is a one-shot, stateless handler: read JSON from stdin →
45
+ * validate with zod → do the work → write JSON to stdout. Exits 1 on
46
+ * error with `{ok: false, reason}` payload. This shape mirrors the SKILL.md
47
+ * "Error contract" sections so the calling agent can branch deterministically
48
+ * on `parsed.ok === false`.
49
+ *
50
+ * Why a single file: the registry is small (~12 handlers), each handler is
51
+ * thin (validation + delegate to existing nodes/readers), and keeping
52
+ * them together makes the dispatcher / capability map obvious. If a handler
53
+ * grows past ~150 lines, lift it into its own file under `skills/`.
54
+ *
55
+ * Mesh path resolution: handlers that read mesh state honor `$MESH_PATH`
56
+ * (set by `okr-bus.yml` when it shells out to the agent). Defaults to
57
+ * `process.cwd()` for local dev.
58
+ *
59
+ * Audit event format: `skill-audit-emit-event` writes a new event taxonomy
60
+ * (event_kind: skill_call | llm_call | artifact_written | review_received |
61
+ * state_transition | human_gate) to `okrs/<id>/audit/events/<run>.jsonl`,
62
+ * distinct from the pipeline runner's `node_kind` events. This is the
63
+ * canonical agentic-SDLC audit format per design §11.1.6.
64
+ */
65
+ const node_crypto_1 = require("node:crypto");
66
+ const fs = __importStar(require("node:fs"));
67
+ const path = __importStar(require("node:path"));
68
+ const yaml = __importStar(require("js-yaml"));
69
+ const zod_1 = require("zod");
70
+ const tavily_search_1 = require("./nodes/tavily-search");
71
+ const arxiv_search_1 = require("./nodes/arxiv-search");
72
+ const hackernews_search_1 = require("./nodes/hackernews-search");
73
+ const uspto_search_1 = require("./nodes/uspto-search");
74
+ const dedupe_and_rank_1 = require("./nodes/dedupe-and-rank");
75
+ // ─────────────────────────────────────────────────────────────────────
76
+ // Mesh path resolution
77
+ // ─────────────────────────────────────────────────────────────────────
78
+ function meshPath() {
79
+ return process.env.MESH_PATH || process.cwd();
80
+ }
81
+ // ─────────────────────────────────────────────────────────────────────
82
+ // Knowledge skills — read mesh state, return structured JSON
83
+ // ─────────────────────────────────────────────────────────────────────
84
+ const KnowledgeOkrInput = zod_1.z.object({ okrId: zod_1.z.string().min(1) });
85
+ /**
86
+ * `knowledge-okr` — read `okrs/<id>/okr.yaml` and return the parsed card.
87
+ * Matches OKRService.readRaw shape. We DO NOT enforce the full BTABoK
88
+ * schema here — agents need the data even when the schema is a few keys
89
+ * behind. They can validate downstream if needed.
90
+ */
91
+ const handleKnowledgeOkr = async (input) => {
92
+ const parsed = KnowledgeOkrInput.safeParse(input);
93
+ if (!parsed.success) {
94
+ return { ok: false, reason: `bad-input: ${parsed.error.message}` };
95
+ }
96
+ const yamlPath = path.join(meshPath(), 'okrs', parsed.data.okrId, 'okr.yaml');
97
+ if (!fs.existsSync(yamlPath)) {
98
+ return { ok: false, reason: 'okr-not-found' };
99
+ }
100
+ try {
101
+ const card = yaml.load(fs.readFileSync(yamlPath, 'utf8'));
102
+ return { ok: true, card };
103
+ }
104
+ catch (err) {
105
+ return { ok: false, reason: `yaml-parse-failed: ${err.message}` };
106
+ }
107
+ };
108
+ const KnowledgeMeshBarInput = zod_1.z.object({ barId: zod_1.z.string().min(1) });
109
+ /**
110
+ * Walk platforms/<p>/bars/* looking for an app.yaml whose application.id
111
+ * matches. Cheap on small portfolios. Returns null when not found.
112
+ */
113
+ function findBarDir(mesh, barId) {
114
+ const platformsDir = path.join(mesh, 'platforms');
115
+ if (!fs.existsSync(platformsDir)) {
116
+ return null;
117
+ }
118
+ for (const p of fs.readdirSync(platformsDir, { withFileTypes: true })) {
119
+ if (!p.isDirectory()) {
120
+ continue;
121
+ }
122
+ const barsDir = path.join(platformsDir, p.name, 'bars');
123
+ if (!fs.existsSync(barsDir)) {
124
+ continue;
125
+ }
126
+ for (const b of fs.readdirSync(barsDir, { withFileTypes: true })) {
127
+ if (!b.isDirectory()) {
128
+ continue;
129
+ }
130
+ const candidate = path.join(barsDir, b.name);
131
+ try {
132
+ const app = yaml.load(fs.readFileSync(path.join(candidate, 'app.yaml'), 'utf8'));
133
+ if (app?.application?.id === barId) {
134
+ return { barDir: candidate, platformSlug: p.name };
135
+ }
136
+ }
137
+ catch { /* ignore non-yaml entries */ }
138
+ }
139
+ }
140
+ return null;
141
+ }
142
+ function readYaml(p) {
143
+ try {
144
+ return yaml.load(fs.readFileSync(p, 'utf8'));
145
+ }
146
+ catch {
147
+ return null;
148
+ }
149
+ }
150
+ function readJson(p) {
151
+ try {
152
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
153
+ }
154
+ catch {
155
+ return null;
156
+ }
157
+ }
158
+ function readDirShallow(p) {
159
+ try {
160
+ return fs.readdirSync(p);
161
+ }
162
+ catch {
163
+ return [];
164
+ }
165
+ }
166
+ /**
167
+ * `knowledge-mesh-bar` — return CALM model + threats + ADRs + app.yaml for
168
+ * one BAR. Per the SKILL.md output contract:
169
+ * { id, name, platformId, calmModel, appYaml, repos, adrs, threats,
170
+ * controls, fitnessFunctions, qualityAttributes }
171
+ */
172
+ const handleKnowledgeMeshBar = async (input) => {
173
+ const parsed = KnowledgeMeshBarInput.safeParse(input);
174
+ if (!parsed.success) {
175
+ return { ok: false, reason: `bad-input: ${parsed.error.message}` };
176
+ }
177
+ const found = findBarDir(meshPath(), parsed.data.barId);
178
+ if (!found) {
179
+ return { ok: false, reason: 'bar-not-found' };
180
+ }
181
+ const appYaml = readYaml(path.join(found.barDir, 'app.yaml')) ?? {};
182
+ const calmModel = readJson(path.join(found.barDir, 'architecture', 'bar.arch.json'));
183
+ const threatModel = readYaml(path.join(found.barDir, 'architecture', 'threat-model.yaml'));
184
+ const controls = readYaml(path.join(found.barDir, 'security', 'security-controls.yaml'));
185
+ const fitnessFunctions = readYaml(path.join(found.barDir, 'architecture', 'fitness-functions.yaml'));
186
+ const qualityAttributes = readYaml(path.join(found.barDir, 'architecture', 'quality-attributes.yaml'));
187
+ const adrDir = path.join(found.barDir, 'architecture', 'ADRs');
188
+ const adrs = [];
189
+ for (const name of readDirShallow(adrDir)) {
190
+ if (!name.endsWith('.md')) {
191
+ continue;
192
+ }
193
+ try {
194
+ const body = fs.readFileSync(path.join(adrDir, name), 'utf8');
195
+ const titleMatch = body.match(/^#\s+(.+)/m);
196
+ adrs.push({
197
+ id: name.replace(/\.md$/, ''),
198
+ title: titleMatch?.[1] ?? name,
199
+ body,
200
+ });
201
+ }
202
+ catch { /* skip unreadable */ }
203
+ }
204
+ const app = appYaml.application ?? {};
205
+ return {
206
+ ok: true,
207
+ bar: {
208
+ id: app.id ?? parsed.data.barId,
209
+ name: app.name ?? parsed.data.barId,
210
+ platformId: found.platformSlug,
211
+ calmModel,
212
+ appYaml,
213
+ repos: Array.isArray(app.repos) ? app.repos : [],
214
+ adrs,
215
+ threats: threatModel,
216
+ controls,
217
+ fitnessFunctions,
218
+ qualityAttributes,
219
+ },
220
+ };
221
+ };
222
+ const KnowledgeMeshPlatformInput = zod_1.z.object({ platformId: zod_1.z.string().min(1) });
223
+ /**
224
+ * `knowledge-mesh-platform` — read platform.arch.json + platform.yaml +
225
+ * platform.decisions.yaml + list of child BARs.
226
+ *
227
+ * Platform id resolution: callers pass either the slug (e.g. "imdb") or
228
+ * the PLT-prefixed id (e.g. "PLT-IMDB"). We try both forms.
229
+ */
230
+ const handleKnowledgeMeshPlatform = async (input) => {
231
+ const parsed = KnowledgeMeshPlatformInput.safeParse(input);
232
+ if (!parsed.success) {
233
+ return { ok: false, reason: `bad-input: ${parsed.error.message}` };
234
+ }
235
+ const mesh = meshPath();
236
+ const platformsDir = path.join(mesh, 'platforms');
237
+ if (!fs.existsSync(platformsDir)) {
238
+ return { ok: false, reason: 'platform-not-found' };
239
+ }
240
+ const requested = parsed.data.platformId;
241
+ const slug = requested.toLowerCase().replace(/^plt-/, '');
242
+ const platformDir = path.join(platformsDir, slug);
243
+ if (!fs.existsSync(platformDir)) {
244
+ return { ok: false, reason: 'platform-not-found' };
245
+ }
246
+ const platformYaml = readYaml(path.join(platformDir, 'platform.yaml')) ?? {};
247
+ const calmModel = readJson(path.join(platformDir, 'platform.arch.json'));
248
+ const decisions = readYaml(path.join(platformDir, 'platform.decisions.yaml'));
249
+ const bars = [];
250
+ for (const b of readDirShallow(path.join(platformDir, 'bars'))) {
251
+ const appYaml = readYaml(path.join(platformDir, 'bars', b, 'app.yaml'));
252
+ const app = appYaml?.application;
253
+ if (app?.id) {
254
+ bars.push({ id: app.id, name: app.name ?? app.id });
255
+ }
256
+ }
257
+ return {
258
+ ok: true,
259
+ platform: {
260
+ id: platformYaml.id ?? `PLT-${slug.toUpperCase()}`,
261
+ slug,
262
+ name: platformYaml.name ?? slug,
263
+ calmModel,
264
+ decisions,
265
+ bars,
266
+ },
267
+ };
268
+ };
269
+ const KnowledgeMeshThreatsInput = zod_1.z.object({
270
+ concern: zod_1.z.string().min(1),
271
+ maxResults: zod_1.z.number().int().positive().optional(),
272
+ });
273
+ /**
274
+ * Walk every `<bar>/architecture/threat-model.yaml` AND any top-level
275
+ * `threats/` library, collect entries, return those whose tags / category /
276
+ * description match the concern keyword (case-insensitive substring).
277
+ */
278
+ const handleKnowledgeMeshThreats = async (input) => {
279
+ const parsed = KnowledgeMeshThreatsInput.safeParse(input);
280
+ if (!parsed.success) {
281
+ return { ok: false, reason: `bad-input: ${parsed.error.message}` };
282
+ }
283
+ const mesh = meshPath();
284
+ const concern = parsed.data.concern.toLowerCase();
285
+ const maxResults = parsed.data.maxResults ?? 20;
286
+ const out = [];
287
+ // Top-level threats library (optional convention)
288
+ const libDir = path.join(mesh, 'threats');
289
+ for (const name of readDirShallow(libDir)) {
290
+ if (!name.endsWith('.yaml') && !name.endsWith('.yml')) {
291
+ continue;
292
+ }
293
+ const data = readYaml(path.join(libDir, name));
294
+ const list = Array.isArray(data) ? data : data?.threats;
295
+ if (Array.isArray(list)) {
296
+ out.push(...list);
297
+ }
298
+ }
299
+ // Per-BAR threat models
300
+ const platformsDir = path.join(mesh, 'platforms');
301
+ for (const p of readDirShallow(platformsDir)) {
302
+ const barsDir = path.join(platformsDir, p, 'bars');
303
+ for (const b of readDirShallow(barsDir)) {
304
+ const tm = readYaml(path.join(barsDir, b, 'architecture', 'threat-model.yaml'));
305
+ if (Array.isArray(tm?.threats)) {
306
+ out.push(...tm.threats);
307
+ }
308
+ }
309
+ }
310
+ const filtered = out.filter(t => {
311
+ const hay = JSON.stringify(t).toLowerCase();
312
+ return hay.includes(concern);
313
+ }).slice(0, maxResults);
314
+ return { ok: true, threats: filtered };
315
+ };
316
+ const KnowledgeMeshAdrsInput = zod_1.z.object({
317
+ concern: zod_1.z.string().min(1),
318
+ scope: zod_1.z.object({
319
+ platformId: zod_1.z.string().optional(),
320
+ barIds: zod_1.z.array(zod_1.z.string()).optional(),
321
+ }).optional(),
322
+ maxResults: zod_1.z.number().int().positive().optional(),
323
+ });
324
+ /**
325
+ * Walk every `<bar>/architecture/ADRs/*.md`, optionally filtered by
326
+ * platform / BAR scope, return entries whose title/body matches the
327
+ * concern (case-insensitive substring).
328
+ */
329
+ const handleKnowledgeMeshAdrs = async (input) => {
330
+ const parsed = KnowledgeMeshAdrsInput.safeParse(input);
331
+ if (!parsed.success) {
332
+ return { ok: false, reason: `bad-input: ${parsed.error.message}` };
333
+ }
334
+ const mesh = meshPath();
335
+ const concern = parsed.data.concern.toLowerCase();
336
+ const maxResults = parsed.data.maxResults ?? 20;
337
+ const barFilter = parsed.data.scope?.barIds ? new Set(parsed.data.scope.barIds) : null;
338
+ const platformFilter = parsed.data.scope?.platformId?.toLowerCase().replace(/^plt-/, '') ?? null;
339
+ const out = [];
340
+ const platformsDir = path.join(mesh, 'platforms');
341
+ for (const p of readDirShallow(platformsDir)) {
342
+ if (platformFilter && p.toLowerCase() !== platformFilter) {
343
+ continue;
344
+ }
345
+ const barsDir = path.join(platformsDir, p, 'bars');
346
+ for (const b of readDirShallow(barsDir)) {
347
+ const appYaml = readYaml(path.join(barsDir, b, 'app.yaml'));
348
+ const barId = appYaml?.application?.id ?? b;
349
+ if (barFilter && !barFilter.has(barId)) {
350
+ continue;
351
+ }
352
+ const adrDir = path.join(barsDir, b, 'architecture', 'ADRs');
353
+ for (const name of readDirShallow(adrDir)) {
354
+ if (!name.endsWith('.md')) {
355
+ continue;
356
+ }
357
+ try {
358
+ const body = fs.readFileSync(path.join(adrDir, name), 'utf8');
359
+ if (!body.toLowerCase().includes(concern)) {
360
+ continue;
361
+ }
362
+ const titleMatch = body.match(/^#\s+(.+)/m);
363
+ const statusMatch = body.match(/^##\s+Status\s*\n+\s*(\S+)/im);
364
+ out.push({
365
+ id: name.replace(/\.md$/, ''),
366
+ title: (titleMatch?.[1] ?? name).trim(),
367
+ status: (statusMatch?.[1] ?? 'unknown').trim(),
368
+ tags: [],
369
+ body,
370
+ barId,
371
+ });
372
+ }
373
+ catch { /* skip */ }
374
+ }
375
+ }
376
+ }
377
+ return { ok: true, adrs: out.slice(0, maxResults) };
378
+ };
379
+ const KnowledgeResearchInput = zod_1.z.object({ okrId: zod_1.z.string().min(1) });
380
+ /**
381
+ * `knowledge-research` — read `okrs/<id>/why/research-doc.md` and surface
382
+ * the parsed structure (R-N findings + Whitespace + References).
383
+ *
384
+ * Parse strategy: the synthesis prompt-pack writes deterministic section
385
+ * headings. We extract by regex; if the doc doesn't follow the schema,
386
+ * we return the raw body so the PRD agent can still reason about it.
387
+ */
388
+ const handleKnowledgeResearch = async (input) => {
389
+ const parsed = KnowledgeResearchInput.safeParse(input);
390
+ if (!parsed.success) {
391
+ return { ok: false, reason: `bad-input: ${parsed.error.message}` };
392
+ }
393
+ const docPath = path.join(meshPath(), 'okrs', parsed.data.okrId, 'why', 'research-doc.md');
394
+ if (!fs.existsSync(docPath)) {
395
+ return { ok: false, reason: 'research-not-merged-yet' };
396
+ }
397
+ const body = fs.readFileSync(docPath, 'utf8');
398
+ /**
399
+ * Split by R-N headings rather than regex-capture the block — JS regex
400
+ * lacks `\Z` and the lookahead-for-end-of-input dance is error-prone.
401
+ * Walk line-by-line, accumulate into the current finding's block.
402
+ */
403
+ const findings = [];
404
+ const lines = body.split('\n');
405
+ let current = null;
406
+ const flush = () => {
407
+ if (!current) {
408
+ return;
409
+ }
410
+ const blockText = current.block.join('\n');
411
+ const supporting = [...blockText.matchAll(/^\s*-\s*(?:Supporting|S):\s*(.+)$/gm)].map(x => x[1].trim());
412
+ const contradicting = [...blockText.matchAll(/^\s*-\s*(?:Contradicting|C):\s*(.+)$/gm)].map(x => x[1].trim());
413
+ const confidenceMatch = blockText.match(/Confidence:\s*(HIGH|MEDIUM|LOW)/i);
414
+ findings.push({
415
+ id: current.id,
416
+ title: current.title,
417
+ supporting,
418
+ contradicting,
419
+ confidence: confidenceMatch?.[1].toUpperCase() ?? 'MEDIUM',
420
+ });
421
+ current = null;
422
+ };
423
+ for (const line of lines) {
424
+ const startMatch = line.match(/^###\s+(R-\d+)\s+(.+?)\s*$/);
425
+ if (startMatch) {
426
+ flush();
427
+ current = { id: startMatch[1], title: startMatch[2].trim(), block: [] };
428
+ continue;
429
+ }
430
+ if (/^##\s/.test(line)) {
431
+ flush();
432
+ }
433
+ if (current) {
434
+ current.block.push(line);
435
+ }
436
+ }
437
+ flush();
438
+ /** Pull bullets out of a labelled `## Section` until the next `## ` or EOF. */
439
+ const pullBullets = (sectionName) => {
440
+ const out = [];
441
+ let inSection = false;
442
+ for (const line of lines) {
443
+ if (new RegExp(`^##\\s+${sectionName}\\b`, 'i').test(line)) {
444
+ inSection = true;
445
+ continue;
446
+ }
447
+ if (inSection && /^##\s/.test(line)) {
448
+ break;
449
+ }
450
+ if (!inSection) {
451
+ continue;
452
+ }
453
+ const bullet = line.match(/^\s*-\s*(.+?)\s*$/);
454
+ if (bullet) {
455
+ out.push(bullet[1]);
456
+ }
457
+ }
458
+ return out;
459
+ };
460
+ const whitespace = pullBullets('Whitespace');
461
+ const references = pullBullets('References');
462
+ return { ok: true, findings, whitespace, references, rawBody: body };
463
+ };
464
+ // ─────────────────────────────────────────────────────────────────────
465
+ // Search skills — thin wrappers over the existing search nodes
466
+ // ─────────────────────────────────────────────────────────────────────
467
+ const SearchQueriesInput = zod_1.z.object({
468
+ queries: zod_1.z.array(zod_1.z.string().min(1)).min(1),
469
+ maxResults: zod_1.z.number().int().positive().optional(),
470
+ });
471
+ const handleTavilySearch = async (input) => {
472
+ const parsed = SearchQueriesInput.safeParse(input);
473
+ if (!parsed.success) {
474
+ return { ok: false, reason: `bad-input: ${parsed.error.message}` };
475
+ }
476
+ const apiKey = process.env.TAVILY_API_KEY;
477
+ if (!apiKey) {
478
+ return { ok: false, reason: 'tavily-api-key-missing' };
479
+ }
480
+ try {
481
+ const res = await (0, tavily_search_1.runTavilySearch)({
482
+ apiKey,
483
+ queries: parsed.data.queries,
484
+ maxResultsPerQuery: parsed.data.maxResults,
485
+ });
486
+ return { ok: true, envelopes: res.envelopes, results: res.results };
487
+ }
488
+ catch (err) {
489
+ return { ok: false, reason: `tavily-failed: ${err.message}` };
490
+ }
491
+ };
492
+ const handleArxivSearch = async (input) => {
493
+ const parsed = SearchQueriesInput.safeParse(input);
494
+ if (!parsed.success) {
495
+ return { ok: false, reason: `bad-input: ${parsed.error.message}` };
496
+ }
497
+ try {
498
+ const res = await (0, arxiv_search_1.runArxivSearch)({
499
+ queries: parsed.data.queries,
500
+ maxResultsPerQuery: parsed.data.maxResults,
501
+ });
502
+ return { ok: true, envelopes: res.envelopes, results: res.results };
503
+ }
504
+ catch (err) {
505
+ return { ok: false, reason: `arxiv-failed: ${err.message}` };
506
+ }
507
+ };
508
+ const handleUsptoSearch = async (input) => {
509
+ const parsed = SearchQueriesInput.safeParse(input);
510
+ if (!parsed.success) {
511
+ return { ok: false, reason: `bad-input: ${parsed.error.message}` };
512
+ }
513
+ const apiKey = process.env.USPTO_API_KEY;
514
+ if (!apiKey) {
515
+ return { ok: false, reason: 'uspto-api-key-missing' };
516
+ }
517
+ try {
518
+ const res = await (0, uspto_search_1.runUsptoSearch)({
519
+ apiKey,
520
+ queries: parsed.data.queries,
521
+ maxResultsPerQuery: parsed.data.maxResults,
522
+ });
523
+ return { ok: true, envelopes: res.envelopes, results: res.results };
524
+ }
525
+ catch (err) {
526
+ return { ok: false, reason: `uspto-failed: ${err.message}` };
527
+ }
528
+ };
529
+ const handleHackerNewsSearch = async (input) => {
530
+ const parsed = SearchQueriesInput.safeParse(input);
531
+ if (!parsed.success) {
532
+ return { ok: false, reason: `bad-input: ${parsed.error.message}` };
533
+ }
534
+ try {
535
+ const res = await (0, hackernews_search_1.runHackerNewsSearch)({
536
+ queries: parsed.data.queries,
537
+ hitsPerQuery: parsed.data.maxResults,
538
+ });
539
+ return { ok: true, envelopes: res.envelopes, results: res.results };
540
+ }
541
+ catch (err) {
542
+ return { ok: false, reason: `hackernews-failed: ${err.message}` };
543
+ }
544
+ };
545
+ // ─────────────────────────────────────────────────────────────────────
546
+ // Pure skills — dedupe + format
547
+ // ─────────────────────────────────────────────────────────────────────
548
+ const ProviderResultSchema = zod_1.z.object({
549
+ provider: zod_1.z.string(),
550
+ fromQuery: zod_1.z.string(),
551
+ title: zod_1.z.string(),
552
+ url: zod_1.z.string(),
553
+ content: zod_1.z.string(),
554
+ score: zod_1.z.number(),
555
+ publishedDate: zod_1.z.string().optional(),
556
+ authors: zod_1.z.array(zod_1.z.string()).optional(),
557
+ });
558
+ const DedupeAndRankInput = zod_1.z.object({
559
+ results: zod_1.z.array(zod_1.z.array(ProviderResultSchema)),
560
+ topN: zod_1.z.number().int().positive().optional(),
561
+ });
562
+ const handleDedupeAndRank = async (input) => {
563
+ const parsed = DedupeAndRankInput.safeParse(input);
564
+ if (!parsed.success) {
565
+ return { ok: false, reason: `bad-input: ${parsed.error.message}` };
566
+ }
567
+ const flat = parsed.data.results.flat();
568
+ const ranked = (0, dedupe_and_rank_1.dedupeAndRank)({ results: flat, topN: parsed.data.topN ?? 50 });
569
+ const providerCounts = {};
570
+ for (const r of ranked) {
571
+ providerCounts[r.provider] = (providerCounts[r.provider] ?? 0) + 1;
572
+ }
573
+ return { ok: true, rankedSources: ranked, providerCounts };
574
+ };
575
+ const RankedSourceSchema = zod_1.z.object({
576
+ id: zod_1.z.string(),
577
+ provider: zod_1.z.string(),
578
+ title: zod_1.z.string(),
579
+ url: zod_1.z.string(),
580
+ retrieved_at: zod_1.z.string(),
581
+ salience_score: zod_1.z.number(),
582
+ excerpt: zod_1.z.string(),
583
+ published_at: zod_1.z.string().optional(),
584
+ authors: zod_1.z.array(zod_1.z.string()).optional(),
585
+ });
586
+ const FormatIssueUpdateInput = zod_1.z.object({
587
+ topic: zod_1.z.string(),
588
+ runId: zod_1.z.string(),
589
+ rankedSources: zod_1.z.array(RankedSourceSchema),
590
+ providerCounts: zod_1.z.record(zod_1.z.string(), zod_1.z.number()),
591
+ gapSignals: zod_1.z.array(zod_1.z.string()).optional(),
592
+ meshContext: zod_1.z.object({
593
+ platformId: zod_1.z.string().optional(),
594
+ barIds: zod_1.z.array(zod_1.z.string()).optional(),
595
+ }),
596
+ });
597
+ const COMMENT_BYTE_CAP = 60_000;
598
+ /**
599
+ * `format-research-issue-update` — render the OKR issue comment that the
600
+ * market-research-agent posts after each iteration. Pure markdown; no LLM.
601
+ * Truncates with a footer when over 60kB (GitHub issue cap is ~65k).
602
+ */
603
+ const handleFormatResearchIssueUpdate = async (input) => {
604
+ const parsed = FormatIssueUpdateInput.safeParse(input);
605
+ if (!parsed.success) {
606
+ return { ok: false, reason: `bad-input: ${parsed.error.message}` };
607
+ }
608
+ const { topic, runId, rankedSources, providerCounts, gapSignals = [], meshContext } = parsed.data;
609
+ const lines = [];
610
+ lines.push(`## 🔍 Market research update — ${topic}`);
611
+ lines.push('');
612
+ lines.push(`Run \`${runId}\` — platform \`${meshContext.platformId ?? '—'}\`, BARs \`${(meshContext.barIds ?? []).join(', ') || '—'}\`.`);
613
+ lines.push('');
614
+ lines.push('| Provider | Ranked |');
615
+ lines.push('|---|---:|');
616
+ for (const [provider, count] of Object.entries(providerCounts)) {
617
+ lines.push(`| ${provider} | ${count} |`);
618
+ }
619
+ lines.push('');
620
+ if (gapSignals.length > 0) {
621
+ lines.push('### Gap signals');
622
+ lines.push('');
623
+ for (const g of gapSignals) {
624
+ lines.push(`- \`${g}\``);
625
+ }
626
+ lines.push('');
627
+ }
628
+ lines.push('### Top-ranked sources');
629
+ lines.push('');
630
+ for (const s of rankedSources) {
631
+ const date = s.published_at ? ` _(${s.published_at.slice(0, 10)})_` : '';
632
+ lines.push(`- \`${s.id}\` **[${s.title}](${s.url})** — ${s.provider}, score ${s.salience_score.toFixed(2)}${date}`);
633
+ if (s.excerpt) {
634
+ lines.push(` > ${s.excerpt.replace(/\s+/g, ' ').trim().slice(0, 400)}`);
635
+ }
636
+ }
637
+ let markdown = lines.join('\n');
638
+ let byteCount = Buffer.byteLength(markdown, 'utf8');
639
+ if (byteCount > COMMENT_BYTE_CAP) {
640
+ markdown = markdown.slice(0, COMMENT_BYTE_CAP) + '\n\n> _Truncated — original exceeded GitHub issue-comment byte cap._';
641
+ byteCount = Buffer.byteLength(markdown, 'utf8');
642
+ }
643
+ return { ok: true, markdown, byteCount };
644
+ };
645
+ // ─────────────────────────────────────────────────────────────────────
646
+ // Audit skill — hash-chained JSONL append, cross-process-safe
647
+ // ─────────────────────────────────────────────────────────────────────
648
+ const AuditEmitInput = zod_1.z.object({
649
+ okrId: zod_1.z.string().min(1),
650
+ runId: zod_1.z.string().min(1),
651
+ eventKind: zod_1.z.enum(['skill_call', 'llm_call', 'artifact_written', 'review_received', 'state_transition', 'human_gate']),
652
+ payload: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()),
653
+ phase: zod_1.z.enum(['why', 'how', 'what']),
654
+ intentThreadUuid: zod_1.z.string().min(1),
655
+ });
656
+ const LOCK_RETRY_LIMIT = 3;
657
+ const LOCK_RETRY_BASE_MS = 50;
658
+ /** Recursive key-sorted JSON stringify so the event hash is canonical. */
659
+ function canonicalStringify(value) {
660
+ if (value === null || typeof value !== 'object') {
661
+ return JSON.stringify(value);
662
+ }
663
+ if (Array.isArray(value)) {
664
+ return '[' + value.map(canonicalStringify).join(',') + ']';
665
+ }
666
+ const obj = value;
667
+ const keys = Object.keys(obj).sort();
668
+ return '{' + keys.map(k => JSON.stringify(k) + ':' + canonicalStringify(obj[k])).join(',') + '}';
669
+ }
670
+ function sha256(text) {
671
+ return (0, node_crypto_1.createHash)('sha256').update(text, 'utf8').digest('hex');
672
+ }
673
+ async function sleep(ms) {
674
+ return new Promise(resolve => setTimeout(resolve, ms));
675
+ }
676
+ /**
677
+ * `audit-emit-event` — append one hash-chained event to
678
+ * `<mesh>/okrs/<id>/audit/events/<runId>.jsonl`.
679
+ *
680
+ * Cross-process serialization: we use an exclusive-create lock file
681
+ * (`<jsonl>.lock`) with bounded retries. Each call reads the existing
682
+ * tail, computes prev_event_hash + event_id, writes the new line, then
683
+ * releases the lock. On terminal contention returns `{ok: false,
684
+ * reason: 'audit-write-failed-after-retries'}` per the SKILL.md
685
+ * contract — agents treat this as non-blocking.
686
+ */
687
+ const handleAuditEmitEvent = async (input) => {
688
+ const parsed = AuditEmitInput.safeParse(input);
689
+ if (!parsed.success) {
690
+ return { ok: false, reason: `bad-input: ${parsed.error.message}` };
691
+ }
692
+ const { okrId, runId, eventKind, payload, phase, intentThreadUuid } = parsed.data;
693
+ const dir = path.join(meshPath(), 'okrs', okrId, 'audit', 'events');
694
+ fs.mkdirSync(dir, { recursive: true });
695
+ const filePath = path.join(dir, `${runId}.jsonl`);
696
+ const lockPath = `${filePath}.lock`;
697
+ for (let attempt = 0; attempt < LOCK_RETRY_LIMIT; attempt++) {
698
+ let lockFd = null;
699
+ try {
700
+ lockFd = fs.openSync(lockPath, 'wx');
701
+ }
702
+ catch (err) {
703
+ if (err.code === 'EEXIST') {
704
+ await sleep(LOCK_RETRY_BASE_MS * (attempt + 1));
705
+ continue;
706
+ }
707
+ return { ok: false, reason: `audit-lock-failed: ${err.message}` };
708
+ }
709
+ try {
710
+ let prevHash = null;
711
+ let nextEventId = 1;
712
+ if (fs.existsSync(filePath)) {
713
+ const existing = fs.readFileSync(filePath, 'utf8').split('\n').filter(l => l.trim().length > 0);
714
+ if (existing.length > 0) {
715
+ const last = JSON.parse(existing[existing.length - 1]);
716
+ prevHash = last.event_hash;
717
+ nextEventId = last.event_id + 1;
718
+ }
719
+ }
720
+ const draft = {
721
+ event_id: nextEventId,
722
+ ts: new Date().toISOString(),
723
+ okr_id: okrId,
724
+ run_id: runId,
725
+ intent_thread_uuid: intentThreadUuid,
726
+ phase,
727
+ event_kind: eventKind,
728
+ payload,
729
+ prev_event_hash: prevHash,
730
+ event_hash: '',
731
+ };
732
+ const hash = sha256(canonicalStringify(draft));
733
+ const finalEvent = { ...draft, event_hash: hash };
734
+ fs.appendFileSync(filePath, JSON.stringify(finalEvent) + '\n', 'utf8');
735
+ return { ok: true, chainHead: hash, eventId: nextEventId };
736
+ }
737
+ finally {
738
+ if (lockFd !== null) {
739
+ fs.closeSync(lockFd);
740
+ }
741
+ try {
742
+ fs.unlinkSync(lockPath);
743
+ }
744
+ catch { /* lock already gone */ }
745
+ }
746
+ }
747
+ return { ok: false, reason: 'audit-write-failed-after-retries' };
748
+ };
749
+ // ─────────────────────────────────────────────────────────────────────
750
+ // Registry + dispatcher
751
+ // ─────────────────────────────────────────────────────────────────────
752
+ exports.SKILLS = {
753
+ 'knowledge-okr': handleKnowledgeOkr,
754
+ 'knowledge-mesh-bar': handleKnowledgeMeshBar,
755
+ 'knowledge-mesh-platform': handleKnowledgeMeshPlatform,
756
+ 'knowledge-mesh-threats': handleKnowledgeMeshThreats,
757
+ 'knowledge-mesh-adrs': handleKnowledgeMeshAdrs,
758
+ 'knowledge-research': handleKnowledgeResearch,
759
+ 'tavily-search': handleTavilySearch,
760
+ 'arxiv-search': handleArxivSearch,
761
+ 'uspto-search': handleUsptoSearch,
762
+ 'hackernews-search': handleHackerNewsSearch,
763
+ 'dedupe-and-rank': handleDedupeAndRank,
764
+ 'format-research-issue-update': handleFormatResearchIssueUpdate,
765
+ 'audit-emit-event': handleAuditEmitEvent,
766
+ };
767
+ function isSkillName(name) {
768
+ return Object.prototype.hasOwnProperty.call(exports.SKILLS, name);
769
+ }
770
+ async function runSkill(name, input) {
771
+ const handler = exports.SKILLS[name];
772
+ if (!handler) {
773
+ return { ok: false, reason: `unknown-skill: ${name}` };
774
+ }
775
+ return handler(input);
776
+ }
777
+ /**
778
+ * Read all of stdin as a UTF-8 string. Returns '' immediately on TTY
779
+ * (no piped input) — handlers will reject via zod with a helpful message.
780
+ */
781
+ async function readStdin() {
782
+ if (process.stdin.isTTY) {
783
+ return '';
784
+ }
785
+ return new Promise((resolve, reject) => {
786
+ let data = '';
787
+ process.stdin.setEncoding('utf8');
788
+ process.stdin.on('data', chunk => { data += chunk; });
789
+ process.stdin.on('end', () => resolve(data));
790
+ process.stdin.on('error', reject);
791
+ });
792
+ }