@nettee/beacon 0.1.5 → 0.1.7

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
@@ -115,12 +115,15 @@ does not read, initialize, or advance the Schedule's cron cursor. Unknown
115
115
  Profiles or Schedules and failed Runs or Deliveries exit non-zero; failure
116
116
  messages include the `run_id` whenever a Run record was created.
117
117
 
118
- ## Feishu Final Outcome cards
118
+ ## Final Outcomes
119
119
 
120
- An Agent explicitly calls one of two Final Outcome tools:
120
+ An Agent explicitly calls exactly one of three Final Outcome tools:
121
121
 
122
122
  - `submit_final_outcome_text` sends a normal Feishu text message.
123
123
  - `submit_final_outcome_card` sends an interactive card.
124
+ - `submit_final_outcome_no_reply` completes the Run without creating a
125
+ Delivery. Its required `reason` is stored for internal audit and is never
126
+ sent to the user.
124
127
 
125
128
  The card tool accepts a title, Feishu-compatible Markdown body, and up to five
126
129
  HTTP(S) link buttons. The first button is styled as primary. The tools accept
@@ -141,11 +144,16 @@ these argument shapes:
141
144
  }
142
145
  ```
143
146
 
147
+ ```json
148
+ { "reason": "The group announcement is outside this Profile's role." }
149
+ ```
150
+
144
151
  The selected form is stored durably as part of the Final Outcome. Both quoted
145
152
  message replies and scheduled chat messages preserve that choice: text remains
146
153
  text, while cards are sent with Feishu's `interactive` message type and include
147
- the card generation time. Manual local triggers render card outcomes as
148
- readable Markdown on stdout.
154
+ the card generation time. A no-reply outcome records a successful Run without
155
+ a Delivery record. Manual local triggers render card outcomes as readable
156
+ Markdown on stdout and print no user-facing content for no-reply outcomes.
149
157
 
150
158
  ## State and failure behavior
151
159
 
@@ -200,6 +208,7 @@ The repository also contains Chinese operator runbooks based on the verified
200
208
 
201
209
  - [Deploy or update Beacon on macmini](docs/macmini-deploy-and-update.md)
202
210
  - [Add a Beacon Profile on macmini](docs/macmini-add-profile.md)
211
+ - [Write a role-bounded Profile Prompt](docs/profile-prompt-writing.md)
203
212
 
204
213
  ## Development checks
205
214
 
@@ -251,10 +260,13 @@ version change takes precedence over the automatic patch bump, and CI rejects
251
260
  a changed version that is not greater than the base branch version.
252
261
 
253
262
  The bump job accepts only branches in this repository, not pull requests from
254
- forks. It uses the job-scoped `github.token` to push the version commit, then
255
- dispatches a fresh CI run for the new commit. This avoids a long-lived personal
256
- access token while working around GitHub's suppression of workflows caused by
257
- bot pushes.
263
+ forks. Configure a repository secret named `AUTO_BUMP_TOKEN` with a fine-grained
264
+ GitHub token whose repository access is limited to `nettee/beacon` and whose
265
+ Contents permission is read/write. The checkout persists that credential for
266
+ the version commit push, so GitHub attributes the PR update to the token owner
267
+ and starts follow-up PR checks without an approval prompt. When the secret is
268
+ absent, CI falls back to the job-scoped `github.token`; the bump still succeeds,
269
+ but GitHub requires a maintainer to approve the resulting PR workflow run.
258
270
 
259
271
  Submit the release candidate through the normal pull request checks. After the
260
272
  initial package bootstrap, each push to `main` runs
@@ -22,6 +22,9 @@ function assertSucceeded(operation, result) {
22
22
  }
23
23
  }
24
24
  export function encodeFeishuFinalOutcome(outcome) {
25
+ if (outcome.kind === "no_reply") {
26
+ throw new Error("No-reply Outcomes must not be encoded for Delivery");
27
+ }
25
28
  if (outcome.kind === "text") {
26
29
  return {
27
30
  msgType: "text",
@@ -84,6 +87,18 @@ export class FeishuGateway {
84
87
  data: { reaction_type: { emoji_type: "OnIt" } },
85
88
  });
86
89
  assertSucceeded("add OnIt reaction", result);
90
+ const reactionId = result.data?.reaction_id;
91
+ if (!reactionId) {
92
+ throw new Error("add OnIt reaction returned no reaction_id");
93
+ }
94
+ return {
95
+ clear: async () => {
96
+ const deleteResult = await this.client.im.v1.messageReaction.delete({
97
+ path: { message_id: messageId, reaction_id: reactionId },
98
+ });
99
+ assertSucceeded("remove OnIt reaction", deleteResult);
100
+ },
101
+ };
87
102
  }
88
103
  async fetchMessage(messageId) {
89
104
  const result = await this.client.im.v1.message.get({
@@ -31,12 +31,14 @@ export class FeishuIntake {
31
31
  });
32
32
  if (!claim.created)
33
33
  return "duplicate";
34
- void this.options
34
+ const acknowledgement = this.options
35
35
  .acknowledge(event.message.message_id)
36
36
  .catch((error) => {
37
37
  console.error(`[beacon] non-critical acknowledgement failure message_id=${event.message.message_id}: ${error instanceof Error ? error.message : String(error)}`);
38
+ return undefined;
38
39
  });
39
- const task = this.options.process(claim.record.triggerKey, async () => {
40
+ const task = this.options
41
+ .process(claim.record.triggerKey, async () => {
40
42
  const normalized = await buildFeishuTriggerInput({
41
43
  messageId: event.message.message_id,
42
44
  chatId: event.message.chat_id,
@@ -54,6 +56,17 @@ export class FeishuIntake {
54
56
  quotedMessages: normalized.quotedMessages,
55
57
  currentMessage: normalized.message,
56
58
  };
59
+ })
60
+ .finally(async () => {
61
+ const activeAcknowledgement = await acknowledgement;
62
+ if (!activeAcknowledgement)
63
+ return;
64
+ try {
65
+ await activeAcknowledgement.clear();
66
+ }
67
+ catch (error) {
68
+ console.error(`[beacon] non-critical acknowledgement cleanup failure message_id=${event.message.message_id}: ${error instanceof Error ? error.message : String(error)}`);
69
+ }
57
70
  });
58
71
  this.active.add(task);
59
72
  void task
@@ -50,7 +50,8 @@ export async function runManualTrigger(configPath, profileId, input, output = wr
50
50
  }));
51
51
  const record = (await store.list()).find((candidate) => candidate.triggerKey === claim.record.triggerKey);
52
52
  if (record?.run?.state !== "succeeded" ||
53
- record.delivery?.state !== "delivered") {
53
+ (record.finalOutcome?.content.kind !== "no_reply" &&
54
+ record.delivery?.state !== "delivered")) {
54
55
  throw new Error(`Manual Trigger failed: ${record?.run?.failure?.code ?? record?.delivery?.failure?.code ?? "unknown"}`);
55
56
  }
56
57
  }
@@ -12,6 +12,7 @@ const buttonSchema = z
12
12
  .strict();
13
13
  export const finalOutcomeContentSchema = z.discriminatedUnion("kind", [
14
14
  z.object({ kind: z.literal("text"), text: nonBlank }).strict(),
15
+ z.object({ kind: z.literal("no_reply"), reason: nonBlank }).strict(),
15
16
  z
16
17
  .object({
17
18
  kind: z.literal("card"),
@@ -27,6 +28,8 @@ export function parseFinalOutcomeContent(value) {
27
28
  export function renderFinalOutcomeAsText(outcome) {
28
29
  if (outcome.kind === "text")
29
30
  return outcome.text;
31
+ if (outcome.kind === "no_reply")
32
+ return "";
30
33
  return [
31
34
  `# ${outcome.title}`,
