@maintainabilityai/research-runner 0.1.20 → 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 CHANGED
@@ -48,6 +48,7 @@ const fs = __importStar(require("node:fs"));
48
48
  const path = __importStar(require("node:path"));
49
49
  const archeologist_1 = require("./runner/archeologist");
50
50
  const prd_1 = require("./runner/prd");
51
+ const skills_1 = require("./runner/skills");
51
52
  const PKG = JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf8'));
52
53
  function parseFlags(argv) {
53
54
  const flags = {};
@@ -191,17 +192,54 @@ async function prdCmd(argv) {
191
192
  });
192
193
  }
193
194
  function help() {
195
+ const skillNames = Object.keys(skills_1.SKILLS).map(n => `skill-${n}`).sort().join('\n ');
194
196
  process.stdout.write(`research-runner v${PKG.version}
195
197
 
196
198
  Usage:
197
199
  research-runner archeologist --brief "<topic>" --scope-level <platform|bar> --scope-id ID [--path research|archaeology] [...]
198
200
  research-runner prd --research-pr <url|path> --scope-level <platform|bar> --scope-id ID [...]
201
+ research-runner skill-<name> # one-shot skill subcommand; reads JSON from stdin, writes JSON to stdout
202
+
203
+ Skills (called by .agent.md tools: declarations under \$MESH_PATH):
204
+ ${skillNames}
199
205
 
200
206
  See README.md for the full flag surface.
201
207
  `);
202
208
  }
