dsh-task-ledger 0.0.45 → 0.0.47

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.
Files changed (36) hide show
  1. package/dist/client.js +130 -46
  2. package/dist/client.js.map +1 -1
  3. package/dist/host/board.d.ts +13 -108
  4. package/dist/host/board.js +19 -285
  5. package/dist/host/errors.d.ts +42 -0
  6. package/dist/host/errors.js +55 -0
  7. package/dist/host/repositories/cache.d.ts +27 -0
  8. package/dist/host/repositories/cache.js +57 -0
  9. package/dist/host/repositories/identity.repository.d.ts +9 -0
  10. package/dist/host/repositories/identity.repository.js +20 -0
  11. package/dist/host/{ledger-read.d.ts → repositories/ledger.repository.d.ts} +10 -4
  12. package/dist/host/repositories/ledger.repository.js +85 -0
  13. package/dist/host/repositories/records.d.ts +5 -0
  14. package/dist/host/repositories/records.js +48 -0
  15. package/dist/host/{script.js → repositories/runner.js} +3 -2
  16. package/dist/host/routes.d.ts +17 -0
  17. package/dist/host/routes.js +215 -64
  18. package/dist/host/services/dates.d.ts +37 -0
  19. package/dist/host/services/dates.js +71 -0
  20. package/dist/host/services/history.service.d.ts +24 -0
  21. package/dist/host/services/history.service.js +61 -0
  22. package/dist/host/services/home.service.d.ts +13 -0
  23. package/dist/host/services/home.service.js +50 -0
  24. package/dist/host/services/session.service.d.ts +110 -0
  25. package/dist/host/services/session.service.js +145 -0
  26. package/dist/host/services/trend.service.d.ts +26 -0
  27. package/dist/host/services/trend.service.js +134 -0
  28. package/dist/host/{skill-registration.d.ts → skill/registration.d.ts} +2 -0
  29. package/dist/host/{skill-registration.js → skill/registration.js} +5 -2
  30. package/dist/host/types.d.ts +94 -0
  31. package/dist/host/workspace.d.ts +8 -0
  32. package/dist/host/workspace.js +6 -9
  33. package/dist/index.js +31 -2
  34. package/package.json +4 -3
  35. package/dist/host/ledger-read.js +0 -64
  36. /package/dist/host/{script.d.ts → repositories/runner.d.ts} +0 -0
