@nettee/beacon 0.4.1 → 0.4.3

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/README.md CHANGED
@@ -68,7 +68,8 @@ and lists the missing paths. Both pairs may exist; the workspace pair wins.
68
68
 
69
69
  Beacon prepends an English platform template for this Run: workspace limit,
70
70
  inbound vs Schedule vs event vs manual capabilities, the `reply_text` / `reply_card` /
71
- `no_reply` / `notify_card` contract, and an optional `submit_feedback` tool.
71
+ `no_reply` / `notify_card` contract, optional `submit_feedback` (instruction gaps),
72
+ and optional `submit_observability_feedback` (RCA observability gaps).
72
73
 
73
74
  Protect the home and secret file before running Beacon:
74
75
 
@@ -8,6 +8,12 @@ export type DeliveryDestination = {
8
8
  name?: string | undefined;
9
9
  description?: string | undefined;
10
10
  };
11
+ /** Resolved Profile skills. Omitted on Profile means Pi auto-discovery (today's behavior). */
12
+ export type ProfileSkills = {
13
+ mode: "explicit";
14
+ /** Absolute paths after ~/$HOME/workspace resolution. */
15
+ paths: string[];
16
+ };
11
17
  export type Profile = {
12
18
  id: string;
13
19
  directory: string;
@@ -28,11 +34,14 @@ export type Profile = {
28
34
  input: string;
29
35
  notify?: DeliveryDestination | undefined;
30
36
  }>;
37
+ skills?: ProfileSkills | undefined;
31
38
  };
32
39
  export type LoadProfileOptions = {
33
40
  channels?: ChannelRegistry;
34
41
  channelsPath?: string;
35
42
  };
36
43
  export declare function workspaceProfileDirectory(workspace: string): string;
44
+ /** Expand `~` / `$HOME` and resolve relative paths against the Profile workspace. */
45
+ export declare function resolveSkillPath(rawPath: string, workspace: string): string;
37
46
  export declare function loadProfile(profileId: string, profilesDirectory?: string, options?: LoadProfileOptions): Promise<Profile>;
38
47
  export declare function profileAdminChatId(profile: Profile): string;
@@ -55,6 +55,12 @@ const scheduleSchema = z
55
55
  notify: notifyDestinationSchema.optional(),
56
56
  })
57
57
  .strict();
58
+ const skillsSchema = z
59
+ .object({
60
+ mode: z.literal("explicit"),
61
+ paths: z.array(z.string().trim().min(1)).default([]),
62
+ })
63
+ .strict();
58
64
  const profileDocumentSchema = z
59
65
  .object({
60
66
  workspace: z.string().min(1),
@@ -75,6 +81,8 @@ const profileDocumentSchema = z
75
81
  })
76
82
  .strict()
77
83
  .optional(),
84
+ /** Omit = Pi default skill discovery. `explicit` → `--no-skills` + `--skill` paths. */
85
+ skills: skillsSchema.optional(),
78
86
  })
79
87
  .strict();
80
88
  function assertProfileId(profileId) {
@@ -179,6 +187,55 @@ function resolveNotifyDestination(destination, channels, channelsPath) {
179
187
  chatId: channel.chatId,
180
188
  };
181
189
  }
