mirror of
https://github.com/TianyiDataScience/openclaw-control-center.git
synced 2026-08-14 00:47:58 +00:00
Add shared hall collaboration guide
This commit is contained in:
@@ -21,6 +21,9 @@ GATEWAY_URL=ws://127.0.0.1:18789
|
||||
# HALL_RUNTIME_HISTORY_LIMIT=120
|
||||
# HALL_RUNTIME_EXECUTION_CHAIN_ENABLED=true
|
||||
# HALL_RUNTIME_EXECUTION_MAX_TURNS=3
|
||||
# Optional shared hall collaboration guide override.
|
||||
# By default, hall reads ./HALL.md from this repo on every turn.
|
||||
# OPENCLAW_HALL_RULES_PATH=/path/to/HALL.md
|
||||
# Optional path overrides when your OpenClaw/Codex data is not in the default home locations.
|
||||
# OPENCLAW_HOME=/path/to/.openclaw
|
||||
# OPENCLAW_CONFIG_PATH=/path/to/openclaw.json
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# Hall Collaboration Guide
|
||||
|
||||
This file defines shared collaboration preferences for the Control Center Collaboration Hall.
|
||||
|
||||
These rules shape tone and teamwork style only.
|
||||
Do not override explicit operator requests, current owner routing, queued execution order, or hard hall safety rules.
|
||||
|
||||
## Default style
|
||||
|
||||
- Sound like sharp coworkers in a busy work chat, not like a memo or a narrator.
|
||||
- Prefer direct, useful replies over ceremony.
|
||||
- Build on the current thread instead of repeating what is already obvious.
|
||||
|
||||
## Discussion
|
||||
|
||||
- The second speaker should add a missing angle, tension, risk, or better alternative instead of paraphrasing the first.
|
||||
- Disagreement is welcome when it improves the result. If you disagree, give a concrete replacement path.
|
||||
- Keep discussion compact unless the operator explicitly asks for a full draft.
|
||||
|
||||
## Execution
|
||||
|
||||
- Only do the current slice. Do not steal later steps from the queue.
|
||||
- If the task asks for a deliverable, post the actual deliverable instead of describing what should be done.
|
||||
|
||||
## Review
|
||||
|
||||
- Reviewer starts with must-fix issues first.
|
||||
- If the work is good enough, say so clearly.
|
||||
- If the work is not ready, point to the smallest concrete change that unblocks the next pass.
|
||||
|
||||
## Handoff
|
||||
|
||||
- A handoff should state what now exists, what still matters, and who acts next.
|
||||
- Mention the next owner directly when the queue already names one.
|
||||
- Keep handoffs concrete enough that the next person can continue without guesswork.
|
||||
@@ -84,6 +84,7 @@ Example UI from a local OpenClaw environment:
|
||||
## Hall workflow
|
||||
- Start with one task in the hall. The first turn stays in `discussion`.
|
||||
- Unless you explicitly `@` one agent, the hall aims to gather at least two short replies so the second person can add a missing angle instead of repeating the first.
|
||||
- Edit the repo-root `HALL.md` file if you want one shared collaboration style for the whole hall. It is injected into discussion, execution, and handoff turns, but it does not override owner routing or queued execution order.
|
||||
- Use `Arrange execution order` to decide the first owner, later owners, and what each person hands off.
|
||||
- Saving the order does **not** start execution.
|
||||
- Once the queue is ready, the decision card will show `Start execution (...)`.
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
## hall 工作流
|
||||
- 从一条任务开始,第一轮会先留在 `讨论中`。
|
||||
- 如果你没有显式 `@` 某个人,hall 会尽量先收两条短回复,让第二个人承接第一人的上下文补缺口,而不是重写一遍。
|
||||
- 如果你想给整个 hall 统一协作风格,可以直接改仓库根目录的 `HALL.md`。它会注入 discussion / execution / handoff 的 prompt,但不会覆盖 owner 路由和已排好的执行顺序。
|
||||
- 用 `安排后续顺序` 来决定谁先做、后面谁接、每一步交接给谁。
|
||||
- 保存顺序 **不会** 自动开始执行。
|
||||
- 队列排好后,结论卡会出现 `开始执行(...)`。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { join, relative } from "node:path";
|
||||
import type { ToolClient } from "../clients/tool-client";
|
||||
import {
|
||||
HALL_RUNTIME_DIRECT_STREAM_ENABLED,
|
||||
@@ -162,6 +162,9 @@ const CONTROL_CENTER_REPO_ENTRY_FILES = [
|
||||
"src/runtime/hall-runtime-dispatch.ts",
|
||||
"src/types.ts",
|
||||
];
|
||||
const HALL_RULES_OVERRIDE_ENV = "OPENCLAW_HALL_RULES_PATH";
|
||||
const HALL_RULES_DEFAULT_FILE = "HALL.md";
|
||||
const HALL_RULES_MAX_CHARS = 6_000;
|
||||
const HALL_REPO_CONTEXT_MAX_FILE_CHARS = 1_600;
|
||||
const HALL_REPO_CONTEXT_MAX_TOTAL_CHARS = 7_200;
|
||||
const HALL_WORKSPACE_PERSONA_FILES = ["AGENTS.md", "SOUL.md", "IDENTITY.md", "USER.md", "README.md"];
|
||||
@@ -386,6 +389,7 @@ function buildHallRuntimePrompt(input: HallRuntimeDispatchInput, repoContext: Ha
|
||||
const roundRosterBlock = discussionMode
|
||||
? buildHallDiscussionRosterBlock(input)
|
||||
: buildHallRuntimeRosterBlock(input);
|
||||
const hallRulesBlock = buildHallRulesPromptBlock();
|
||||
const selfWorkspacePersona = !discussionMode || isDiscussionParticipantExplicitlyMentioned(input)
|
||||
? describeHallParticipantWorkspacePersona(input.participant)
|
||||
: "";
|
||||
@@ -404,6 +408,7 @@ function buildHallRuntimePrompt(input: HallRuntimeDispatchInput, repoContext: Ha
|
||||
recentMessages.length > 0 ? `Recent agent contributions already in thread: ${countRecentAgentContributors(recentMessages)}.` : "",
|
||||
transcriptBlock,
|
||||
roundRosterBlock,
|
||||
hallRulesBlock,
|
||||
!discussionMode ? `Your semantic responsibility is ${role}.` : "",
|
||||
selfWorkspacePersona ? `Your workspace persona and job boundary: ${selfWorkspacePersona}` : "",
|
||||
taskArtifactBlock,
|
||||
@@ -537,6 +542,19 @@ function buildHallRuntimePrompt(input: HallRuntimeDispatchInput, repoContext: Ha
|
||||
].filter(Boolean).join("\n");
|
||||
}
|
||||
|
||||
function buildHallRulesPromptBlock(): string {
|
||||
const guide = readHallRulesGuide();
|
||||
if (!guide) return "";
|
||||
return [
|
||||
`Shared hall collaboration guide (${guide.label}):`,
|
||||
"Apply these shared preferences to tone, collaboration style, disagreement style, review style, and handoff wording whenever they do not conflict with an explicit operator request, the current owner, queued execution order, or hard hall routing/state rules.",
|
||||
guide.content,
|
||||
guide.truncated
|
||||
? "Only the leading portion of the shared hall guide was included. Keep the file concise if you want every rule to be visible."
|
||||
: "",
|
||||
].filter(Boolean).join("\n");
|
||||
}
|
||||
|
||||
function buildHallRuntimeArtifactBlock(
|
||||
artifacts: TaskArtifact[] | undefined,
|
||||
language: HallResponseLanguage,
|
||||
@@ -825,6 +843,40 @@ function describeHallParticipantWorkspacePersona(participant: HallParticipant):
|
||||
return summary;
|
||||
}
|
||||
|
||||
function readHallRulesGuide(): { label: string; content: string; truncated: boolean } | undefined {
|
||||
const sourcePath = resolveHallRulesGuidePath();
|
||||
if (!existsSync(sourcePath)) return undefined;
|
||||
let raw = "";
|
||||
try {
|
||||
raw = readFileSync(sourcePath, "utf8");
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = raw.replace(/\r\n/g, "\n").trim();
|
||||
if (!normalized) return undefined;
|
||||
const truncated = normalized.length > HALL_RULES_MAX_CHARS;
|
||||
return {
|
||||
label: displayHallRulesGuidePath(sourcePath),
|
||||
content: truncated
|
||||
? `${normalized.slice(0, Math.max(0, HALL_RULES_MAX_CHARS - 16)).trimEnd()}\n[truncated]`
|
||||
: normalized,
|
||||
truncated,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveHallRulesGuidePath(): string {
|
||||
const explicit = process.env[HALL_RULES_OVERRIDE_ENV]?.trim();
|
||||
if (explicit) return explicit;
|
||||
return join(CONTROL_CENTER_REPO_ROOT, HALL_RULES_DEFAULT_FILE);
|
||||
}
|
||||
|
||||
function displayHallRulesGuidePath(sourcePath: string): string {
|
||||
const normalizedSource = sourcePath.trim();
|
||||
const repoRelative = relative(CONTROL_CENTER_REPO_ROOT, normalizedSource).trim();
|
||||
if (repoRelative && !repoRelative.startsWith("..")) return repoRelative;
|
||||
return normalizedSource;
|
||||
}
|
||||
|
||||
export function summarizeWorkspacePersonaFromFiles(workspaceRoot: string): string {
|
||||
const candidates: Array<{ text: string; score: number }> = [];
|
||||
for (const fileName of HALL_WORKSPACE_PERSONA_FILES) {
|
||||
|
||||
@@ -253,6 +253,7 @@ const SHARED_DOCUMENT_FILE_CANDIDATES = [
|
||||
"TOOLS.md",
|
||||
join(".learnings", "LEARNINGS.md"),
|
||||
join("control-center", "README.md"),
|
||||
join("control-center", "HALL.md"),
|
||||
] as const;
|
||||
const AGENT_DOCUMENT_FILE_CANDIDATES = [
|
||||
"AGENTS.md",
|
||||
@@ -12213,6 +12214,7 @@ function documentFilePriority(relativePath: string): number {
|
||||
"identity.md",
|
||||
"soul.md",
|
||||
"user.md",
|
||||
"hall.md",
|
||||
"tasks.md",
|
||||
"bootstrap.md",
|
||||
"heartbeat.md",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
import { pathToFileURL } from "node:url";
|
||||
@@ -24,6 +26,166 @@ test("workspace persona summary reuses existing agent files instead of hall-only
|
||||
assert.match(coqPersona, /(每日新闻|趋势简报|早晚报主编)/);
|
||||
});
|
||||
|
||||
test("shared hall guide is injected into discussion and execution prompts from an override path", async () => {
|
||||
const tempDir = await mkdtemp(join(tmpdir(), "hall-rules-guide-"));
|
||||
const hallRulesPath = join(tempDir, "custom-hall.md");
|
||||
const previousRulesPath = process.env.OPENCLAW_HALL_RULES_PATH;
|
||||
const capturedPrompts: string[] = [];
|
||||
|
||||
await writeFile(
|
||||
hallRulesPath,
|
||||
[
|
||||
"# Shared Hall Rules",
|
||||
"- Push useful disagreement when it sharpens the output.",
|
||||
"- Reviewer only raises must-fix issues before optional polish.",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
try {
|
||||
process.env.OPENCLAW_HALL_RULES_PATH = hallRulesPath;
|
||||
|
||||
const client = {
|
||||
sessionsHistory: async () => ({ history: [] }),
|
||||
agentRun: async (request: { sessionKey?: string; message: string }) => {
|
||||
capturedPrompts.push(request.message);
|
||||
return {
|
||||
ok: true,
|
||||
text: "第一版先锁住了。",
|
||||
rawText: "",
|
||||
sessionKey: request.sessionKey,
|
||||
};
|
||||
},
|
||||
} as never;
|
||||
|
||||
await dispatchHallRuntimeTurn({
|
||||
client,
|
||||
hall: {
|
||||
hallId: "hall",
|
||||
participants: [],
|
||||
updatedAt: new Date().toISOString(),
|
||||
} as never,
|
||||
taskCard: {
|
||||
taskCardId: "discussion-card",
|
||||
hallId: "hall",
|
||||
projectId: "project",
|
||||
taskId: "task-global-discussion",
|
||||
title: "我想录一个群聊功能视频",
|
||||
description: "先讨论整体讲法",
|
||||
stage: "discussion",
|
||||
status: "todo",
|
||||
createdByParticipantId: "operator",
|
||||
blockers: [],
|
||||
requiresInputFrom: [],
|
||||
mentionedParticipantIds: [],
|
||||
plannedExecutionOrder: [],
|
||||
plannedExecutionItems: [],
|
||||
sessionKeys: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
} as never,
|
||||
participant: {
|
||||
participantId: "coq",
|
||||
agentId: "coq",
|
||||
displayName: "Coq-每日新闻",
|
||||
semanticRole: "planner",
|
||||
aliases: [],
|
||||
active: true,
|
||||
} as never,
|
||||
triggerMessage: {
|
||||
hallId: "hall",
|
||||
messageId: "trigger-discussion",
|
||||
kind: "task",
|
||||
authorParticipantId: "operator",
|
||||
authorLabel: "Operator",
|
||||
content: "先给我一个整体方向",
|
||||
createdAt: new Date().toISOString(),
|
||||
} as never,
|
||||
mode: "discussion",
|
||||
});
|
||||
|
||||
await dispatchHallRuntimeTurn({
|
||||
client,
|
||||
hall: {
|
||||
hallId: "hall",
|
||||
participants: [
|
||||
{
|
||||
participantId: "monkey",
|
||||
agentId: "monkey",
|
||||
displayName: "Monkey",
|
||||
semanticRole: "coder",
|
||||
aliases: [],
|
||||
active: true,
|
||||
},
|
||||
],
|
||||
updatedAt: new Date().toISOString(),
|
||||
} as never,
|
||||
taskCard: {
|
||||
taskCardId: "execution-card",
|
||||
hallId: "hall",
|
||||
projectId: "project",
|
||||
taskId: "task-global-execution",
|
||||
title: "我想录一个群聊功能视频",
|
||||
description: "开始写开头",
|
||||
stage: "execution",
|
||||
status: "doing",
|
||||
createdByParticipantId: "operator",
|
||||
currentOwnerParticipantId: "monkey",
|
||||
currentOwnerLabel: "Monkey",
|
||||
blockers: [],
|
||||
requiresInputFrom: [],
|
||||
mentionedParticipantIds: [],
|
||||
plannedExecutionOrder: [],
|
||||
plannedExecutionItems: [
|
||||
{
|
||||
itemId: "step-1",
|
||||
participantId: "monkey",
|
||||
task: "Write three spoken openings",
|
||||
},
|
||||
],
|
||||
currentExecutionItem: {
|
||||
itemId: "step-1",
|
||||
participantId: "monkey",
|
||||
task: "Write three spoken openings",
|
||||
},
|
||||
sessionKeys: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
} as never,
|
||||
participant: {
|
||||
participantId: "monkey",
|
||||
agentId: "monkey",
|
||||
displayName: "Monkey",
|
||||
semanticRole: "coder",
|
||||
aliases: [],
|
||||
active: true,
|
||||
} as never,
|
||||
triggerMessage: {
|
||||
hallId: "hall",
|
||||
messageId: "trigger-execution",
|
||||
kind: "decision",
|
||||
authorParticipantId: "operator",
|
||||
authorLabel: "Operator",
|
||||
content: "开始执行",
|
||||
createdAt: new Date().toISOString(),
|
||||
} as never,
|
||||
mode: "execution",
|
||||
});
|
||||
} finally {
|
||||
if (previousRulesPath === undefined) delete process.env.OPENCLAW_HALL_RULES_PATH;
|
||||
else process.env.OPENCLAW_HALL_RULES_PATH = previousRulesPath;
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
assert.equal(capturedPrompts.length, 2);
|
||||
for (const prompt of capturedPrompts) {
|
||||
assert.match(prompt, /Shared hall collaboration guide/i);
|
||||
assert.match(prompt, /Push useful disagreement when it sharpens the output\./);
|
||||
assert.match(prompt, /Reviewer only raises must-fix issues before optional polish\./);
|
||||
}
|
||||
assert.match(capturedPrompts[1], /Your current execution item: Write three spoken openings/);
|
||||
});
|
||||
|
||||
test("brand-new hall threads use a thread-scoped runtime session instead of the shared hall agent session", async () => {
|
||||
const observedSessionKeys: string[] = [];
|
||||
await dispatchHallRuntimeTurn({
|
||||
|
||||
Reference in New Issue
Block a user