dsh-task-ledger 0.0.74 → 0.0.75

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 (90) hide show
  1. package/dist/client.js +128 -128
  2. package/dist/client.js.map +1 -1
  3. package/dist/host/board.d.ts +4 -4
  4. package/dist/host/board.js +6 -6
  5. package/dist/host/errors.d.ts +3 -3
  6. package/dist/host/errors.js +13 -7
  7. package/dist/host/repositories/cache.d.ts +1 -1
  8. package/dist/host/repositories/cache.js +4 -2
  9. package/dist/host/repositories/identity.repository.d.ts +1 -1
  10. package/dist/host/repositories/identity.repository.js +14 -7
  11. package/dist/host/repositories/ledger.repository.d.ts +1 -1
  12. package/dist/host/repositories/ledger.repository.js +34 -23
  13. package/dist/host/repositories/records.d.ts +1 -1
  14. package/dist/host/repositories/records.js +3 -3
  15. package/dist/host/repositories/runner.d.ts +5 -4
  16. package/dist/host/repositories/runner.js +41 -19
  17. package/dist/host/routes.d.ts +4 -4
  18. package/dist/host/routes.js +145 -96
  19. package/dist/host/services/dates.js +4 -4
  20. package/dist/host/services/history.service.d.ts +5 -2
  21. package/dist/host/services/history.service.js +24 -13
  22. package/dist/host/services/home.service.d.ts +8 -2
  23. package/dist/host/services/home.service.js +29 -14
  24. package/dist/host/services/ledger-write.service.d.ts +8 -3
  25. package/dist/host/services/ledger-write.service.js +14 -9
  26. package/dist/host/services/session.service.d.ts +6 -6
  27. package/dist/host/services/session.service.js +47 -37
  28. package/dist/host/services/trend.service.d.ts +9 -2
  29. package/dist/host/services/trend.service.js +42 -21
  30. package/dist/host/skill/registration.d.ts +1 -1
  31. package/dist/host/skill/registration.js +16 -11
  32. package/dist/host/workspace.d.ts +1 -1
  33. package/dist/host/workspace.js +6 -2
  34. package/dist/index.js +12 -12
  35. package/package.json +57 -58
  36. package/skill/README.md +29 -247
  37. package/skill/SKILL.md +173 -88
  38. package/skill/package.json +22 -22
  39. package/skill/references/log-note.md +3 -1
  40. package/skill/references/state-model.md +29 -27
  41. package/skill/references/troubleshooting.md +97 -32
  42. package/skill/scripts/ledgerkit/__init__.py +0 -0
  43. package/skill/scripts/ledgerkit/actions/__init__.py +0 -0
  44. package/skill/scripts/ledgerkit/actions/atom.py +154 -0
  45. package/skill/scripts/ledgerkit/actions/bootstrap.py +27 -0
  46. package/skill/scripts/ledgerkit/actions/claim.py +82 -0
  47. package/skill/scripts/ledgerkit/actions/close_task.py +180 -0
  48. package/skill/scripts/ledgerkit/actions/guard.py +140 -0
  49. package/skill/scripts/ledgerkit/actions/handoff.py +74 -0
  50. package/skill/scripts/ledgerkit/actions/list_tasks.py +53 -0
  51. package/skill/scripts/ledgerkit/actions/log_progress.py +60 -0
  52. package/skill/scripts/ledgerkit/actions/new_task.py +120 -0
  53. package/skill/scripts/ledgerkit/actions/query.py +33 -0
  54. package/skill/scripts/ledgerkit/actions/query_history.py +173 -0
  55. package/skill/scripts/ledgerkit/actions/registry.py +82 -0
  56. package/skill/scripts/ledgerkit/actions/release.py +51 -0
  57. package/skill/scripts/ledgerkit/actions/result.py +88 -0
  58. package/skill/scripts/ledgerkit/actions/set_task.py +43 -0
  59. package/skill/scripts/ledgerkit/actions/show_task.py +27 -0
  60. package/skill/scripts/ledgerkit/actions/validate.py +224 -0
  61. package/skill/scripts/ledgerkit/core/__init__.py +0 -0
  62. package/skill/scripts/ledgerkit/core/history_md.py +350 -0
  63. package/skill/scripts/ledgerkit/core/identity.py +43 -0
  64. package/skill/scripts/ledgerkit/core/ids.py +58 -0
  65. package/skill/scripts/ledgerkit/core/labels.py +79 -0
  66. package/skill/scripts/ledgerkit/core/paths.py +93 -0
  67. package/skill/scripts/ledgerkit/core/time.py +102 -0
  68. package/skill/scripts/ledgerkit/core/worklog_md.py +232 -0
  69. package/skill/scripts/ledgerkit/env.py +93 -0
  70. package/skill/scripts/ledgerkit/errors.py +15 -0
  71. package/skill/scripts/ledgerkit/notices.py +28 -0
  72. package/skill/scripts/ledgerkit/shell/__init__.py +0 -0
  73. package/skill/scripts/ledgerkit/shell/cli.py +259 -0
  74. package/skill/scripts/ledgerkit/shell/gitops.py +655 -0
  75. package/skill/scripts/ledgerkit/shell/merge.py +266 -0
  76. package/skill/scripts/ledgerkit/shell/presync.py +23 -0
  77. package/skill/scripts/ledgerkit/shell/render.py +165 -0
  78. package/skill/scripts/ledgerkit/shell/runner.py +225 -0
  79. package/skill/scripts/ledgerkit/store/__init__.py +0 -0
  80. package/skill/scripts/ledgerkit/store/atomic.py +130 -0
  81. package/skill/scripts/ledgerkit/store/dirty.py +108 -0
  82. package/skill/scripts/ledgerkit/store/history_store.py +189 -0
  83. package/skill/scripts/ledgerkit/store/json_store.py +166 -0
  84. package/skill/scripts/ledgerkit/store/locks.py +64 -0
  85. package/skill/scripts/ledgerkit/store/owners_index.py +135 -0
  86. package/skill/scripts/ledgerkit/store/task_store.py +31 -0
  87. package/skill/scripts/ledgerkit/store/worklog_store.py +168 -0
  88. package/skill/scripts/progress.py +8 -3109
  89. package/skill/references/taxonomy.md +0 -26
  90. package/skill/references/worklog.md +0 -39
