Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# FlyBot in a container (#352): one person, one instance. `dsh web` with the FlyBot core,
# social, tree, workflow and timeline packages of this checkout. Build it yourself; nothing is published.
# social, tree, workflow, timeline and project packages of this checkout. Build it yourself; nothing is published.
# docker compose up -d --build (see docs/docker.md)
# No keys or tokens are baked in: DEEPSEEK_API_KEY comes from the environment or from the
# credentials file on the data volume.
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ npx -y @deepseek-ai/dsh@0.2.1-rc.2 plugin --profile web add "github:ThinkFlowLab
```

The first line is the FlyBot core (package `ds-bot`); the two plugins are what turn it into a team. Add
`/plugins/workflow` or `/plugins/timeline` the same way if you want them.
`/plugins/workflow`, `/plugins/timeline` or `/plugins/project` the same way if you want them.

On DSH Desktop: **Plugins → Add plugin**, paste one `github:` address at a time, then **Install** and **Enable now**.

Expand Down Expand Up @@ -153,7 +153,8 @@ old credentials are kept as a backup. Details: [social subsystem](docs/subsystem

- Core options, models and updates: [`core/README.md`](core/README.md)
- Plugins: [`tree`](plugins/tree/README.md), [`social`](plugins/social/README.md),
[`workflow`](plugins/workflow/README.md), [`timeline`](plugins/timeline/README.md)
[`workflow`](plugins/workflow/README.md), [`timeline`](plugins/timeline/README.md),
[`project`](plugins/project/README.md)
- Architecture and subsystem pages: [docs/architecture.md](docs/architecture.md),
[docs/subsystems/](docs/subsystems/README.md)
- Known issues: [docs/known-issues.md](docs/known-issues.md)
Expand Down
5 changes: 3 additions & 2 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ npx -y @deepseek-ai/dsh@0.2.1-rc.2 plugin --profile web add "github:ThinkFlowLab
```

第一行是 FlyBot 核心(包名 `ds-bot`),后两行才把它变成一支团队。想要工作流或时间线,照样再加
`/plugins/workflow`、`/plugins/timeline`。
`/plugins/workflow`、`/plugins/timeline`、`/plugins/project`。

桌面版:**插件 → 添加插件**,一次粘一个 `github:` 地址,然后**安装**并**立即启用**。

Expand Down Expand Up @@ -139,7 +139,8 @@ npx -y @deepseek-ai/dsh@0.2.1-rc.2 web --port 3080

