@nettee/beacon 0.2.1 → 0.2.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 +10 -3
- package/dist/config/profile.js +26 -21
- package/dist/dashboard/export.js +2 -2
- package/dist/dashboard/records.js +2 -0
- package/dist/dashboard/server.js +27 -0
- package/dist/dashboard/session-prompt.js +7 -0
- package/dist/outcome/instructions.js +4 -5
- package/dist/run/orchestrator.js +20 -10
- package/dist/run/profile-runner.js +4 -1
- package/dist/runtime/system-prompt.js +54 -5
- package/dist/state/trigger-store.js +1 -0
- package/examples/profiles/example/persona.md +5 -0
- package/examples/profiles/example/profile.yaml +0 -1
- package/examples/profiles/example/task.md +5 -0
- package/package.json +1 -1
- package/examples/profiles/example/prompt.md +0 -9
package/README.md
CHANGED
|
@@ -53,9 +53,16 @@ Copy the files under [`examples`](examples) into a private Beacon home and repla
|
|
|
53
53
|
└── profiles/
|
|
54
54
|
└── example/
|
|
55
55
|
├── profile.yaml
|
|
56
|
-
|
|
56
|
+
├── persona.md
|
|
57
|
+
└── task.md
|
|
57
58
|
```
|
|
58
59
|
|
|
60
|
+
Each Profile directory must contain `persona.md` (identity) and `task.md`
|
|
61
|
+
(process). Beacon prepends an English platform template for this Run: workspace
|
|
62
|
+
limit, inbound vs Schedule vs manual capabilities, and the `reply` /
|
|
63
|
+
`no_reply` / `notify_card` contract. It does not read yaml `prompt:` or
|
|
64
|
+
`prompt.md`. Missing either Markdown file fails Profile load.
|
|
65
|
+
|
|
59
66
|
Protect the home and secret file before running Beacon:
|
|
60
67
|
|
|
61
68
|
```sh
|
|
@@ -95,7 +102,7 @@ created by Beacon 0.1.2 and earlier; when omitted it defaults to `sessions/`
|
|
|
95
102
|
beside `config.yaml`. When configured, it must be absolute. Beacon creates
|
|
96
103
|
per-Run directories with mode `0700` when Pi starts.
|
|
97
104
|
|
|
98
|
-
Configuration is strict: unknown YAML/JSON fields, YAML aliases or warnings, missing paths, duplicate Schedule IDs, invalid timezones/cron expressions,
|
|
105
|
+
Configuration is strict: unknown YAML/JSON fields, YAML aliases or warnings, missing paths, duplicate Schedule IDs, invalid timezones/cron expressions, missing `persona.md` or `task.md`, escaped Profile markdown, permissive secret or runtime-environment permissions, and missing Profile credentials all fail startup. Beacon validates all Profiles before opening a Feishu connection.
|
|
99
106
|
|
|
100
107
|
Each Schedule uses a five-field cron expression and an IANA timezone. Profile
|
|
101
108
|
`admin.chat_id` is the private chat used for schedule `reply` / `no_reply` and
|
|
@@ -237,7 +244,7 @@ The repository also contains Chinese operator runbooks based on the verified
|
|
|
237
244
|
|
|
238
245
|
- [Deploy or update Beacon on macmini](docs/macmini-deploy-and-update.md)
|
|
239
246
|
- [Add a Beacon Profile on macmini](docs/macmini-add-profile.md)
|
|
240
|
-
- [Write a role-bounded Profile
|
|
247
|
+
- [Write a role-bounded Profile (`persona.md` / `task.md`)](docs/profile-prompt-writing.md)
|
|
241
248
|
|
|
242
249
|
## Development checks
|
|
243
250
|
|
package/dist/config/profile.js
CHANGED
|
@@ -5,6 +5,8 @@ import { z } from "zod";
|
|
|
5
5
|
import { nextOccurrence } from "../schedule/cron.js";
|
|
6
6
|
import { parseStrictYaml } from "./yaml.js";
|
|
7
7
|
const profileIdPattern = /^[a-z0-9](?:[a-z0-9_-]{0,62})$/;
|
|
8
|
+
export const profilePersonaFile = "persona.md";
|
|
9
|
+
export const profileTaskFile = "task.md";
|
|
8
10
|
const scheduleSchema = z
|
|
9
11
|
.object({
|
|
10
12
|
id: z.string().regex(profileIdPattern),
|
|
@@ -35,7 +37,6 @@ const scheduleSchema = z
|
|
|
35
37
|
.strict();
|
|
36
38
|
const profileDocumentSchema = z
|
|
37
39
|
.object({
|
|
38
|
-
prompt: z.string().min(1),
|
|
39
40
|
workspace: z.string().min(1),
|
|
40
41
|
runtime: z.literal("pi"),
|
|
41
42
|
model: z
|
|
@@ -60,6 +61,26 @@ function isWithin(parent, child) {
|
|
|
60
61
|
const path = relative(parent, child);
|
|
61
62
|
return path === "" || (!path.startsWith("..") && !isAbsolute(path));
|
|
62
63
|
}
|
|
64
|
+
async function loadRequiredMarkdown(canonicalProfileDirectory, filename) {
|
|
65
|
+
const filePath = join(canonicalProfileDirectory, filename);
|
|
66
|
+
let canonicalFilePath;
|
|
67
|
+
try {
|
|
68
|
+
canonicalFilePath = await realpath(filePath);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
throw new Error(`Cannot read Profile ${filename} at ${filePath}`, {
|
|
72
|
+
cause: error,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (!isWithin(canonicalProfileDirectory, canonicalFilePath)) {
|
|
76
|
+
throw new Error(`Profile ${filename} must stay inside ${canonicalProfileDirectory}`);
|
|
77
|
+
}
|
|
78
|
+
const text = (await readFile(canonicalFilePath, "utf8")).trim();
|
|
79
|
+
if (!text) {
|
|
80
|
+
throw new Error(`Profile ${filename} must not be empty: ${canonicalFilePath}`);
|
|
81
|
+
}
|
|
82
|
+
return text;
|
|
83
|
+
}
|
|
63
84
|
export async function loadProfile(profileId, profilesDirectory = join(homedir(), ".beacon", "profiles")) {
|
|
64
85
|
assertProfileId(profileId);
|
|
65
86
|
const profileDirectory = join(profilesDirectory, profileId);
|
|
@@ -83,26 +104,9 @@ export async function loadProfile(profileId, profilesDirectory = join(homedir(),
|
|
|
83
104
|
if (config.schedules.length > 0 && !config.admin) {
|
|
84
105
|
throw new Error(`Profile ${profileId} with schedules must declare admin.chat_id`);
|
|
85
106
|
}
|
|
86
|
-
if (isAbsolute(config.prompt)) {
|
|
87
|
-
throw new Error(`Profile prompt path must be relative to ${profileDirectory}`);
|
|
88
|
-
}
|
|
89
|
-
const promptPath = resolve(profileDirectory, config.prompt);
|
|
90
107
|
const canonicalProfileDirectory = await realpath(profileDirectory);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
canonicalPromptPath = await realpath(promptPath);
|
|
94
|
-
}
|
|
95
|
-
catch (error) {
|
|
96
|
-
throw new Error(`Cannot read Profile prompt at ${promptPath}`, {
|
|
97
|
-
cause: error,
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
if (!isWithin(canonicalProfileDirectory, canonicalPromptPath)) {
|
|
101
|
-
throw new Error(`Profile prompt must stay inside ${canonicalProfileDirectory}`);
|
|
102
|
-
}
|
|
103
|
-
const prompt = (await readFile(canonicalPromptPath, "utf8")).trim();
|
|
104
|
-
if (!prompt)
|
|
105
|
-
throw new Error(`Profile prompt must not be empty: ${canonicalPromptPath}`);
|
|
108
|
+
const persona = await loadRequiredMarkdown(canonicalProfileDirectory, profilePersonaFile);
|
|
109
|
+
const task = await loadRequiredMarkdown(canonicalProfileDirectory, profileTaskFile);
|
|
106
110
|
const workspace = isAbsolute(config.workspace)
|
|
107
111
|
? config.workspace
|
|
108
112
|
: resolve(dirname(configPath), config.workspace);
|
|
@@ -121,7 +125,8 @@ export async function loadProfile(profileId, profilesDirectory = join(homedir(),
|
|
|
121
125
|
return {
|
|
122
126
|
id: profileId,
|
|
123
127
|
directory: canonicalProfileDirectory,
|
|
124
|
-
|
|
128
|
+
persona,
|
|
129
|
+
task,
|
|
125
130
|
workspace,
|
|
126
131
|
runtime: config.runtime,
|
|
127
132
|
model: config.model,
|
package/dist/dashboard/export.js
CHANGED
|
@@ -3,7 +3,7 @@ import { mkdtemp, readdir, readFile, realpath, rm } from "node:fs/promises";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { basename, dirname, isAbsolute, join, sep } from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
|
-
import { fillExportedHtmlSystemPrompt,
|
|
6
|
+
import { fillExportedHtmlSystemPrompt, pickExportedSystemPrompt, } from "./session-prompt.js";
|
|
7
7
|
const execFileAsync = promisify(execFile);
|
|
8
8
|
function isInside(root, candidate) {
|
|
9
9
|
return candidate === root || candidate.startsWith(`${root}${sep}`);
|
|
@@ -44,7 +44,7 @@ export async function exportSessionHtml(request) {
|
|
|
44
44
|
windowsHide: true,
|
|
45
45
|
});
|
|
46
46
|
const html = await readFile(outputPath, "utf8");
|
|
47
|
-
const systemPrompt =
|
|
47
|
+
const systemPrompt = pickExportedSystemPrompt(await readFile(jsonlPath, "utf8"), request.systemPrompt);
|
|
48
48
|
return systemPrompt
|
|
49
49
|
? fillExportedHtmlSystemPrompt(html, systemPrompt)
|
|
50
50
|
: html;
|
|
@@ -41,12 +41,14 @@ function summarize(value, hasSessionFile) {
|
|
|
41
41
|
acceptedAt,
|
|
42
42
|
kind: asString(input?.kind) ?? "unknown",
|
|
43
43
|
scheduleId: asString(input?.scheduleId),
|
|
44
|
+
hasNotifyTarget: asRecord(record.notifyTarget) !== undefined,
|
|
44
45
|
runId: asString(run?.runId),
|
|
45
46
|
state: asString(run?.state),
|
|
46
47
|
failureCode: asString(failure?.code),
|
|
47
48
|
deliveryState: asString(delivery?.state),
|
|
48
49
|
sessionPath: asString(run?.sessionPath),
|
|
49
50
|
hasSessionFile,
|
|
51
|
+
systemPrompt: asString(run?.systemPrompt),
|
|
50
52
|
};
|
|
51
53
|
}
|
|
52
54
|
async function loadSummary(path) {
|
package/dist/dashboard/server.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createServer, } from "node:http";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { loadProfile } from "../config/profile.js";
|
|
5
|
+
import { buildAgentSystemPrompt, } from "../runtime/system-prompt.js";
|
|
4
6
|
import { exportSessionHtml } from "./export.js";
|
|
5
7
|
import { findRunSummary, listRunSummaries, runIdPattern, toRunListItem, } from "./records.js";
|
|
6
8
|
import { sendStaticFile } from "./static.js";
|
|
@@ -18,6 +20,30 @@ function sendText(response, status, body) {
|
|
|
18
20
|
export function defaultUiDirectory() {
|
|
19
21
|
return join(dirname(fileURLToPath(import.meta.url)), "ui");
|
|
20
22
|
}
|
|
23
|
+
function triggerFromRunSummary(summary) {
|
|
24
|
+
if (summary.kind === "schedule") {
|
|
25
|
+
return {
|
|
26
|
+
kind: "schedule",
|
|
27
|
+
...(summary.scheduleId ? { scheduleId: summary.scheduleId } : {}),
|
|
28
|
+
notify: summary.hasNotifyTarget,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (summary.kind === "feishu_message") {
|
|
32
|
+
return { kind: "feishu_message", notify: false };
|
|
33
|
+
}
|
|
34
|
+
return { kind: "manual", notify: summary.hasNotifyTarget };
|
|
35
|
+
}
|
|
36
|
+
async function beaconOwnedSystemPrompt(summary, profilesDirectory) {
|
|
37
|
+
if (summary.systemPrompt)
|
|
38
|
+
return summary.systemPrompt;
|
|
39
|
+
try {
|
|
40
|
+
return buildAgentSystemPrompt(await loadProfile(summary.profileId, profilesDirectory), triggerFromRunSummary(summary));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Old runs without a stored prompt: reconstruction needs a readable Profile.
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
21
47
|
export async function startDashboard(options) {
|
|
22
48
|
const cache = new Map();
|
|
23
49
|
const inflight = new Map();
|
|
@@ -63,6 +89,7 @@ export async function startDashboard(options) {
|
|
|
63
89
|
sessionDirectory: options.sessionDirectory,
|
|
64
90
|
runId: summary.runId,
|
|
65
91
|
sessionPath: summary.sessionPath,
|
|
92
|
+
systemPrompt: await beaconOwnedSystemPrompt(summary, options.profilesDirectory),
|
|
66
93
|
timeoutMs: options.exportTimeoutMs,
|
|
67
94
|
}).finally(() => {
|
|
68
95
|
inflight.delete(runId);
|
|
@@ -23,6 +23,13 @@ function systemMessageText(content, sections) {
|
|
|
23
23
|
.filter((part) => part.length > 0)
|
|
24
24
|
.join("\n\n");
|
|
25
25
|
}
|
|
26
|
+
function nonempty(value) {
|
|
27
|
+
return value && value.length > 0 ? value : undefined;
|
|
28
|
+
}
|
|
29
|
+
/** Prefer a JSONL system message; otherwise the Beacon-owned `--system-prompt` text. */
|
|
30
|
+
export function pickExportedSystemPrompt(jsonl, beaconOwned) {
|
|
31
|
+
return systemPromptFromJsonl(jsonl) ?? nonempty(beaconOwned);
|
|
32
|
+
}
|
|
26
33
|
export function systemPromptFromJsonl(jsonl) {
|
|
27
34
|
const contents = [];
|
|
28
35
|
const sections = new Map();
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
export const inboundOutOfRoleReply = "这条消息不在我的职责范围内。";
|
|
2
|
-
export const
|
|
3
|
-
"
|
|
4
|
-
"`
|
|
5
|
-
"`
|
|
6
|
-
"`notify_card` posts a titled Markdown card to the Schedule's configured group. Call it only when this Run is a Schedule with a notify target. Inbound messages cannot notify.",
|
|
2
|
+
export const finalOutcomeContractInstructions = [
|
|
3
|
+
"`reply` 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`.",
|
|
4
|
+
"`no_reply` finishes a Run without messaging anyone. Its reason is stored for audit and is never sent.",
|
|
5
|
+
"`notify_card` posts a titled Markdown card to the Schedule's configured notify group. Do not fill `chat_id`.",
|
|
7
6
|
"Beacon ignores ordinary assistant final text for Delivery.",
|
|
8
7
|
];
|
package/dist/run/orchestrator.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { isAbsolute, join } from "node:path";
|
|
3
3
|
import { inboundOutOfRoleReply } from "../outcome/instructions.js";
|
|
4
4
|
import { PiRuntimeError } from "../runtime/pi-rpc.js";
|
|
5
|
-
import { buildAgentSystemPrompt } from "../runtime/system-prompt.js";
|
|
5
|
+
import { agentSystemPromptTrigger, buildAgentSystemPrompt, profilePromptDigest, } from "../runtime/system-prompt.js";
|
|
6
6
|
function promptFor(input) {
|
|
7
7
|
if (input.kind === "manual") {
|
|
8
8
|
return ["An operator manually triggered this Run.", "", input.text].join("\n");
|
|
@@ -80,7 +80,15 @@ export class RunOrchestrator {
|
|
|
80
80
|
throw new Error(`Unknown Trigger ${triggerKey}`);
|
|
81
81
|
return found;
|
|
82
82
|
}
|
|
83
|
-
|
|
83
|
+
agentSystemPrompt(trigger, existing) {
|
|
84
|
+
return existing && existing.length > 0
|
|
85
|
+
? existing
|
|
86
|
+
: buildAgentSystemPrompt(this.options.profile, trigger);
|
|
87
|
+
}
|
|
88
|
+
triggerFor(record, input) {
|
|
89
|
+
return agentSystemPromptTrigger(input ?? record.input, record.notifyTarget);
|
|
90
|
+
}
|
|
91
|
+
newRun(runId, state, trigger, failure) {
|
|
84
92
|
const timestamp = this.timestamp();
|
|
85
93
|
return {
|
|
86
94
|
runId,
|
|
@@ -92,9 +100,8 @@ export class RunOrchestrator {
|
|
|
92
100
|
provider: this.options.profile.model.provider,
|
|
93
101
|
model: this.options.profile.model.id,
|
|
94
102
|
workspace: this.options.profile.workspace,
|
|
95
|
-
promptDigest:
|
|
96
|
-
|
|
97
|
-
.digest("hex"),
|
|
103
|
+
promptDigest: profilePromptDigest(this.options.profile),
|
|
104
|
+
systemPrompt: this.agentSystemPrompt(trigger),
|
|
98
105
|
...(failure ? { failure } : {}),
|
|
99
106
|
};
|
|
100
107
|
}
|
|
@@ -166,7 +173,8 @@ export class RunOrchestrator {
|
|
|
166
173
|
await this.options.store.update(triggerKey, (current) => ({
|
|
167
174
|
...current,
|
|
168
175
|
run: {
|
|
169
|
-
...(current.run ??
|
|
176
|
+
...(current.run ??
|
|
177
|
+
this.newRun(runId, "queued", this.triggerFor(current))),
|
|
170
178
|
state: "failed",
|
|
171
179
|
finishedAt: this.timestamp(),
|
|
172
180
|
failure: { code, summary: detail },
|
|
@@ -193,6 +201,7 @@ export class RunOrchestrator {
|
|
|
193
201
|
sessionId: current.run.runId,
|
|
194
202
|
sessionPath: join(this.options.sessionDirectory, this.options.profile.id, current.run.runId),
|
|
195
203
|
};
|
|
204
|
+
const systemPrompt = this.agentSystemPrompt(this.triggerFor(current, input), current.run.systemPrompt);
|
|
196
205
|
return {
|
|
197
206
|
...current,
|
|
198
207
|
run: {
|
|
@@ -200,6 +209,7 @@ export class RunOrchestrator {
|
|
|
200
209
|
...session,
|
|
201
210
|
state: "starting",
|
|
202
211
|
startedAt: this.timestamp(),
|
|
212
|
+
systemPrompt,
|
|
203
213
|
},
|
|
204
214
|
};
|
|
205
215
|
});
|
|
@@ -214,7 +224,7 @@ export class RunOrchestrator {
|
|
|
214
224
|
workspace: this.options.profile.workspace,
|
|
215
225
|
provider: this.options.profile.model.provider,
|
|
216
226
|
model: this.options.profile.model.id,
|
|
217
|
-
systemPrompt:
|
|
227
|
+
systemPrompt: record.run.systemPrompt,
|
|
218
228
|
outcome: { ...submission.binding, cliPath: this.options.beaconCliPath },
|
|
219
229
|
session: {
|
|
220
230
|
id: record.run.sessionId,
|
|
@@ -260,7 +270,7 @@ export class RunOrchestrator {
|
|
|
260
270
|
catch (error) {
|
|
261
271
|
await this.options.store.update(triggerKey, (current) => ({
|
|
262
272
|
...current,
|
|
263
|
-
run: this.newRun(runId, "failed", {
|
|
273
|
+
run: this.newRun(runId, "failed", this.triggerFor(current), {
|
|
264
274
|
code: "trigger_normalization_failed",
|
|
265
275
|
summary: summary(error),
|
|
266
276
|
}),
|
|
@@ -276,7 +286,7 @@ export class RunOrchestrator {
|
|
|
276
286
|
await this.options.store.update(triggerKey, (current) => ({
|
|
277
287
|
...current,
|
|
278
288
|
input,
|
|
279
|
-
run: this.newRun(runId, "queued"),
|
|
289
|
+
run: this.newRun(runId, "queued", this.triggerFor(current, input)),
|
|
280
290
|
}));
|
|
281
291
|
const queued = this.options.queue.enqueue(() => this.execute(triggerKey, input, runId));
|
|
282
292
|
if (!queued.accepted) {
|
|
@@ -27,7 +27,10 @@ export function createProfileRunner(profile, outcomes, beaconCliPath, runAgent =
|
|
|
27
27
|
workspace: profile.workspace,
|
|
28
28
|
provider: profile.model.provider,
|
|
29
29
|
model: profile.model.id,
|
|
30
|
-
systemPrompt: buildAgentSystemPrompt(profile
|
|
30
|
+
systemPrompt: buildAgentSystemPrompt(profile, {
|
|
31
|
+
kind: "feishu_message",
|
|
32
|
+
notify: false,
|
|
33
|
+
}),
|
|
31
34
|
outcome: { ...submission.binding, cliPath: beaconCliPath },
|
|
32
35
|
});
|
|
33
36
|
return submission.take();
|
|
@@ -1,10 +1,59 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { finalOutcomeContractInstructions } from "../outcome/instructions.js";
|
|
3
|
+
export function agentSystemPromptTrigger(input, notifyTarget) {
|
|
4
|
+
const notify = notifyTarget !== undefined;
|
|
5
|
+
if (input?.kind === "schedule") {
|
|
6
|
+
return { kind: "schedule", scheduleId: input.scheduleId, notify };
|
|
7
|
+
}
|
|
8
|
+
if (input?.kind === "feishu_message") {
|
|
9
|
+
return { kind: "feishu_message", notify: false };
|
|
10
|
+
}
|
|
11
|
+
return { kind: "manual", notify };
|
|
12
|
+
}
|
|
13
|
+
export function profilePromptDigest(profile) {
|
|
14
|
+
return createHash("sha256")
|
|
15
|
+
.update(profile.persona)
|
|
16
|
+
.update("\n")
|
|
17
|
+
.update(profile.task)
|
|
18
|
+
.digest("hex");
|
|
19
|
+
}
|
|
20
|
+
export function buildAgentSystemPrompt(profile, trigger) {
|
|
3
21
|
return [
|
|
4
|
-
profile.prompt,
|
|
5
|
-
"",
|
|
6
22
|
`All local file reads, searches, and modifications must stay within the workspace directory and its descendants: ${profile.workspace}`,
|
|
7
23
|
"",
|
|
8
|
-
...
|
|
24
|
+
...thisRunCapabilityInstructions(trigger),
|
|
25
|
+
"",
|
|
26
|
+
...finalOutcomeContractInstructions,
|
|
27
|
+
"",
|
|
28
|
+
profile.persona,
|
|
29
|
+
"",
|
|
30
|
+
profile.task,
|
|
9
31
|
].join("\n");
|
|
10
32
|
}
|
|
33
|
+
function thisRunCapabilityInstructions(trigger) {
|
|
34
|
+
if (trigger.kind === "feishu_message") {
|
|
35
|
+
return [
|
|
36
|
+
"This Run is an inbound Feishu message.",
|
|
37
|
+
"Close the conversational channel with `reply`. Inbound Runs must call `reply` even when the message is outside this Profile's role: send a short text saying the request is out of scope.",
|
|
38
|
+
"Do not call `no_reply`. Do not call `notify_card`.",
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
const notifyLine = trigger.notify
|
|
42
|
+
? "You may also call `notify_card` at most once."
|
|
43
|
+
: "Do not call `notify_card`.";
|
|
44
|
+
if (trigger.kind === "schedule") {
|
|
45
|
+
const label = trigger.scheduleId
|
|
46
|
+
? `This Run is Schedule ${trigger.scheduleId}.`
|
|
47
|
+
: "This Run is a Schedule.";
|
|
48
|
+
return [
|
|
49
|
+
label,
|
|
50
|
+
"Close the conversational channel with exactly one of `reply` or `no_reply`.",
|
|
51
|
+
notifyLine,
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
return [
|
|
55
|
+
"This Run is a manual operator trigger.",
|
|
56
|
+
"Close the conversational channel with exactly one of `reply` or `no_reply`.",
|
|
57
|
+
notifyLine,
|
|
58
|
+
];
|
|
59
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
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 a Feishu message is outside this role, do not inspect the workspace. Explain in one or two sentences that the request is out of scope.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
For in-role work, inspect the configured workspace and produce a concise user-facing result.
|
|
2
|
+
|
|
3
|
+
Scheduled Runs that have nothing to report should stay silent on the admin channel. If there is a report for the notify group, send a card to that group and stay silent on the admin channel unless the admin also needs a text update.
|
|
4
|
+
|
|
5
|
+
Inbound in-role work should report the result to the user. Required operations must fail visibly; do not fabricate fallback data or report success after incomplete work.
|
package/package.json
CHANGED
|
@@ -1,9 +0,0 @@
|
|
|
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 a Feishu message is outside this role, call `reply` with a short text saying the request is out of scope. Do not inspect the workspace. Do not call `no_reply` on inbound messages.
|
|
6
|
-
|
|
7
|
-
Scheduled Runs that have nothing to report should call `no_reply`. If a Schedule has a notify group and there is a report for that group, call `notify_card` and then `no_reply` unless the admin also needs a text update.
|
|
8
|
-
|
|
9
|
-
For in-role work, stay inside the configured workspace and call `reply` with a concise user-facing result. Required operations must fail visibly; do not fabricate fallback data or report success after incomplete work.
|