@@ -4,10 +4,10 @@
4
4
  * (20 万条量级实测:527ms vs 合并端点 4.4s,见 docs/design/host-data-plane.md §四)。
5
5
  * 慢路径(趋势)在 ./trend.service.ts;组合入口(/board 兼容全量)在 ../board.ts。
6
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';
7
+ import { settleWorkspaces } from "../errors.js";
8
+ import { getCurrentIdentity } from "../repositories/identity.repository.js";
9
+ import { readTasks } from "../repositories/ledger.repository.js";
10
+ import { daysSince } from "./dates.js";
11
11
  /** 停滞阈值:进行中且 updated 至今满这么多天没更新(2026-10-19 拍板,显示级)。 */
12
12
  export const STALE_DAYS = 3;
13
13
  /** 逾期阈值:进行中且 started 至今满这么多天还没做完——中途更新不影响(2026-10-19 拍板 7 → 14 天)。 */
@@ -16,18 +16,25 @@ export const OVERDUE_DAYS = 14;
16
16
  export const BACKLOG_DAYS = 30;
17
17
  /** 快路径读法:单工作区 readTasks + git 身份(并行;供 settle 逐工作区调)。 */
18
18
  export async function readTaskScope(workspace) {
19
- const [tasks, identity] = await Promise.all([readTasks(workspace), getCurrentIdentity(workspace)]);
19
+ const [tasks, identity] = await Promise.all([
20
+ readTasks(workspace),
21
+ getCurrentIdentity(workspace),
22
+ ]);
20
23
  return { workspace, tasks, identity };
21
24
  }
22
25
  /** 在办行加工 + 六个统计面 + identities(纯函数;`now` 显式传入)。 */
