@nettee/beacon 0.2.2 → 0.2.4
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 +20 -6
- package/dist/config/profile.js +88 -21
- package/dist/dashboard/records.js +1 -0
- package/dist/dashboard/server.js +15 -2
- package/dist/outcome/instructions.js +4 -5
- package/dist/run/orchestrator.js +15 -13
- package/dist/run/profile-runner.js +4 -1
- package/dist/runtime/system-prompt.js +54 -5
- package/examples/profiles/example/profile.yaml +1 -2
- package/examples/workspace/.beacon-profile/persona.md +5 -0
- package/examples/workspace/.beacon-profile/task.md +5 -0
- package/package.json +1 -1
- package/examples/profiles/example/prompt.md +0 -9
package/README.md
CHANGED
|
@@ -50,12 +50,26 @@ Copy the files under [`examples`](examples) into a private Beacon home and repla
|
|
|
50
50
|
├── config.yaml
|
|
51
51
|
├── runtime.env
|
|
52
52
|
├── secrets.json
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
├── profiles/
|
|
54
|
+
│ └── example/
|
|
55
|
+
│ └── profile.yaml
|
|
56
|
+
└── workspace/
|
|
57
|
+
└── .beacon-profile/
|
|
58
|
+
├── persona.md
|
|
59
|
+
└── task.md
|
|
57
60
|
```
|
|
58
61
|
|
|
62
|
+
yaml names the workspace directory only. Beacon then loads `persona.md`
|
|
63
|
+
(identity) and `task.md` (process) as a pair: `{workspace}/.beacon-profile/`
|
|
64
|
+
first; if that pair is missing, `{profileDir}/`. It never mixes the two
|
|
65
|
+
places, never reads yaml `prompt:` / persona / task paths, and never falls
|
|
66
|
+
back to `prompt.md`. If neither place has a complete pair, Profile load fails
|
|
67
|
+
and lists the missing paths. Both pairs may exist; the workspace pair wins.
|
|
68
|
+
|
|
69
|
+
Beacon prepends an English platform template for this Run: workspace limit,
|
|
70
|
+
inbound vs Schedule vs manual capabilities, and the `reply` / `no_reply` /
|
|
71
|
+
`notify_card` contract.
|
|
72
|
+
|
|
59
73
|
Protect the home and secret file before running Beacon:
|
|
60
74
|
|
|
61
75
|
```sh
|
|
@@ -95,7 +109,7 @@ created by Beacon 0.1.2 and earlier; when omitted it defaults to `sessions/`
|
|
|
95
109
|
beside `config.yaml`. When configured, it must be absolute. Beacon creates
|
|
96
110
|
per-Run directories with mode `0700` when Pi starts.
|
|
97
111
|
|
|
98
|
-
Configuration is strict: unknown YAML/JSON fields, YAML aliases or warnings, missing paths, duplicate Schedule IDs, invalid timezones/cron expressions,
|
|
112
|
+
Configuration is strict: unknown YAML/JSON fields, YAML aliases or warnings, missing paths, duplicate Schedule IDs, invalid timezones/cron expressions, a missing complete `persona.md` + `task.md` pair, 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
113
|
|
|
100
114
|
Each Schedule uses a five-field cron expression and an IANA timezone. Profile
|
|
101
115
|
`admin.chat_id` is the private chat used for schedule `reply` / `no_reply` and
|
|
@@ -237,7 +251,7 @@ The repository also contains Chinese operator runbooks based on the verified
|
|
|
237
251
|
|
|
238
252
|
- [Deploy or update Beacon on macmini](docs/macmini-deploy-and-update.md)
|
|
239
253
|
- [Add a Beacon Profile on macmini](docs/macmini-add-profile.md)
|
|
240
|
-
- [Write a role-bounded Profile
|
|
254
|
+
- [Write a role-bounded Profile (`persona.md` / `task.md`)](docs/profile-prompt-writing.md)
|
|
241
255
|
|
|
242
256
|
## Development checks
|
|
243
257
|
|
package/dist/config/profile.js
CHANGED
|
@@ -5,6 +5,9 @@ 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";
|
|
10
|
+
export const workspaceProfileDirectoryName = ".beacon-profile";
|
|
8
11
|
const scheduleSchema = z
|
|
9
12
|
.object({
|
|
10
13
|
id: z.string().regex(profileIdPattern),
|
|
@@ -35,7 +38,6 @@ const scheduleSchema = z
|
|
|
35
38
|
.strict();
|
|
36
39
|
const profileDocumentSchema = z
|
|
37
40
|
.object({
|
|
38
|
-
prompt: z.string().min(1),
|
|
39
41
|
workspace: z.string().min(1),
|
|
40
42
|
runtime: z.literal("pi"),
|
|
41
43
|
model: z
|
|
@@ -60,6 +62,88 @@ function isWithin(parent, child) {
|
|
|
60
62
|
const path = relative(parent, child);
|
|
61
63
|
return path === "" || (!path.startsWith("..") && !isAbsolute(path));
|
|
62
64
|
}
|
|
65
|
+
export function workspaceProfileDirectory(workspace) {
|
|
66
|
+
return join(workspace, workspaceProfileDirectoryName);
|
|
67
|
+
}
|
|
68
|
+
async function isExistingFile(path) {
|
|
69
|
+
try {
|
|
70
|
+
return (await stat(path)).isFile();
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function markdownPairPaths(directory) {
|
|
77
|
+
return {
|
|
78
|
+
persona: join(directory, profilePersonaFile),
|
|
79
|
+
task: join(directory, profileTaskFile),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
async function missingMarkdownPaths(directory) {
|
|
83
|
+
const paths = markdownPairPaths(directory);
|
|
84
|
+
const missing = [];
|
|
85
|
+
if (!(await isExistingFile(paths.persona)))
|
|
86
|
+
missing.push(paths.persona);
|
|
87
|
+
if (!(await isExistingFile(paths.task)))
|
|
88
|
+
missing.push(paths.task);
|
|
89
|
+
return missing;
|
|
90
|
+
}
|
|
91
|
+
async function markdownPairExists(directory) {
|
|
92
|
+
return (await missingMarkdownPaths(directory)).length === 0;
|
|
93
|
+
}
|
|
94
|
+
async function loadRequiredMarkdown(canonicalDirectory, filename) {
|
|
95
|
+
const filePath = join(canonicalDirectory, filename);
|
|
96
|
+
let canonicalFilePath;
|
|
97
|
+
try {
|
|
98
|
+
canonicalFilePath = await realpath(filePath);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
throw new Error(`Cannot read Profile ${filename} at ${filePath}`, {
|
|
102
|
+
cause: error,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
if (!isWithin(canonicalDirectory, canonicalFilePath)) {
|
|
106
|
+
throw new Error(`Profile ${filename} must stay inside ${canonicalDirectory}`);
|
|
107
|
+
}
|
|
108
|
+
const text = (await readFile(canonicalFilePath, "utf8")).trim();
|
|
109
|
+
if (!text) {
|
|
110
|
+
throw new Error(`Profile ${filename} must not be empty: ${canonicalFilePath}`);
|
|
111
|
+
}
|
|
112
|
+
return text;
|
|
113
|
+
}
|
|
114
|
+
async function loadMarkdownPair(directory, boundary) {
|
|
115
|
+
const canonicalBoundary = await realpath(boundary);
|
|
116
|
+
let canonicalDirectory;
|
|
117
|
+
try {
|
|
118
|
+
canonicalDirectory = await realpath(directory);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
throw new Error(`Cannot read Profile markdown at ${directory}`, {
|
|
122
|
+
cause: error,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
if (!isWithin(canonicalBoundary, canonicalDirectory)) {
|
|
126
|
+
throw new Error(`Profile markdown must stay inside ${canonicalBoundary}`);
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
persona: await loadRequiredMarkdown(canonicalDirectory, profilePersonaFile),
|
|
130
|
+
task: await loadRequiredMarkdown(canonicalDirectory, profileTaskFile),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
async function loadPersonaAndTask(profileId, workspace, profileDirectory) {
|
|
134
|
+
const workspaceDirectory = workspaceProfileDirectory(workspace);
|
|
135
|
+
if (await markdownPairExists(workspaceDirectory)) {
|
|
136
|
+
return loadMarkdownPair(workspaceDirectory, workspace);
|
|
137
|
+
}
|
|
138
|
+
if (await markdownPairExists(profileDirectory)) {
|
|
139
|
+
return loadMarkdownPair(profileDirectory, profileDirectory);
|
|
140
|
+
}
|
|
141
|
+
const missing = [
|
|
142
|
+
...(await missingMarkdownPaths(workspaceDirectory)),
|
|
143
|
+
...(await missingMarkdownPaths(profileDirectory)),
|
|
144
|
+
];
|
|
145
|
+
throw new Error(`Profile ${profileId} has no complete ${profilePersonaFile} + ${profileTaskFile} pair. Missing: ${missing.join(", ")}`);
|
|
146
|
+
}
|
|
63
147
|
export async function loadProfile(profileId, profilesDirectory = join(homedir(), ".beacon", "profiles")) {
|
|
64
148
|
assertProfileId(profileId);
|
|
65
149
|
const profileDirectory = join(profilesDirectory, profileId);
|
|
@@ -83,26 +167,7 @@ export async function loadProfile(profileId, profilesDirectory = join(homedir(),
|
|
|
83
167
|
if (config.schedules.length > 0 && !config.admin) {
|
|
84
168
|
throw new Error(`Profile ${profileId} with schedules must declare admin.chat_id`);
|
|
85
169
|
}
|
|
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
170
|
const canonicalProfileDirectory = await realpath(profileDirectory);
|
|
91
|
-
let canonicalPromptPath;
|
|
92
|
-
try {
|
|
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}`);
|
|
106
171
|
const workspace = isAbsolute(config.workspace)
|
|
107
172
|
? config.workspace
|
|
108
173
|
: resolve(dirname(configPath), config.workspace);
|
|
@@ -118,10 +183,12 @@ export async function loadProfile(profileId, profilesDirectory = join(homedir(),
|
|
|
118
183
|
if (!workspaceStat.isDirectory()) {
|
|
119
184
|
throw new Error(`Profile workspace is not a directory: ${workspace}`);
|
|
120
185
|
}
|
|
186
|
+
const { persona, task } = await loadPersonaAndTask(profileId, workspace, canonicalProfileDirectory);
|
|
121
187
|
return {
|
|
122
188
|
id: profileId,
|
|
123
189
|
directory: canonicalProfileDirectory,
|
|
124
|
-
|
|
190
|
+
persona,
|
|
191
|
+
task,
|
|
125
192
|
workspace,
|
|
126
193
|
runtime: config.runtime,
|
|
127
194
|
model: config.model,
|
|
@@ -41,6 +41,7 @@ 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),
|
package/dist/dashboard/server.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createServer, } from "node:http";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { loadProfile } from "../config/profile.js";
|
|
5
|
-
import { buildAgentSystemPrompt } from "../runtime/system-prompt.js";
|
|
5
|
+
import { buildAgentSystemPrompt, } from "../runtime/system-prompt.js";
|
|
6
6
|
import { exportSessionHtml } from "./export.js";
|
|
7
7
|
import { findRunSummary, listRunSummaries, runIdPattern, toRunListItem, } from "./records.js";
|
|
8
8
|
import { sendStaticFile } from "./static.js";
|
|
@@ -20,11 +20,24 @@ function sendText(response, status, body) {
|
|
|
20
20
|
export function defaultUiDirectory() {
|
|
21
21
|
return join(dirname(fileURLToPath(import.meta.url)), "ui");
|
|
22
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
|
+
}
|
|
23
36
|
async function beaconOwnedSystemPrompt(summary, profilesDirectory) {
|
|
24
37
|
if (summary.systemPrompt)
|
|
25
38
|
return summary.systemPrompt;
|
|
26
39
|
try {
|
|
27
|
-
return buildAgentSystemPrompt(await loadProfile(summary.profileId, profilesDirectory));
|
|
40
|
+
return buildAgentSystemPrompt(await loadProfile(summary.profileId, profilesDirectory), triggerFromRunSummary(summary));
|
|
28
41
|
}
|
|
29
42
|
catch {
|
|
30
43
|
// Old runs without a stored prompt: reconstruction needs a readable Profile.
|
|
@@ -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,12 +80,15 @@ export class RunOrchestrator {
|
|
|
80
80
|
throw new Error(`Unknown Trigger ${triggerKey}`);
|
|
81
81
|
return found;
|
|
82
82
|
}
|
|
83
|
-
agentSystemPrompt(existing) {
|
|
83
|
+
agentSystemPrompt(trigger, existing) {
|
|
84
84
|
return existing && existing.length > 0
|
|
85
85
|
? existing
|
|
86
|
-
: buildAgentSystemPrompt(this.options.profile);
|
|
86
|
+
: buildAgentSystemPrompt(this.options.profile, trigger);
|
|
87
87
|
}
|
|
88
|
-
|
|
88
|
+
triggerFor(record, input) {
|
|
89
|
+
return agentSystemPromptTrigger(input ?? record.input, record.notifyTarget);
|
|
90
|
+
}
|
|
91
|
+
newRun(runId, state, trigger, failure) {
|
|
89
92
|
const timestamp = this.timestamp();
|
|
90
93
|
return {
|
|
91
94
|
runId,
|
|
@@ -97,10 +100,8 @@ export class RunOrchestrator {
|
|
|
97
100
|
provider: this.options.profile.model.provider,
|
|
98
101
|
model: this.options.profile.model.id,
|
|
99
102
|
workspace: this.options.profile.workspace,
|
|
100
|
-
promptDigest:
|
|
101
|
-
|
|
102
|
-
.digest("hex"),
|
|
103
|
-
systemPrompt: this.agentSystemPrompt(),
|
|
103
|
+
promptDigest: profilePromptDigest(this.options.profile),
|
|
104
|
+
systemPrompt: this.agentSystemPrompt(trigger),
|
|
104
105
|
...(failure ? { failure } : {}),
|
|
105
106
|
};
|
|
106
107
|
}
|
|
@@ -172,7 +173,8 @@ export class RunOrchestrator {
|
|
|
172
173
|
await this.options.store.update(triggerKey, (current) => ({
|
|
173
174
|
...current,
|
|
174
175
|
run: {
|
|
175
|
-
...(current.run ??
|
|
176
|
+
...(current.run ??
|
|
177
|
+
this.newRun(runId, "queued", this.triggerFor(current))),
|
|
176
178
|
state: "failed",
|
|
177
179
|
finishedAt: this.timestamp(),
|
|
178
180
|
failure: { code, summary: detail },
|
|
@@ -199,7 +201,7 @@ export class RunOrchestrator {
|
|
|
199
201
|
sessionId: current.run.runId,
|
|
200
202
|
sessionPath: join(this.options.sessionDirectory, this.options.profile.id, current.run.runId),
|
|
201
203
|
};
|
|
202
|
-
const systemPrompt = this.agentSystemPrompt(current.run.systemPrompt);
|
|
204
|
+
const systemPrompt = this.agentSystemPrompt(this.triggerFor(current, input), current.run.systemPrompt);
|
|
203
205
|
return {
|
|
204
206
|
...current,
|
|
205
207
|
run: {
|
|
@@ -268,7 +270,7 @@ export class RunOrchestrator {
|
|
|
268
270
|
catch (error) {
|
|
269
271
|
await this.options.store.update(triggerKey, (current) => ({
|
|
270
272
|
...current,
|
|
271
|
-
run: this.newRun(runId, "failed", {
|
|
273
|
+
run: this.newRun(runId, "failed", this.triggerFor(current), {
|
|
272
274
|
code: "trigger_normalization_failed",
|
|
273
275
|
summary: summary(error),
|
|
274
276
|
}),
|
|
@@ -284,7 +286,7 @@ export class RunOrchestrator {
|
|
|
284
286
|
await this.options.store.update(triggerKey, (current) => ({
|
|
285
287
|
...current,
|
|
286
288
|
input,
|
|
287
|
-
run: this.newRun(runId, "queued"),
|
|
289
|
+
run: this.newRun(runId, "queued", this.triggerFor(current, input)),
|
|
288
290
|
}));
|
|
289
291
|
const queued = this.options.queue.enqueue(() => this.execute(triggerKey, input, runId));
|
|
290
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.
|