@@ -0,0 +1,24 @@
1
+ import { type DegradedInfo } from '../errors.js';
2
+ import type { FilterOptions, HistoryRecord, LedgerFilter, LedgerWorkspace, TrendScope } from '../types.js';
3
+ /** 已结案列表默认分页大小(「加载更多」= 调用方加大 limit 重取)。 */
4
+ export declare const DEFAULT_HISTORY_LIMIT = 500;
5
+ /**
6
+ * 已结案列表:默认近 13 个月 + limit 500,跨工作区合并后按创建时间倒序。
7
+ * shortcut: 分页用「加大 limit 重取」的笨办法(skill 脚本无 offset);
8
+ * 单工作区内 limit 已生效,合并边界的全局精确排序暂不保证(500 条一页无感)。
9
+ * 要全量老数据:调用方显式传 months: undefined(不拼 --months,脚本读全部,按秒计)。
10
+ */
11
+ export declare function queryClosed(workspaces: readonly LedgerWorkspace[], filter?: LedgerFilter): Promise<{
12
+ records: HistoryRecord[];
13
+ degraded: readonly DegradedInfo[];
14
+ }>;
15
+ /** 候选值收集(纯函数):owner/category/tag 去重 + 排序。 */
16
+ export declare function collectFilterOptions(scopes: readonly TrendScope[]): FilterOptions;
17
+ /**
18
+ * 筛选项候选:在办(全量)+ 近 13 个月历史的去重值(owner / category / tag)。
19
+ * 按需调用(/filters 路由),非首屏——历史侧在极端量级下是百毫秒级。单工作区坏 ⇒ 进 degraded。
20
+ */
21
+ export declare function getFilterOptions(workspaces: readonly LedgerWorkspace[]): Promise<{
22
+ options: FilterOptions;
23
+ degraded: readonly DegradedInfo[];
24
+ }>;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * 已结案列表 / 筛选项候选用例(history tab 与筛选控件用;非首屏,按需调用)。
3
+ */
4
+ import { readHistory, readTasks } from '../repositories/ledger.repository.js';
5
+ import { settleWorkspaces } from '../errors.js';
6
+ import { RECENT_MONTHS, readTrendScope } from './trend.service.js';
7
+ /** 已结案列表默认分页大小(「加载更多」= 调用方加大 limit 重取)。 */
8
+ export const DEFAULT_HISTORY_LIMIT = 500;
9
+ /**
10
+ * 已结案列表:默认近 13 个月 + limit 500,跨工作区合并后按创建时间倒序。
11
+ * shortcut: 分页用「加大 limit 重取」的笨办法(skill 脚本无 offset);
12
+ * 单工作区内 limit 已生效,合并边界的全局精确排序暂不保证(500 条一页无感)。
13
+ * 要全量老数据:调用方显式传 months: undefined(不拼 --months,脚本读全部,按秒计)。
14
+ */
15
+ export async function queryClosed(workspaces, filter = {}) {
16
+ const windowed = { months: RECENT_MONTHS, ...filter };
17
+ const { data, degraded } = await settleWorkspaces(workspaces, 'history', (workspace) => readHistory(workspace, windowed), (rows) => rows.flat()
18
+ .sort((a, b) => String(b.created ?? b.time ?? '').localeCompare(String(a.created ?? a.time ?? '')))
19
+ .slice(0, filter.limit ?? DEFAULT_HISTORY_LIMIT));
20
+ return { records: data, degraded };
21
+ }
22
+ /** 候选值收集(纯函数):owner/category/tag 去重 + 排序。 */
23
+ export function collectFilterOptions(scopes) {
24
+ const owners = new Set();
25
+ const categories = new Set();
26
+ const tags = new Set();
27
+ const addTask = (task) => {
28
+ if (task.owner !== undefined && task.owner !== null && task.owner !== '')
29
+ owners.add(task.owner);
30
+ if (task.creator !== undefined && task.creator !== null && task.creator !== '')
31
+ owners.add(task.creator);
32
+ if (task.category !== undefined && task.category !== null && task.category !== '')
33
+ categories.add(task.category);
34
+ for (const tag of task.tags ?? [])
35
+ tags.add(tag);
36
+ };
37
+ const addRecord = (record) => {
38
+ if (record.owner !== undefined && record.owner !== '')
39
+ owners.add(record.owner);
40
+ if (record.category !== undefined && record.category !== null && record.category !== '')
41
+ categories.add(record.category);
42
+ for (const tag of record.tags ?? [])
43
+ tags.add(tag);
44
+ };
45
+ for (const scope of scopes) {
46
+ for (const task of scope.tasks)
47
+ addTask(task);
48
+ for (const record of scope.history)
49
+ addRecord(record);
50
+ }
51
+ const sorted = (values) => [...values].sort((a, b) => a.localeCompare(b, 'zh-Hans-CN'));
52
+ return { owners: sorted(owners), categories: sorted(categories), tags: sorted(tags) };
53
+ }
54
+ /**
55
+ * 筛选项候选:在办(全量)+ 近 13 个月历史的去重值(owner / category / tag)。
56
+ * 按需调用(/filters 路由),非首屏——历史侧在极端量级下是百毫秒级。单工作区坏 ⇒ 进 degraded。
57
+ */
58
+ export async function getFilterOptions(workspaces) {
59
+ const { data, degraded } = await settleWorkspaces(workspaces, 'history', readTrendScope, collectFilterOptions);
60
+ return { options: data, degraded };
61
+ }
@@ -0,0 +1,13 @@
1
+ import { type DegradedInfo } from '../errors.js';
2
+ import type { BoardSnapshot, LedgerWorkspace, TaskScope } from '../types.js';
3
+ /** 停滞判定阈值:doing / blocked 且 created 至今超过这么多天(2026-10-17 拍板,显示级)。 */
4
+ export declare const STALE_DAYS = 14;
5
+ /** 快路径读法:单工作区 readTasks + git 身份(并行;供 settle 逐工作区调)。 */
6
+ export declare function readTaskScope(workspace: LedgerWorkspace): Promise<TaskScope>;
7
+ /** 在办行加工 + 六个统计面 + identities(纯函数;`now` 显式传入)。 */
8
+ export declare function computeSnapshot(scopes: readonly TaskScope[], now: Date): BoardSnapshot;
9
+ /** 快路径端点:在办快照(统计卡 + 身份 + 行加工)。单工作区坏 ⇒ 进 degraded,其余照常。 */
10
+ export declare function getSnapshot(workspaces: readonly LedgerWorkspace[]): Promise<{
11
+ snapshot: BoardSnapshot;
12
+ degraded: readonly DegradedInfo[];
13
+ }>;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * 首页**快路径**用例:在办快照 + 统计卡。
3
+ * 只读 progress.json(list)+ git 身份,**不碰 history**——首屏耗时与历史总量解耦
4
+ * (20 万条量级实测:527ms vs 合并端点 4.4s,见 docs/design/host-data-plane.md §四)。
5
+ * 慢路径(趋势)在 ./trend.service.ts;组合入口(/board 兼容全量)在 ../board.ts。
6
+ */
7
+ import { readTasks } from '../repositories/ledger.repository.js';
8
+ import { getCurrentIdentity } from '../repositories/identity.repository.js';
9
+ import { settleWorkspaces } from '../errors.js';
10
+ import { daysSince } from './dates.js';
11
+ /** 停滞判定阈值:doing / blocked 且 created 至今超过这么多天(2026-10-17 拍板,显示级)。 */
12
+ export const STALE_DAYS = 14;
13
+ /** 快路径读法:单工作区 readTasks + git 身份(并行;供 settle 逐工作区调)。 */
14
+ export async function readTaskScope(workspace) {
15
+ const [tasks, identity] = await Promise.all([readTasks(workspace), getCurrentIdentity(workspace)]);
16
+ return { workspace, tasks, identity };
17
+ }
18
+ /** 在办行加工 + 六个统计面 + identities(纯函数;`now` 显式传入)。 */
19
+ export function computeSnapshot(scopes, now) {
20
+ const withMeta = scopes.flatMap((scope) => scope.tasks.map((task) => {
21
+ const working = task.status === 'doing' || task.status === 'blocked';
22
+ const ageDays = working ? daysSince(task.started ?? task.created, now) : null;
23
+ return { ...task, ageDays, stale: ageDays !== null && ageDays > STALE_DAYS };
24
+ }));
25
+ const openTasks = withMeta.filter((task) => task.status === 'open');
26
+ const blockedTasks = withMeta.filter((task) => task.status === 'blocked');
27
+ const myActive = scopes.reduce((sum, scope) => sum + scope.tasks.filter((task) => task.owner === scope.identity && (task.status === 'doing' || task.status === 'blocked')).length, 0);
28
+ const myBlocked = scopes.reduce((sum, scope) => sum + scope.tasks.filter((task) => task.owner === scope.identity && task.status === 'blocked').length, 0);
29
+ const identities = {};
30
+ for (const scope of scopes)
31
+ identities[scope.workspace.id] = scope.identity;
32
+ const stats = {
33
+ myActive,
34
+ myBlocked,
35
+ open: openTasks.length,
36
+ doing: withMeta.filter((task) => task.status === 'doing').length,
37
+ blocked: blockedTasks.length,
38
+ stale: withMeta.filter((task) => task.stale).length,
39
+ staleDays: STALE_DAYS,
40
+ oldestOpenDays: openTasks.length === 0
41
+ ? null
42
+ : openTasks.reduce((max, task) => Math.max(max, daysSince(task.created, now) ?? 0), 0),
43
+ };
44
+ return { at: now.toISOString(), workspaces: scopes.map((scope) => scope.workspace), tasks: withMeta, stats, identities };
45
+ }
46
+ /** 快路径端点:在办快照(统计卡 + 身份 + 行加工)。单工作区坏 ⇒ 进 degraded,其余照常。 */
47
+ export async function getSnapshot(workspaces) {
48
+ const { data: scopes, degraded } = await settleWorkspaces(workspaces, 'list', readTaskScope, (rows) => rows);
49
+ return { snapshot: computeSnapshot(scopes, new Date()), degraded };
50
+ }
@@ -0,0 +1,110 @@
1
+ import type { LedgerWorkspace } from '../types.js';
2
+ /** 模型路由:provider 与 model 必须成对。 */
3
+ export interface ModelRoute {
4
+ readonly provider: string;
5
+ readonly model: string;
6
+ }
7
+ /** 会话首条 user 消息(`@deepseek-ai/dsh-llm` UserMessage 的最小同形面)。 */
8
+ export interface UserMessageLike {
9
+ readonly id: string;
10
+ readonly role: 'user';
11
+ readonly content: readonly {
12
+ readonly type: 'text';
13
+ readonly text: string;
14
+ }[];
15
+ /**
16
+ * 生产者自有 kind(宿主 v4 格式要求;`kind: 'plugin'` 已废弃会被拒)。
17
+ * form/summary 给官方界面渲染成通知卡用。
18
+ */
19
+ readonly source: {
20
+ readonly kind: string;
21
+ readonly form: 'notice';
22
+ readonly summary: string;
23
+ };
24
+ }
25
+ /** `ctx.agents` 最小面(`CreateAgentOptions` 见 session-handoff 文档 §二)。 */
26
+ export interface AgentsLike {
27
+ create(options: {
28
+ readonly sessionId: string;
29
+ readonly meta?: {
30
+ readonly cwd?: string;
31
+ readonly agentPreset?: string;
32
+ };
33
+ readonly agentOptions?: {
34
+ readonly provider?: string;
35
+ readonly model?: string;
36
+ };
37
+ readonly setup?: (agentCtx: unknown) => void | Promise<void>;
38
+ }): Promise<{
39
+ readonly agent: {
40
+ send(message: UserMessageLike, target: 'next-turn' | 'next-step', wakeup: boolean): void;
41
+ };
42
+ }>;
43
+ }
44
+ /** `ctx.agentPresets` 最小面:resolve 省略 id = 部署默认;mount 只在 setup 里调得到。 */
45
+ export interface AgentPresetsLike {
46
+ resolve(id?: string): Promise<{
47
+ readonly id: string;
48
+ }>;
49
+ mount(agentCtx: unknown, id?: string): Promise<unknown>;
50
+ }
51
+ /** `ctx.agentDefaultModel` 最小面(部署默认模型)。 */
52
+ export interface DefaultModelLike {
53
+ currentSelection(): ModelRoute;
54
+ }
55
+ /** 交接失败的原因码(路由层据此给状态码,文案在这里写成能直接上界面的)。 */
56
+ export type HandoffErrorCode = 'no-agents' | 'no-preset' | 'no-model' | 'task-missing' | 'create-failed' | 'send-failed';
57
+ export declare class HandoffError extends Error {
58
+ readonly code: HandoffErrorCode;
59
+ constructor(code: HandoffErrorCode, message: string);
60
+ }
61
+ /** 结束任务的预置提示词入参(纯数据,便于冒烟直接断言文案)。 */
62
+ export interface ClosePromptInput {
63
+ readonly title: string;
64
+ readonly taskId: string;
65
+ /** 任务所在工作区绝对路径(`--repo` 就是它)。 */
66
+ readonly workspacePath: string;
67
+ /** 过程文档相对 `ledger/` 的路径;缺失传 null,提示词如实说「没有」。 */
68
+ readonly worklogPath: string | null;
69
+ /** 用户在弹框里留的话(可为空)。 */
70
+ readonly message: string;
71
+ }
72
+ /**
73
+ * 「结束任务」提示词(2026-10-18 用户口述定稿)。
74
+ *
75
+ * 结构照用户原话:**预置说明在前,用户留言放最末**(越靠后、越只讲一件事的段落遵守率越高 ——
76
+ * 参考插件真机结论)。三道硬约束缺一不可:
77
+ * ① 不要开新会话(用户:我要结束这个流程了,不能让模型再开一堆框);
78
+ * ② 读过程文档后直接收尾(别再回头问,用户已经留言了);
79
+ * ③ 写台账只经 skill 脚本(插件不手改文件,close 由模型执行)。
80
+ */
81
+ export declare function buildClosePrompt(input: ClosePromptInput): string;
82
+ /** 交接入参(路由层校验完协议字段后给)。 */
83
+ export interface CloseHandoffRequest {
84
+ readonly workspace: LedgerWorkspace;
85
+ readonly taskId: string;
86
+ readonly message: string;
87
+ /** 用户选的模型;不给 = 用宿主默认。 */
88
+ readonly model?: ModelRoute;
89
+ }
90
+ export interface CloseHandoffResult {
91
+ readonly sessionId: string;
92
+ /** 非致命问题(如会话没能挂进工作区列表),如实回给调用方显示,不吞。 */
93
+ readonly warning?: string;
94
+ }
95
+ /** 依赖面(都是惰性服务,缺了给 503 人话,不硬崩)。 */
96
+ export interface CloseHandoffDeps {
97
+ readonly agents: AgentsLike | undefined;
98
+ readonly presets: AgentPresetsLike | undefined;
99
+ readonly defaultModel: DefaultModelLike | undefined;
100
+ }
101
+ /**
102
+ * 起一个「结束任务」会话并发出首条提示词。
103
+ * 失败抛 {@link HandoffError}(带原因码);成功返回 sessionId 供客户端 `openSession` 跳转。
104
+ */
105
+ export declare function startCloseSession(deps: CloseHandoffDeps, request: CloseHandoffRequest): Promise<CloseHandoffResult>;
106
+ /**
107
+ * 模型路由:用户选了就必须给全一对(只有一半 = 配错,直接人话拒绝);
108
+ * 没选就走宿主部署默认。**没有兜底猜测**——猜一个模型去结案比报错更糟。
109
+ */
110
+ export declare function resolveModelRoute(defaultModel: DefaultModelLike | undefined, explicit?: ModelRoute): ModelRoute;
@@ -0,0 +1,145 @@
1
+ /**
2
+ * 会话交接(按钮侧方法层):把一条任务交给模型去收尾。
3
+ *
4
+ * 事实([`external/dsh-session-handoff.md`](../../../../docs/design/external/dsh-session-handoff.md)):
5
+ * 浏览器半**没有**「往会话里发自由文本」的官方 API,唯一链路 = host `ctx.agents.create()` 自建会话
6
+ * + `agent.send()` 发预置提示词,客户端再 `openSession` 跳过去。
7
+ *
8
+ * 两条硬约束(都是参考插件真机踩出来的,不是猜的):
9
+ * ① **必须带 preset**:`create` 里不 mount preset 的 agent 落到「空的全局层」——只剩根作用域的东西,
10
+ * fs / bash 全无,模型连 `progress.py` 都跑不起来 ⇒ 这种会话建了等于白建。
11
+ * ② **provider + model 必须成对显式给**:缺省**不是**「走宿主默认路由」。用户没选模型时,
12
+ * 这里按宿主 `agentDefaultModel.currentSelection()` 取部署默认;取不到就报 503,不建半个会话。
13
+ *
14
+ * 本文件只管「结束任务」这一条(2026-10-18 用户拍板:认领 / 释放由 host 直写、不开会话;
15
+ * 结束要走会话,因为要带用户留言、要模型读过程文档后写摘要)。
16
+ */
17
+ import { randomUUID } from 'node:crypto';
18
+ import { getTask } from '../repositories/ledger.repository.js';
19
+ import { SKILL_SCRIPT } from '../skill/registration.js';
20
+ export class HandoffError extends Error {
21
+ code;
22
+ constructor(code, message) {
23
+ super(message);
24
+ this.code = code;
25
+ this.name = 'HandoffError';
26
+ }
27
+ }
28
+ /**
29
+ * 「结束任务」提示词(2026-10-18 用户口述定稿)。
30
+ *
31
+ * 结构照用户原话:**预置说明在前,用户留言放最末**(越靠后、越只讲一件事的段落遵守率越高 ——
32
+ * 参考插件真机结论)。三道硬约束缺一不可:
33
+ * ① 不要开新会话(用户:我要结束这个流程了,不能让模型再开一堆框);
34
+ * ② 读过程文档后直接收尾(别再回头问,用户已经留言了);
35
+ * ③ 写台账只经 skill 脚本(插件不手改文件,close 由模型执行)。
36
+ */
37
+ export function buildClosePrompt(input) {
38
+ const worklog = input.worklogPath === null || input.worklogPath === ''
39
+ ? '(这条任务没有登记过程文档)'
40
+ : `${input.workspacePath}/ledger/${input.worklogPath}`;
41
+ const message = input.message.trim() === '' ? '(用户没有留具体内容)' : input.message.trim();
42
+ return [
43
+ `用户从任务台账看板点了「结束」,要把任务「${input.title}」(任务号 ${input.taskId})结案。`,
44
+ '',
45
+ `工作区:${input.workspacePath}`,
46
+ `过程文档:${worklog}`,
47
+ '',
48
+ '请按顺序做完这三件,然后停下:',
49
+ '1. 读过程文档的「背景 / 待办 / 下一步」,核对这次到底做了什么、还留了什么坑。',
50
+ `2. 用 task-ledger skill 的脚本结案(在工作区目录下、原样执行这一条命令):`,
51
+ ` python3 ${SKILL_SCRIPT} close --id ${input.taskId} --repo ${input.workspacePath} --summary "<一句话说清做成了什么、留了什么坑>"`,
52
+ ' `--summary` 是必填参数,不许编套话;命令报错就原样报告错误,不要自己绕过、不要手改台账文件。',
53
+ '3. 收尾即结束——这就是本条消息的全部要求。',
54
+ '',
55
+ '【硬约束】',
56
+ '- 不要在会话里继续派活,**不要新建会话或子 agent**:这是收尾动作,不是新任务的开始。',
57
+ '- 用户已经明确要结束,不会再回答提问:信息不足就按留言与过程文档如实写进摘要/结论,**不要停下来反问**。',
58
+ '- 只动这一个任务,不顺带改别的记录;写台账只经上面那个 skill 脚本。',
59
+ '',
60
+ '用户现在处理结束,以下是用户的留言:',
61
+ message,
62
+ ].join('\n');
63
+ }
64
+ /**
65
+ * 起一个「结束任务」会话并发出首条提示词。
66
+ * 失败抛 {@link HandoffError}(带原因码);成功返回 sessionId 供客户端 `openSession` 跳转。
67
+ */
68
+ export async function startCloseSession(deps, request) {
69
+ const agents = deps.agents;
70
+ if (agents === undefined) {
71
+ throw new HandoffError('no-agents', '宿主未暴露 agents 服务,起不了会话(按钮交接暂不可用)');
72
+ }
73
+ const presets = deps.presets;
74
+ if (presets === undefined) {
75
+ throw new HandoffError('no-preset', '宿主未暴露 agentPresets 服务:起出来的会话没有 fs / bash 工具,模型跑不了 close');
76
+ }
77
+ const route = resolveModelRoute(deps.defaultModel, request.model);
78
+ // 任务此刻必须还在在办里(刚被别处结案 ⇒ 明确告知,不拿空标题去建会话)。
79
+ const task = await getTask(request.workspace, request.taskId);
80
+ if (task === null) {
81
+ throw new HandoffError('task-missing', `任务 ${request.taskId} 不在「${request.workspace.title}」的在办列表里(可能已被结案)`);
82
+ }
83
+ let presetId;
84
+ try {
85
+ presetId = (await presets.resolve()).id;
86
+ }
87
+ catch (error) {
88
+ throw new HandoffError('no-preset', `解析默认 preset 失败:${error instanceof Error ? error.message : String(error)}`);
89
+ }
90
+ const sessionId = randomUUID();
91
+ let handle;
92
+ try {
93
+ handle = await agents.create({
94
+ sessionId,
95
+ // cwd 必须是该工作区真实路径(会话 header 校验 + 工作区归组都按它判)。
96
+ meta: { cwd: request.workspace.path, agentPreset: presetId },
97
+ agentOptions: { provider: route.provider, model: route.model },
98
+ // setup 是发布前唯一能挂工具 / prompt sections / skill 的时机;抛错则整个作用域回滚。
99
+ setup: async (agentCtx) => { await presets.mount(agentCtx, presetId); },
100
+ });
101
+ }
102
+ catch (error) {
103
+ throw new HandoffError('create-failed', `会话没建起来(preset=${presetId}):${error instanceof Error ? error.message : String(error)}`);
104
+ }
105
+ const prompt = buildClosePrompt({
106
+ title: task.title ?? request.taskId,
107
+ taskId: request.taskId,
108
+ workspacePath: request.workspace.path,
109
+ worklogPath: task.worklog ?? null,
110
+ message: request.message,
111
+ });
112
+ try {
113
+ handle.agent.send({
114
+ id: randomUUID(),
115
+ role: 'user',
116
+ content: [{ type: 'text', text: prompt }],
117
+ source: { kind: 'task-ledger', form: 'notice', summary: `结束任务 · ${task.title ?? request.taskId}` },
118
+ }, 'next-turn', true);
119
+ }
120
+ catch (error) {
121
+ throw new HandoffError('send-failed', `会话已建(${sessionId})但首条消息没发出去:${error instanceof Error ? error.message : String(error)}`);
122
+ }
123
+ return { sessionId };
124
+ }
125
+ /**
126
+ * 模型路由:用户选了就必须给全一对(只有一半 = 配错,直接人话拒绝);
127
+ * 没选就走宿主部署默认。**没有兜底猜测**——猜一个模型去结案比报错更糟。
128
+ */
129
+ export function resolveModelRoute(defaultModel, explicit) {
130
+ if (explicit !== undefined) {
131
+ if (explicit.provider === '' || explicit.model === '') {
132
+ throw new HandoffError('no-model', 'provider 与 model 必须成对给出(只给一半会被拒,不做猜测)');
133
+ }
134
+ return explicit;
135
+ }
136
+ if (defaultModel !== undefined) {
137
+ try {
138
+ const route = defaultModel.currentSelection();
139
+ if (route.provider !== '' && route.model !== '')
140
+ return { provider: route.provider, model: route.model };
141
+ }
142
+ catch { /* 读不到就往下走,报同一句人话 */ }
143
+ }
144
+ throw new HandoffError('no-model', '宿主没有可用的默认模型:请在请求里给出 provider + model');
145
+ }
@@ -0,0 +1,26 @@
1
+ import { type DegradedInfo } from '../errors.js';
2
+ import type { ClosedSummary, LedgerFilter, LedgerWorkspace, TrendPeriods, TrendScope } from '../types.js';
3
+ /** 历史窗口:只读最近 13 个月的文件(2026-10-17 拍板;脚本默认读全部,窗口由插件显式传)。 */
4
+ export declare const RECENT_MONTHS = 13;
5
+ /** 趋势图桶数:近 8 周(2026-10-17 定稿)。 */
6
+ export declare const WEEKLY_WEEKS = 8;
7
+ /** 慢路径读法:单工作区 readTasks + readHistory(months) + git 身份(并行;供 settle 逐工作区调)。 */
8
+ export declare function readTrendScope(workspace: LedgerWorkspace): Promise<TrendScope>;
9
+ /**
10
+ * 近 N 周分组桶(完成按结案时间、新增按创建时间)× 所有 / 我的(纯函数)。
11
+ *
12
+ * 新增必须**合并两个来源**:已结案的任务被 close 移出 progress.json,只在 history 里;
13
+ * 在办的只在 progress.json 里。两者不相交(close 是移动),合并即全集、不会重复计。
14
+ * remaining:某任务在「第 i 周截止时」仍未完成 ⇔ 创建 ≤ weekEnd_i 且(未结案 或 结案 > weekEnd_i)。
15
+ */
16
+ export declare function computeClosedSummary(scopes: readonly TrendScope[], now: Date): ClosedSummary;
17
+ /** 四个自然周期(只算「全部工作区」范围;图表切换不带动卡片)——纯函数。 */
18
+ export declare function computeTrendPeriods(scopes: readonly TrendScope[], now: Date): TrendPeriods;
19
+ /** 慢路径端点:趋势 + 周期(读 history)。异步加载,首屏不等。单工作区坏 ⇒ 进 degraded。 */
20
+ export declare function getTrend(workspaces: readonly LedgerWorkspace[]): Promise<{
21
+ closed: ClosedSummary;
22
+ trend: TrendPeriods;
23
+ degraded: readonly DegradedInfo[];
24
+ }>;
25
+ /** 趋势端点用的历史 filter(窗口由本模块策略决定,repository 零默认值注入)。 */
26
+ export declare function trendHistoryFilter(extra?: LedgerFilter): LedgerFilter;
@@ -0,0 +1,134 @@
1
+ /**
2
+ * 首页**慢路径**用例:近 8 周趋势 + 四个自然周期。
3
+ * 读 history(months=RECENT_MONTHS)+ 在办 + git 身份——首屏不等它(异步/按需拉)。
4
+ *
5
+ * ⚠️ 窗口必须是 13 个月而不是 2:8 周桶在月初会横跨 3 个自然月(如 10 月第 1 周回溯到 8 月),
6
+ * `--months` 是**文件级**过滤,缩到 2 会静默漏掉 8 月结案记录、趋势少算(2026-10-18 架构评审结论)。
7
+ */
8
+ import { readHistory, readTasks } from '../repositories/ledger.repository.js';
9
+ import { getCurrentIdentity } from '../repositories/identity.repository.js';
10
+ import { settleWorkspaces } from '../errors.js';
11
+ import { buildWeeks, dayOf, inWindow, monthWindow, mondayOf, weekWindow, ymd } from './dates.js';
12
+ /** 历史窗口:只读最近 13 个月的文件(2026-10-17 拍板;脚本默认读全部,窗口由插件显式传)。 */
13
+ export const RECENT_MONTHS = 13;
14
+ /** 趋势图桶数:近 8 周(2026-10-17 定稿)。 */
15
+ export const WEEKLY_WEEKS = 8;
16
+ /** 慢路径读法:单工作区 readTasks + readHistory(months) + git 身份(并行;供 settle 逐工作区调)。 */
17
+ export async function readTrendScope(workspace) {
18
+ const [tasks, history, identity] = await Promise.all([
19
+ readTasks(workspace),
20
+ readHistory(workspace, { months: RECENT_MONTHS }),
21
+ getCurrentIdentity(workspace),
22
+ ]);
23
+ return { workspace, tasks, history, identity };
24
+ }
25
+ /**
26
+ * 近 N 周分组桶(完成按结案时间、新增按创建时间)× 所有 / 我的(纯函数)。
27
+ *
28
+ * 新增必须**合并两个来源**:已结案的任务被 close 移出 progress.json,只在 history 里;
29
+ * 在办的只在 progress.json 里。两者不相交(close 是移动),合并即全集、不会重复计。
30
+ * remaining:某任务在「第 i 周截止时」仍未完成 ⇔ 创建 ≤ weekEnd_i 且(未结案 或 结案 > weekEnd_i)。
31
+ */
32
+ export function computeClosedSummary(scopes, now) {
33
+ const weeks = buildWeeks(WEEKLY_WEEKS, now);
34
+ const weekEnds = weeks.map((slot) => slot.end);
35
+ const make = () => new Map(weeks.map((slot) => [slot.week, { done: 0, created: 0, remaining: 0 }]));
36
+ const bucketsAll = make();
37
+ const bucketsMine = make();
38
+ const bump = (map, week, field) => {
39
+ const bucket = map.get(week);
40
+ if (bucket !== undefined)
41
+ map.set(week, { ...bucket, [field]: bucket[field] + 1 });
42
+ };
43
+ const bumpRemaining = (map, createdDay, closeDay) => {
44
+ if (createdDay === null)
45
+ return;
46
+ for (let i = 0; i < weeks.length; i += 1) {
47
+ if (createdDay <= weekEnds[i] && (closeDay === null || closeDay > weekEnds[i])) {
48
+ const bucket = map.get(weeks[i].week);
49
+ if (bucket !== undefined)
50
+ bucket.remaining += 1;
51
+ }
52
+ }
53
+ };
54
+ for (const scope of scopes) {
55
+ const identity = scope.identity;
56
+ for (const task of scope.tasks) {
57
+ const created = dayOf(task.created);
58
+ if (created === null)
59
+ continue;
60
+ const week = ymd(mondayOf(created));
61
+ const createdMine = identity !== null && (task.creator ?? null) === identity;
62
+ bump(bucketsAll, week, 'created');
63
+ if (createdMine)
64
+ bump(bucketsMine, week, 'created');
65
+ const ownedMine = identity !== null && (task.owner ?? null) === identity;
66
+ bumpRemaining(bucketsAll, created, null);
67
+ if (ownedMine)
68
+ bumpRemaining(bucketsMine, created, null);
69
+ }
70
+ for (const record of scope.history) {
71
+ const closed = dayOf(record.time);
72
+ if (closed !== null) {
73
+ const week = ymd(mondayOf(closed));
74
+ bump(bucketsAll, week, 'done');
75
+ if (identity !== null && (record.owner ?? null) === identity)
76
+ bump(bucketsMine, week, 'done');
77
+ }
78
+ const created = dayOf(record.created);
79
+ if (created !== null) {
80
+ const week = ymd(mondayOf(created));
81
+ bump(bucketsAll, week, 'created');
82
+ // 新增(我的)= 我创建的(creator);旧 8 列历史无 creator 列 ⇒ 兜底 owner
83
+ const createdMine = identity !== null && ((record.creator ?? null) === identity
84
+ || ((record.creator ?? null) === null && (record.owner ?? null) === identity));
85
+ if (createdMine)
86
+ bump(bucketsMine, week, 'created');
87
+ }
88
+ const ownedMine = identity !== null && (record.owner ?? null) === identity;
89
+ bumpRemaining(bucketsAll, created, closed);
90
+ if (ownedMine)
91
+ bumpRemaining(bucketsMine, created, closed);
92
+ }
93
+ }
94
+ const toBuckets = (map) => weeks.map((slot, i) => ({
95
+ week: slot.week, end: weekEnds[i], ...(map.get(slot.week) ?? { done: 0, created: 0, remaining: 0 }),
96
+ }));
97
+ return { weekly: { all: toBuckets(bucketsAll), mine: toBuckets(bucketsMine) } };
98
+ }
99
+ /** 四个自然周期(只算「全部工作区」范围;图表切换不带动卡片)——纯函数。 */
100
+ export function computeTrendPeriods(scopes, now) {
101
+ const windows = {
102
+ thisWeek: weekWindow(0, now),
103
+ lastWeek: weekWindow(-1, now),
104
+ thisMonth: monthWindow(0, now),
105
+ lastMonth: monthWindow(-1, now),
106
+ };
107
+ const doneEntries = scopes.flatMap((scope) => scope.history.map((record) => ({ day: dayOf(record.time), owner: record.owner ?? null })));
108
+ const createdEntries = scopes.flatMap((scope) => [
109
+ ...scope.tasks.map((task) => ({ day: dayOf(task.created), owner: task.owner ?? null })),
110
+ ...scope.history.map((record) => ({ day: dayOf(record.created), owner: record.owner ?? null })),
111
+ ]);
112
+ const period = (window) => ({
113
+ done: doneEntries.filter((entry) => inWindow(entry.day, window)).length,
114
+ created: createdEntries.filter((entry) => inWindow(entry.day, window)).length,
115
+ from: window.from,
116
+ to: window.to,
117
+ });
118
+ return {
119
+ thisWeek: period(windows.thisWeek),
120
+ lastWeek: period(windows.lastWeek),
121
+ thisMonth: period(windows.thisMonth),
122
+ lastMonth: period(windows.lastMonth),
123
+ };
124
+ }
125
+ /** 慢路径端点:趋势 + 周期(读 history)。异步加载,首屏不等。单工作区坏 ⇒ 进 degraded。 */
126
+ export async function getTrend(workspaces) {
127
+ const now = new Date();
128
+ const { data: scopes, degraded } = await settleWorkspaces(workspaces, 'history', readTrendScope, (rows) => rows);
129
+ return { closed: computeClosedSummary(scopes, now), trend: computeTrendPeriods(scopes, now), degraded };
130
+ }
131
+ /** 趋势端点用的历史 filter(窗口由本模块策略决定,repository 零默认值注入)。 */
132
+ export function trendHistoryFilter(extra = {}) {
133
+ return { months: RECENT_MONTHS, ...extra };
134
+ }
@@ -1,3 +1,5 @@
1
+ /** skill 脚本绝对路径(提示词里直接给「原样执行这一条」的命令用;与注册共用同一份目录知识)。 */
2
+ export declare const SKILL_SCRIPT: string;
1
3
  /** 宿主 ctx.skills.register 的最小面(外部事实文档的结构面)。 */
