@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.
- package/dist/cli.js +38 -0
- package/dist/runner/court-recorder.d.ts +52 -0
- package/dist/runner/court-recorder.js +77 -0
- package/dist/runner/hatters-tag-builder.d.ts +78 -0
- package/dist/runner/hatters-tag-builder.js +58 -0
- package/dist/runner/skills.d.ts +20 -0
- package/dist/runner/skills.js +792 -0
- package/dist/schemas/audit-event.d.ts +144 -0
- package/dist/schemas/audit-event.js +11 -0
- package/package.json +1 -1
|
@@ -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
|
+
}
|