32
35
  "",
@@ -1,5 +1,5 @@
1
1
  export const finalOutcomeSystemInstructions = [
2
- "When your work is complete, call exactly one of submit_final_outcome_text or submit_final_outcome_card with the exact user-facing response.",
3
- "Use submit_final_outcome_text for a normal response, or submit_final_outcome_card when a titled Markdown summary and link buttons materially improve the result.",
2
+ "When your work is complete, call exactly one of submit_final_outcome_text, submit_final_outcome_card, or submit_final_outcome_no_reply.",
3
+ "Use submit_final_outcome_text for a normal response, submit_final_outcome_card when a titled Markdown summary and link buttons materially improve the result, or submit_final_outcome_no_reply when the triggering message is outside the Profile's role and should receive no response.",
4
4
  "Beacon ignores ordinary assistant final text for Delivery.",
5
5
  ];
@@ -209,7 +209,8 @@ export class RunOrchestrator {
209
209
  submittedAt: this.timestamp(),
210
210
  },
211
211
  }));
212
- await this.deliver(triggerKey);
212
+ if (outcome.kind !== "no_reply")
213
+ await this.deliver(triggerKey);
213
214
  }
214
215
  catch (error) {
215
216
  submission.cancel();
@@ -297,6 +298,7 @@ export class RunOrchestrator {
297
298
  continue;
298
299
  }
299
300
  if (record.finalOutcome &&
301
+ record.finalOutcome.content.kind !== "no_reply" &&
300
302
  !record.delivery &&
301
303
  (record.run.state === "succeeded" || record.run.state === "failed")) {
302
304
  await this.deliver(record.triggerKey);
@@ -11,6 +11,9 @@ export function cardOutcomeFromToolParams(params) {
11
11
  buttons: params.buttons ?? [],
12
12
  });
13
13
  }