209
+ /**
210
+ * skill-* dispatcher. Reads a JSON object from stdin, calls runSkill,
211
+ * writes the result as JSON to stdout. Exits non-zero on `ok: false` so
212
+ * the calling agent (or shell wrapper) can detect failure via exit code
213
+ * in addition to the structured `{ok: false, reason}` payload.
214
+ */
215
+ async function skillCmd(skillName) {
216
+ if (!(0, skills_1.isSkillName)(skillName)) {
217
+ process.stdout.write(JSON.stringify({ ok: false, reason: `unknown-skill: ${skillName}` }) + '\n');
218
+ process.exit(1);
219
+ }
220
+ const stdinRaw = await (0, skills_1.readStdin)();
221
+ let input = {};
222
+ if (stdinRaw.trim().length > 0) {
223
+ try {
224
+ input = JSON.parse(stdinRaw);
225
+ }
226
+ catch (err) {
227
+ process.stdout.write(JSON.stringify({ ok: false, reason: `bad-stdin-json: ${err.message}` }) + '\n');
228
+ process.exit(1);
229
+ }
230
+ }
231
+ const result = await (0, skills_1.runSkill)(skillName, input);
232
+ process.stdout.write(JSON.stringify(result) + '\n');
233
+ if (result.ok === false) {
234
+ process.exit(1);
235
+ }
236
+ }
203
237
  async function main() {
204
238
  const [, , subcommand, ...rest] = process.argv;
239
+ if (subcommand && subcommand.startsWith('skill-')) {
240
+ await skillCmd(subcommand.slice('skill-'.length));
241
+ return;
242
+ }
205
243
  switch (subcommand) {
206
244
  case 'archeologist':
207
245
  await archeologistCmd(rest);
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Strictly the fields a CloudEvents v1.0 envelope requires. Optional
3
+ * fields per the spec (datacontenttype, dataschema, subject, time) are
4
+ * folded in via `CloudEventsOptional`.
5
+ *
6
+ * See https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md
7
+ */
8
+ export interface CloudEventsEnvelope<T = Record<string, unknown>> {
9
+ /** CloudEvents spec version. Fixed for our emitter. */
10
+ specversion: '1.0';
11
+ /** Reverse-DNS event-source identifier — e.g. `maintainabilityai/research-runner`. */
12
+ source: string;
13
+ /** Event type — maps to `audit-emitter` event_kind plus an `mai.` prefix. */
14
+ type: string;
15
+ /** Unique id — UUID v4 generated by the emitter. */
16
+ id: string;
17
+ /** ISO timestamp when the event was emitted. */
18
+ time: string;
19
+ /** MIME type of `data`. Always `application/json` for our emitter. */
20
+ datacontenttype: 'application/json';
21
+ /** OKR phase that contextualizes this event. */
22
+ subject: string;
23
+ /** The raw audit-emitter event payload. */
24
+ data: T;
25
+ }
26
+ export interface CourtRecorderInput<T> {
27
+ /** Reverse-DNS source identifier — e.g. `maintainabilityai/research-runner`. */
28
+ source: string;
29
+ /** Event kind from `audit-emitter` — wrapped into `mai.<kind>` for CE `type`. */
30
+ eventKind: string;
31
+ /** OKR phase (`why` | `how` | `what` | `setup` etc.) → goes in `subject`. */
32
+ phase: string;
33
+ /** The original audit-emitter event payload. Becomes `data`. */
34
+ payload: T;
35
+ /** Override timestamp (defaults to `new Date().toISOString()`). */
36
+ time?: string;
37
+ /** Override id (defaults to `crypto.randomUUID()`). */
38
+ id?: string;
39
+ }
40
+ /**
41
+ * Build a single CloudEvents v1.0 envelope around an audit-event payload.
42
+ * Stateless + deterministic given an injected `id` + `time` — handy for
43
+ * unit tests.
44
+ */
45
+ export declare function buildCloudEventsEnvelope<T>(input: CourtRecorderInput<T>): CloudEventsEnvelope<T>;
46
+ /**
47
+ * Serialize an envelope to a single JSONL line (NO trailing newline; the
48
+ * caller appends `\n` when writing). Throws if the envelope can't be
49
+ * round-tripped through JSON.stringify — that signals a non-serializable
50
+ * payload, which is a programmer bug, not a runtime condition.
51
+ */
52
+ export declare function serializeCloudEventsEnvelope<T>(env: CloudEventsEnvelope<T>): string;
@@ -0,0 +1,77 @@
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.buildCloudEventsEnvelope = buildCloudEventsEnvelope;
37
+ exports.serializeCloudEventsEnvelope = serializeCloudEventsEnvelope;
38
+ /**
39
+ * Court Recorder — CloudEvents v1.0 envelope emitter for the agentic-SDLC
40
+ * audit chain. Wraps `audit-emitter`'s raw event payload in a CloudEvents
41
+ * 1.0 envelope so the resulting JSONL is SIEM-compatible without further
42
+ * transformation.
43
+ *
44
+ * Phase B-PR4 ships the emitter shape; agents will adopt it in their
45
+ * `audit-emit-event` Skill backend (B-PR1a). Each line of
46
+ * `okrs/<id>/audit/events/<run-id>.jsonl` is a fully-valid CloudEvents
47
+ * envelope whose `data` property carries the original audit-event JSON.
48
+ *
49
+ * Spec: vscode-extension/design/agentic-sdlc.md §11 (Court Recorder).
50
+ */
51
+ const crypto = __importStar(require("node:crypto"));
52
+ /**
53
+ * Build a single CloudEvents v1.0 envelope around an audit-event payload.
54
+ * Stateless + deterministic given an injected `id` + `time` — handy for
55
+ * unit tests.
56
+ */
57
+ function buildCloudEventsEnvelope(input) {
58
+ return {
59
+ specversion: '1.0',
60
+ source: input.source,
61
+ type: `mai.${input.eventKind}`,
62
+ id: input.id ?? crypto.randomUUID(),
63
+ time: input.time ?? new Date().toISOString(),
64
+ datacontenttype: 'application/json',
65
+ subject: input.phase,
66
+ data: input.payload,
67
+ };
68
+ }
69
+ /**
70
+ * Serialize an envelope to a single JSONL line (NO trailing newline; the
71
+ * caller appends `\n` when writing). Throws if the envelope can't be
72
+ * round-tripped through JSON.stringify — that signals a non-serializable
73
+ * payload, which is a programmer bug, not a runtime condition.
74
+ */
75
+ function serializeCloudEventsEnvelope(env) {
76
+ return JSON.stringify(env);
77
+ }
@@ -55,6 +55,35 @@ export interface HattersTagAttestation {
55
55
  security?: number | null;
56
56
  };
57
57
  }
58
+ /**
59
+ * Evidence-mode block (v4 §11.1.7) — forces authors to be honest about
60
+ * whether the artifact's citations came from a fresh provider search or
61
+ * from cached/reused sources.
62
+ *
63
+ * Why this exists: the first round of agentic runs (run #21) loaded the
64
+ * SKILL.md context but had no live skill backends, so the agent silently
65
+ * fell back to reading repo files and produced an updated artifact that
66
+ * LOOKED grounded but cited zero live signals. The validator workflow
67
+ * (B-PR1c) cross-checks `fresh_provider_search_performed === true`
68
+ * against the audit JSONL's `skill_call` events for the four search
69
+ * providers — mismatch ⇒ `degraded-evidence` label ⇒ governance-pass
70
+ * promotion blocked.
71
+ *
72
+ * live — every cited source came from a fresh provider call this run
73
+ * cached — agent reused prior research without re-running providers
74
+ * mixed — some cited sources are fresh, some carried forward
75
+ *
76
+ * `degraded_reason` is REQUIRED on `cached` / `mixed` so the gate can
77
+ * surface a human-readable cause (e.g. "tavily-skill-backend-missing",
78
+ * "rate-limited", "rerun-after-review").
79
+ */
80
+ export interface HattersTagEvidence {
81
+ evidence_mode: 'live' | 'cached' | 'mixed';
82
+ /** True iff at least one of the four search providers (tavily/arxiv/uspto/hackernews) was successfully called this run. */
83
+ fresh_provider_search_performed: boolean;
84
+ /** Free-text cause when evidence_mode !== 'live'. */
85
+ degraded_reason?: string;
86
+ }
58
87
  export interface HattersTagInput {
59
88
  run_id: string;
60
89
  /** Git SHA of the mesh repo at run start. */
@@ -94,6 +123,8 @@ export interface HattersTagInput {
94
123
  okr?: HattersTagOkrContext;
95
124
  /** v4: Phase B+ agent runs populate this; legacy runs omit it. */
96
125
  attestation?: HattersTagAttestation;
126
+ /** v4 §11.1.7: agentic runs populate this; legacy CI runs omit it. */
127
+ evidence?: HattersTagEvidence;
97
128
  }
98
129
  /** Build the full Hatter's Tag block, including heading + fenced YAML. */
99
130
  export declare function buildHattersTag(input: HattersTagInput): string;
@@ -75,6 +75,18 @@ function buildHattersTag(input) {
75
75
  }
76
76
  }
77
77
  }