2
4
  export interface SkillRegistration {
3
5
  name: string;
@@ -4,10 +4,13 @@
4
4
  */
5
5
  import { readFileSync } from 'node:fs';
6
6
  import { fileURLToPath } from 'node:url';
7
- /** 内嵌 skill 目录:dist/host/skill-registration.js → 包根 skill/(copy-skill 产物)。 */
8
- const SKILL_DIR = fileURLToPath(new URL('../../skill', import.meta.url));
7
+ /** 内嵌 skill 目录:dist/host/skill/registration.js → 包根 skill/(copy-skill 产物;
8
+ * 比原 host/skill-registration.js 深一层 ⇒ 相对路径多一级 `../`)。 */
9
+ const SKILL_DIR = fileURLToPath(new URL('../../../skill', import.meta.url));
9
10
  /** SKILL.md 里指示脚本位置的占位符(注册时替换为绝对路径,模型据此找到脚本)。 */
10
11
  const SKILL_DIR_TOKEN = '<本 SKILL.md 所在目录>';
12
+ /** skill 脚本绝对路径(提示词里直接给「原样执行这一条」的命令用;与注册共用同一份目录知识)。 */
13
+ export const SKILL_SCRIPT = `${SKILL_DIR}/scripts/progress.py`;
11
14
  /** frontmatter 最小解析:只取 name / description(不引 YAML 库;值去首尾引号)。 */
12
15
  function parseFrontmatter(text) {
13
16
  const match = /^---\n([\s\S]*?)\n---/.exec(text);