@colin4k1024/tsp 2.5.2 → 2.5.4

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.
@@ -0,0 +1,221 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+
7
+ const DEFAULT_TARGET = 'claude';
8
+
9
+ const TARGET_STATE_DIRS = Object.freeze({
10
+ claude: ['.claude', 'loops'],
11
+ codex: ['.codex', 'loops'],
12
+ opencode: ['.config', 'opencode', 'loops'],
13
+ cangming: ['.cangming', 'loops'],
14
+ codewhale: ['.codewhale', 'loops'],
15
+ codebuddy: ['.codebuddy', 'loops'],
16
+ });
17
+
18
+ const LEGACY_CLAUDE_DIRS = Object.freeze({
19
+ goals: ['.claude', 'goals'],
20
+ triage: ['.claude', 'triage'],
21
+ heartbeatLastRun: ['.claude', 'heartbeat-last-run.json'],
22
+ });
23
+
24
+ function readHome() {
25
+ return process.env.HOME || process.env.USERPROFILE || os.homedir();
26
+ }
27
+
28
+ function normalizeTarget(target) {
29
+ return String(target || process.env.TSP_LOOP_TARGET || DEFAULT_TARGET).trim().toLowerCase() || DEFAULT_TARGET;
30
+ }
31
+
32
+ function projectLocalStateDir(projectRoot) {
33
+ if (!projectRoot) return null;
34
+ return path.join(path.resolve(projectRoot), '.tsp', 'loops');
35
+ }
36
+
37
+ function targetDefaultStateDir(target) {
38
+ const home = readHome();
39
+ const parts = TARGET_STATE_DIRS[normalizeTarget(target)] || TARGET_STATE_DIRS[DEFAULT_TARGET];
40
+ return path.join(home, ...parts);
41
+ }
42
+
43
+ function getLoopStateDir(options = {}) {
44
+ if (process.env.TSP_LOOP_STATE_DIR) {
45
+ return path.resolve(process.env.TSP_LOOP_STATE_DIR);
46
+ }
47
+
48
+ if (options.stateDir) {
49
+ return path.resolve(options.stateDir);
50
+ }
51
+
52
+ if (options.projectRoot) {
53
+ return projectLocalStateDir(options.projectRoot);
54
+ }
55
+
56
+ return targetDefaultStateDir(options.target);
57
+ }
58
+
59
+ function ensureDir(dirPath) {
60
+ fs.mkdirSync(dirPath, { recursive: true });
61
+ return dirPath;
62
+ }
63
+
64
+ function ensureLoopStateDir(options = {}) {
65
+ return ensureDir(getLoopStateDir(options));
66
+ }
67
+
68
+ function getNamespaceDir(namespace, options = {}) {
69
+ return path.join(getLoopStateDir(options), namespace);
70
+ }
71
+
72
+ function ensureNamespaceDir(namespace, options = {}) {
73
+ return ensureDir(getNamespaceDir(namespace, options));
74
+ }
75
+
76
+ function getGoalPath(goalId, options = {}) {
77
+ return path.join(getNamespaceDir('goals', options), `${goalId}.json`);
78
+ }
79
+
80
+ function getTriageInboxPath(options = {}) {
81
+ return path.join(getNamespaceDir('triage', options), 'inbox.jsonl');
82
+ }
83
+
84
+ function getHeartbeatPath(loopId = 'default', options = {}) {
85
+ return path.join(getNamespaceDir('heartbeat', options), `${loopId}.json`);
86
+ }
87
+
88
+ function getLoopMarkdownStatePath(loopId, options = {}) {
89
+ return path.join(getNamespaceDir('state', options), `${loopId}.md`);
90
+ }
91
+
92
+ function writeJson(filePath, value) {
93
+ ensureDir(path.dirname(filePath));
94
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
95
+ return filePath;
96
+ }
97
+
98
+ function readJson(filePath) {
99
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
100
+ }
101
+
102
+ function saveGoal(goal, options = {}) {
103
+ return writeJson(getGoalPath(goal.goalId, options), goal);
104
+ }
105
+
106
+ function loadGoal(goalId, options = {}) {
107
+ const filePath = getGoalPath(goalId, options);
108
+ if (fs.existsSync(filePath)) {
109
+ return readJson(filePath);
110
+ }
111
+
112
+ const legacyPath = getLegacyGoalPath(goalId);
113
+ if (!options.disableLegacyLookup && legacyPath && fs.existsSync(legacyPath)) {
114
+ return readJson(legacyPath);
115
+ }
116
+
117
+ return null;
118
+ }
119
+
120
+ function listJsonFiles(dirPath) {
121
+ if (!fs.existsSync(dirPath)) return [];
122
+ return fs.readdirSync(dirPath)
123
+ .filter(fileName => fileName.endsWith('.json'))
124
+ .map(fileName => path.join(dirPath, fileName));
125
+ }
126
+
127
+ function listGoals(filter, options = {}) {
128
+ const filePaths = listJsonFiles(getNamespaceDir('goals', options));
129
+
130
+ if (!options.disableLegacyLookup) {
131
+ const legacyDir = getLegacyDir('goals');
132
+ if (legacyDir && path.resolve(legacyDir) !== path.resolve(getNamespaceDir('goals', options))) {
133
+ filePaths.push(...listJsonFiles(legacyDir));
134
+ }
135
+ }
136
+
137
+ const seenGoalIds = new Set();
138
+ return filePaths
139
+ .map(filePath => {
140
+ try {
141
+ return readJson(filePath);
142
+ } catch {
143
+ return null;
144
+ }
145
+ })
146
+ .filter(goal => {
147
+ if (!goal || !goal.goalId || seenGoalIds.has(goal.goalId)) return false;
148
+ seenGoalIds.add(goal.goalId);
149
+ return true;
150
+ })
151
+ .filter(goal => !filter || goal.state === filter)
152
+ .sort((a, b) => new Date(b.updatedAt || 0) - new Date(a.updatedAt || 0));
153
+ }
154
+
155
+ function appendTriageItem(item, options = {}) {
156
+ const inboxPath = getTriageInboxPath(options);
157
+ ensureDir(path.dirname(inboxPath));
158
+ fs.appendFileSync(inboxPath, `${JSON.stringify(item)}\n`, 'utf8');
159
+ return inboxPath;
160
+ }
161
+
162
+ function saveHeartbeat(loopId, value, options = {}) {
163
+ return writeJson(getHeartbeatPath(loopId, options), value);
164
+ }
165
+
166
+ function loadHeartbeat(loopId = 'default', options = {}) {
167
+ const filePath = getHeartbeatPath(loopId, options);
168
+ if (fs.existsSync(filePath)) {
169
+ return readJson(filePath);
170
+ }
171
+
172
+ const legacyPath = getLegacyHeartbeatLastRunPath();
173
+ if (!options.disableLegacyLookup && loopId === 'last-run' && legacyPath && fs.existsSync(legacyPath)) {
174
+ return readJson(legacyPath);
175
+ }
176
+
177
+ return null;
178
+ }
179
+
180
+ function saveLoopMarkdownState(loopId, content, options = {}) {
181
+ const statePath = getLoopMarkdownStatePath(loopId, options);
182
+ ensureDir(path.dirname(statePath));
183
+ fs.writeFileSync(statePath, String(content), 'utf8');
184
+ return statePath;
185
+ }
186
+
187
+ function getLegacyDir(namespace) {
188
+ const parts = LEGACY_CLAUDE_DIRS[namespace];
189
+ if (!parts) return null;
190
+ return path.join(readHome(), ...parts);
191
+ }
192
+
193
+ function getLegacyGoalPath(goalId) {
194
+ const legacyDir = getLegacyDir('goals');
195
+ return legacyDir ? path.join(legacyDir, `${goalId}.json`) : null;
196
+ }
197
+
198
+ function getLegacyHeartbeatLastRunPath() {
199
+ return path.join(readHome(), ...LEGACY_CLAUDE_DIRS.heartbeatLastRun);
200
+ }
201
+
202
+ module.exports = {
203
+ TARGET_STATE_DIRS,
204
+ getLoopStateDir,
205
+ ensureLoopStateDir,
206
+ getNamespaceDir,
207
+ ensureNamespaceDir,
208
+ getGoalPath,
209
+ getTriageInboxPath,
210
+ getHeartbeatPath,
211
+ getLoopMarkdownStatePath,
212
+ saveGoal,
213
+ loadGoal,
214
+ listGoals,
215
+ appendTriageItem,
216
+ saveHeartbeat,
217
+ loadHeartbeat,
218
+ saveLoopMarkdownState,
219
+ targetDefaultStateDir,
220
+ projectLocalStateDir,
221
+ };
@@ -0,0 +1,193 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const DEFAULT_CONTEXT_LIMIT = 200000;
7
+ const EXTENDED_CONTEXT_LIMIT = 1000000;
8
+ const DEFAULT_TAIL_BYTES = 65536;
9
+ const EXPANDED_TAIL_BYTES = 262144;
10
+
11
+ function resolveContextLimit(modelId) {
12
+ const envLimit = Number(process.env.CLAUDE_CONTEXT_LIMIT);
13
+ if (Number.isFinite(envLimit) && envLimit > 0) return envLimit;
14
+
15
+ if (typeof modelId === 'string' && /\[1[mM]\]/i.test(modelId)) {
16
+ return EXTENDED_CONTEXT_LIMIT;
17
+ }
18
+
19
+ return DEFAULT_CONTEXT_LIMIT;
20
+ }
21
+
22
+ function readTailLines(filePath, tailBytes = DEFAULT_TAIL_BYTES) {
23
+ let fd;
24
+ try {
25
+ const stat = fs.statSync(filePath);
26
+ if (stat.size === 0) return [];
27
+
28
+ const readSize = Math.min(tailBytes, stat.size);
29
+ const buffer = Buffer.alloc(readSize);
30
+ fd = fs.openSync(filePath, 'r');
31
+ fs.readSync(fd, buffer, 0, readSize, stat.size - readSize);
32
+ fs.closeSync(fd);
33
+ fd = null;
34
+
35
+ const text = buffer.toString('utf8');
36
+ const lines = text.split('\n');
37
+
38
+ // Drop the first line if we started mid-file (likely partial)
39
+ if (stat.size > readSize && lines.length > 0) {
40
+ lines.shift();
41
+ }
42
+
43
+ return lines.filter(line => line.trim().length > 0);
44
+ } catch (_) {
45
+ if (fd != null) try { fs.closeSync(fd); } catch (__) { /* ignore */ }
46
+ return [];
47
+ }
48
+ }
49
+
50
+ function normalizeUsage(raw) {
51
+ if (!raw || typeof raw !== 'object') return null;
52
+
53
+ const inputTokens = Number(raw.input_tokens || raw.prompt_tokens || 0) || 0;
54
+ const outputTokens = Number(raw.output_tokens || raw.completion_tokens || 0) || 0;
55
+ const cacheCreationTokens = Number(raw.cache_creation_input_tokens || raw.cache_creation_prompt_tokens || 0) || 0;
56
+ const cacheReadTokens = Number(raw.cache_read_input_tokens || raw.cache_read_prompt_tokens || raw.cached_tokens || 0) || 0;
57
+ const totalTokens = Number(raw.total_tokens || 0) || 0;
58
+
59
+ // CCometixLine's context-window segment treats active context as prompt-side
60
+ // tokens: input + cache creation + cache read. The current turn's output is
61
+ // tracked separately because it becomes prompt-side context on the next turn.
62
+ const promptSideContextTokens = inputTokens + cacheCreationTokens + cacheReadTokens;
63
+ const contextTokens = promptSideContextTokens || totalTokens || (inputTokens + outputTokens);
64
+ if (contextTokens === 0) return null;
65
+
66
+ return {
67
+ inputTokens,
68
+ outputTokens,
69
+ cacheCreationTokens,
70
+ cacheReadTokens,
71
+ totalTokens,
72
+ contextTokens,
73
+ };
74
+ }
75
+
76
+ function parseTranscriptUsage(transcriptPath) {
77
+ if (!transcriptPath || typeof transcriptPath !== 'string') return null;
78
+ if (!fs.existsSync(transcriptPath)) return null;
79
+
80
+ let lines = readTailLines(transcriptPath, DEFAULT_TAIL_BYTES);
81
+
82
+ // Scan from bottom up for the last assistant message with usage
83
+ for (let i = lines.length - 1; i >= 0; i--) {
84
+ const line = lines[i].trim();
85
+ if (!line) continue;
86
+
87
+ let entry;
88
+ try {
89
+ entry = JSON.parse(line);
90
+ } catch (_) {
91
+ continue;
92
+ }
93
+
94
+ // Handle summary entries — they indicate compaction happened
95
+ if (entry.type === 'summary' && entry.leafUuid) {
96
+ const projectDir = path.dirname(transcriptPath);
97
+ const usage = findUsageByLeafUuid(entry.leafUuid, projectDir);
98
+ if (usage) return usage;
99
+ continue;
100
+ }
101
+
102
+ if (entry.type !== 'assistant') continue;
103
+ if (!entry.message || !entry.message.usage) continue;
104
+
105
+ return normalizeUsage(entry.message.usage);
106
+ }
107
+
108
+ // If not found in default tail, try expanded read
109
+ if (lines.length > 0) {
110
+ lines = readTailLines(transcriptPath, EXPANDED_TAIL_BYTES);
111
+ for (let i = lines.length - 1; i >= 0; i--) {
112
+ const line = lines[i].trim();
113
+ if (!line) continue;
114
+
115
+ let entry;
116
+ try {
117
+ entry = JSON.parse(line);
118
+ } catch (_) {
119
+ continue;
120
+ }
121
+
122
+ if (entry.type !== 'assistant') continue;
123
+ if (!entry.message || !entry.message.usage) continue;
124
+
125
+ return normalizeUsage(entry.message.usage);
126
+ }
127
+ }
128
+
129
+ return null;
130
+ }
131
+
132
+ function findUsageByLeafUuid(leafUuid, projectDir) {
133
+ if (!projectDir || !fs.existsSync(projectDir)) return null;
134
+
135
+ let sessionFiles;
136
+ try {
137
+ sessionFiles = fs.readdirSync(projectDir)
138
+ .filter(f => f.endsWith('.jsonl'))
139
+ .map(f => path.join(projectDir, f));
140
+ } catch (_) {
141
+ return null;
142
+ }
143
+
144
+ for (const filePath of sessionFiles) {
145
+ const lines = readTailLines(filePath, EXPANDED_TAIL_BYTES);
146
+ for (let i = lines.length - 1; i >= 0; i--) {
147
+ const line = lines[i].trim();
148
+ if (!line) continue;
149
+
150
+ let entry;
151
+ try {
152
+ entry = JSON.parse(line);
153
+ } catch (_) {
154
+ continue;
155
+ }
156
+
157
+ if (entry.uuid === leafUuid && entry.type === 'assistant' && entry.message?.usage) {
158
+ return normalizeUsage(entry.message.usage);
159
+ }
160
+ }
161
+ }
162
+
163
+ return null;
164
+ }
165
+
166
+ function resolveTranscriptMetrics(transcriptPath, modelId) {
167
+ const usage = parseTranscriptUsage(transcriptPath);
168
+ if (!usage) return null;
169
+
170
+ const contextLimit = resolveContextLimit(modelId);
171
+ const usagePct = Math.max(0, Math.min(100, Math.round((usage.contextTokens / contextLimit) * 100)));
172
+ const remainingTokens = Math.max(0, contextLimit - usage.contextTokens);
173
+ const remainingPct = Math.max(0, Math.min(100, Math.round((remainingTokens / contextLimit) * 100)));
174
+
175
+ return {
176
+ usagePct,
177
+ contextTokens: usage.contextTokens,
178
+ contextLimit,
179
+ remainingTokens,
180
+ remainingPct,
181
+ source: 'transcript_usage',
182
+ };
183
+ }
184
+
185
+ module.exports = {
186
+ resolveContextLimit,
187
+ readTailLines,
188
+ parseTranscriptUsage,
189
+ resolveTranscriptMetrics,
190
+ normalizeUsage,
191
+ DEFAULT_CONTEXT_LIMIT,
192
+ EXTENDED_CONTEXT_LIMIT,
193
+ };
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const os = require('os');
5
+
6
+ const CANGMING_HOME = path.join(os.homedir(), '.config', 'cangming');
7
+ const PLUGIN_DIR = path.join(CANGMING_HOME, 'plugins', 'team-skills-platform');
8
+
9
+ console.log('=== Cangming 安装验证 ===\n');
10
+
11
+ let passed = 0;
12
+ let failed = 0;
13
+
14
+ function check(description, condition) {
15
+ if (condition) {
16
+ console.log(`✅ ${description}`);
17
+ passed++;
18
+ } else {
19
+ console.log(`❌ ${description}`);
20
+ failed++;
21
+ }
22
+ }
23
+
24
+ console.log('📁 目录结构检查:');
25
+ check('CANGMING_HOME 目录存在', fs.existsSync(CANGMING_HOME));
26
+ check('AGENTS.md 文件存在', fs.existsSync(path.join(CANGMING_HOME, 'AGENTS.md')));
27
+ check('agents 目录存在', fs.existsSync(path.join(CANGMING_HOME, 'agents')));
28
+ check('command 目录存在', fs.existsSync(path.join(CANGMING_HOME, 'command')));
29
+ check('plugins 目录存在', fs.existsSync(path.join(CANGMING_HOME, 'plugins')));
30
+ check('team-skills-platform 插件存在', fs.existsSync(PLUGIN_DIR));
31
+
32
+ console.log('\n📄 文件内容检查:');
33
+
34
+ const agentsMdPath = path.join(CANGMING_HOME, 'AGENTS.md');
35
+ if (fs.existsSync(agentsMdPath)) {
36
+ const content = fs.readFileSync(agentsMdPath, 'utf8');
37
+ check('AGENTS.md 包含团队技能平台标记', content.includes('<!-- team-skills-platform -->'));
38
+ check('AGENTS.md 包含角色索引', content.includes('## 可用角色'));
39
+ check('AGENTS.md 包含命令索引', content.includes('## 核心团队命令'));
40
+ check('AGENTS.md 包含插件根路径', content.includes('## 插件根路径'));
41
+ }
42
+
43
+ console.log('\n👥 Agents 检查:');
44
+ const agentsDir = path.join(CANGMING_HOME, 'agents');
45
+ if (fs.existsSync(agentsDir)) {
46
+ const agentFiles = fs.readdirSync(agentsDir).filter(f => f.endsWith('.md'));
47
+ check(`agents 目录包含文件 (${agentFiles.length})`, agentFiles.length > 0);
48
+
49
+ const roleAgents = ['tech-lead.md', 'product-manager.md', 'architect.md', 'frontend-engineer.md',
50
+ 'backend-engineer.md', 'qa-engineer.md', 'devops-engineer.md'];
51
+ for (const agent of roleAgents) {
52
+ check(`角色 agent ${agent} 存在`, fs.existsSync(path.join(agentsDir, agent)));
53
+ }
54
+
55
+ const specialistAgents = agentFiles.filter(f => f.startsWith('specialist-'));
56
+ check(`specialist agents 存在 (${specialistAgents.length})`, specialistAgents.length > 0);
57
+ }
58
+
59
+ console.log('\n📝 Commands 检查:');
60
+ const commandsDir = path.join(CANGMING_HOME, 'command');
61
+ if (fs.existsSync(commandsDir)) {
62
+ const commandFiles = fs.readdirSync(commandsDir).filter(f => f.endsWith('.md'));
63
+ check(`commands 目录包含文件 (${commandFiles.length})`, commandFiles.length > 0);
64
+
65
+ const coreCommands = ['team-intake.md', 'team-plan.md', 'team-execute.md', 'team-review.md',
66
+ 'team-release.md', 'handoff.md'];
67
+ for (const cmd of coreCommands) {
68
+ check(`核心命令 ${cmd} 存在`, fs.existsSync(path.join(commandsDir, cmd)));
69
+ }
70
+ }
71
+
72
+ console.log('\n🎯 Skills 检查:');
73
+ const skillsDir = path.join(PLUGIN_DIR, 'skills');
74
+ if (fs.existsSync(skillsDir)) {
75
+ const skillDirs = fs.readdirSync(skillsDir, { withFileTypes: true })
76
+ .filter(d => d.isDirectory())
77
+ .map(d => d.name);
78
+ check(`skills 目录包含目录 (${skillDirs.length})`, skillDirs.length > 0);
79
+ }
80
+
81
+ console.log('\n📜 Rules 检查:');
82
+ const rulesDir = path.join(PLUGIN_DIR, 'rules');
83
+ if (fs.existsSync(rulesDir)) {
84
+ const ruleItems = fs.readdirSync(rulesDir);
85
+ check(`rules 目录包含内容 (${ruleItems.length})`, ruleItems.length > 0);
86
+ check('common 规则目录存在', fs.existsSync(path.join(rulesDir, 'common')));
87
+ }
88
+
89
+ // 总结
90
+ console.log('\n=== 测试总结 ===');
91
+ console.log(`通过: ${passed}`);
92
+ console.log(`失败: ${failed}`);
93
+ console.log(`总计: ${passed + failed}`);
94
+
95
+ if (failed === 0) {
96
+ console.log('\n🎉 所有测试通过!Cangming 安装成功。');
97
+ console.log('\n下一步:');
98
+ console.log('1. 启动 Cangming: cangming');
99
+ console.log('2. 查看可用角色: AGENTS.md 中包含所有角色索引');
100
+ console.log('3. 执行团队命令: /team-intake, /team-plan 等');
101
+ process.exit(0);
102
+ } else {
103
+ console.log('\n⚠️ 部分测试失败,请检查安装。');
104
+ process.exit(1);
105
+ }
@@ -12,7 +12,7 @@ origin: adapted from ECC
12
12
 
13
13
  ## 何时激活
14
14
 
15
- - 会话上下文超过 70% 使用率
15
+ - 会话上下文超过 65% 使用率
16
16
  - 需要重组上下文以保留关键信息
17
17
  - 长会话中需要压缩早期对话
18
18
  - 需要按重要性重新组织 specialist 输出
@@ -25,6 +25,7 @@ origin: adapted from ECC
25
25
 
26
26
  | Trigger | 条件 | 行为 |
27
27
  |---------|------|------|
28
+ | `context_65%` | 上下文使用 > 65% | 触发 advisory 压缩建议 |
28
29
  | `context_70%` | 上下文使用 > 70% | 触发压缩建议 |
29
30
  | `context_85%` | 上下文使用 > 85% | 强制压缩 |
30
31
  | `logical_break` | 检测到逻辑断点 | 建议整理 |
@@ -95,11 +96,19 @@ Specialist:输出详细代码审查报告
95
96
  系统:保存结论到 memory store
96
97
  ```
97
98
 
99
+ ## Context 计算与 Compact 轮次
100
+
101
+ - 运行时使用 `scripts/lib/context-window.js` 统一计算上下文压力。
102
+ - 优先消费 CCometixLine-compatible remaining context,例如 `ccometixline.context_window.remaining_percentage`、`ccometixline_context_window.remaining_tokens`、`TSP_CONTEXT_WINDOW_JSON` 或 `CCOMETIXLINE_CONTEXT_FILE`。
103
+ - 如果没有外部 remaining 信号,则退回 Claude `context_window`、transcript JSONL usage、bridge file 和 transcript size fallback。
104
+ - `scripts/hooks/pre-compact.js` 每次 PreCompact 会递增 `.tsp/context/compact-state.json` 的 session / total compact count;`suggest-compact.js` 输出 `compact_count`。
105
+
98
106
  ## 触发阈值
99
107
 
100
108
  | 使用率 | 紧迫度 | 建议操作 |
101
109
  |--------|--------|---------|
102
- | < 70% | low | 无需操作 |
110
+ | < 65% | low | 无需操作 |
111
+ | 65-70% | advisory | 提醒控制上下文增长 |
103
112
  | 70-85% | medium | 建议压缩,可选择性执行 |
104
113
  | 85-95% | high | 强烈建议压缩 |
105
114
  | > 95% | critical | 必须立即压缩 |