78
+ if (input.evidence) {
79
+ lines.push('evidence:');
80
+ lines.push(` evidence_mode: ${input.evidence.evidence_mode}`);
81
+ lines.push(` fresh_provider_search_performed: ${input.evidence.fresh_provider_search_performed}`);
82
+ if (input.evidence.degraded_reason) {
83
+ // Escape any colons / hashes that would break the bare YAML scalar.
84
+ const escaped = /[:#]/.test(input.evidence.degraded_reason)
85
+ ? JSON.stringify(input.evidence.degraded_reason)
86
+ : input.evidence.degraded_reason;
87
+ lines.push(` degraded_reason: ${escaped}`);
88
+ }
89
+ }
78
90
  lines.push('audit:');
79
91
  lines.push(` event_count: ${input.audit.event_count}`);
80
92
  lines.push(` chain_root_hash: ${input.audit.chain_root_hash}`);
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Shape every skill returns. Tagged union so the agent can branch on `ok`.
3
+ * Handlers MUST NOT throw — they return `{ok: false, reason}` instead so
4
+ * the calling agent can keep going (per SKILL.md error contracts).
5
+ */
6
+ export type SkillResult = ({
7
+ ok: true;
8
+ } & Record<string, unknown>) | {
9
+ ok: false;
10
+ reason: string;
11
+ };
12
+ export type SkillHandler = (input: unknown) => Promise<SkillResult>;
13
+ export declare const SKILLS: Record<string, SkillHandler>;
14
+ export declare function isSkillName(name: string): boolean;
15
+ export declare function runSkill(name: string, input: unknown): Promise<SkillResult>;
16
+ /**
17
+ * Read all of stdin as a UTF-8 string. Returns '' immediately on TTY
18
+ * (no piped input) — handlers will reject via zod with a helpful message.
19
+ */
20
+ export declare function readStdin(): Promise<string>;
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maintainabilityai/research-runner",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "description": "Research + PRD agent runner — orchestrates the Archeologist and PRD pipelines for the MaintainabilityAI governance mesh",
5
5
  "license": "MIT",
6
6
  "author": "MaintainabilityAI",