23
26
  export function computeSnapshot(scopes, now) {
24
27
  const withMeta = scopes.flatMap((scope) => scope.tasks.map((task) => {
25
28
  // blocked 只是旧数据(新任务不可能再有);读侧一律当进行中
26
- const working = task.status === 'doing' || task.status === 'blocked';
27
- const ageDays = working ? daysSince(task.started ?? task.created, now) : null;
29
+ const working = task.status === "doing" || task.status === "blocked";
30
+ const ageDays = working
31
+ ? daysSince(task.started ?? task.created, now)
32
+ : null;
28
33
  // 停滞看「最后更新」,逾期看「接任务至今」,积压看「创建至今还没人认领」——三个不同的问题,互不覆盖
29
- const idleDays = working ? daysSince(task.updated ?? task.started ?? task.created, now) : null;
30
- const idleOpen = task.status === 'open' ? daysSince(task.created, now) : null;
34
+ const idleDays = working
35
+ ? daysSince(task.updated ?? task.started ?? task.created, now)
36
+ : null;
37
+ const idleOpen = task.status === "open" ? daysSince(task.created, now) : null;
31
38
  return {
32
39
  ...task,
33
40
  ageDays,
@@ -39,8 +46,10 @@ export function computeSnapshot(scopes, now) {
39
46
  backlog: idleOpen !== null && idleOpen >= BACKLOG_DAYS,
40
47
  };
41
48
  }));
42
- const openTasks = withMeta.filter((task) => task.status === 'open');
43
- const myActive = scopes.reduce((sum, scope) => sum + scope.tasks.filter((task) => task.owner === scope.identity && (task.status === 'doing' || task.status === 'blocked')).length, 0);
49
+ const openTasks = withMeta.filter((task) => task.status === "open");
50
+ const myActive = scopes.reduce((sum, scope) => sum +
51
+ scope.tasks.filter((task) => task.owner === scope.identity &&
52
+ (task.status === "doing" || task.status === "blocked")).length, 0);
44
53
  const identities = {};
45
54
  for (const scope of scopes)
46
55
  identities[scope.workspace.id] = scope.identity;
@@ -53,7 +62,7 @@ export function computeSnapshot(scopes, now) {
53
62
  myOverdue: overdueTasks.filter(mine).length,
54
63
  myStale: staleTasks.filter(mine).length,
55
64
  open: openTasks.length,
56
- doing: withMeta.filter((task) => task.status === 'doing' || task.status === 'blocked').length,
65
+ doing: withMeta.filter((task) => task.status === "doing" || task.status === "blocked").length,
57
66
  overdue: overdueTasks.length,
58
67
  stale: staleTasks.length,
59
68
  backlog: backlogTasks.length,
@@ -64,10 +73,16 @@ export function computeSnapshot(scopes, now) {
64
73
  ? null
65
74
  : openTasks.reduce((max, task) => Math.max(max, daysSince(task.created, now) ?? 0), 0),
66
75
  };
67
- return { at: now.toISOString(), workspaces: scopes.map((scope) => scope.workspace), tasks: withMeta, stats, identities };
76
+ return {
77
+ at: now.toISOString(),
78
+ workspaces: scopes.map((scope) => scope.workspace),
79
+ tasks: withMeta,
80
+ stats,
81
+ identities,
82
+ };
68
83
  }
69
84
  /** 快路径端点:在办快照(统计卡 + 身份 + 行加工)。单工作区坏 ⇒ 进 degraded,其余照常。 */
70
85
  export async function getSnapshot(workspaces) {
71
- const { data: scopes, degraded } = await settleWorkspaces(workspaces, 'list', readTaskScope, (rows) => rows);
86
+ const { data: scopes, degraded } = await settleWorkspaces(workspaces, "list", readTaskScope, (rows) => rows);
72
87
  return { snapshot: computeSnapshot(scopes, new Date()), degraded };
73
88
  }
@@ -1,5 +1,10 @@
1
- import type { LedgerWorkspace } from '../types.js';
2
- /** 认领:只有 open 且无人认领才能接(脚本内判定),owner = 当前工作区的 git 身份。 */
1
+ import type { LedgerWorkspace } from "../types.js";
2
+ /** 认领:只有 open 且无人认领才能接(脚本内判定),owner = 工作区的 git 邮箱。 */
3
3
  export declare function claimTask(workspace: LedgerWorkspace, taskId: string): Promise<string>;
4
- /** 释放:退回待认领(owner / started 清空),只许当前 owner 本人(脚本内判定)。 */
4
+ /**
5
+ * 释放:写回公共池(owner / started 清空、status 回 open),只许当前 owner 本人(脚本内判定)。
6
+ *
7
+ * 走 `set --owner ''`(空 owner = 释放)而不是 `set --status open`:后者只改状态,
8
+ * 分片里那行不会回到公共池,别人永远看不到这条待认领。
9
+ */
5
10
  export declare function releaseTask(workspace: LedgerWorkspace, taskId: string): Promise<string>;
@@ -1,27 +1,32 @@
1
1
  /**
2
2
  * 台账写方法(2026-10-18 拍板 B 混合制):认领 / 释放没有判断量,host 直跑 skill 脚本,**不开会话**。
3
3
  *
4
- * 唯一要点:子进程 cwd 必须是工作区 —— `progress.py` 的 owner 取 `git config user.name`,
5
- * 走的是进程 cwd,`--repo` 只解决路径、不解决身份(2026-10-18 核实结论)。
4
+ * 身份:脚本按 `--repo`(= 工作区路径)读该仓库的 `git config user.email` 作为 owner,
5
+ * 与宿主进程自己的 cwd / 身份无关。
6
6
  *
7
7
  * 校验全在脚本里(认领要求 open 且无 owner;释放要求是当前 owner),本层不重复实现。
8
8
  */
9
- import { WRITE_TIMEOUT_MS, runLedgerCommand } from '../repositories/runner.js';
9
+ import { runLedgerCommand, WRITE_TIMEOUT_MS } from "../repositories/runner.js";
10
10
  /** 返回脚本在退出码 0 时打到 stderr 的提示(同步失败等);没有提示就是空串。 */
11
11
  async function run(workspace, sub, args) {
12
- const result = await runLedgerCommand(sub, [...args, '--repo', workspace.path], {
12
+ const result = await runLedgerCommand(sub, [...args, "--repo", workspace.path], {
13
13
  cwd: workspace.path,
14
14
  timeoutMs: WRITE_TIMEOUT_MS,
15
15
  });
16
16
  if (!result.ok)
17
17
  throw new Error(`${workspace.title}:${result.error}`);
18
- return result.notice ?? '';
18
+ return result.notice ?? "";
19
19
  }
20
- /** 认领:只有 open 且无人认领才能接(脚本内判定),owner = 当前工作区的 git 身份。 */
20
+ /** 认领:只有 open 且无人认领才能接(脚本内判定),owner = 工作区的 git 邮箱。 */
21
21
  export function claimTask(workspace, taskId) {
22
- return run(workspace, 'claim', ['--id', taskId]);
22
+ return run(workspace, "claim", ["--id", taskId]);
23
23
  }
24
- /** 释放:退回待认领(owner / started 清空),只许当前 owner 本人(脚本内判定)。 */
24
+ /**
25
+ * 释放:写回公共池(owner / started 清空、status 回 open),只许当前 owner 本人(脚本内判定)。
26
+ *
27
+ * 走 `set --owner ''`(空 owner = 释放)而不是 `set --status open`:后者只改状态,
28
+ * 分片里那行不会回到公共池,别人永远看不到这条待认领。
29
+ */
25
30
  export function releaseTask(workspace, taskId) {
26
- return run(workspace, 'set', ['--id', taskId, '--status', 'open']);
31
+ return run(workspace, "set", ["--id", taskId, "--owner", ""]);
27
32
  }
@@ -1,4 +1,4 @@
1
- import type { LedgerWorkspace } from '../types.js';
1
+ import type { LedgerWorkspace } from "../types.js";
2
2
  /** 模型路由:provider 与 model 必须成对。 */
3
3
  export interface ModelRoute {
4
4
  readonly provider: string;
@@ -7,9 +7,9 @@ export interface ModelRoute {
7
7
  /** 会话首条 user 消息(`@deepseek-ai/dsh-llm` UserMessage 的最小同形面)。 */
8
8
  export interface UserMessageLike {
9
9
  readonly id: string;
10
- readonly role: 'user';
10
+ readonly role: "user";
11
11
  readonly content: readonly {
12
- readonly type: 'text';
12
+ readonly type: "text";
13
13
  readonly text: string;
14
14
  }[];
15
15
  /**
@@ -18,7 +18,7 @@ export interface UserMessageLike {
18
18
  */
19
19
  readonly source: {
20
20
  readonly kind: string;
21
- readonly form: 'notice';
21
+ readonly form: "notice";
22
22
  readonly summary: string;
23
23
  };
24
24
  }
@@ -37,7 +37,7 @@ export interface AgentsLike {
37
37
  readonly setup?: (agentCtx: unknown) => void | Promise<void>;
38
38
  }): Promise<{
39
39
  readonly agent: {
40
- send(message: UserMessageLike, target: 'next-turn' | 'next-step', wakeup: boolean): void;
40
+ send(message: UserMessageLike, target: "next-turn" | "next-step", wakeup: boolean): void;
41
41
  };
42
42
  }>;
43
43
  }
@@ -53,7 +53,7 @@ export interface DefaultModelLike {
53
53
  currentSelection(): ModelRoute;
54
54
  }
55
55
  /** 交接失败的原因码(路由层据此给状态码,文案在这里写成能直接上界面的)。 */
56
- export type HandoffErrorCode = 'no-agents' | 'no-preset' | 'no-model' | 'task-missing' | 'create-failed' | 'send-failed';
56
+ export type HandoffErrorCode = "no-agents" | "no-preset" | "no-model" | "task-missing" | "create-failed" | "send-failed";
57
57
  export declare class HandoffError extends Error {
58
58
  readonly code: HandoffErrorCode;
59
59
  constructor(code: HandoffErrorCode, message: string);
@@ -14,15 +14,15 @@
14
14
  * 本文件只管「结束任务」这一条(2026-10-18 用户拍板:认领 / 释放由 host 直写、不开会话;
15
15
  * 结束要走会话,因为要带用户留言、要模型读过程文档后写摘要)。
16
16
  */
17
- import { randomUUID } from 'node:crypto';
18
- import { getTask } from '../repositories/ledger.repository.js';
19
- import { SKILL_SCRIPT } from '../skill/registration.js';
17
+ import { randomUUID } from "node:crypto";
18
+ import { getTask } from "../repositories/ledger.repository.js";
19
+ import { SKILL_SCRIPT } from "../skill/registration.js";
20
20
  export class HandoffError extends Error {
21
21
  code;
22
22
  constructor(code, message) {
23
23
  super(message);
24
24
  this.code = code;
25
- this.name = 'HandoffError';
25
+ this.name = "HandoffError";
26
26
  }
27
27
  }
28
28
  /**
@@ -35,31 +35,33 @@ export class HandoffError extends Error {
35
35
  * ③ 写台账只经 skill 脚本(插件不手改文件,close 由模型执行)。
36
36
  */
37
37
  export function buildClosePrompt(input) {
38
- const worklog = input.worklogPath === null || input.worklogPath === ''
39
- ? '(这条任务没有登记过程文档)'
38
+ const worklog = input.worklogPath === null || input.worklogPath === ""
39
+ ? "(这条任务没有登记过程文档)"
40
40
  : `${input.workspacePath}/ledger/${input.worklogPath}`;
41
- const message = input.message.trim() === '' ? '(用户没有留具体内容)' : input.message.trim();
41
+ const message = input.message.trim() === ""
42
+ ? "(用户没有留具体内容)"
43
+ : input.message.trim();
42
44
  return [
43
45
  `用户从任务台账看板点了「结束」,要把任务「${input.title}」(任务号 ${input.taskId})结案。`,
44
- '',
46
+ "",
45
47
  `工作区:${input.workspacePath}`,
46
48
  `过程文档:${worklog}`,
47
- '',
48
- '请按顺序做完这三件,然后停下:',
49
- '1. 读过程文档的「背景 / 待办 / 下一步」,核对这次到底做了什么、还留了什么坑。',
49
+ "",
50
+ "请按顺序做完这三件,然后停下:",
51
+ "1. 读过程文档的「背景 / 待办 / 下一步」,核对这次到底做了什么、还留了什么坑。",
50
52
  `2. 用 task-ledger skill 的脚本结案(在工作区目录下、原样执行这一条命令):`,
51
53
  ` python3 ${SKILL_SCRIPT} close --id ${input.taskId} --repo ${input.workspacePath} --summary "<一句话说清做成了什么、留了什么坑>"`,
52
- ' `--summary` 是必填参数,不许编套话;命令报错就原样报告错误,不要自己绕过、不要手改台账文件。',
53
- '3. 收尾即结束——这就是本条消息的全部要求。',
54
- '',
55
- '【硬约束】',
56
- '- 不要在会话里继续派活,**不要新建会话或子 agent**:这是收尾动作,不是新任务的开始。',
57
- '- 用户已经明确要结束,不会再回答提问:信息不足就按留言与过程文档如实写进摘要/结论,**不要停下来反问**。',
58
- '- 只动这一个任务,不顺带改别的记录;写台账只经上面那个 skill 脚本。',
59
- '',
60
- '用户现在处理结束,以下是用户的留言:',
54
+ " `--summary` 是必填参数,不许编套话;命令报错就原样报告错误,不要自己绕过、不要手改台账文件。",
55
+ "3. 收尾即结束——这就是本条消息的全部要求。",
56
+ "",
57
+ "【硬约束】",
58
+ "- 不要在会话里继续派活,**不要新建会话或子 agent**:这是收尾动作,不是新任务的开始。",
59
+ "- 用户已经明确要结束,不会再回答提问:信息不足就按留言与过程文档如实写进摘要/结论,**不要停下来反问**。",
60
+ "- 只动这一个任务,不顺带改别的记录;写台账只经上面那个 skill 脚本。",
61
+ "",
62
+ "用户现在处理结束,以下是用户的留言:",
61
63
  message,
62
- ].join('\n');
64
+ ].join("\n");
63
65
  }
64
66
  /**
65
67
  * 起一个「结束任务」会话并发出首条提示词。
@@ -68,24 +70,24 @@ export function buildClosePrompt(input) {
68
70
  export async function startCloseSession(deps, request) {
69
71
  const agents = deps.agents;
70
72
  if (agents === undefined) {
71
- throw new HandoffError('no-agents', '宿主未暴露 agents 服务,起不了会话(按钮交接暂不可用)');
73
+ throw new HandoffError("no-agents", "宿主未暴露 agents 服务,起不了会话(按钮交接暂不可用)");
72
74
  }
73
75
  const presets = deps.presets;
74
76
  if (presets === undefined) {
75
- throw new HandoffError('no-preset', '宿主未暴露 agentPresets 服务:起出来的会话没有 fs / bash 工具,模型跑不了 close');
77
+ throw new HandoffError("no-preset", "宿主未暴露 agentPresets 服务:起出来的会话没有 fs / bash 工具,模型跑不了 close");
76
78
  }
77
79
  const route = resolveModelRoute(deps.defaultModel, request.model);
78
80
  // 任务此刻必须还在在办里(刚被别处结案 ⇒ 明确告知,不拿空标题去建会话)。
79
81
  const task = await getTask(request.workspace, request.taskId);
80
82
  if (task === null) {
81
- throw new HandoffError('task-missing', `任务 ${request.taskId} 不在「${request.workspace.title}」的在办列表里(可能已被结案)`);
83
+ throw new HandoffError("task-missing", `任务 ${request.taskId} 不在「${request.workspace.title}」的在办列表里(可能已被结案)`);
82
84
  }
83
85
  let presetId;
84
86
  try {
85
87
  presetId = (await presets.resolve()).id;
86
88
  }
87
89
  catch (error) {
88
- throw new HandoffError('no-preset', `解析默认 preset 失败:${error instanceof Error ? error.message : String(error)}`);
90
+ throw new HandoffError("no-preset", `解析默认 preset 失败:${error instanceof Error ? error.message : String(error)}`);
89
91
  }
90
92
  const sessionId = randomUUID();
91
93
  let handle;
@@ -96,11 +98,13 @@ export async function startCloseSession(deps, request) {
96
98
  meta: { cwd: request.workspace.path, agentPreset: presetId },
97
99
  agentOptions: { provider: route.provider, model: route.model },
98
100
  // setup 是发布前唯一能挂工具 / prompt sections / skill 的时机;抛错则整个作用域回滚。
99
- setup: async (agentCtx) => { await presets.mount(agentCtx, presetId); },
101
+ setup: async (agentCtx) => {
102
+ await presets.mount(agentCtx, presetId);
103
+ },
100
104
  });
101
105
  }
102
106
  catch (error) {
103
- throw new HandoffError('create-failed', `会话没建起来(preset=${presetId}):${error instanceof Error ? error.message : String(error)}`);
107
+ throw new HandoffError("create-failed", `会话没建起来(preset=${presetId}):${error instanceof Error ? error.message : String(error)}`);
104
108
  }
105
109
  const prompt = buildClosePrompt({
106
110
  title: task.title ?? request.taskId,
@@ -112,13 +116,17 @@ export async function startCloseSession(deps, request) {
112
116
  try {
113
117
  handle.agent.send({
114
118
  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
+ role: "user",
120
+ content: [{ type: "text", text: prompt }],
121
+ source: {
122
+ kind: "task-ledger",
123
+ form: "notice",
124
+ summary: `结束任务 · ${task.title ?? request.taskId}`,
125
+ },
126
+ }, "next-turn", true);
119
127
  }
120
128
  catch (error) {
121
- throw new HandoffError('send-failed', `会话已建(${sessionId})但首条消息没发出去:${error instanceof Error ? error.message : String(error)}`);
129
+ throw new HandoffError("send-failed", `会话已建(${sessionId})但首条消息没发出去:${error instanceof Error ? error.message : String(error)}`);
122
130
  }
123
131
  return { sessionId };
124
132
  }
@@ -128,18 +136,20 @@ export async function startCloseSession(deps, request) {
128
136
  */
129
137
  export function resolveModelRoute(defaultModel, explicit) {
130
138
  if (explicit !== undefined) {
131
- if (explicit.provider === '' || explicit.model === '') {
132
- throw new HandoffError('no-model', 'provider 与 model 必须成对给出(只给一半会被拒,不做猜测)');
139
+ if (explicit.provider === "" || explicit.model === "") {
140
+ throw new HandoffError("no-model", "provider 与 model 必须成对给出(只给一半会被拒,不做猜测)");
133
141
  }
134
142
  return explicit;
135
143
  }
136
144
  if (defaultModel !== undefined) {
137
145
  try {
138
146
  const route = defaultModel.currentSelection();
139
- if (route.provider !== '' && route.model !== '')
147
+ if (route.provider !== "" && route.model !== "")
140
148
  return { provider: route.provider, model: route.model };
141
149
  }
142
- catch { /* 读不到就往下走,报同一句人话 */ }
150
+ catch {
151
+ /* 读不到就往下走,报同一句人话 */
152
+ }
143
153
  }
144
- throw new HandoffError('no-model', '宿主没有可用的默认模型:请在请求里给出 provider + model');
154
+ throw new HandoffError("no-model", "宿主没有可用的默认模型:请在请求里给出 provider + model");
145
155
  }
@@ -1,5 +1,12 @@
1
- import { type DegradedInfo } from '../errors.js';
2
- import type { ClosedSummary, LedgerFilter, LedgerWorkspace, TrendPeriods, TrendScope } from '../types.js';
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 { type DegradedInfo } from "../errors.js";
9
+ import type { ClosedSummary, LedgerFilter, LedgerWorkspace, TrendPeriods, TrendScope } from "../types.js";
3
10
  /** 历史窗口:只读最近 13 个月的文件(2026-10-17 拍板;脚本默认读全部,窗口由插件显式传)。 */
4
11
  export declare const RECENT_MONTHS = 13;
5
12
  /** 趋势图桶数:近 8 周(2026-10-17 定稿)。 */
@@ -5,10 +5,10 @@
5
5
  * ⚠️ 窗口必须是 13 个月而不是 2:8 周桶在月初会横跨 3 个自然月(如 10 月第 1 周回溯到 8 月),
6
6
  * `--months` 是**文件级**过滤,缩到 2 会静默漏掉 8 月结案记录、趋势少算(2026-10-18 架构评审结论)。
7
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';
8
+ import { settleWorkspaces } from "../errors.js";
9
+ import { getCurrentIdentity } from "../repositories/identity.repository.js";
10
+ import { readHistory, readTasks } from "../repositories/ledger.repository.js";
11
+ import { buildWeeks, dayOf, inWindow, mondayOf, monthWindow, weekWindow, ymd, } from "./dates.js";
12
12
  /** 历史窗口:只读最近 13 个月的文件(2026-10-17 拍板;脚本默认读全部,窗口由插件显式传)。 */
13
13
  export const RECENT_MONTHS = 13;
14
14
  /** 趋势图桶数:近 8 周(2026-10-17 定稿)。 */
@@ -44,7 +44,8 @@ export function computeClosedSummary(scopes, now) {
44
44
  if (createdDay === null)
45
45
  return;
46
46
  for (let i = 0; i < weeks.length; i += 1) {
47
- if (createdDay <= weekEnds[i] && (closeDay === null || closeDay > weekEnds[i])) {
47
+ if (createdDay <= weekEnds[i] &&
48
+ (closeDay === null || closeDay > weekEnds[i])) {
48
49
  const bucket = map.get(weeks[i].week);
49
50
  if (bucket !== undefined)
50
51
  bucket.remaining += 1;
@@ -59,9 +60,9 @@ export function computeClosedSummary(scopes, now) {
59
60
  continue;
60
61
  const week = ymd(mondayOf(created));
61
62
  const createdMine = identity !== null && (task.creator ?? null) === identity;
62
- bump(bucketsAll, week, 'created');
63
+ bump(bucketsAll, week, "created");
63
64
  if (createdMine)
64
- bump(bucketsMine, week, 'created');
65
+ bump(bucketsMine, week, "created");
65
66
  const ownedMine = identity !== null && (task.owner ?? null) === identity;
66
67
  bumpRemaining(bucketsAll, created, null);
67
68
  if (ownedMine)
@@ -71,19 +72,21 @@ export function computeClosedSummary(scopes, now) {
71
72
  const closed = dayOf(record.time);
72
73
  if (closed !== null) {
73
74
  const week = ymd(mondayOf(closed));
74
- bump(bucketsAll, week, 'done');
75
+ bump(bucketsAll, week, "done");
75
76
  if (identity !== null && (record.owner ?? null) === identity)
76
- bump(bucketsMine, week, 'done');
77
+ bump(bucketsMine, week, "done");
77
78
  }
78
79
  const created = dayOf(record.created);
79
80
  if (created !== null) {
80
81
  const week = ymd(mondayOf(created));
81
- bump(bucketsAll, week, 'created');
82
+ bump(bucketsAll, week, "created");
82
83
  // 新增(我的)= 我创建的(creator);旧 8 列历史无 creator 列 ⇒ 兜底 owner
83
- const createdMine = identity !== null && ((record.creator ?? null) === identity
84
- || ((record.creator ?? null) === null && (record.owner ?? null) === identity));
84
+ const createdMine = identity !== null &&
85
+ ((record.creator ?? null) === identity ||
86
+ ((record.creator ?? null) === null &&
87
+ (record.owner ?? null) === identity));
85
88
  if (createdMine)
86
- bump(bucketsMine, week, 'created');
89
+ bump(bucketsMine, week, "created");
87
90
  }
88
91
  const ownedMine = identity !== null && (record.owner ?? null) === identity;
89
92
  bumpRemaining(bucketsAll, created, closed);
@@ -92,9 +95,13 @@ export function computeClosedSummary(scopes, now) {
92
95
  }
93
96
  }
94
97
  const toBuckets = (map) => weeks.map((slot, i) => ({
95
- week: slot.week, end: weekEnds[i], ...(map.get(slot.week) ?? { done: 0, created: 0, remaining: 0 }),
98
+ week: slot.week,
99
+ end: weekEnds[i],
100
+ ...(map.get(slot.week) ?? { done: 0, created: 0, remaining: 0 }),
96
101
  }));
97
- return { weekly: { all: toBuckets(bucketsAll), mine: toBuckets(bucketsMine) } };
102
+ return {
103
+ weekly: { all: toBuckets(bucketsAll), mine: toBuckets(bucketsMine) },
104
+ };
98
105
  }
99
106
  /** 四个自然周期(只算「全部工作区」范围;图表切换不带动卡片)——纯函数。 */
100
107
  export function computeTrendPeriods(scopes, now) {
@@ -104,14 +111,24 @@ export function computeTrendPeriods(scopes, now) {
104
111
  thisMonth: monthWindow(0, now),
105
112
  lastMonth: monthWindow(-1, now),
106
113
  };
107
- const doneEntries = scopes.flatMap((scope) => scope.history.map((record) => ({ day: dayOf(record.time), owner: record.owner ?? null })));
114
+ const doneEntries = scopes.flatMap((scope) => scope.history.map((record) => ({
115
+ day: dayOf(record.time),
116
+ owner: record.owner ?? null,
117
+ })));
108
118
  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 })),
119
+ ...scope.tasks.map((task) => ({
120
+ day: dayOf(task.created),
121
+ owner: task.owner ?? null,
122
+ })),
123
+ ...scope.history.map((record) => ({
124
+ day: dayOf(record.created),
125
+ owner: record.owner ?? null,
126
+ })),
111
127
  ]);
112
128
  const period = (window) => ({
113
129
  done: doneEntries.filter((entry) => inWindow(entry.day, window)).length,
114
- created: createdEntries.filter((entry) => inWindow(entry.day, window)).length,
130
+ created: createdEntries.filter((entry) => inWindow(entry.day, window))
131
+ .length,
115
132
  from: window.from,
116
133
  to: window.to,
117
134
  });
@@ -125,8 +142,12 @@ export function computeTrendPeriods(scopes, now) {
125
142
  /** 慢路径端点:趋势 + 周期(读 history)。异步加载,首屏不等。单工作区坏 ⇒ 进 degraded。 */
126
143
  export async function getTrend(workspaces) {
127
144
  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 };
145
+ const { data: scopes, degraded } = await settleWorkspaces(workspaces, "history", readTrendScope, (rows) => rows);
146
+ return {
147
+ closed: computeClosedSummary(scopes, now),
148
+ trend: computeTrendPeriods(scopes, now),
149
+ degraded,
150
+ };
130
151
  }
131
152
  /** 趋势端点用的历史 filter(窗口由本模块策略决定,repository 零默认值注入)。 */
132
153
  export function trendHistoryFilter(extra = {}) {
@@ -9,7 +9,7 @@ export interface SkillRegistration {
9
9
  path?: string;
10
10
  source?: string;
11
11
  resourceBase?: {
12
- kind: 'directory';
12
+ kind: "directory";
13
13
  path: string;
14
14
  };
15
15
  metadata?: Record<string, unknown>;
@@ -2,27 +2,32 @@
2
2
  * 内嵌 skill 的注册体组装(任务 2「skill 注册最小闭环」)。
3
3
  * 宿主事实(0.2.0-rc.2 npm pack 亲验):external/dsh-skill-registration.md。
4
4
  */
5
- import { readFileSync } from 'node:fs';
6
- import { fileURLToPath } from 'node:url';
5
+ import { readFileSync } from "node:fs";
6
+ import { fileURLToPath } from "node:url";
7
7
  /** 内嵌 skill 目录:dist/host/skill/registration.js → 包根 skill/(copy-skill 产物;
8
8
  * 比原 host/skill-registration.js 深一层 ⇒ 相对路径多一级 `../`)。 */
9
- const SKILL_DIR = fileURLToPath(new URL('../../../skill', import.meta.url));
9
+ const SKILL_DIR = fileURLToPath(new URL("../../../skill", import.meta.url));
10
10
  /** SKILL.md 里指示脚本位置的占位符(注册时替换为绝对路径,模型据此找到脚本)。 */
11
- const SKILL_DIR_TOKEN = '<本 SKILL.md 所在目录>';
11
+ const SKILL_DIR_TOKEN = "<本 SKILL.md 所在目录>";
12
12
  /** skill 脚本绝对路径(提示词里直接给「原样执行这一条」的命令用;与注册共用同一份目录知识)。 */
13
13
  export const SKILL_SCRIPT = `${SKILL_DIR}/scripts/progress.py`;
14
14
  /** frontmatter 最小解析:只取 name / description(不引 YAML 库;值去首尾引号)。 */
15
15
  function parseFrontmatter(text) {
16
16
  const match = /^---\n([\s\S]*?)\n---/.exec(text);
17
17
  if (match === null)
18
- throw new Error('SKILL.md 缺 frontmatter(--- 包裹的头)');
18
+ throw new Error("SKILL.md 缺 frontmatter(--- 包裹的头)");
19
19
  const field = (key) => {
20
- const line = (match[1] ?? '').split('\n').find((item) => item.startsWith(`${key}:`));
20
+ const line = (match[1] ?? "")
21
+ .split("\n")
22
+ .find((item) => item.startsWith(`${key}:`));
21
23
  if (line === undefined)
22
24
  throw new Error(`SKILL.md frontmatter 缺 ${key}`);
23
- return line.slice(key.length + 1).trim().replace(/^["']|["']$/g, '');
25
+ return line
26
+ .slice(key.length + 1)
27
+ .trim()
28
+ .replace(/^["']|["']$/g, "");
24
29
  };
25
- return { name: field('name'), description: field('description') };
30
+ return { name: field("name"), description: field("description") };
26
31
  }
27
32
  /**
28
33
  * 组装注册体:读内嵌 SKILL.md → 取 name / description → 正文里的脚本路径占位符
@@ -33,14 +38,14 @@ function parseFrontmatter(text) {
33
38
  * 拍板后如改走 resourceBase,只动本函数,调用方无感。
34
39
  */
35
40
  export function buildSkillRegistration(skillDir = SKILL_DIR) {
36
- const text = readFileSync(`${skillDir}/SKILL.md`, 'utf-8');
41
+ const text = readFileSync(`${skillDir}/SKILL.md`, "utf-8");
37
42
  const { name, description } = parseFrontmatter(text);
38
43
  const content = text.split(SKILL_DIR_TOKEN).join(skillDir);
39
44
  if (content.includes(SKILL_DIR_TOKEN)) {
40
- throw new Error('占位符替换失败:SKILL.md 里仍残留脚本路径占位符');
45
+ throw new Error("占位符替换失败:SKILL.md 里仍残留脚本路径占位符");
41
46
  }
42
47
  if (!content.includes(`${skillDir}/scripts/progress.py`)) {
43
- throw new Error('占位符替换异常:替换后找不到脚本路径');
48
+ throw new Error("占位符替换异常:替换后找不到脚本路径");
44
49
  }
45
50
  return { name, description, content, path: `${skillDir}/SKILL.md` };
46
51
  }
@@ -1,4 +1,4 @@
1
- import type { LedgerWorkspace } from './types.js';
1
+ import type { LedgerWorkspace } from "./types.js";
2
2
  /** 宿主 workspaceRegistry.list() 条目的最小面(结构声明;事实与坐标见 external/dsh-workspace-registry.md)。 */
3
3
  export interface RegistryWorkspace {
4
4
  readonly id: string;
@@ -1,4 +1,4 @@
1
- import { hasLedger } from './repositories/ledger.repository.js';
1
+ import { hasLedger } from "./repositories/ledger.repository.js";
2
2
  /**
3
3
  * 工作区发现:宿主 registry 全量 → 逐个查 <path>/ledger/progress.json,只返回有的。
4
4
  * 未初始化(有 ledger/ 无 progress.json)= 隐藏,后端无此概念(2026-10-17 拍板)。
@@ -9,5 +9,9 @@ import { hasLedger } from './repositories/ledger.repository.js';
9
9
  export function resolveWorkspaces(registryList) {
10
10
  return registryList
11
11
  .filter((workspace) => hasLedger(workspace.path))
12
- .map((workspace) => ({ id: workspace.id, title: workspace.title, path: workspace.path }));
12
+ .map((workspace) => ({
13
+ id: workspace.id,
14
+ title: workspace.title,
15
+ path: workspace.path,
16
+ }));
13
17
  }