- 核心选项、模型与更新:[`core/README.md`](core/README.md)
- 插件:[`tree`](plugins/tree/README.md)、[`social`](plugins/social/README.md)、
[`workflow`](plugins/workflow/README.md)、[`timeline`](plugins/timeline/README.md)
[`workflow`](plugins/workflow/README.md)、[`timeline`](plugins/timeline/README.md)、
[`project`](plugins/project/README.md)
- 架构与子系统页:[docs/architecture.md](docs/architecture.md)、[docs/subsystems/](docs/subsystems/README.md)
- 已知问题:[docs/known-issues.md](docs/known-issues.md)
- 参与开发:[CONTRIBUTING.md](CONTRIBUTING.md)、[AGENTS.md](AGENTS.md)、[LESSONS.md](LESSONS.md)
Expand Down
1 change: 1 addition & 0 deletions changelog.d/project-plugin.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The Project domain now ships as its own peer plugin, `@flybot/project` (`plugins/project`), instead of living inside the core: Project State, the Project board, the Project's own Chief, the team dossier, the Project metrics and the Project Assistant moved out of `core/src/host/project-*.js`, and the `project_state` / `project_digest` tools are registered by that plugin. Settings → Plugins gains a **Project** switch that turns the whole domain off at the next start; with it off (or with the plugin not installed) the core keeps its documented fallbacks — a Project is created without a Chief or an assistant and the board and the dossier answer empty. Nothing a Project does changes: same reads, same refusals, same strings, and every existing test moved with the code. See [#2684](https://github.com/ThinkFlowLab/FlyBot/issues/2684).
7 changes: 1 addition & 6 deletions core/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import * as authz from './src/host/authz.js'
import * as roomMembership from './src/host/room-membership.js'
import * as reach from './src/host/reach.js'
import * as tasks from './src/host/tasks.js'
import * as projectBoard from './src/host/project-board.js'
import * as taskDrive from './src/host/task-drive.js'
import * as models from './src/host/models.js'
import * as permissions from './src/host/permissions.js'
Expand All @@ -25,7 +24,6 @@ import * as prompt from './src/host/prompt.js'
import * as timeContext from './src/host/time-context.js'
import * as tools from './src/host/tools.js'
import * as decisions from './src/host/decisions.js'
import * as projectState from './src/host/project-state.js'
import * as meetings from './src/host/meetings.js'
import * as chatIndex from './src/host/chat-index.js'
import * as groups from './src/host/groups.js'
Expand All @@ -41,9 +39,6 @@ import * as connectors from './src/host/connectors.js'
import * as connectorAccess from './src/host/connector-access.js'
import * as capabilities from './src/host/capabilities.js'
import * as schedules from './src/host/schedules.js'
import * as projectAssistant from './src/host/project-assistant.js'
import * as projectChief from './src/host/project-chief.js'
import * as projectTeam from './src/host/project-team.js'
import * as automationDefault from './src/host/automation-default.js'
import * as usage from './src/host/usage.js'
import * as update from './src/host/update.js'
Expand All @@ -63,7 +58,7 @@ export const inject = ['llm', 'tools', 'systemPrompt', 'sessionController', 'ses
// the functions other modules call; they call them through `rt` at run time, never during
// install. Listeners run and tools are listed in registration order, so this order holds:
// usage meters model requests after every listener that rewrites them.
const MODULES = [diagnostics, store, harness, memberAuth, authz, models, permissions, turns, busyAck, steeringInterrupt, delivery, roster, folders, agents, prompt, timeContext, tools, decisions, projectState, meetings, chatIndex, groups, roomMembership, reach, tasks, projectBoard, taskDrive, approvals, resume, memory, parts, turnBudget, images, secrets, connectors, connectorAccess, capabilities, schedules, projectAssistant, projectChief, projectTeam, automationDefault, browser, usage, update, dshUpdate, hooks, remoteChat, workflowStore, health, channel]
const MODULES = [diagnostics, store, harness, memberAuth, authz, models, permissions, turns, busyAck, steeringInterrupt, delivery, roster, folders, agents, prompt, timeContext, tools, decisions, meetings, chatIndex, groups, roomMembership, reach, tasks, taskDrive, approvals, resume, memory, parts, turnBudget, images, secrets, connectors, connectorAccess, capabilities, schedules, automationDefault, browser, usage, update, dshUpdate, hooks, remoteChat, workflowStore, health, channel]

export function apply(ctx, config = {}) {
const rt = createRuntime(ctx, config)
Expand Down
3 changes: 2 additions & 1 deletion core/scripts/install-doctor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const PACKAGES = [
['plugins/tree', '@flybot/tree'],
['plugins/workflow', '@flybot/workflow'],
['plugins/timeline', '@flybot/timeline'],
['plugins/project', '@flybot/project'],
]

export function parseNodeVersion(versionString) {
Expand Down Expand Up @@ -76,7 +77,7 @@ export function buildInstallCommands({
dshPackage = '@deepseek-ai/dsh@0.2.1-rc.2',
timeline = false, // @flybot/timeline is opt-in (#281): its install command is printed only on request
} = {}) {
return ['/core', '/plugins/social', '/plugins/tree', '/plugins/workflow', ...(timeline ? ['/plugins/timeline'] : [])]
return ['/core', '/plugins/social', '/plugins/tree', '/plugins/workflow', '/plugins/project', ...(timeline ? ['/plugins/timeline'] : [])]
.map(path => `npx -y ${dshPackage} plugin --profile ${profile} add "github:${repo}#${ref}&path:${path}"`)
}

Expand Down
4 changes: 4 additions & 0 deletions core/scripts/local-ci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,7 @@ echo "=== $step ==="
step="plugin tests (workflow)"
echo "=== $step ==="
(cd ../plugins/workflow && npm test)

step="plugin tests (project)"
echo "=== $step ==="
(cd ../plugins/project && npm test)
2 changes: 1 addition & 1 deletion core/src/host/authz.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ const TASK_ENDPOINTS = new Set([
'project-memory', 'project-memory-write', 'project-delete', 'project-assistant-owner',
// The role switch (#1836) is the third of the same family after `project-board` (#1713) and
// `project-assistant-owner` (#1759): `channel.js` dispatches it and `store.js` implements it, and
// the policy in front of the write is `changeRole` (`project-board.js`), which refuses a
// the policy in front of the write is `changeRole` (plugins/project/src/project-board.js), which refuses a
// non-owner itself. Leaving it out only answered `bot/not-found: Unknown endpoint project-role`
// before the handler ran, so the role menu failed on every host with a member session.
'project-role',
Expand Down
4 changes: 4 additions & 0 deletions core/src/host/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ export const MAX_HOPS = 6
// character note was refused as "over 600 characters". Count with `[...text].length`, as
// `chat-index.js` and `busy-ack.js` already do, and keep both writers on that same unit.
export const MAX_ENTRY = 600
// The role label a Project Assistant carries (#2684). The meetings module refuses a close from
// this role, and the Project assistant (now in plugins/project) creates the Bot with it; the seam
// hands the plugin this same constant, so the two cannot spell it differently.
export const ASSISTANT_ROLE = 'Project Assistant'
// Bot requests a Bot's own chat may hold unread; past this the Bot is most likely
// stuck, and more messages only pile up behind it.
export const MAX_UNREAD_REQUESTS = 10
Expand Down
12 changes: 12 additions & 0 deletions core/src/host/day-key.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// The natural day a timestamp falls in, in the caller's time zone (#2684). `en-CA` prints ISO
// order, which is what the comparison needs; an unknown zone throws here, so a caller that takes
// one from outside (a board read) resolves it with `resolveTimeZone` first.
//
// This was exported by `project-metrics.js`; the core's turn budget needs the same key, and the
// metrics moved to `plugins/project`, so the function lives here and the plugin takes it back
// through the host seam `flybot.projectHost()` (`core/src/host/project-host.js`).
export function dayKey(ms, timeZone = 'UTC') {
if (!Number.isFinite(ms)) return ''
const parts = new Intl.DateTimeFormat('en-CA', { timeZone, year: 'numeric', month: '2-digit', day: '2-digit' })
return parts.format(new Date(ms))
}
6 changes: 6 additions & 0 deletions core/src/host/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { createPluginModel } from './plugin-model.js'
import { defineTool } from './tool-schema.js'
import { retiredBot } from './store.js'
import { pluginEnabledAtStart, pluginSwitches, setPluginSwitch } from './plugins.js'
import { projectHost as createProjectHost } from './project-host.js'
import { codePoints } from './text.js'
import { cut } from '../shared/text-boundary.js'
import { EPHEMERAL_DENIED_TOOLS } from './tools.js'
Expand Down Expand Up @@ -1160,6 +1161,11 @@ export function install(rt) {
// apply(), and registers nothing when this answers false. Read from the state file
// the last run wrote, because plugins load before the team is loaded.
pluginEnabled: id => pluginEnabledAtStart(rt.statePath, id),
// The Project domain's host (#2684): plugins/project implements Project State, the board, the
// Chief, the dossier, the metrics and the assistant, and cannot import core/src/*. It asks for
// the runtime and the shared pure helpers once, in its own apply(); a core without the plugin
// never calls this and keeps the store's existing fallbacks.
projectHost: () => createProjectHost(rt),
plugin,
scope: build,
bot: {
Expand Down
4 changes: 2 additions & 2 deletions core/src/host/meetings.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import { join } from 'node:path'
import { cut } from './text.js'
import { defineTool } from './tool-schema.js'
import { output } from './tools.js'
import { workDirFor } from './project-state.js'
import { ASSISTANT_ROLE } from './project-assistant.js'
import { workDirFor } from './work-dir.js'
import { ASSISTANT_ROLE } from './constants.js'

// The Project Assistant takes a position like anyone else, but it does not write the decision
// file and does not close the meeting (the Chief or the owner does).
Expand Down
11 changes: 7 additions & 4 deletions core/src/host/plugins.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// The four FlyBot feature plugins and the owner's switch for each (#2564).
// The FlyBot feature plugins and the owner's switch for each (#2564). The Project domain is one
// of them since #2684: its code lives in plugins/project and this switch is how an owner turns
// the whole domain (tools, board, Chief, assistant) off without uninstalling anything.
//
// The switch does not install or uninstall anything: the packages stay in the DSH
// profile as they are, and the answer lives in FlyBot's own state.json. A plugin reads
Expand All @@ -13,20 +15,21 @@ export const FEATURE_PLUGINS = Object.freeze([
{ id: 'tree', name: 'Tree', package: '@flybot/tree', purpose: 'Run Bots as a tree under a coordinator.' },
{ id: 'timeline', name: 'Timeline', package: '@flybot/timeline', purpose: 'Read the team activity as a timeline.' },
{ id: 'workflow', name: 'Workflow', package: '@flybot/workflow', purpose: 'Run workflow graphs across several Bots.' },
{ id: 'project', name: 'Project', package: '@flybot/project', purpose: 'Run a Project: Project State, its board, its own Chief, its team and its assistant.' },
])

export const FEATURE_PLUGIN_IDS = Object.freeze(FEATURE_PLUGINS.map(plugin => plugin.id))

const known = id => FEATURE_PLUGIN_IDS.includes(id)

// The four switches as the Settings page shows them: on unless state.json says off.
// The switches as the Settings page shows them: on unless state.json says off.
export function pluginSwitches(state) {
const saved = state?.flybot?.plugins ?? {}
return FEATURE_PLUGINS.map(plugin => ({ ...plugin, enabled: saved[plugin.id]?.enabled !== false }))
}

// Writes one switch into the state the next save persists. Only these four ids are
// accepted, so a typo cannot grow a fifth switch nobody reads.
// Writes one switch into the state the next save persists. Only these ids are accepted,
// so a typo cannot grow a switch nobody reads.
export function setPluginSwitch(state, id, enabled) {
const name = String(id ?? '')
if (!known(name)) throw new Error(`flybot: unknown plugin ${JSON.stringify(name)}`)
Expand Down
56 changes: 56 additions & 0 deletions core/src/host/project-host.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// The Project domain's host seam (#2684). The domain — Project State, the board, the Project's own
// Chief, the team dossier, the metrics and the assistant — moved to `plugins/project`, which ships
// as its own package and therefore cannot import `core/src/*`. The core hands it the two things it
// cannot get by itself: the runtime it puts its seams on, and the pure helpers the two packages
// share (one function each, never a copy: a second implementation of `cut` or `dayKey` would drift).
//
// The seam is the *core's* side of the contract, not a third-party plugin surface: only
// `plugins/project` asks for it, and only in its `apply()`. A core without the plugin installed
// never calls it — the store's Project endpoints then keep the fallbacks they already have (a
// Project is created without a Chief or an assistant, the board and the dossier answer empty).
//
// Every helper is a pure function or a constant; nothing here reads state or writes anything when
// the object is built, so a caller can build it once at startup.
import { ASSISTANT_ROLE, MAX_ENTRY } from './constants.js'
import { dayKey } from './day-key.js'
import { advanceRefusal } from './meetings.js'
import { dirKind } from './real-dir.js'
import { codePoints, cut } from './text.js'
import { defineTool } from './tool-schema.js'
import { output } from './tools.js'
import { workDirFor } from './work-dir.js'
import { clockIn, inQuietHours, quietHoursOf, readQuietHours } from '../shared/quiet-hours.js'
import { columnOf, priorityOf } from '../shared/task-board.js'

export const HELPER_NAMES = Object.freeze([
'cut', 'codePoints', 'dirKind', 'defineTool', 'output', 'columnOf', 'priorityOf', 'advanceRefusal',
'workDirFor', 'dayKey', 'MAX_ENTRY', 'ASSISTANT_ROLE',
'inQuietHours', 'clockIn', 'quietHoursOf', 'readQuietHours',
])

export function projectHost(rt) {
return {
// The live runtime: the seams the domain installs (`rt.projectBoard`, `rt.projectChief`,
// `rt.projectTeam`, `rt.projectAssistant`) are the ones the core's store, meetings and channel
// already call by those names.
runtime: rt,
helpers: {
cut,
codePoints,
dirKind,
defineTool,
output,
columnOf,
priorityOf,
advanceRefusal,
workDirFor,
dayKey,
MAX_ENTRY,
ASSISTANT_ROLE,
inQuietHours,
clockIn,
quietHoursOf,
readQuietHours,
},
}
}
Loading
Loading