14
+ export function noReplyOutcomeFromToolParams(params) {
15
+ return parseFinalOutcomeContent({ kind: "no_reply", reason: params.reason });
16
+ }
14
17
  async function invokeBeaconCli(outcome, signal) {
15
18
  const cliPath = process.env.BEACON_CLI_PATH;
16
19
  if (!cliPath)
@@ -47,7 +50,7 @@ export default function registerOutcomeTools(pi) {
47
50
  pi.registerTool({
48
51
  name: "submit_final_outcome_text",
49
52
  label: "Submit Final Outcome Text",
50
- description: "Submit the final response as a normal text message. Call either this tool or submit_final_outcome_card exactly once after completing the task.",
53
+ description: "Submit the final response as a normal text message. Call exactly one Final Outcome tool after completing the task.",
51
54
  parameters: {
52
55
  type: "object",
53
56
  properties: {
@@ -68,7 +71,7 @@ export default function registerOutcomeTools(pi) {
68
71
  pi.registerTool({
69
72
  name: "submit_final_outcome_card",
70
73
  label: "Submit Final Outcome Card",
71
- description: "Submit the final response as a structured Feishu card. Call either this tool or submit_final_outcome_text exactly once after completing the task.",
74
+ description: "Submit the final response as a structured Feishu card. Call exactly one Final Outcome tool after completing the task.",
72
75
  parameters: {
73
76
  type: "object",
74
77
  properties: {
@@ -104,4 +107,25 @@ export default function registerOutcomeTools(pi) {
104
107
  return acceptedResult();
105
108
  },
106
109
  });
110
+ pi.registerTool({
111
+ name: "submit_final_outcome_no_reply",
112
+ label: "Submit No Reply Outcome",
113
+ description: "Complete the run without sending a reply. Use this exactly once when the triggering message is outside the Profile's role.",
114
+ parameters: {
115
+ type: "object",
116
+ properties: {
117
+ reason: {
118
+ type: "string",
119
+ minLength: 1,
120
+ description: "A concise internal reason why the message should not receive a reply.",
121
+ },
122
+ },
123
+ required: ["reason"],
124
+ additionalProperties: false,
125
+ },
126
+ async execute(_toolCallId, params, signal) {
127
+ await invokeBeaconCli(noReplyOutcomeFromToolParams(params), signal);
128
+ return acceptedResult();
129
+ },
130
+ });
107
131
  }
@@ -41,9 +41,9 @@ export async function triggerScheduleOnce(options) {
41
41
  if (!record?.run) {
42
42
  throw new Error(`Schedule Trigger produced no Run record: trigger_id=${claim.record.triggerId}`);
43
43
  }
44
- if (record.run.state !== "succeeded" ||
45
- record.delivery?.state !== "delivered") {
46
- const failure = record.run.failure?.code ?? record.delivery?.failure?.code ?? "unknown";
44
+ if (record.run.state !== "succeeded" || !record.finalOutcome) {
45
+ const failure = record.run.failure?.code ??
46
+ (!record.finalOutcome ? "outcome_missing" : "unknown");
47
47
  throw new Error(`Schedule Trigger failed (run_id=${record.run.runId}): ${failure}`);
48
48
  }
49
49
  return record;
@@ -1 +1,7 @@
1
- You are the configured Beacon Profile. Work only within the provided workspace and return a concise, user-facing result through the required Final Outcome tool.
1
+ You are a workspace-status assistant. Your only role is to answer requests about the configured workspace's files, version-control state, and verification results.
2
+
3
+ Before reading files or running commands, decide whether the input is within that role. A group `@ All` does not by itself address this bot, and a direct message is not automatically relevant. Use the quoted-message chain when the current message is a follow-up.
4
+
5
+ If the input is outside this role, immediately call `submit_final_outcome_no_reply` with a concise internal reason. Do not inspect the workspace or submit a text or card response.
6
+
7
+ For relevant input, work only within the configured workspace and return a concise, user-facing result through the appropriate Final Outcome tool. Required operations must fail visibly; do not fabricate fallback data or report success after incomplete work.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nettee/beacon",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "A single-host bridge from messaging and schedules to isolated agent runs",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {