@openturtle/cli 0.3.7 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -1
- package/dist/commands/collaboration.js +41 -6
- package/dist/commands/local-knowledge.js +13 -1
- package/dist/commands/read-boundary.js +80 -0
- package/dist/commands/resources.js +25 -2
- package/dist/commands/worklogs.js +118 -0
- package/dist/index.js +41 -24
- package/dist/installers/agents.js +5 -2
- package/package.json +1 -1
- package/skill/openturtle-cli/SKILL.md +66 -5
package/README.md
CHANGED
|
@@ -56,12 +56,17 @@ ot knowledge search "部署"
|
|
|
56
56
|
ot knowledge local search "部署"
|
|
57
57
|
ot meetings list --project <project-id>
|
|
58
58
|
ot work summary --project <project-id>
|
|
59
|
+
ot worklogs show <worklog-id>
|
|
60
|
+
ot worklogs list --project <project-id> --to <iso-time> --scope team
|
|
61
|
+
ot context get --project <project-id> --query "本周风险"
|
|
59
62
|
ot worklogs record --summary "完成联调" --type progress --todo <todo-id>
|
|
60
63
|
ot report draft --today
|
|
61
64
|
```
|
|
62
65
|
|
|
63
66
|
`ot install` 只安装 CLI 使用指引。Agent 直接执行 `ot ... --json`,不需要 MCP 或 Hook。
|
|
64
67
|
|
|
68
|
+
普通项目综合问答优先使用一次聚合 `context get`,最多再做一次定向补查。日常回答不要使用 `--trace`、`--include-candidates`、`--help` 或全量对象列表;两次读取后仍缺信息时应明确标记“未召回”并停止。`trace` 只用于召回审计和故障诊断。
|
|
69
|
+
|
|
65
70
|
## 常用场景
|
|
66
71
|
|
|
67
72
|
### 看我的待办
|
|
@@ -95,6 +100,30 @@ ot knowledge search "日报" --local-only
|
|
|
95
100
|
ot knowledge local search "日报"
|
|
96
101
|
```
|
|
97
102
|
|
|
103
|
+
需要可重复的服务端语料边界时,使用带时区的 `--as-of` 或不可变快照:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
ot knowledge search "日报" --project <project-id> --as-of 2026-07-27T07:28:32Z
|
|
107
|
+
ot context get --project <project-id> --query "上次回读验收还有什么缺口" --snapshot <snapshot-id>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
使用 `--as-of`、`--snapshot` 或任一排除参数时,聚合搜索只返回服务端云端结果,避免无版本边界的本地索引混入;这类参数不能与 `--local-only` 同时使用。
|
|
111
|
+
|
|
112
|
+
### 导出不可变项目快照
|
|
113
|
+
|
|
114
|
+
创建快照是写操作,先检查 dry-run:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
ot projects snapshot export <project-id> \
|
|
118
|
+
--as-of 2026-07-27T07:28:32Z \
|
|
119
|
+
--exclude-tag evaluation-artifact \
|
|
120
|
+
--dry-run
|
|
121
|
+
ot projects snapshot list --project <project-id>
|
|
122
|
+
ot projects snapshot show <snapshot-id>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
快照包含捕获策略、对象版本时间、逐对象内容哈希和 manifest hash。创建后不能更新或删除。
|
|
126
|
+
|
|
98
127
|
### 上传录音并处理会议
|
|
99
128
|
|
|
100
129
|
```bash
|
|
@@ -360,9 +389,34 @@ ot roster list --project <project-id>
|
|
|
360
389
|
ot work summary --project <project-id>
|
|
361
390
|
ot digest get --project <project-id> --window this_week
|
|
362
391
|
ot context get --project <project-id> --query "本周风险" --section risk
|
|
392
|
+
ot context get --project <project-id> --query "上次回读验收还有什么缺口" --snapshot <snapshot-id>
|
|
393
|
+
# 仅在诊断召回时使用:
|
|
394
|
+
ot context get --project <project-id> --query "上次回读验收还有什么缺口" --trace --include-candidates
|
|
363
395
|
ot context item meeting <meeting-id> --project <project-id>
|
|
364
396
|
```
|
|
365
397
|
|
|
398
|
+
可重复读取边界:
|
|
399
|
+
|
|
400
|
+
```bash
|
|
401
|
+
ot goals list --project <project-id> --as-of 2026-07-27T07:28:32Z
|
|
402
|
+
ot todos list --project <project-id> --exclude-tag evaluation-artifact
|
|
403
|
+
ot worklogs list --project <project-id> --exclude-id <worklog-id> --scope team
|
|
404
|
+
ot meetings list --project <project-id> --snapshot <snapshot-id>
|
|
405
|
+
ot knowledge search "关键词" --project <project-id> --snapshot <snapshot-id>
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
`--exclude-id`、`--exclude-type` 和 `--exclude-tag` 可重复传入。`--snapshot` 与 `--as-of` 或任一排除参数互斥;非法组合会在 CLI 和服务端分别快速失败。服务端 `as_of` 采用保守语义:对象必须在边界前创建且最后更新时间不晚于边界,WorkLog 的 `occurred_at` 也必须不晚于边界。
|
|
409
|
+
|
|
410
|
+
项目快照:
|
|
411
|
+
|
|
412
|
+
```bash
|
|
413
|
+
ot projects snapshot export <project-id> --as-of <iso-time> --exclude-tag evaluation-artifact --dry-run
|
|
414
|
+
ot projects snapshot list --project <project-id> --page 1 --page-size 20
|
|
415
|
+
ot projects snapshot show <snapshot-id>
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
`snapshot export` 会持久化不可变 corpus,因此属于写操作;Agent 必须先展示 dry-run,并在用户确认后才执行正式创建。`show` 会返回完整 manifest/corpus,`list` 只返回摘要。
|
|
419
|
+
|
|
366
420
|
目标:
|
|
367
421
|
|
|
368
422
|
```bash
|
|
@@ -387,6 +441,8 @@ ot todos submit-review <todo-id> --body-file review-submission.json --dry-run
|
|
|
387
441
|
ot todos review <todo-id> --body-file review-decision.json --dry-run
|
|
388
442
|
```
|
|
389
443
|
|
|
444
|
+
Todo 响应的顶层 typed 字段是当前 canonical 值。机器消费者应读取 `field_schema_version`、`canonical_fields` 和 `metadata_precedence`;`legacy_metadata` 仅保留历史兼容,`superseded_fields` 指明旧 metadata 键已被哪个顶层字段取代。不得把旧 `v0.4` 或 `J0`-`J9` metadata 当成当前验收标准。
|
|
445
|
+
|
|
390
446
|
知识对象:
|
|
391
447
|
|
|
392
448
|
```bash
|
|
@@ -419,7 +475,7 @@ ot knowledge local update <asset-id> --body-file local-note.json --dry-run
|
|
|
419
475
|
ot knowledge local remove <asset-id> --dry-run
|
|
420
476
|
```
|
|
421
477
|
|
|
422
|
-
`ot knowledge search` 返回 `{ query, cloud, local }` 聚合结果;`--cloud-only` 和 `--local-only` 可限定范围。知识对象是可复用的结论、规范、经验、决策沉淀和操作手册。来源文件是知识背后的原文、附件或仓库文档,本地知识索引可脱离 Desktop
|
|
478
|
+
`ot knowledge search` 返回 `{ query, cloud, local }` 聚合结果;`--cloud-only` 和 `--local-only` 可限定范围。知识对象是可复用的结论、规范、经验、决策沉淀和操作手册。来源文件是知识背后的原文、附件或仓库文档,本地知识索引可脱离 Desktop 使用。显式服务端边界会自动关闭本地聚合;边界参数与 `--local-only` 冲突时命令直接报错。
|
|
423
479
|
|
|
424
480
|
会议:
|
|
425
481
|
|
|
@@ -445,12 +501,19 @@ ot meetings updates ignore <meeting-id> <update-id> --dry-run
|
|
|
445
501
|
工作日志:
|
|
446
502
|
|
|
447
503
|
```bash
|
|
504
|
+
ot worklogs show <worklog-id>
|
|
448
505
|
ot worklogs list --today
|
|
506
|
+
ot worklogs list --project <project-id> --to <iso-time> --scope team --page-size 100
|
|
507
|
+
ot worklogs list --project <project-id> --window this_week --event-type progress
|
|
449
508
|
ot worklogs record --summary "完成 Qoder CLI 接入验证" --type progress --todo <todo-id>
|
|
450
509
|
ot worklogs record --summary "Codex CLI 登录需要用户确认" --type blocker
|
|
451
510
|
ot worklogs create --body-file worklog.json --dry-run
|
|
452
511
|
```
|
|
453
512
|
|
|
513
|
+
`worklogs list` 支持 `--project`、`--from`、`--to`、`--event-type`、`--scope team|managed|mine`、`--page` 和 `--page-size`。`--from` / `--to` 是包含边界,必须是带 `Z` 或 UTC offset 的 ISO 8601 时间。
|
|
514
|
+
|
|
515
|
+
`--today` 等价于 `--window today`。窗口可选 `today`、`yesterday`、`this_week`、`last_week` 和 `this_month`;不传窗口即保持既有全量读取。日历边界按运行 CLI 的系统本地时区计算,再作为 UTC ISO 时间发送给服务端。`--today` / `--window` 不能和显式 `--from` / `--to` 混用。无效窗口、scope、时间和分页值会在请求前报错。
|
|
516
|
+
|
|
454
517
|
结构化写操作统一支持 `--body-file <json>`,也可用 `--body-file -` 从 stdin 读取。建议先执行同一条命令的 `--dry-run`,确认 `request.path` 和 `request.body` 后再去掉该参数。
|
|
455
518
|
|
|
456
519
|
只读 API 还提供受限逃生口,只允许 `/api/` 下的 GET:
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import { ApiClient, listQuery } from '../core/api-client.js';
|
|
2
2
|
import { collectOption, parseKeyValueOptions, readJsonBody } from '../core/input.js';
|
|
3
|
+
import { addReadBoundaryOptions, addSnapshotPolicyOptions, buildReadBoundaryQuery, buildSnapshotExportBody, } from './read-boundary.js';
|
|
4
|
+
const PROJECT_ACTIVE_TODO_STATUSES = ['draft', 'pending', 'in_progress', 'pending_review', 'failed'];
|
|
5
|
+
function withProjectTodoSummary(project) {
|
|
6
|
+
const byStatus = project.todo_stats?.by_status;
|
|
7
|
+
const projectActiveTodoCount = byStatus
|
|
8
|
+
? PROJECT_ACTIVE_TODO_STATUSES.reduce((total, status) => total + Number(byStatus[status] || 0), 0)
|
|
9
|
+
: null;
|
|
10
|
+
return {
|
|
11
|
+
...project,
|
|
12
|
+
active_todo_count_scope: 'current_user',
|
|
13
|
+
project_active_todo_count: projectActiveTodoCount,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
3
16
|
export function createCollaborationCommands(client = new ApiClient()) {
|
|
4
17
|
const mutate = async (method, path, body, dryRun = false) => {
|
|
5
18
|
if (dryRun)
|
|
@@ -19,9 +32,10 @@ export function createCollaborationCommands(client = new ApiClient()) {
|
|
|
19
32
|
const response = await client.get('/api/projects', {
|
|
20
33
|
keyword: query,
|
|
21
34
|
activated_only: false,
|
|
35
|
+
include_todo_stats: true,
|
|
22
36
|
page_size: 100,
|
|
23
37
|
});
|
|
24
|
-
const items = Array.isArray(response) ? response : response.items || [];
|
|
38
|
+
const items = (Array.isArray(response) ? response : response.items || []).map((item) => withProjectTodoSummary(item));
|
|
25
39
|
const exact = items.find((item) => item.id === query || String(item.name || '').toLowerCase() === query.toLowerCase());
|
|
26
40
|
return {
|
|
27
41
|
resolved: Boolean(exact),
|
|
@@ -29,6 +43,9 @@ export function createCollaborationCommands(client = new ApiClient()) {
|
|
|
29
43
|
candidates: exact ? [] : items,
|
|
30
44
|
};
|
|
31
45
|
},
|
|
46
|
+
listProjectSnapshots: (projectId, options) => client.get(`/api/projects/${encodeURIComponent(projectId)}/snapshots`, listQuery(options)),
|
|
47
|
+
showProjectSnapshot: (snapshotId) => client.get(`/api/projects/snapshots/${encodeURIComponent(snapshotId)}`),
|
|
48
|
+
exportProjectSnapshot: (projectId, options, dryRun = false) => mutate('POST', `/api/projects/${encodeURIComponent(projectId)}/snapshots`, buildSnapshotExportBody(options), dryRun),
|
|
32
49
|
async listRoster(teamId, projectId) {
|
|
33
50
|
const team = teamId ? { id: teamId } : await client.get('/api/teams/me');
|
|
34
51
|
const resolvedTeamId = String(team.id || teamId || '');
|
|
@@ -185,6 +202,20 @@ export function registerCollaborationCommands(program, groups, callbacks) {
|
|
|
185
202
|
projects
|
|
186
203
|
.command('resolve <query>')
|
|
187
204
|
.action(action(async (query) => print(await commands.resolveProject(query))));
|
|
205
|
+
const snapshots = projects.command('snapshot').description('Immutable Project corpus snapshots');
|
|
206
|
+
addSnapshotPolicyOptions(snapshots.command('export <projectId>').option('--dry-run')).action(action(async (projectId, options) => print(await commands.exportProjectSnapshot(projectId, options, Boolean(options.dryRun)))));
|
|
207
|
+
snapshots
|
|
208
|
+
.command('show <snapshotId>')
|
|
209
|
+
.action(action(async (snapshotId) => print(await commands.showProjectSnapshot(snapshotId))));
|
|
210
|
+
snapshots
|
|
211
|
+
.command('list')
|
|
212
|
+
.requiredOption('--project <id>')
|
|
213
|
+
.option('--page <number>', 'page number', Number)
|
|
214
|
+
.option('--page-size <number>', 'items per page', Number)
|
|
215
|
+
.action(action(async (options) => print(await commands.listProjectSnapshots(options.project, {
|
|
216
|
+
page: options.page,
|
|
217
|
+
page_size: options.pageSize,
|
|
218
|
+
}))));
|
|
188
219
|
program
|
|
189
220
|
.command('roster')
|
|
190
221
|
.description('Current team roster')
|
|
@@ -220,7 +251,7 @@ export function registerCollaborationCommands(program, groups, callbacks) {
|
|
|
220
251
|
.option('--dry-run')
|
|
221
252
|
.action(action(async (options) => print(await commands.runOrgSyncSynthesis(options, Boolean(options.dryRun)))));
|
|
222
253
|
const context = program.command('context').description('Viewer-scoped project context');
|
|
223
|
-
context
|
|
254
|
+
addReadBoundaryOptions(context
|
|
224
255
|
.command('get')
|
|
225
256
|
.option('--project <id>')
|
|
226
257
|
.option('--goal <id>')
|
|
@@ -230,7 +261,8 @@ export function registerCollaborationCommands(program, groups, callbacks) {
|
|
|
230
261
|
.option('--match <mode>', 'any|all|exact', 'any')
|
|
231
262
|
.option('--mode <mode>', 'brief|search', 'brief')
|
|
232
263
|
.option('--limit <number>', 'maximum context items', Number)
|
|
233
|
-
.
|
|
264
|
+
.option('--trace', 'include retrieval accounting')
|
|
265
|
+
.option('--include-candidates', 'include ranked candidate details')).action(action(async (options) => print(await commands.resolveContext({
|
|
234
266
|
project_id: options.project,
|
|
235
267
|
goal_id: options.goal,
|
|
236
268
|
person_id: options.person,
|
|
@@ -239,6 +271,9 @@ export function registerCollaborationCommands(program, groups, callbacks) {
|
|
|
239
271
|
match_mode: options.match,
|
|
240
272
|
mode: options.mode,
|
|
241
273
|
limit: options.limit,
|
|
274
|
+
trace: options.trace,
|
|
275
|
+
include_candidates: options.includeCandidates,
|
|
276
|
+
...buildReadBoundaryQuery(options),
|
|
242
277
|
}))));
|
|
243
278
|
context
|
|
244
279
|
.command('item <sourceType> <sourceId>')
|
|
@@ -275,17 +310,17 @@ export function registerCollaborationCommands(program, groups, callbacks) {
|
|
|
275
310
|
.option('--dry-run')
|
|
276
311
|
.action(action(async (knowledgeId, attachmentId, options) => print(await commands.downloadKnowledgeAttachment(knowledgeId, attachmentId, options.output, Boolean(options.force), Boolean(options.dryRun)))));
|
|
277
312
|
const meetings = program.command('meetings').description('Meeting upload, minutes, knowledge, and project updates');
|
|
278
|
-
meetings
|
|
313
|
+
addReadBoundaryOptions(meetings
|
|
279
314
|
.command('list')
|
|
280
315
|
.option('--project <id>')
|
|
281
316
|
.option('--status <status>')
|
|
282
317
|
.option('--page <number>', 'page number', Number)
|
|
283
|
-
.option('--page-size <number>', 'items per page', Number)
|
|
284
|
-
.action(action(async (options) => print(await commands.listMeetings({
|
|
318
|
+
.option('--page-size <number>', 'items per page', Number)).action(action(async (options) => print(await commands.listMeetings({
|
|
285
319
|
project_id: options.project,
|
|
286
320
|
status: options.status,
|
|
287
321
|
page: options.page,
|
|
288
322
|
page_size: options.pageSize,
|
|
323
|
+
...buildReadBoundaryQuery(options),
|
|
289
324
|
}))));
|
|
290
325
|
meetings
|
|
291
326
|
.command('show <meetingId>')
|
|
@@ -54,10 +54,19 @@ export function createLocalKnowledgeCommands(root = localKnowledgeStoreRoot()) {
|
|
|
54
54
|
export async function searchKnowledgeEverywhere(query, options, fetchCloud, root = localKnowledgeStoreRoot()) {
|
|
55
55
|
if (options.cloudOnly && options.localOnly)
|
|
56
56
|
throw new Error('Use --cloud-only or --local-only, not both');
|
|
57
|
+
const hasServerBoundary = Boolean(options.asOf ||
|
|
58
|
+
options.snapshot ||
|
|
59
|
+
asStringArray(options.excludeId).length ||
|
|
60
|
+
asStringArray(options.excludeType).length ||
|
|
61
|
+
asStringArray(options.excludeTag).length);
|
|
62
|
+
if (hasServerBoundary && options.localOnly) {
|
|
63
|
+
throw new Error('Server read-boundary options cannot be combined with --local-only because the local index has no matching corpus boundary');
|
|
64
|
+
}
|
|
65
|
+
const cloudOnly = Boolean(options.cloudOnly || hasServerBoundary);
|
|
57
66
|
const local = createLocalKnowledgeCommands(root);
|
|
58
67
|
const [cloudResult, localResult] = await Promise.all([
|
|
59
68
|
options.localOnly ? Promise.resolve(null) : fetchCloud(),
|
|
60
|
-
|
|
69
|
+
cloudOnly
|
|
61
70
|
? Promise.resolve(null)
|
|
62
71
|
: local.search(query, Number(options.limit) || 20, normalizeMatchMode(options.match)),
|
|
63
72
|
]);
|
|
@@ -67,6 +76,9 @@ export async function searchKnowledgeEverywhere(query, options, fetchCloud, root
|
|
|
67
76
|
local: localResult,
|
|
68
77
|
};
|
|
69
78
|
}
|
|
79
|
+
function asStringArray(value) {
|
|
80
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === 'string' && Boolean(item)) : [];
|
|
81
|
+
}
|
|
70
82
|
export function registerLocalKnowledgeCommands(knowledge, callbacks) {
|
|
71
83
|
const commands = createLocalKnowledgeCommands();
|
|
72
84
|
const local = knowledge.command('local').description('Local indexed knowledge available without Desktop');
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { collectOption } from '../core/input.js';
|
|
2
|
+
const READ_OBJECT_TYPES = ['goal', 'todo', 'work_log', 'meeting', 'knowledge'];
|
|
3
|
+
export function addReadBoundaryOptions(command) {
|
|
4
|
+
return addSnapshotPolicyOptions(command).option('--snapshot <id>', 'read exclusively from an immutable project snapshot');
|
|
5
|
+
}
|
|
6
|
+
export function addSnapshotPolicyOptions(command) {
|
|
7
|
+
return command
|
|
8
|
+
.option('--as-of <iso>', 'server-enforced ISO timestamp with timezone')
|
|
9
|
+
.option('--exclude-id <id>', 'repeatable object ID exclusion', collectOption, [])
|
|
10
|
+
.option('--exclude-type <type>', 'repeatable: goal|todo|work_log|meeting|knowledge', collectOption, [])
|
|
11
|
+
.option('--exclude-tag <tag>', 'repeatable tag exclusion', collectOption, []);
|
|
12
|
+
}
|
|
13
|
+
export function buildReadBoundaryQuery(options) {
|
|
14
|
+
const asOf = optionalIsoTimestamp(options.asOf, '--as-of');
|
|
15
|
+
const excludeId = uniqueStrings(options.excludeId);
|
|
16
|
+
const excludeType = uniqueStrings(options.excludeType);
|
|
17
|
+
const excludeTag = uniqueStrings(options.excludeTag);
|
|
18
|
+
for (const value of excludeType) {
|
|
19
|
+
if (!READ_OBJECT_TYPES.includes(value)) {
|
|
20
|
+
throw new Error(`Invalid --exclude-type "${value}". Expected one of: ${READ_OBJECT_TYPES.join(', ')}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (options.snapshot && (asOf || excludeId.length || excludeType.length || excludeTag.length)) {
|
|
24
|
+
throw new Error('--snapshot cannot be combined with --as-of or exclusion options');
|
|
25
|
+
}
|
|
26
|
+
return compact({
|
|
27
|
+
as_of: asOf,
|
|
28
|
+
exclude_id: excludeId.length ? excludeId : undefined,
|
|
29
|
+
exclude_type: excludeType.length ? excludeType : undefined,
|
|
30
|
+
exclude_tag: excludeTag.length ? excludeTag : undefined,
|
|
31
|
+
snapshot_id: options.snapshot,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export function buildSnapshotExportBody(options) {
|
|
35
|
+
if (options.snapshot)
|
|
36
|
+
throw new Error('--snapshot is not valid when exporting a new snapshot');
|
|
37
|
+
const query = buildReadBoundaryQuery(options);
|
|
38
|
+
return compact({
|
|
39
|
+
as_of: query.as_of,
|
|
40
|
+
exclude_id: query.exclude_id,
|
|
41
|
+
exclude_type: query.exclude_type,
|
|
42
|
+
exclude_tag: query.exclude_tag,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function uniqueStrings(values) {
|
|
46
|
+
return [...new Set((values || []).map((value) => value.trim()).filter(Boolean))];
|
|
47
|
+
}
|
|
48
|
+
function optionalIsoTimestamp(value, name) {
|
|
49
|
+
if (value == null)
|
|
50
|
+
return undefined;
|
|
51
|
+
const match = value.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,9}))?)?(Z|[+-]\d{2}:\d{2})$/);
|
|
52
|
+
if (!match)
|
|
53
|
+
throw new Error(`${name} must be an ISO 8601 timestamp with timezone`);
|
|
54
|
+
const [, yearText, monthText, dayText, hourText, minuteText, secondText = '0', , offset] = match;
|
|
55
|
+
const year = Number(yearText);
|
|
56
|
+
const month = Number(monthText);
|
|
57
|
+
const day = Number(dayText);
|
|
58
|
+
const hour = Number(hourText);
|
|
59
|
+
const minute = Number(minuteText);
|
|
60
|
+
const second = Number(secondText);
|
|
61
|
+
const maxDay = month >= 1 && month <= 12 ? new Date(Date.UTC(year, month, 0)).getUTCDate() : 0;
|
|
62
|
+
const offsetParts = offset === 'Z' ? [0, 0] : offset.slice(1).split(':').map(Number);
|
|
63
|
+
if (year < 1 ||
|
|
64
|
+
month < 1 ||
|
|
65
|
+
month > 12 ||
|
|
66
|
+
day < 1 ||
|
|
67
|
+
day > maxDay ||
|
|
68
|
+
hour > 23 ||
|
|
69
|
+
minute > 59 ||
|
|
70
|
+
second > 59 ||
|
|
71
|
+
offsetParts[0] > 23 ||
|
|
72
|
+
offsetParts[1] > 59 ||
|
|
73
|
+
Number.isNaN(Date.parse(value))) {
|
|
74
|
+
throw new Error(`${name} must be a valid ISO 8601 timestamp with timezone`);
|
|
75
|
+
}
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
function compact(value) {
|
|
79
|
+
return Object.fromEntries(Object.entries(value).filter(([, item]) => item != null && item !== ''));
|
|
80
|
+
}
|
|
@@ -3,16 +3,34 @@ import path from 'node:path';
|
|
|
3
3
|
import { ApiClient, listQuery } from '../core/api-client.js';
|
|
4
4
|
import { readAuth } from '../core/auth.js';
|
|
5
5
|
import { initProjectStore, listEvents } from '../core/store.js';
|
|
6
|
+
import { buildReadBoundaryQuery } from './read-boundary.js';
|
|
6
7
|
export function createResourceCommands(client = new ApiClient()) {
|
|
7
8
|
return {
|
|
8
9
|
async listGoals(options) {
|
|
9
|
-
return client.get('/api/goals', listQuery(
|
|
10
|
+
return client.get('/api/goals', listQuery({
|
|
11
|
+
project_id: options.project_id || options.project,
|
|
12
|
+
mine: options.mine,
|
|
13
|
+
status: options.status,
|
|
14
|
+
scope: options.scope,
|
|
15
|
+
role: options.role,
|
|
16
|
+
page: options.page,
|
|
17
|
+
page_size: options.pageSize || options.page_size,
|
|
18
|
+
...buildReadBoundaryQuery(options),
|
|
19
|
+
}));
|
|
10
20
|
},
|
|
11
21
|
async showGoal(goalId) {
|
|
12
22
|
return client.get(`/api/goals/${encodeURIComponent(goalId)}`);
|
|
13
23
|
},
|
|
14
24
|
async listTodos(options) {
|
|
15
|
-
return client.get('/api/todos', listQuery(
|
|
25
|
+
return client.get('/api/todos', listQuery({
|
|
26
|
+
project_id: options.project_id || options.project,
|
|
27
|
+
mine: options.mine,
|
|
28
|
+
status: options.status,
|
|
29
|
+
scope: options.scope,
|
|
30
|
+
page: options.page,
|
|
31
|
+
page_size: options.pageSize || options.page_size,
|
|
32
|
+
...buildReadBoundaryQuery(options),
|
|
33
|
+
}));
|
|
16
34
|
},
|
|
17
35
|
async showTodo(todoId) {
|
|
18
36
|
return client.get(`/api/todos/${encodeURIComponent(todoId)}`);
|
|
@@ -48,6 +66,7 @@ export function createResourceCommands(client = new ApiClient()) {
|
|
|
48
66
|
person: options.person,
|
|
49
67
|
scope: options.scope,
|
|
50
68
|
limit: options.limit,
|
|
69
|
+
...buildReadBoundaryQuery(options),
|
|
51
70
|
}));
|
|
52
71
|
},
|
|
53
72
|
async listKnowledgeSources(options) {
|
|
@@ -59,6 +78,9 @@ export function createResourceCommands(client = new ApiClient()) {
|
|
|
59
78
|
async listWorklogs(options) {
|
|
60
79
|
return client.get('/api/work-logs', listQuery(options));
|
|
61
80
|
},
|
|
81
|
+
async showWorklog(workLogId) {
|
|
82
|
+
return client.get(`/api/work-logs/${encodeURIComponent(workLogId)}`);
|
|
83
|
+
},
|
|
62
84
|
async recordWorklog(summary, type = 'note', references = {}) {
|
|
63
85
|
return client.post('/api/work-logs', {
|
|
64
86
|
...(references.project ? { project_id: references.project } : {}),
|
|
@@ -99,6 +121,7 @@ function knowledgeQuery(options) {
|
|
|
99
121
|
person: options.person,
|
|
100
122
|
page: options.page,
|
|
101
123
|
page_size: options.pageSize,
|
|
124
|
+
...buildReadBoundaryQuery(options),
|
|
102
125
|
});
|
|
103
126
|
}
|
|
104
127
|
export function draftDailyReport(workspace, options = {}) {
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const WORKLOG_SCOPES = ['team', 'managed', 'mine'];
|
|
2
|
+
const WORKLOG_WINDOWS = ['today', 'yesterday', 'this_week', 'last_week', 'this_month'];
|
|
3
|
+
export function buildWorklogListQuery(options, now = new Date()) {
|
|
4
|
+
const scope = optionalEnum(options.scope, '--scope', WORKLOG_SCOPES);
|
|
5
|
+
const page = optionalInteger(options.page, '--page', 1);
|
|
6
|
+
const pageSize = optionalInteger(options.pageSize, '--page-size', 1, 100);
|
|
7
|
+
const dateFrom = optionalIsoTimestamp(options.from, '--from');
|
|
8
|
+
const dateTo = optionalIsoTimestamp(options.to, '--to');
|
|
9
|
+
if (options.today && options.window) {
|
|
10
|
+
throw new Error('--today cannot be combined with --window');
|
|
11
|
+
}
|
|
12
|
+
const window = options.today ? 'today' : optionalEnum(options.window, '--window', WORKLOG_WINDOWS);
|
|
13
|
+
if (window && (dateFrom || dateTo)) {
|
|
14
|
+
throw new Error('--today/--window cannot be combined with --from or --to');
|
|
15
|
+
}
|
|
16
|
+
if (dateFrom && dateTo && Date.parse(dateFrom) > Date.parse(dateTo)) {
|
|
17
|
+
throw new Error('--from must be earlier than or equal to --to');
|
|
18
|
+
}
|
|
19
|
+
const windowRange = window ? worklogWindowRange(window, now) : {};
|
|
20
|
+
return compactQuery({
|
|
21
|
+
...buildReadBoundaryQuery(options),
|
|
22
|
+
project_id: options.project,
|
|
23
|
+
date_from: windowRange.date_from ?? dateFrom,
|
|
24
|
+
date_to: windowRange.date_to ?? dateTo,
|
|
25
|
+
event_type: options.eventType,
|
|
26
|
+
scope,
|
|
27
|
+
page,
|
|
28
|
+
page_size: pageSize,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export function worklogWindowRange(window, now = new Date()) {
|
|
32
|
+
const year = now.getFullYear();
|
|
33
|
+
const month = now.getMonth();
|
|
34
|
+
const day = now.getDate();
|
|
35
|
+
const today = new Date(year, month, day);
|
|
36
|
+
let start;
|
|
37
|
+
let next;
|
|
38
|
+
if (window === 'today') {
|
|
39
|
+
start = today;
|
|
40
|
+
next = new Date(year, month, day + 1);
|
|
41
|
+
}
|
|
42
|
+
else if (window === 'yesterday') {
|
|
43
|
+
start = new Date(year, month, day - 1);
|
|
44
|
+
next = today;
|
|
45
|
+
}
|
|
46
|
+
else if (window === 'this_week') {
|
|
47
|
+
const mondayOffset = (today.getDay() + 6) % 7;
|
|
48
|
+
start = new Date(year, month, day - mondayOffset);
|
|
49
|
+
next = new Date(year, month, day - mondayOffset + 7);
|
|
50
|
+
}
|
|
51
|
+
else if (window === 'last_week') {
|
|
52
|
+
const mondayOffset = (today.getDay() + 6) % 7;
|
|
53
|
+
start = new Date(year, month, day - mondayOffset - 7);
|
|
54
|
+
next = new Date(year, month, day - mondayOffset);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
start = new Date(year, month, 1);
|
|
58
|
+
next = new Date(year, month + 1, 1);
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
date_from: start.toISOString(),
|
|
62
|
+
date_to: inclusiveMicrosecondBefore(next),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function optionalIsoTimestamp(value, name) {
|
|
66
|
+
if (value == null)
|
|
67
|
+
return undefined;
|
|
68
|
+
const match = value.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,9}))?)?(Z|[+-]\d{2}:\d{2})$/);
|
|
69
|
+
if (!match)
|
|
70
|
+
throw new Error(`${name} must be an ISO 8601 timestamp with timezone`);
|
|
71
|
+
const [, yearText, monthText, dayText, hourText, minuteText, secondText = '0', , offset] = match;
|
|
72
|
+
const year = Number(yearText);
|
|
73
|
+
const month = Number(monthText);
|
|
74
|
+
const day = Number(dayText);
|
|
75
|
+
const hour = Number(hourText);
|
|
76
|
+
const minute = Number(minuteText);
|
|
77
|
+
const second = Number(secondText);
|
|
78
|
+
const maxDay = month >= 1 && month <= 12 ? new Date(Date.UTC(year, month, 0)).getUTCDate() : 0;
|
|
79
|
+
const offsetParts = offset === 'Z' ? [0, 0] : offset.slice(1).split(':').map(Number);
|
|
80
|
+
if (year < 1 ||
|
|
81
|
+
month < 1 ||
|
|
82
|
+
month > 12 ||
|
|
83
|
+
day < 1 ||
|
|
84
|
+
day > maxDay ||
|
|
85
|
+
hour > 23 ||
|
|
86
|
+
minute > 59 ||
|
|
87
|
+
second > 59 ||
|
|
88
|
+
offsetParts[0] > 23 ||
|
|
89
|
+
offsetParts[1] > 59 ||
|
|
90
|
+
Number.isNaN(Date.parse(value))) {
|
|
91
|
+
throw new Error(`${name} must be a valid ISO 8601 timestamp with timezone`);
|
|
92
|
+
}
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
function optionalInteger(value, name, minimum, maximum) {
|
|
96
|
+
if (value == null)
|
|
97
|
+
return undefined;
|
|
98
|
+
if (!Number.isInteger(value) || value < minimum || (maximum != null && value > maximum)) {
|
|
99
|
+
const range = maximum == null ? `at least ${minimum}` : `between ${minimum} and ${maximum}`;
|
|
100
|
+
throw new Error(`${name} must be an integer ${range}`);
|
|
101
|
+
}
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
function optionalEnum(value, name, allowed) {
|
|
105
|
+
if (value == null)
|
|
106
|
+
return undefined;
|
|
107
|
+
if (!allowed.includes(value)) {
|
|
108
|
+
throw new Error(`Invalid ${name} "${value}". Expected one of: ${allowed.join(', ')}`);
|
|
109
|
+
}
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
function compactQuery(query) {
|
|
113
|
+
return Object.fromEntries(Object.entries(query).filter(([, value]) => value != null && value !== ''));
|
|
114
|
+
}
|
|
115
|
+
function inclusiveMicrosecondBefore(value) {
|
|
116
|
+
return new Date(value.getTime() - 1).toISOString().replace(/\.999Z$/, '.999999Z');
|
|
117
|
+
}
|
|
118
|
+
import { buildReadBoundaryQuery } from './read-boundary.js';
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,8 @@ import { Command } from 'commander';
|
|
|
4
4
|
import { registerCollaborationCommands } from './commands/collaboration.js';
|
|
5
5
|
import { registerLocalKnowledgeCommands, searchKnowledgeEverywhere } from './commands/local-knowledge.js';
|
|
6
6
|
import { doctor, draftDailyReport, createResourceCommands } from './commands/resources.js';
|
|
7
|
+
import { addReadBoundaryOptions } from './commands/read-boundary.js';
|
|
8
|
+
import { buildWorklogListQuery } from './commands/worklogs.js';
|
|
7
9
|
import { ApiClient, ApiError } from './core/api-client.js';
|
|
8
10
|
import { scheduleAutomaticUpdate, updateCli } from './core/auto-update.js';
|
|
9
11
|
import { clearAuth, readAuth, redactToken, writeAuth } from './core/auth.js';
|
|
@@ -152,25 +154,23 @@ program
|
|
|
152
154
|
}));
|
|
153
155
|
const resources = createResourceCommands();
|
|
154
156
|
const goals = program.command('goals');
|
|
155
|
-
goals
|
|
156
|
-
.
|
|
157
|
-
.
|
|
158
|
-
.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
.action(action(async (options) => print(await resources.listGoals({ project_id: options.project, mine: options.mine, status: options.status }), options.json)));
|
|
157
|
+
addReadBoundaryOptions(goals.command('list').option('--project <id>').option('--mine').option('--status <status>').option('--json')).action(action(async (options) => print(await resources.listGoals({
|
|
158
|
+
project_id: options.project,
|
|
159
|
+
mine: options.mine,
|
|
160
|
+
status: options.status,
|
|
161
|
+
...readBoundaryOptions(options),
|
|
162
|
+
}), options.json)));
|
|
162
163
|
goals
|
|
163
164
|
.command('show <goalId>')
|
|
164
165
|
.option('--json')
|
|
165
166
|
.action(action(async (goalId, options) => print(await resources.showGoal(goalId), options.json)));
|
|
166
167
|
const todos = program.command('todos');
|
|
167
|
-
todos
|
|
168
|
-
.
|
|
169
|
-
.
|
|
170
|
-
.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
.action(action(async (options) => print(await resources.listTodos({ project_id: options.project, mine: options.mine, status: options.status }), options.json)));
|
|
168
|
+
addReadBoundaryOptions(todos.command('list').option('--project <id>').option('--mine').option('--status <status>').option('--json')).action(action(async (options) => print(await resources.listTodos({
|
|
169
|
+
project_id: options.project,
|
|
170
|
+
mine: options.mine,
|
|
171
|
+
status: options.status,
|
|
172
|
+
...readBoundaryOptions(options),
|
|
173
|
+
}), options.json)));
|
|
174
174
|
todos
|
|
175
175
|
.command('show <todoId>')
|
|
176
176
|
.option('--json')
|
|
@@ -212,7 +212,7 @@ todos
|
|
|
212
212
|
print(await resources.updateTodoProgress(todoId, options.message));
|
|
213
213
|
}));
|
|
214
214
|
const knowledge = program.command('knowledge');
|
|
215
|
-
knowledge
|
|
215
|
+
addReadBoundaryOptions(knowledge
|
|
216
216
|
.command('list')
|
|
217
217
|
.option('--project <id>')
|
|
218
218
|
.option('--q <keyword>')
|
|
@@ -222,13 +222,12 @@ knowledge
|
|
|
222
222
|
.option('--person <name-or-id>')
|
|
223
223
|
.option('--page <number>', 'page number', Number)
|
|
224
224
|
.option('--page-size <number>', 'items per page', Number)
|
|
225
|
-
.option('--json')
|
|
226
|
-
.action(action(async (options) => print(await resources.listKnowledge(options), options.json)));
|
|
225
|
+
.option('--json')).action(action(async (options) => print(await resources.listKnowledge(options), options.json)));
|
|
227
226
|
knowledge
|
|
228
227
|
.command('show <knowledgeId>')
|
|
229
228
|
.option('--json')
|
|
230
229
|
.action(action(async (knowledgeId, options) => print(await resources.showKnowledge(knowledgeId), options.json)));
|
|
231
|
-
knowledge
|
|
230
|
+
addReadBoundaryOptions(knowledge
|
|
232
231
|
.command('search <keyword>')
|
|
233
232
|
.option('--project <id>')
|
|
234
233
|
.option('--person <name-or-id>')
|
|
@@ -237,8 +236,7 @@ knowledge
|
|
|
237
236
|
.option('--match <mode>', 'local match mode: any|all|exact', 'any')
|
|
238
237
|
.option('--cloud-only')
|
|
239
238
|
.option('--local-only')
|
|
240
|
-
.option('--json')
|
|
241
|
-
.action(action(async (keyword, options) => print(await searchKnowledgeEverywhere(keyword, options, () => resources.searchKnowledge(keyword, options)), options.json)));
|
|
239
|
+
.option('--json')).action(action(async (keyword, options) => print(await searchKnowledgeEverywhere(keyword, options, () => resources.searchKnowledge(keyword, options)), options.json)));
|
|
242
240
|
knowledge
|
|
243
241
|
.command('sources')
|
|
244
242
|
.option('--path <path>')
|
|
@@ -269,12 +267,22 @@ program
|
|
|
269
267
|
project: options.project,
|
|
270
268
|
}), options.json)));
|
|
271
269
|
const worklogs = program.command('worklogs');
|
|
272
|
-
worklogs
|
|
270
|
+
addReadBoundaryOptions(worklogs
|
|
273
271
|
.command('list')
|
|
274
|
-
.option('--
|
|
275
|
-
.option('--
|
|
272
|
+
.option('--project <id>')
|
|
273
|
+
.option('--from <iso>', 'inclusive ISO timestamp with timezone')
|
|
274
|
+
.option('--to <iso>', 'inclusive ISO timestamp with timezone')
|
|
275
|
+
.option('--event-type <type>')
|
|
276
|
+
.option('--scope <scope>', 'team|managed|mine')
|
|
277
|
+
.option('--page <number>', 'page number', Number)
|
|
278
|
+
.option('--page-size <number>', 'items per page (1-100)', Number)
|
|
279
|
+
.option('--today', 'current local calendar day')
|
|
280
|
+
.option('--window <window>', 'today|yesterday|this_week|last_week|this_month')
|
|
281
|
+
.option('--json')).action(action(async (options) => print(await resources.listWorklogs(buildWorklogListQuery(options)), options.json)));
|
|
282
|
+
worklogs
|
|
283
|
+
.command('show <workLogId>')
|
|
276
284
|
.option('--json')
|
|
277
|
-
.action(action(async (options) => print(await resources.
|
|
285
|
+
.action(action(async (workLogId, options) => print(await resources.showWorklog(workLogId), options.json)));
|
|
278
286
|
worklogs
|
|
279
287
|
.command('record')
|
|
280
288
|
.option('--summary <text>')
|
|
@@ -412,6 +420,15 @@ function requireOption(value, name) {
|
|
|
412
420
|
throw new Error(`Missing required option ${name}`);
|
|
413
421
|
}
|
|
414
422
|
}
|
|
423
|
+
function readBoundaryOptions(options) {
|
|
424
|
+
return {
|
|
425
|
+
asOf: options.asOf,
|
|
426
|
+
excludeId: options.excludeId,
|
|
427
|
+
excludeType: options.excludeType,
|
|
428
|
+
excludeTag: options.excludeTag,
|
|
429
|
+
snapshot: options.snapshot,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
415
432
|
function expandTargets(target) {
|
|
416
433
|
if (target === 'all')
|
|
417
434
|
return ['claude', 'codex', 'cursor', 'qoder', 'kiro'];
|
|
@@ -25,7 +25,8 @@ export function installCursor(options) {
|
|
|
25
25
|
'---',
|
|
26
26
|
'',
|
|
27
27
|
'Run OpenTurtle operations with the direct `ot ... --json` CLI. Do not use an OpenTurtle MCP server or install OpenTurtle hooks.',
|
|
28
|
-
'
|
|
28
|
+
'On first use in a new execution environment, run `command -v ot` and `ot --json doctor`; do not repeat these checks in the same task.',
|
|
29
|
+
'For ordinary project synthesis, use OpenTurtle API data as the sole authority, start with one aggregated `ot context get` call using natural business-language queries, and allow at most one targeted follow-up. Never prime the request with project-specific tracking codes. Do not inspect repository files, Git state, or local agent memory. Before answering, remove tracking keys from prose and source labels; keep only machine IDs in citation or audit fields. New object titles and summaries must use natural business language, with tracking keys stored separately.',
|
|
29
30
|
'Before a write, run the same command with `--dry-run` and inspect the request path and body.',
|
|
30
31
|
'Todo quality: include source context, scope/environment, concrete steps, measurable acceptance criteria, and evidence. Ask one concise question when required information is unclear.',
|
|
31
32
|
'',
|
|
@@ -41,7 +42,8 @@ export function installKiro(options) {
|
|
|
41
42
|
'# OpenTurtle CLI',
|
|
42
43
|
'',
|
|
43
44
|
'Run OpenTurtle operations with the direct `ot ... --json` CLI. Do not use an OpenTurtle MCP server or install OpenTurtle hooks.',
|
|
44
|
-
'
|
|
45
|
+
'On first use in a new execution environment, run `command -v ot` and `ot --json doctor`; do not repeat these checks in the same task.',
|
|
46
|
+
'For ordinary project synthesis, use OpenTurtle API data as the sole authority, start with one aggregated `ot context get` call using natural business-language queries, and allow at most one targeted follow-up. Never prime the request with project-specific tracking codes. Do not inspect repository files, Git state, or local agent memory. Before answering, remove tracking keys from prose and source labels; keep only machine IDs in citation or audit fields. New object titles and summaries must use natural business language, with tracking keys stored separately.',
|
|
45
47
|
'Before a write, run the same command with `--dry-run` and inspect the request path and body.',
|
|
46
48
|
'Todo quality: include source context, scope/environment, concrete steps, measurable acceptance criteria, and evidence. Ask one concise question when required information is unclear.',
|
|
47
49
|
'',
|
|
@@ -135,6 +137,7 @@ function appendAgentsSnippet(workspace) {
|
|
|
135
137
|
'',
|
|
136
138
|
'Use the direct `ot ... --json` CLI for OpenTurtle projects, goals, todos, knowledge, meetings, and worklogs. Do not use an OpenTurtle MCP server or install OpenTurtle hooks.',
|
|
137
139
|
'Run `ot --json doctor` when authentication or server reachability is uncertain, and use `--dry-run` before writes.',
|
|
140
|
+
'For ordinary project synthesis, use OpenTurtle API data as the sole authority, use one aggregated `ot context get` call with natural business-language queries, and allow at most one targeted follow-up. Never prime the request with project-specific tracking codes. Do not inspect repository files, Git state, or local agent memory. Before answering, remove tracking keys from prose and source labels; keep only machine IDs in citation or audit fields. New object titles and summaries must use natural business language, with tracking keys stored separately.',
|
|
138
141
|
'Todo quality: include source context, scope/environment, concrete steps, measurable acceptance criteria, and evidence. Ask one concise question when required information is unclear.',
|
|
139
142
|
'',
|
|
140
143
|
'<!-- /OPENTURTLE-CLI -->',
|
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@ Desktop 会在隐藏系统上下文中注入已启用 Connector 的命令、能
|
|
|
11
11
|
|
|
12
12
|
用户要求新增外部服务时,先准备符合 `schemaVersion: 1` 的 manifest,再调用 `openturtle-desktop` 的 `propose_connector`。该调用只提交待审批提案;用户必须在“扩展 > 连接器”批准,安装和写操作仍需单独确认。不得直接修改 Desktop 配置或 Connector 目录。
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
每个新执行环境首次使用前运行;当前任务已经验证过后不要重复:
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
17
|
command -v ot
|
|
@@ -20,15 +20,49 @@ ot --json doctor
|
|
|
20
20
|
|
|
21
21
|
缺少登录态时让用户运行 `ot auth login --web`,不要索取或输出 token。Desktop 内置 Agent 会自动复用 Desktop 当前登录态、团队和项目上下文;独立终端使用 CLI 自己的登录配置。
|
|
22
22
|
|
|
23
|
+
只读 OpenTurtle 业务问题以 OpenTurtle API 返回的 Project Context 和业务对象为唯一权威来源,不读取仓库源码、`CLAUDE.md`、Git 状态、其他本地项目文件或 Codex/Claude 的历史 Memory。只有用户明确要求结合本地历史,或询问代码并要求修改时,才进入对应本地资料流程。Skill 已给出可用命令时不要预先调用 `--help`,只在命令真实报参数错误后查询一次对应帮助。
|
|
24
|
+
|
|
25
|
+
## 聚合优先与读取预算
|
|
26
|
+
|
|
27
|
+
普通用户询问项目现状、决策、工作包、会议结论、风险或下一步时,默认测试的是服务端 Project Context 聚合能力:
|
|
28
|
+
|
|
29
|
+
1. 已知项目和语料边界时,直接执行一次 `context get`。把用户问题拆成 2-6 个自然业务短句,用可重复的 `--query` 一次提交;默认 `--limit 20`,不要先用更大 limit 再缩小重跑。不得把内部实验编号、工作包代号或机器 ID 当成用户必须知道的检索口令,也不要在系统提示或示例中枚举当前项目的实际代号。内部追踪键只放在来源或审计字段;标题包含追踪键时,正文略去该前缀并使用自然业务名称。
|
|
30
|
+
2. 普通综合问答最多执行 2 次 OpenTurtle 业务读取。第二次只能是一次定向 `context get` 或一个已知 ID 的 `show`,不能再展开全量 `goals/todos/worklogs/knowledge/meetings list` 查询链。
|
|
31
|
+
3. 两次读取后仍缺信息时,明确写“未召回”及缺失项并停止。不要为了让答案看起来完整而继续搜索。
|
|
32
|
+
4. 普通回答禁止使用 `--trace`、`--include-candidates`、全量对象列表和全量 WorkLog。`trace` 只用于用户明确要求审计召回或诊断缺失;一次诊断只运行一个 trace 查询。
|
|
33
|
+
5. 机器 ID 在命令和来源中必须完整复制,不得截断、缩写或凭记忆重写。聚合结果已经覆盖用户问题时立即作答。
|
|
34
|
+
6. 普通项目综合问答以 60 秒内完成为目标。项目解析完成后优先只做一次 Context 读取;只有缺失事实会实质改变答案时才做第二次定向读取,不为扩写背景或增加篇幅补查。
|
|
35
|
+
7. 作答前做一次展示层检查:除来源中的机器 ID 外,标题、段落、小标题、列表和来源标签都不得出现仅用于追踪的编号或代号。原始标题或摘要带追踪前缀时,按对象类型和业务内容改述;无法确定自然名称时使用对象类型加内容摘要,不得原样复制追踪键。
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
ot context get --project <id> --snapshot <snapshot-id> \
|
|
39
|
+
--query "当前目标和成功标准" \
|
|
40
|
+
--query "正在推进哪些工作,目前分别是什么状态" \
|
|
41
|
+
--query "最近一次会议讨论了什么,哪些问题尚未决定" \
|
|
42
|
+
--query "当前正式决策和验证路径" \
|
|
43
|
+
--query "最近验收结果和遗留缺口" \
|
|
44
|
+
--limit 20
|
|
45
|
+
```
|
|
46
|
+
|
|
23
47
|
## 读取流程
|
|
24
48
|
|
|
25
|
-
1.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
49
|
+
1. 项目未知时优先用 `ot projects resolve <query>` 定位;已有项目 ID 或唯一的当前项目时不要再 list/resolve。
|
|
50
|
+
`active_todo_count` 是当前用户相关的活跃待办,项目整体进度使用 `project_active_todo_count` 和 `todo_stats`,不得把个人数字表述为项目总数。
|
|
51
|
+
2. 只有问题涉及具体人员时才用 `ot roster list --project <id>` 解析成员;先匹配 canonical `display_name`,再参考 aliases 和项目角色,重名或无法唯一匹配时询问用户。
|
|
52
|
+
3. 用一次 `ot context get --project <id> --query <term>` 获取项目上下文;复杂问题用同一命令的多个 `--query`,遵守上面的读取预算。
|
|
53
|
+
4. 聚合结果缺少一个关键事实时,第二次读取可按已知对象使用 `ot goals/todos/knowledge/meetings/worklogs show`,或执行一次更精确的 Context 查询。对象 list/search 只用于用户明确要求浏览集合,不用于普通综合问答补全。
|
|
29
54
|
5. 记录 Todo 执行进展时优先使用 `ot worklogs record --summary "..." --type progress --todo <todo-id>`,可同时传 `--project` / `--goal`,不要只把关联 ID 塞进 metadata。
|
|
30
55
|
6. 高层命令缺失时才使用只读逃生口 `ot request get /api/...`。
|
|
31
56
|
|
|
57
|
+
需要可重复语料边界时,优先让服务端执行,不要先全量读取再用 `jq` 猜边界:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
ot context get --project <id> --query <term> --as-of <iso> --exclude-tag evaluation-artifact
|
|
61
|
+
ot worklogs list --project <id> --snapshot <snapshot-id> --scope team
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`--exclude-id`、`--exclude-type`、`--exclude-tag` 可重复传入。`--snapshot` 与 `--as-of` 或任一排除参数互斥,非法组合必须报错。显式边界下 `knowledge search` 只使用云端语料;边界参数不能与 `--local-only` 混用。
|
|
65
|
+
|
|
32
66
|
## 写入规则
|
|
33
67
|
|
|
34
68
|
- 所有结构化写入使用 `--body-file <json>` 或 `--body-file -`,避免 shell 转义损坏内容。
|
|
@@ -36,6 +70,7 @@ ot --json doctor
|
|
|
36
70
|
- 创建目标、Todo、正式决策/风险、提交审核、审核通过/驳回、确认会议项目更新前,必须展示明确草稿和影响。
|
|
37
71
|
- 项目归属只能来自用户明确选择,或受管 Goal/Meeting 唯一解析出的项目;无法唯一确定时停止写入并询问。
|
|
38
72
|
- Knowledge 保存可长期复用的结论和完整会议纪要;短进展、决策、风险和产物引用写入 WorkLog。原始文件是 Knowledge 的来源材料,不是第二套知识系统。
|
|
73
|
+
- 新建或更新 Goal、Todo、WorkLog、Knowledge 和 Meeting Update 时,面向用户的 `title`、`summary`、正文和验收描述必须使用自然业务语言。需要内部追踪键时放入 metadata 或关系字段,不得把它作为标题前缀或要求用户在后续提问中使用。
|
|
39
74
|
|
|
40
75
|
## Todo 与 Goal
|
|
41
76
|
|
|
@@ -43,6 +78,8 @@ Todo 草稿至少包含来源、范围/环境、2-5 个行动步骤、可判断
|
|
|
43
78
|
|
|
44
79
|
`ot goals` 管理 OpenTurtle 团队/项目目标。当前 Agent 会话的 Goal 使用 Provider 官方 `/goal` 命令;不要将会话 Goal 写入 OpenTurtle 项目目标,也不要尝试同步两者状态。
|
|
45
80
|
|
|
81
|
+
读取 Todo 时以顶层 typed 字段为 canonical。`field_schema_version`、`canonical_fields` 和 `metadata_precedence` 是机器契约;`legacy_metadata` 只用于审计历史,`superseded_fields` 标明旧键的替代字段。不得把旧 `v0.4` 或 `J0`-`J9` metadata 当成当前验收标准。
|
|
82
|
+
|
|
46
83
|
```bash
|
|
47
84
|
ot goals create --body-file goal.json --dry-run
|
|
48
85
|
ot todos create --body-file todo.json --dry-run
|
|
@@ -51,6 +88,30 @@ ot todos submit-review <todo-id> --body-file review.json --dry-run
|
|
|
51
88
|
ot todos review <todo-id> --body-file decision.json --dry-run
|
|
52
89
|
```
|
|
53
90
|
|
|
91
|
+
## WorkLog
|
|
92
|
+
|
|
93
|
+
优先使用结构化读命令,不要用全量 `worklogs list` 后在客户端猜项目或时间范围。读取单条记录用 `ot worklogs show <worklog-id>`;项目历史或快照前数据使用服务端过滤:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
ot worklogs list --project <project-id> --to <iso-time> --scope team --page-size 100
|
|
97
|
+
ot worklogs list --project <project-id> --window this_week --event-type progress
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`--today` 等价于 `--window today`。日历窗口按运行 CLI 的系统本地时区计算;精确截止时间必须使用带时区的 `--from` / `--to`。窗口参数不能与显式时间边界混用,无效窗口、scope、时间和分页值会直接报错。
|
|
101
|
+
|
|
102
|
+
## Project Snapshot 与 Context Trace
|
|
103
|
+
|
|
104
|
+
项目快照是持久化写操作,必须先 dry-run 并经用户确认;列表和读取是只读:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
ot projects snapshot export <project-id> --as-of <iso> --exclude-tag evaluation-artifact --dry-run
|
|
108
|
+
ot projects snapshot list --project <project-id>
|
|
109
|
+
ot projects snapshot show <snapshot-id>
|
|
110
|
+
ot context get --project <project-id> --query <term> --snapshot <snapshot-id>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
快照建立后不可更新或删除。普通业务问答直接使用给定 snapshot ID,不要先 `show/list` 快照。只有服务端明确返回 snapshot 无效时才核对一次完整 ID 和快照列表。诊断召回时可给单个明确查询加 `--trace --include-candidates`;trace 返回候选数、选中/遗漏数、rank、score、匹配原因和截断信息,不能把一次召回当成稳定修复。
|
|
114
|
+
|
|
54
115
|
## Knowledge
|
|
55
116
|
|
|
56
117
|
保存前展示标题、摘要、正文范围、Project/Goal、来源 URL、附件和上下文引用。只有用户明确选择并确认的结论才能沉淀;不得自动上传或持久化 Desktop 既有会话历史。
|