190
+ /** Expand `~` / `$HOME` and resolve relative paths against the Profile workspace. */
191
+ export function resolveSkillPath(rawPath, workspace) {
192
+ const trimmed = rawPath.trim();
193
+ const home = homedir();
194
+ if (trimmed === "~")
195
+ return home;
196
+ if (trimmed.startsWith("~/"))
197
+ return resolve(home, trimmed.slice(2));
198
+ if (trimmed === "$HOME")
199
+ return home;
200
+ if (trimmed.startsWith("$HOME/") || trimmed.startsWith("$HOME\\")) {
201
+ return resolve(home, trimmed.slice("$HOME/".length));
202
+ }
203
+ if (isAbsolute(trimmed))
204
+ return trimmed;
205
+ return resolve(workspace, trimmed);
206
+ }
207
+ async function assertValidSkillPath(profileId, resolvedPath) {
208
+ let pathStat;
209
+ try {
210
+ pathStat = await stat(resolvedPath);
211
+ }
212
+ catch (error) {
213
+ throw new Error(`Profile ${profileId} skill path does not exist: ${resolvedPath}`, { cause: error });
214
+ }
215
+ if (pathStat.isDirectory()) {
216
+ const skillMd = join(resolvedPath, "SKILL.md");
217
+ if (!(await isExistingFile(skillMd))) {
218
+ throw new Error(`Profile ${profileId} skill directory must contain SKILL.md: ${resolvedPath}`);
219
+ }
220
+ return;
221
+ }
222
+ if (pathStat.isFile()) {
223
+ if (!resolvedPath.endsWith(".md")) {
224
+ throw new Error(`Profile ${profileId} skill file must end with .md: ${resolvedPath}`);
225
+ }
226
+ return;
227
+ }
228
+ throw new Error(`Profile ${profileId} skill path must be a directory or .md file: ${resolvedPath}`);
229
+ }
230
+ async function resolveProfileSkills(profileId, workspace, skills) {
231
+ const paths = [];
232
+ for (const rawPath of skills.paths) {
233
+ const resolved = resolveSkillPath(rawPath, workspace);
234
+ await assertValidSkillPath(profileId, resolved);
235
+ paths.push(resolved);
236
+ }
237
+ return { mode: "explicit", paths };
238
+ }
182
239
  export async function loadProfile(profileId, profilesDirectory = join(homedir(), ".beacon", "profiles"), options = {}) {
183
240
  assertProfileId(profileId);
184
241
  const profileDirectory = join(profilesDirectory, profileId);
@@ -225,6 +282,9 @@ export async function loadProfile(profileId, profilesDirectory = join(homedir(),
225
282
  throw new Error(`Profile workspace is not a directory: ${workspace}`);
226
283
  }
227
284
  const { persona, task } = await loadPersonaAndTask(profileId, workspace, canonicalProfileDirectory);
285
+ const skills = config.skills
286
+ ? await resolveProfileSkills(profileId, workspace, config.skills)
287
+ : undefined;
228
288
  return {
229
289
  id: profileId,
230
290
  directory: canonicalProfileDirectory,
@@ -258,6 +318,7 @@ export async function loadProfile(profileId, profilesDirectory = join(homedir(),
258
318
  }
259
319
  : {}),
260
320
  })),
321
+ ...(skills ? { skills } : {}),
261
322
  };
262
323
  }
263
324
  export function profileAdminChatId(profile) {
@@ -31,6 +31,12 @@ const scheduleSchema = z
31
31
  notify: notifyDestinationSchema.optional(),
32
32
  })
33
33
  .strict();
34
+ const skillsSchema = z
35
+ .object({
36
+ mode: z.literal("explicit"),
37
+ paths: z.array(z.string().trim().min(1)).default([]),
38
+ })
39
+ .strict();
34
40
  const profileDocumentSchema = z
35
41
  .object({
36
42
  workspace: z.string().min(1),
@@ -51,6 +57,7 @@ const profileDocumentSchema = z
51
57
  })
52
58
  .strict()
53
59
  .optional(),
60
+ skills: skillsSchema.optional(),
54
61
  })
55
62
  .strict();
56
63
  function toNotifyView(notify) {
@@ -86,6 +93,9 @@ function toDetailView(profileId, document) {
86
93
  notify: toNotifyView(document.listener.notify),
87
94
  }
88
95
  : null,
96
+ skills: document.skills
97
+ ? { mode: document.skills.mode, paths: document.skills.paths }
98
+ : null,
89
99
  secrets: {
90
100
  present: false,
91
101
  note: "Feishu app credentials are stored in secrets.json and are not exposed by the dashboard.",
@@ -1,5 +1,5 @@
1
1
  import type { RunRow } from "@nettee/beacon-shared";
2
- import type { FeedbackRecord } from "../domain/types.js";
2
+ import type { FeedbackRecord, ObservabilityFeedbackRecord } from "../domain/types.js";
3
3
  export declare const runIdPattern: RegExp;
4
4
  export type RunSummary = {
5
5
  profileId: string;
@@ -18,6 +18,7 @@ export type RunSummary = {
18
18
  hasSessionFile: boolean;
19
19
  systemPrompt: string | undefined;
20
20
  feedback: FeedbackRecord | undefined;
21
+ observabilityFeedback: ObservabilityFeedbackRecord | undefined;
21
22
  };
22
23
  export declare function listRunSummaries(profilesDirectory: string): Promise<RunSummary[]>;
23
24
  export declare function kindLabel(row: Pick<RunSummary, "kind" | "scheduleId" | "eventSource" | "eventType">): string;
@@ -1,6 +1,6 @@
1
1
  import { readdir, readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import { feedbackRecordSchema } from "../outcome/content.js";
3
+ import { feedbackRecordSchema, observabilityFeedbackRecordSchema, } from "../outcome/content.js";
4
4
  export const runIdPattern = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
5
5
  function asRecord(value) {
6
6
  return typeof value === "object" && value !== null
@@ -14,6 +14,10 @@ function asFeedback(value) {
14
14
  const parsed = feedbackRecordSchema.safeParse(value);
15
15
  return parsed.success ? parsed.data : undefined;
16
16
  }
17
+ function asObservabilityFeedback(value) {
18
+ const parsed = observabilityFeedbackRecordSchema.safeParse(value);
19
+ return parsed.success ? parsed.data : undefined;
20
+ }
17
21
  async function readDirents(path) {
18
22
  try {
19
23
  return await readdir(path, { withFileTypes: true });
@@ -61,6 +65,7 @@ function summarize(value, hasSessionFile) {
61
65
  hasSessionFile,
62
66
  systemPrompt: asString(run?.systemPrompt),
63
67
  feedback: asFeedback(record.feedback),
68
+ observabilityFeedback: asObservabilityFeedback(record.observabilityFeedback),
64
69
  };
65
70
  }
66
71
  async function loadSummary(path) {
@@ -126,6 +131,12 @@ export function toRunListItem(row) {
126
131
  summary: item.summary,
127
132
  }))
128
133
  : null,
134
+ observabilityFeedback: row.observabilityFeedback && row.observabilityFeedback.items.length > 0
135
+ ? row.observabilityFeedback.items.map((item) => ({
136
+ priority: item.priority,
137
+ summary: item.summary,
138
+ }))
139
+ : null,
129
140
  };
130
141
  }
131
142
  export async function findRunSummary(profilesDirectory, runId) {
@@ -84,6 +84,20 @@ export type FeedbackRecord = {
84
84
  items: FeedbackItem[];
85
85
  submittedAt: string;
86
86
  };
87
+ /** Observability-gap feedback may include low; instruction feedback may not. */
88
+ export declare const observabilityFeedbackPriorities: readonly ["high", "medium", "low"];
89
+ export type ObservabilityFeedbackPriority = (typeof observabilityFeedbackPriorities)[number];
90
+ export type ObservabilityFeedbackItem = {
91
+ priority: ObservabilityFeedbackPriority;
92
+ summary: string;
93
+ };
94
+ export type ObservabilityFeedbackContent = {
95
+ items: ObservabilityFeedbackItem[];
96
+ };
97
+ export type ObservabilityFeedbackRecord = {
98
+ items: ObservabilityFeedbackItem[];
99
+ submittedAt: string;
100
+ };
87
101
  export type DeliveryContent = TextReplyContent | CardContent;
88
102
  export type RunRecord = {
89
103
  runId: string;
@@ -132,6 +146,8 @@ export type TriggerRecord = {
132
146
  finalOutcome?: FinalOutcomeRecord | undefined;
133
147
  /** Present only when the Agent called `submit_feedback`. */
134
148
  feedback?: FeedbackRecord | undefined;
149
+ /** Present only when the Agent called `submit_observability_feedback`. */
150
+ observabilityFeedback?: ObservabilityFeedbackRecord | undefined;
135
151
  delivery?: DeliveryRecord | undefined;
136
152
  notifyDelivery?: DeliveryRecord | undefined;
137
153
  };
@@ -17,3 +17,9 @@ export const failureCodes = [
17
17
  "delivery_interrupted",
18
18
  ];
19
19
  export const feedbackPriorities = ["high", "medium"];
20
+ /** Observability-gap feedback may include low; instruction feedback may not. */
21
+ export const observabilityFeedbackPriorities = [
22
+ "high",
23
+ "medium",
24
+ "low",
25
+ ];
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import type { CardContent, DeliveryContent, FeedbackContent, FinalOutcomeContent, ReplyContent } from "../domain/types.js";
2
+ import type { CardContent, DeliveryContent, FeedbackContent, FinalOutcomeContent, ObservabilityFeedbackContent, ReplyContent } from "../domain/types.js";
3
3
  export declare const feedbackItemSchema: z.ZodObject<{
4
4
  priority: z.ZodEnum<{
5
5
  high: "high";
@@ -26,6 +26,35 @@ export declare const feedbackRecordSchema: z.ZodObject<{
26
26
  }, z.core.$strict>>;
27
27
  submittedAt: z.ZodString;
28
28
  }, z.core.$strict>;
29
+ export declare const observabilityFeedbackItemSchema: z.ZodObject<{
30
+ priority: z.ZodEnum<{
31
+ high: "high";
32
+ medium: "medium";
33
+ low: "low";
34
+ }>;
35
+ summary: z.ZodString;
36
+ }, z.core.$strict>;
37
+ export declare const observabilityFeedbackContentSchema: z.ZodObject<{
38
+ items: z.ZodArray<z.ZodObject<{
39
+ priority: z.ZodEnum<{
40
+ high: "high";
41
+ medium: "medium";
42
+ low: "low";
43
+ }>;
44
+ summary: z.ZodString;
45
+ }, z.core.$strict>>;
46
+ }, z.core.$strict>;
47
+ export declare const observabilityFeedbackRecordSchema: z.ZodObject<{
48
+ items: z.ZodArray<z.ZodObject<{
49
+ priority: z.ZodEnum<{
50
+ high: "high";
51
+ medium: "medium";
52
+ low: "low";
53
+ }>;
54
+ summary: z.ZodString;
55
+ }, z.core.$strict>>;
56
+ submittedAt: z.ZodString;
57
+ }, z.core.$strict>;
29
58
  export declare const finalOutcomeContentSchema: z.ZodObject<{
30
59
  reply: z.ZodDiscriminatedUnion<[z.ZodObject<{
31
60
  kind: z.ZodLiteral<"text">;
@@ -56,6 +85,7 @@ export type OutcomePatch = {
56
85
  reply?: ReplyContent | undefined;
57
86
  notify?: CardContent | undefined;
58
87
  feedback?: FeedbackContent | undefined;
88
+ observabilityFeedback?: ObservabilityFeedbackContent | undefined;
59
89
  };
60
90
  export declare function parseFinalOutcomeContent(value: unknown): FinalOutcomeContent;
61
91
  export declare function parseOutcomePatch(value: unknown): OutcomePatch;
@@ -46,6 +46,23 @@ export const feedbackRecordSchema = z
46
46
  submittedAt: z.string().datetime({ offset: true }),
47
47
  })
48
48
  .strict();
49
+ export const observabilityFeedbackItemSchema = z
50
+ .object({
51
+ priority: z.enum(["high", "medium", "low"]),
52
+ summary: nonBlank.max(1024),
53
+ })
54
+ .strict();
55
+ export const observabilityFeedbackContentSchema = z
56
+ .object({
57
+ items: z.array(observabilityFeedbackItemSchema).min(1).max(3),
58
+ })
59
+ .strict();
60
+ export const observabilityFeedbackRecordSchema = z
61
+ .object({
62
+ items: z.array(observabilityFeedbackItemSchema).min(1).max(3),
63
+ submittedAt: z.string().datetime({ offset: true }),
64
+ })
65
+ .strict();
49
66
  export const finalOutcomeContentSchema = z
50
67
  .object({
51
68
  reply: replySchema,
@@ -57,12 +74,14 @@ const outcomePatchSchema = z
57
74
  reply: replySchema.optional(),
58
75
  notify: cardSchema.optional(),
59
76
  feedback: feedbackContentSchema.optional(),
77
+ observabilityFeedback: observabilityFeedbackContentSchema.optional(),
60
78
  })
61
79
  .strict()
62
80
  .refine((value) => value.reply !== undefined ||
63
81
  value.notify !== undefined ||
64
- value.feedback !== undefined, {
65
- message: "Outcome submission must include reply, notify, or feedback",
82
+ value.feedback !== undefined ||
83
+ value.observabilityFeedback !== undefined, {
84
+ message: "Outcome submission must include reply, notify, feedback, or observabilityFeedback",
66
85
  });
67
86
  const legacyOutcomeSchema = z.discriminatedUnion("kind", [
68
87
  textReplySchema,
@@ -106,6 +125,9 @@ export function mergeOutcome(current, patch) {
106
125
  if (patch.feedback && current?.feedback) {
107
126
  throw new Error("Feedback already submitted");
108
127
  }
128
+ if (patch.observabilityFeedback && current?.observabilityFeedback) {
129
+ throw new Error("Observability feedback already submitted");
130
+ }
109
131
  return {
110
132
  ...(current?.reply || patch.reply
111
133
  ? { reply: patch.reply ?? current?.reply }
@@ -116,6 +138,11 @@ export function mergeOutcome(current, patch) {
116
138
  ...(current?.feedback || patch.feedback
117
139
  ? { feedback: patch.feedback ?? current?.feedback }
118
140
  : {}),
141
+ ...(current?.observabilityFeedback || patch.observabilityFeedback
142
+ ? {
143
+ observabilityFeedback: patch.observabilityFeedback ?? current?.observabilityFeedback,
144
+ }
145
+ : {}),
119
146
  };
120
147
  }
121
148
  /** Stable English summary when the Agent settles without closing reply/no_reply. */
@@ -1,3 +1,4 @@
1
1
  export declare const inboundOutOfRoleReply = "\u8FD9\u6761\u6D88\u606F\u4E0D\u5728\u6211\u7684\u804C\u8D23\u8303\u56F4\u5185\u3002";
2
2
  export declare const finalOutcomeContractInstructions: readonly ["`reply_text` sends plain text. For an inbound Feishu message, quote-reply that message. For a Schedule, message the Profile admin. For a manual Run, print to the operator. Do not fill `chat_id`.", "`reply_card` sends a titled Markdown card on the conversational channel (inbound quote-reply, or Schedule admin chat). It closes that channel by itself. Do not also call `reply_text` or `no_reply` on the same Run. Do not fill `chat_id`.", "`no_reply` finishes a Run without messaging the conversational channel. Its reason is stored for audit and is never sent. Do not use it on inbound Feishu messages.", "`notify_card` posts a titled Markdown card to this Run's configured notify group. Call it only when this Run has a notify target. Do not fill `chat_id`.", "Which tool to use for a given business result is decided by the Profile persona/task (or the situation), not by these platform lines alone.", "Beacon ignores ordinary assistant final text for Delivery."];
3
- export declare const feedbackContractInstructions: readonly ["If this Run had a real high or medium problem with instructions, Skills, dependencies, or tools, you may call `submit_feedback` once with one to three items.", "If there are none, do not call it. Do not invent items. Do not report low-priority issues, taste, or problems you already worked around without needing a platform change."];
3
+ export declare const feedbackContractInstructions: readonly ["If this Run had a real high or medium problem with instructions, Skills, dependencies, or tools, you may call `submit_feedback` once with one to three items.", "If there are none, do not call it. Do not invent items. Do not report low-priority issues, taste, or problems you already worked around without needing a platform change.", "Do not put observability gaps in `submit_feedback`. Use `submit_observability_feedback` for those."];
4
+ export declare const observabilityFeedbackContractInstructions: readonly ["If root-cause analysis on this Run was held back by missing observability (logs, metrics, traces, event correlation, or similar signals), you may call `submit_observability_feedback` once with one to three items at high, medium, or low priority.", "Say what signal is missing and how adding it would make RCA more useful or more certain. If there are none, do not call it. Do not invent items.", "Do not put instruction, Skill, dependency, or tool problems in `submit_observability_feedback`. Use `submit_feedback` for those."];
@@ -10,4 +10,10 @@ export const finalOutcomeContractInstructions = [
10
10
  export const feedbackContractInstructions = [
11
11
  "If this Run had a real high or medium problem with instructions, Skills, dependencies, or tools, you may call `submit_feedback` once with one to three items.",
12
12
  "If there are none, do not call it. Do not invent items. Do not report low-priority issues, taste, or problems you already worked around without needing a platform change.",
13
+ "Do not put observability gaps in `submit_feedback`. Use `submit_observability_feedback` for those.",
14
+ ];
15
+ export const observabilityFeedbackContractInstructions = [
16
+ "If root-cause analysis on this Run was held back by missing observability (logs, metrics, traces, event correlation, or similar signals), you may call `submit_observability_feedback` once with one to three items at high, medium, or low priority.",
17
+ "Say what signal is missing and how adding it would make RCA more useful or more certain. If there are none, do not call it. Do not invent items.",
18
+ "Do not put instruction, Skill, dependency, or tool problems in `submit_observability_feedback`. Use `submit_feedback` for those.",
13
19
  ];
@@ -1,4 +1,4 @@
1
- import type { FeedbackRecord, FinalOutcomeContent } from "../domain/types.js";
1
+ import type { FeedbackRecord, FinalOutcomeContent, ObservabilityFeedbackRecord } from "../domain/types.js";
2
2
  export type OutcomeBinding = {
3
3
  socketPath: string;
4
4
  runToken: string;
@@ -7,6 +7,7 @@ export type OutcomeSubmission = {
7
7
  binding: OutcomeBinding;
8
8
  take(): FinalOutcomeContent;
9
9
  takeFeedback(): FeedbackRecord | undefined;
10
+ takeObservabilityFeedback(): ObservabilityFeedbackRecord | undefined;
10
11
  cancel(): void;
11
12
  };
12
13
  export type OutcomeSink = {
@@ -76,6 +76,10 @@ export async function startOutcomeServer(options = {}) {
76
76
  if (patch.feedback && !previous?.feedback) {
77
77
  submission.feedbackSubmittedAt = new Date().toISOString();
78
78
  }
79
+ if (patch.observabilityFeedback && !previous?.observabilityFeedback) {
80
+ submission.observabilityFeedbackSubmittedAt =
81
+ new Date().toISOString();
82
+ }
79
83
  }
80
84
  catch (error) {
81
85
  respond(socket, {
@@ -113,6 +117,16 @@ export async function startOutcomeServer(options = {}) {
113
117
  }
114
118
  return { items, submittedAt: record.feedbackSubmittedAt };
115
119
  },
120
+ takeObservabilityFeedback() {
121
+ const items = record.outcome?.observabilityFeedback?.items;
122
+ if (!items || record.observabilityFeedbackSubmittedAt === undefined) {
123
+ return undefined;
124
+ }
125
+ return {
126
+ items,
127
+ submittedAt: record.observabilityFeedbackSubmittedAt,
128
+ };
129
+ },
116
130
  cancel() {
117
131
  submissions.delete(runToken);
118
132
  },
@@ -253,7 +253,7 @@ export class RunOrchestrator {
253
253
  await this.deliverField(triggerKey, "notifyDelivery", current.notifyTarget, outcome.notify, create && current.notifyDelivery === undefined);
254
254
  }
255
255
  }
256
- async fail(triggerKey, runId, code, error, submitted, settle) {
256
+ async fail(triggerKey, runId, code, error, submitted, submittedObservability, settle) {
257
257
  let detail = isMissingReplyOutcomeError(error)
258
258
  ? MISSING_REPLY_OUTCOME_SUMMARY
259
259
  : summary(error);
@@ -278,6 +278,9 @@ export class RunOrchestrator {
278
278
  submittedAt: this.timestamp(),
279
279
  },
280
280
  ...(submitted ? { feedback: submitted } : {}),
281
+ ...(submittedObservability
282
+ ? { observabilityFeedback: submittedObservability }
283
+ : {}),
281
284
  }));
282
285
  await this.deliverOutcome(triggerKey);
283
286
  }
@@ -326,6 +329,9 @@ export class RunOrchestrator {
326
329
  path: record.run.sessionPath,
327
330
  name: `Beacon ${this.options.profile.id} ${runId}`,
328
331
  },
332
+ ...(this.options.profile.skills
333
+ ? { skills: this.options.profile.skills }
334
+ : {}),
329
335
  });
330
336
  settle = {
331
337
  stopReason: completion.stopReason,
@@ -333,6 +339,7 @@ export class RunOrchestrator {
333
339
  text: completion.text,
334
340
  };
335
341
  const submitted = submission.takeFeedback();
342
+ const submittedObservability = submission.takeObservabilityFeedback();
336
343
  const outcome = normalizeAgentOutcome(input, submission.take(), record.notifyTarget);
337
344
  await this.options.store.update(triggerKey, (current) => ({
338
345
  ...current,
@@ -349,18 +356,22 @@ export class RunOrchestrator {
349
356
  submittedAt: this.timestamp(),
350
357
  },
351
358
  ...(submitted ? { feedback: submitted } : {}),
359
+ ...(submittedObservability
360
+ ? { observabilityFeedback: submittedObservability }
361
+ : {}),
352
362
  }));
353
363
  await this.deliverOutcome(triggerKey);
354
364
  }
355
365
  catch (error) {
356
366
  const submitted = submission.takeFeedback();
367
+ const submittedObservability = submission.takeObservabilityFeedback();
357
368
  submission.cancel();
358
369
  const code = error instanceof PiRuntimeError
359
370
  ? error.code
360
371
  : isMissingReplyOutcomeError(error)
361
372
  ? "outcome_missing"
362
373
  : "runtime_exit_failed";
363
- await this.fail(triggerKey, runId, code, error, submitted, settle);
374
+ await this.fail(triggerKey, runId, code, error, submitted, submittedObservability, settle);
364
375
  }
365
376
  }
366
377
  async process(triggerKey, normalize) {
@@ -32,6 +32,7 @@ export function createProfileRunner(profile, outcomes, beaconCliPath, runAgent =
32
32
  notify: false,
33
33
  }),
34
34
  outcome: { ...submission.binding, cliPath: beaconCliPath },
35
+ ...(profile.skills ? { skills: profile.skills } : {}),
35
36
  });
36
37
  return submission.take();
37
38
  };
@@ -1,4 +1,4 @@
1
- import type { CardContent, FeedbackContent, FinalOutcomeContent } from "../domain/types.js";
1
+ import type { CardContent, FeedbackContent, FinalOutcomeContent, ObservabilityFeedbackContent } from "../domain/types.js";
2
2
  type ToolResult = {
3
3
  content: Array<{
4
4
  type: "text";
@@ -25,6 +25,9 @@ export type CardOutcomeToolParams = {
25
25
  export type FeedbackToolParams = {
26
26
  items: FeedbackContent["items"];
27
27
  };
28
+ export type ObservabilityFeedbackToolParams = {
29
+ items: ObservabilityFeedbackContent["items"];
30
+ };
28
31
  type ExtensionApi = {
29
32
  registerTool<TParams>(tool: {
30
33
  name: string;
@@ -39,5 +42,6 @@ export declare function replyCardFromToolParams(params: CardOutcomeToolParams):
39
42
  export declare function noReplyFromToolParams(params: NoReplyOutcomeToolParams): FinalOutcomeContent;
40
43
  export declare function notifyCardFromToolParams(params: CardOutcomeToolParams): CardContent;
41
44
  export declare function feedbackFromToolParams(params: FeedbackToolParams): FeedbackContent;
45
+ export declare function observabilityFeedbackFromToolParams(params: ObservabilityFeedbackToolParams): ObservabilityFeedbackContent;
42
46
  export default function registerOutcomeTools(pi: ExtensionApi): void;
43
47
  export {};
@@ -39,6 +39,15 @@ export function feedbackFromToolParams(params) {
39
39
  throw new Error("submit_feedback produced no items");
40
40
  return patch.feedback;
41
41
  }
42
+ export function observabilityFeedbackFromToolParams(params) {
43
+ const patch = parseOutcomePatch({
44
+ observabilityFeedback: { items: params.items },
45
+ });
46
+ if (!patch.observabilityFeedback) {
47
+ throw new Error("submit_observability_feedback produced no items");
48
+ }
49
+ return patch.observabilityFeedback;
50
+ }
42
51
  async function invokeBeaconCli(outcome, signal) {
43
52
  const cliPath = process.env.BEACON_CLI_PATH;
44
53
  if (!cliPath)
@@ -209,7 +218,7 @@ export default function registerOutcomeTools(pi) {
209
218
  pi.registerTool({
210
219
  name: "submit_feedback",
211
220
  label: "Submit Feedback",
212
- description: "Optionally report one to three high or medium problems with instructions, Skills, dependencies, or tools on this Run. Call at most once, and only when such a problem actually existed. Do not call this tool when there is nothing to report.",
221
+ description: "Optionally report one to three high or medium problems with instructions, Skills, dependencies, or tools on this Run. Call at most once, and only when such a problem actually existed. Do not use this for missing observability — use submit_observability_feedback instead. Do not call this tool when there is nothing to report.",
213
222
  parameters: {
214
223
  type: "object",
215
224
  properties: {
@@ -249,4 +258,52 @@ export default function registerOutcomeTools(pi) {
249
258
  };
250
259
  },
251
260
  });
261
+ pi.registerTool({
262
+ name: "submit_observability_feedback",
263
+ label: "Submit Observability Feedback",
264
+ description: "Optionally report one to three observability gaps (missing logs, metrics, traces, or correlation signals) that held back root-cause analysis on this Run. Priority may be high, medium, or low. Call at most once, and only when such a gap actually existed. Do not use this for instruction/Skill/dependency/tool problems — use submit_feedback instead. Do not call this tool when there is nothing to report.",
265
+ parameters: {
266
+ type: "object",
267
+ properties: {
268
+ items: {
269
+ type: "array",
270
+ minItems: 1,
271
+ maxItems: 3,
272
+ description: "One to three observability findings. Omit the tool call entirely when there are none.",
273
+ items: {
274
+ type: "object",
275
+ properties: {
276
+ priority: {
277
+ type: "string",
278
+ enum: ["high", "medium", "low"],
279
+ description: "high, medium, or low — how much the missing signal limited RCA confidence.",
280
+ },
281
+ summary: {
282
+ type: "string",
283
+ minLength: 1,
284
+ maxLength: 1024,
285
+ description: "One or two sentences naming the missing signal and how adding it would help RCA.",
286
+ },
287
+ },
288
+ required: ["priority", "summary"],
289
+ additionalProperties: false,
290
+ },
291
+ },
292
+ },
293
+ required: ["items"],
294
+ additionalProperties: false,
295
+ },
296
+ async execute(_toolCallId, params, signal) {
297
+ await invokeBeaconCli({ observabilityFeedback: { items: params.items } }, signal);
298
+ return {
299
+ content: [
300
+ {
301
+ type: "text",
302
+ text: "Observability feedback accepted by Beacon.",
303
+ },
304
+ ],
305
+ details: { submitted: true },
306
+ };
307
+ },
308
+ });
252
309
  }
@@ -1,3 +1,4 @@
1
+ import type { ProfileSkills } from "../config/profile.js";
1
2
  import type { OutcomeBinding } from "../outcome/server.js";
2
3
  type PiOutcomeBinding = OutcomeBinding & {
3
4
  cliPath: string;
@@ -14,6 +15,8 @@ export type PiRunRequest = {
14
15
  path: string;
15
16
  name?: string | undefined;
16
17
  } | undefined;
18
+ /** When set with mode explicit: `--no-skills` plus each `--skill` path. Omit = Pi discovery. */
19
+ skills?: ProfileSkills | undefined;
17
20
  };
18
21
  export type PiRunResult = {
19
22
  text: string;
@@ -38,6 +41,8 @@ export declare class PiRuntimeError extends Error {
38
41
  }
39
42
  /** Collect thinking / reasoning blocks from an assistant message content array. */
40
43
  export declare function collectThinking(content: unknown[]): string;
44
+ /** Exported for unit tests that assert argv without spawning a real Pi. */
45
+ export declare function buildArguments(request: PiRunRequest): string[];
41
46
  /**
42
47
  * Read NDJSON frames from Pi stdout with a hard per-frame byte cap.
43
48
  *
@@ -65,7 +65,8 @@ export function collectThinking(content) {
65
65
  .join("\n")
66
66
  .trim();
67
67
  }
68
- function buildArguments(request) {
68
+ /** Exported for unit tests that assert argv without spawning a real Pi. */
69
+ export function buildArguments(request) {
69
70
  const args = ["--mode", "rpc", "--no-approve"];
70
71
  if (request.session) {
71
72
  args.push("--session-dir", request.session.path, "--session-id", request.session.id);
@@ -81,6 +82,12 @@ function buildArguments(request) {
81
82
  args.push("--model", request.model);
82
83
  if (request.systemPrompt)
83
84
  args.push("--system-prompt", request.systemPrompt);
85
+ if (request.skills?.mode === "explicit") {
86
+ args.push("--no-skills");
87
+ for (const skillPath of request.skills.paths) {
88
+ args.push("--skill", skillPath);
89
+ }
90
+ }
84
91
  if (request.outcome) {
85
92
  args.push("--extension", fileURLToPath(new URL(import.meta.url.endsWith(".ts")
86
93
  ? "../../dist/runtime/pi-outcome-extension.js"