@colin4k1024/tsp 2.5.1 → 2.5.3
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/bin/lib/install-surface.js +5 -0
- package/commands/dashboard.md +105 -0
- package/commands/goal.md +142 -0
- package/commands/heartbeat.md +129 -0
- package/commands/triage.md +108 -0
- package/hooks/harness-context-monitor.js +4 -0
- package/hooks/harness-statusline.js +34 -11
- package/hooks/hooks.json +23 -23
- package/manifests/install-modules.json +98 -31
- package/package.json +2 -1
- package/schemas/goal.schema.json +172 -0
- package/scripts/hooks/session-start-goal-resume.js +95 -0
- package/scripts/hooks/suggest-compact.js +122 -19
- package/scripts/lib/blame-attribution.js +210 -0
- package/scripts/lib/completion-oracle.js +351 -0
- package/scripts/lib/heartbeat-scheduler.js +265 -0
- package/scripts/lib/install/request.js +1 -1
- package/scripts/lib/install-manifests.js +9 -1
- package/scripts/lib/install-targets/cangming-home.js +143 -0
- package/scripts/lib/install-targets/codewhale-home.js +187 -0
- package/scripts/lib/install-targets/registry.js +5 -1
- package/scripts/lib/transcript-usage.js +183 -0
- package/scripts/lib/wave-cost-advisor.js +155 -0
- package/scripts/test-cangming-install.js +105 -0
- package/skills/goal-convergence/SKILL.md +150 -0
- package/skills/loop-heartbeat/SKILL.md +120 -0
- package/skills/mcp-connector-bridge/SKILL.md +132 -0
- package/skills/rework-loop/SKILL.md +131 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
createInstallTargetAdapter,
|
|
6
|
+
createManagedOperation,
|
|
7
|
+
normalizeRelativePath,
|
|
8
|
+
} = require('./helpers');
|
|
9
|
+
|
|
10
|
+
const PLUGIN_NAME = require('../team-skills-data.json').plugin.name;
|
|
11
|
+
|
|
12
|
+
function addOperation(operations, seen, operation) {
|
|
13
|
+
const key = `${operation.sourceRelativePath}=>${operation.destinationPath}`;
|
|
14
|
+
if (seen.has(key)) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
seen.add(key);
|
|
18
|
+
operations.push(operation);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function addCopyOperation(operations, seen, moduleId, sourceRelativePath, destinationPath, strategy = 'preserve-relative-path') {
|
|
22
|
+
addOperation(operations, seen, createManagedOperation({
|
|
23
|
+
moduleId,
|
|
24
|
+
sourceRelativePath,
|
|
25
|
+
destinationPath,
|
|
26
|
+
strategy,
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sourceDir(input, sourceRelativePath) {
|
|
31
|
+
return path.join(input.repoRoot || '', normalizeRelativePath(sourceRelativePath));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function rootAgentFileName(sourcePath, fileName) {
|
|
35
|
+
const normalizedSourcePath = normalizeRelativePath(sourcePath);
|
|
36
|
+
if (normalizedSourcePath === 'agents/specialists' || normalizedSourcePath.startsWith('agents/specialists/')) {
|
|
37
|
+
return `specialist-${fileName}`;
|
|
38
|
+
}
|
|
39
|
+
return fileName;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function addRootAgentOperations(operations, seen, moduleId, sourceRelativePath, input, targetRoot) {
|
|
43
|
+
const normalizedSourcePath = normalizeRelativePath(sourceRelativePath);
|
|
44
|
+
if (normalizedSourcePath !== 'agents' && !normalizedSourcePath.startsWith('agents/')) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const agentDirs = normalizedSourcePath === 'agents'
|
|
49
|
+
? ['roles', 'specialists']
|
|
50
|
+
: [''];
|
|
51
|
+
|
|
52
|
+
for (const agentDir of agentDirs) {
|
|
53
|
+
const sourcePath = agentDir ? path.join(normalizedSourcePath, agentDir) : normalizedSourcePath;
|
|
54
|
+
const absoluteSourceDir = sourceDir(input, sourcePath);
|
|
55
|
+
if (!input.repoRoot || !fs.existsSync(absoluteSourceDir)) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const stat = fs.statSync(absoluteSourceDir);
|
|
60
|
+
if (stat.isFile() && path.extname(absoluteSourceDir) === '.md') {
|
|
61
|
+
const fileName = rootAgentFileName(path.dirname(sourcePath), path.basename(sourcePath));
|
|
62
|
+
addCopyOperation(
|
|
63
|
+
operations,
|
|
64
|
+
seen,
|
|
65
|
+
moduleId,
|
|
66
|
+
sourcePath,
|
|
67
|
+
path.join(targetRoot, 'agents', fileName),
|
|
68
|
+
'flatten-agent-copy'
|
|
69
|
+
);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!stat.isDirectory()) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const entry of fs.readdirSync(absoluteSourceDir, { withFileTypes: true }).sort((left, right) => (
|
|
78
|
+
left.name.localeCompare(right.name)
|
|
79
|
+
))) {
|
|
80
|
+
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
81
|
+
const fileName = rootAgentFileName(sourcePath, entry.name);
|
|
82
|
+
addCopyOperation(
|
|
83
|
+
operations,
|
|
84
|
+
seen,
|
|
85
|
+
moduleId,
|
|
86
|
+
path.join(sourcePath, entry.name),
|
|
87
|
+
path.join(targetRoot, 'agents', fileName),
|
|
88
|
+
'flatten-agent-copy'
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function planCangmingOperations(input, adapter) {
|
|
96
|
+
const targetRoot = adapter.resolveRoot(input);
|
|
97
|
+
const pluginRoot = path.join(targetRoot, 'plugins', PLUGIN_NAME);
|
|
98
|
+
const operations = [];
|
|
99
|
+
const seen = new Set();
|
|
100
|
+
|
|
101
|
+
for (const module of Array.isArray(input.modules) ? input.modules : []) {
|
|
102
|
+
for (const rawSourcePath of Array.isArray(module.paths) ? module.paths : []) {
|
|
103
|
+
const sourceRelativePath = normalizeRelativePath(rawSourcePath);
|
|
104
|
+
|
|
105
|
+
addCopyOperation(
|
|
106
|
+
operations,
|
|
107
|
+
seen,
|
|
108
|
+
module.id,
|
|
109
|
+
sourceRelativePath,
|
|
110
|
+
path.join(pluginRoot, sourceRelativePath),
|
|
111
|
+
'plugin-copy'
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
if (sourceRelativePath === 'commands' || sourceRelativePath.startsWith('commands/')) {
|
|
115
|
+
const commandSuffix = sourceRelativePath === 'commands'
|
|
116
|
+
? ''
|
|
117
|
+
: sourceRelativePath.slice('commands/'.length);
|
|
118
|
+
addCopyOperation(
|
|
119
|
+
operations,
|
|
120
|
+
seen,
|
|
121
|
+
module.id,
|
|
122
|
+
sourceRelativePath,
|
|
123
|
+
path.join(targetRoot, 'command', commandSuffix),
|
|
124
|
+
'opencode-command-copy'
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
addRootAgentOperations(operations, seen, module.id, sourceRelativePath, input, targetRoot);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return operations;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = createInstallTargetAdapter({
|
|
136
|
+
id: 'cangming-home',
|
|
137
|
+
target: 'cangming',
|
|
138
|
+
kind: 'home',
|
|
139
|
+
rootSegments: ['.config', 'cangming'],
|
|
140
|
+
installStatePathSegments: ['ecc-install-state.json'],
|
|
141
|
+
nativeRootRelativePath: '.cangming',
|
|
142
|
+
planOperations: planCangmingOperations,
|
|
143
|
+
});
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
createInstallTargetAdapter,
|
|
6
|
+
createManagedOperation,
|
|
7
|
+
normalizeRelativePath,
|
|
8
|
+
} = require('./helpers');
|
|
9
|
+
|
|
10
|
+
const PLUGIN_NAME = require('../team-skills-data.json').plugin.name;
|
|
11
|
+
|
|
12
|
+
function addOperation(operations, seen, operation) {
|
|
13
|
+
const key = `${operation.sourceRelativePath}=>${operation.destinationPath}`;
|
|
14
|
+
if (seen.has(key)) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
seen.add(key);
|
|
18
|
+
operations.push(operation);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function addCopyOperation(operations, seen, moduleId, sourceRelativePath, destinationPath, strategy = 'preserve-relative-path') {
|
|
22
|
+
addOperation(operations, seen, createManagedOperation({
|
|
23
|
+
moduleId,
|
|
24
|
+
sourceRelativePath,
|
|
25
|
+
destinationPath,
|
|
26
|
+
strategy,
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sourceDir(input, sourceRelativePath) {
|
|
31
|
+
return path.join(input.repoRoot || '', normalizeRelativePath(sourceRelativePath));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function addAgentOperations(operations, seen, moduleId, sourceRelativePath, input, targetRoot) {
|
|
35
|
+
const normalizedSourcePath = normalizeRelativePath(sourceRelativePath);
|
|
36
|
+
if (normalizedSourcePath !== 'agents' && !normalizedSourcePath.startsWith('agents/')) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const agentDirs = normalizedSourcePath === 'agents'
|
|
41
|
+
? ['roles', 'specialists']
|
|
42
|
+
: [''];
|
|
43
|
+
|
|
44
|
+
for (const agentDir of agentDirs) {
|
|
45
|
+
const sourcePath = agentDir ? path.join(normalizedSourcePath, agentDir) : normalizedSourcePath;
|
|
46
|
+
const absoluteSourceDir = sourceDir(input, sourcePath);
|
|
47
|
+
if (!input.repoRoot || !fs.existsSync(absoluteSourceDir)) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const stat = fs.statSync(absoluteSourceDir);
|
|
52
|
+
if (stat.isFile() && path.extname(absoluteSourceDir) === '.md') {
|
|
53
|
+
const fileName = path.basename(sourcePath);
|
|
54
|
+
addCopyOperation(
|
|
55
|
+
operations, seen, moduleId, sourcePath,
|
|
56
|
+
path.join(targetRoot, 'agents', fileName),
|
|
57
|
+
'flatten-agent-copy'
|
|
58
|
+
);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!stat.isDirectory()) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const entry of fs.readdirSync(absoluteSourceDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
67
|
+
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
68
|
+
const prefix = agentDir === 'specialists' ? 'specialist-' : '';
|
|
69
|
+
addCopyOperation(
|
|
70
|
+
operations, seen, moduleId,
|
|
71
|
+
path.join(sourcePath, entry.name),
|
|
72
|
+
path.join(targetRoot, 'agents', `${prefix}${entry.name}`),
|
|
73
|
+
'flatten-agent-copy'
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function addRuleOperations(operations, seen, moduleId, sourceRelativePath, input, targetRoot) {
|
|
81
|
+
const normalizedSourcePath = normalizeRelativePath(sourceRelativePath);
|
|
82
|
+
if (normalizedSourcePath !== 'rules' && !normalizedSourcePath.startsWith('rules/')) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const absoluteSourceDir = sourceDir(input, normalizedSourcePath);
|
|
87
|
+
if (!input.repoRoot || !fs.existsSync(absoluteSourceDir) || !fs.statSync(absoluteSourceDir).isDirectory()) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const entries = fs.readdirSync(absoluteSourceDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
const entryPath = path.join(absoluteSourceDir, entry.name);
|
|
94
|
+
if (entry.isDirectory()) {
|
|
95
|
+
const subEntries = fs.readdirSync(entryPath, { withFileTypes: true });
|
|
96
|
+
for (const sub of subEntries) {
|
|
97
|
+
if (sub.isFile() && sub.name.endsWith('.md')) {
|
|
98
|
+
addCopyOperation(
|
|
99
|
+
operations, seen, moduleId,
|
|
100
|
+
path.join(normalizedSourcePath, entry.name, sub.name),
|
|
101
|
+
path.join(targetRoot, 'rules', entry.name, sub.name),
|
|
102
|
+
'rule-copy'
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
107
|
+
addCopyOperation(
|
|
108
|
+
operations, seen, moduleId,
|
|
109
|
+
path.join(normalizedSourcePath, entry.name),
|
|
110
|
+
path.join(targetRoot, 'rules', entry.name),
|
|
111
|
+
'rule-copy'
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function planCodeWhaleOperations(input, adapter) {
|
|
118
|
+
const targetRoot = adapter.resolveRoot(input);
|
|
119
|
+
const operations = [];
|
|
120
|
+
const seen = new Set();
|
|
121
|
+
|
|
122
|
+
for (const module of Array.isArray(input.modules) ? input.modules : []) {
|
|
123
|
+
for (const rawSourcePath of Array.isArray(module.paths) ? module.paths : []) {
|
|
124
|
+
const sourceRelativePath = normalizeRelativePath(rawSourcePath);
|
|
125
|
+
|
|
126
|
+
// Skills: preserve directory structure
|
|
127
|
+
if (sourceRelativePath === 'skills' || sourceRelativePath.startsWith('skills/')) {
|
|
128
|
+
addCopyOperation(
|
|
129
|
+
operations, seen, module.id, sourceRelativePath,
|
|
130
|
+
path.join(targetRoot, 'skills', sourceRelativePath.replace(/^skills\/?/, '')),
|
|
131
|
+
'skill-copy'
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Commands: flatten to commands/
|
|
136
|
+
if (sourceRelativePath === 'commands' || sourceRelativePath.startsWith('commands/')) {
|
|
137
|
+
const commandSuffix = sourceRelativePath === 'commands'
|
|
138
|
+
? ''
|
|
139
|
+
: sourceRelativePath.slice('commands/'.length);
|
|
140
|
+
addCopyOperation(
|
|
141
|
+
operations, seen, module.id, sourceRelativePath,
|
|
142
|
+
path.join(targetRoot, 'commands', commandSuffix),
|
|
143
|
+
'command-copy'
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Agents: flatten roles + specialists
|
|
148
|
+
addAgentOperations(operations, seen, module.id, sourceRelativePath, input, targetRoot);
|
|
149
|
+
|
|
150
|
+
// Rules: preserve namespace/file structure
|
|
151
|
+
addRuleOperations(operations, seen, module.id, sourceRelativePath, input, targetRoot);
|
|
152
|
+
|
|
153
|
+
// Contexts: direct copy
|
|
154
|
+
if (sourceRelativePath === 'contexts' || sourceRelativePath.startsWith('contexts/')) {
|
|
155
|
+
const contextSuffix = sourceRelativePath === 'contexts'
|
|
156
|
+
? ''
|
|
157
|
+
: sourceRelativePath.slice('contexts/'.length);
|
|
158
|
+
addCopyOperation(
|
|
159
|
+
operations, seen, module.id, sourceRelativePath,
|
|
160
|
+
path.join(targetRoot, 'contexts', contextSuffix),
|
|
161
|
+
'context-copy'
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Hooks: copy hooks directory for reference, config.toml injection handled by post-install
|
|
166
|
+
if (sourceRelativePath === 'hooks' || sourceRelativePath.startsWith('hooks/')) {
|
|
167
|
+
addCopyOperation(
|
|
168
|
+
operations, seen, module.id, sourceRelativePath,
|
|
169
|
+
path.join(targetRoot, 'hooks', sourceRelativePath.replace(/^hooks\/?/, '')),
|
|
170
|
+
'hook-copy'
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return operations;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = createInstallTargetAdapter({
|
|
180
|
+
id: 'codewhale-home',
|
|
181
|
+
target: 'codewhale',
|
|
182
|
+
kind: 'home',
|
|
183
|
+
rootSegments: ['.codewhale'],
|
|
184
|
+
installStatePathSegments: ['ecc-install-state.json'],
|
|
185
|
+
nativeRootRelativePath: '.codewhale',
|
|
186
|
+
planOperations: planCodeWhaleOperations,
|
|
187
|
+
});
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
const antigravityProject = require('./antigravity-project');
|
|
2
2
|
const augmentProject = require('./augment-project');
|
|
3
|
+
const cangmingHome = require('./cangming-home');
|
|
3
4
|
const claudeHome = require('./claude-home');
|
|
4
5
|
const codebuddyProject = require('./codebuddy-project');
|
|
5
6
|
const codexHome = require('./codex-home');
|
|
7
|
+
const codewhaleHome = require('./codewhale-home');
|
|
6
8
|
const copilotHome = require('./copilot-home');
|
|
7
9
|
const cursorProject = require('./cursor-project');
|
|
8
10
|
const geminiProject = require('./gemini-project');
|
|
9
11
|
const opencodeHome = require('./opencode-home');
|
|
10
12
|
const windsurfProject = require('./windsurf-project');
|
|
11
13
|
|
|
12
|
-
const PUBLIC_INSTALL_TARGETS = Object.freeze(['claude', 'codex', 'opencode']);
|
|
14
|
+
const PUBLIC_INSTALL_TARGETS = Object.freeze(['claude', 'codex', 'opencode', 'cangming', 'codewhale']);
|
|
13
15
|
const TARGET_ALIASES = Object.freeze({
|
|
14
16
|
'claude-code': 'claude',
|
|
15
17
|
claudecode: 'claude',
|
|
@@ -22,6 +24,8 @@ const ADAPTERS = Object.freeze([
|
|
|
22
24
|
codexHome,
|
|
23
25
|
geminiProject,
|
|
24
26
|
opencodeHome,
|
|
27
|
+
cangmingHome,
|
|
28
|
+
codewhaleHome,
|
|
25
29
|
codebuddyProject,
|
|
26
30
|
copilotHome,
|
|
27
31
|
windsurfProject,
|
|
@@ -0,0 +1,183 @@
|
|
|
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
|
+
|
|
58
|
+
const contextTokens = inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens;
|
|
59
|
+
if (contextTokens === 0) return null;
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
inputTokens,
|
|
63
|
+
outputTokens,
|
|
64
|
+
cacheCreationTokens,
|
|
65
|
+
cacheReadTokens,
|
|
66
|
+
contextTokens,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parseTranscriptUsage(transcriptPath) {
|
|
71
|
+
if (!transcriptPath || typeof transcriptPath !== 'string') return null;
|
|
72
|
+
if (!fs.existsSync(transcriptPath)) return null;
|
|
73
|
+
|
|
74
|
+
let lines = readTailLines(transcriptPath, DEFAULT_TAIL_BYTES);
|
|
75
|
+
|
|
76
|
+
// Scan from bottom up for the last assistant message with usage
|
|
77
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
78
|
+
const line = lines[i].trim();
|
|
79
|
+
if (!line) continue;
|
|
80
|
+
|
|
81
|
+
let entry;
|
|
82
|
+
try {
|
|
83
|
+
entry = JSON.parse(line);
|
|
84
|
+
} catch (_) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Handle summary entries — they indicate compaction happened
|
|
89
|
+
if (entry.type === 'summary' && entry.leafUuid) {
|
|
90
|
+
const projectDir = path.dirname(transcriptPath);
|
|
91
|
+
const usage = findUsageByLeafUuid(entry.leafUuid, projectDir);
|
|
92
|
+
if (usage) return usage;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (entry.type !== 'assistant') continue;
|
|
97
|
+
if (!entry.message || !entry.message.usage) continue;
|
|
98
|
+
|
|
99
|
+
return normalizeUsage(entry.message.usage);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// If not found in default tail, try expanded read
|
|
103
|
+
if (lines.length > 0) {
|
|
104
|
+
lines = readTailLines(transcriptPath, EXPANDED_TAIL_BYTES);
|
|
105
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
106
|
+
const line = lines[i].trim();
|
|
107
|
+
if (!line) continue;
|
|
108
|
+
|
|
109
|
+
let entry;
|
|
110
|
+
try {
|
|
111
|
+
entry = JSON.parse(line);
|
|
112
|
+
} catch (_) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (entry.type !== 'assistant') continue;
|
|
117
|
+
if (!entry.message || !entry.message.usage) continue;
|
|
118
|
+
|
|
119
|
+
return normalizeUsage(entry.message.usage);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function findUsageByLeafUuid(leafUuid, projectDir) {
|
|
127
|
+
if (!projectDir || !fs.existsSync(projectDir)) return null;
|
|
128
|
+
|
|
129
|
+
let sessionFiles;
|
|
130
|
+
try {
|
|
131
|
+
sessionFiles = fs.readdirSync(projectDir)
|
|
132
|
+
.filter(f => f.endsWith('.jsonl'))
|
|
133
|
+
.map(f => path.join(projectDir, f));
|
|
134
|
+
} catch (_) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (const filePath of sessionFiles) {
|
|
139
|
+
const lines = readTailLines(filePath, EXPANDED_TAIL_BYTES);
|
|
140
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
141
|
+
const line = lines[i].trim();
|
|
142
|
+
if (!line) continue;
|
|
143
|
+
|
|
144
|
+
let entry;
|
|
145
|
+
try {
|
|
146
|
+
entry = JSON.parse(line);
|
|
147
|
+
} catch (_) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (entry.uuid === leafUuid && entry.type === 'assistant' && entry.message?.usage) {
|
|
152
|
+
return normalizeUsage(entry.message.usage);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function resolveTranscriptMetrics(transcriptPath, modelId) {
|
|
161
|
+
const usage = parseTranscriptUsage(transcriptPath);
|
|
162
|
+
if (!usage) return null;
|
|
163
|
+
|
|
164
|
+
const contextLimit = resolveContextLimit(modelId);
|
|
165
|
+
const usagePct = Math.max(0, Math.min(100, Math.round((usage.contextTokens / contextLimit) * 100)));
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
usagePct,
|
|
169
|
+
contextTokens: usage.contextTokens,
|
|
170
|
+
contextLimit,
|
|
171
|
+
source: 'transcript_usage',
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = {
|
|
176
|
+
resolveContextLimit,
|
|
177
|
+
readTailLines,
|
|
178
|
+
parseTranscriptUsage,
|
|
179
|
+
resolveTranscriptMetrics,
|
|
180
|
+
normalizeUsage,
|
|
181
|
+
DEFAULT_CONTEXT_LIMIT,
|
|
182
|
+
EXTENDED_CONTEXT_LIMIT,
|
|
183
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* wave-cost-advisor.js
|
|
5
|
+
*
|
|
6
|
+
* Budget-aware wave scheduling for Loop Engineering.
|
|
7
|
+
* Calculates optimal parallelism based on remaining budget, estimated costs,
|
|
8
|
+
* and worker availability before launching a wave.
|
|
9
|
+
*
|
|
10
|
+
* Integrates with wave-execution skill and the workflow executor.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const DEFAULT_COST_PER_WORKER = 0.50; // USD per worker iteration (conservative estimate)
|
|
14
|
+
const DEFAULT_MAX_PARALLEL = 4;
|
|
15
|
+
const DEFAULT_BUDGET = 10.0; // USD
|
|
16
|
+
|
|
17
|
+
function createCostAdvisor(options = {}) {
|
|
18
|
+
const totalBudget = options.totalBudget || DEFAULT_BUDGET;
|
|
19
|
+
let spent = options.spent || 0;
|
|
20
|
+
const maxParallel = options.maxParallel || DEFAULT_MAX_PARALLEL;
|
|
21
|
+
const costPerWorker = options.costPerWorker || DEFAULT_COST_PER_WORKER;
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
get remaining() { return totalBudget - spent; },
|
|
25
|
+
get spent() { return spent; },
|
|
26
|
+
get totalBudget() { return totalBudget; },
|
|
27
|
+
|
|
28
|
+
recordSpend(amount) {
|
|
29
|
+
spent += amount;
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
adviseBatchSize(taskCount, options = {}) {
|
|
33
|
+
const estimatedIterations = options.estimatedIterations || 1;
|
|
34
|
+
const taskCost = options.costPerTask || costPerWorker;
|
|
35
|
+
const maxWorkers = Math.min(taskCount, maxParallel);
|
|
36
|
+
|
|
37
|
+
if (spent >= totalBudget) {
|
|
38
|
+
return {
|
|
39
|
+
recommended: 0,
|
|
40
|
+
reason: 'budget_exhausted',
|
|
41
|
+
remaining: 0,
|
|
42
|
+
canAfford: 0,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const remaining = totalBudget - spent;
|
|
47
|
+
const costPerBatch = maxWorkers * taskCost * estimatedIterations;
|
|
48
|
+
|
|
49
|
+
if (costPerBatch <= remaining) {
|
|
50
|
+
return {
|
|
51
|
+
recommended: maxWorkers,
|
|
52
|
+
reason: 'within_budget',
|
|
53
|
+
remaining,
|
|
54
|
+
estimatedCost: costPerBatch,
|
|
55
|
+
canAfford: maxWorkers,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Reduce parallelism to fit budget
|
|
60
|
+
const affordableWorkers = Math.max(1, Math.floor(remaining / (taskCost * estimatedIterations)));
|
|
61
|
+
const actualWorkers = Math.min(affordableWorkers, taskCount);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
recommended: actualWorkers,
|
|
65
|
+
reason: 'budget_constrained',
|
|
66
|
+
remaining,
|
|
67
|
+
estimatedCost: actualWorkers * taskCost * estimatedIterations,
|
|
68
|
+
canAfford: affordableWorkers,
|
|
69
|
+
requestedButDenied: maxWorkers - actualWorkers,
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
adviseWavePlan(waves, options = {}) {
|
|
74
|
+
const taskCost = options.costPerTask || costPerWorker;
|
|
75
|
+
const plan = [];
|
|
76
|
+
let projectedSpend = spent;
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < waves.length; i++) {
|
|
79
|
+
const wave = waves[i];
|
|
80
|
+
const taskCount = Array.isArray(wave) ? wave.length : wave.taskCount || 1;
|
|
81
|
+
const remaining = totalBudget - projectedSpend;
|
|
82
|
+
|
|
83
|
+
if (remaining <= 0) {
|
|
84
|
+
plan.push({
|
|
85
|
+
waveIndex: i,
|
|
86
|
+
status: 'deferred',
|
|
87
|
+
reason: 'budget_exhausted',
|
|
88
|
+
tasks: taskCount,
|
|
89
|
+
parallelism: 0,
|
|
90
|
+
});
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const maxForWave = Math.min(taskCount, maxParallel);
|
|
95
|
+
const waveCost = maxForWave * taskCost;
|
|
96
|
+
|
|
97
|
+
if (waveCost <= remaining) {
|
|
98
|
+
plan.push({
|
|
99
|
+
waveIndex: i,
|
|
100
|
+
status: 'full',
|
|
101
|
+
tasks: taskCount,
|
|
102
|
+
parallelism: maxForWave,
|
|
103
|
+
estimatedCost: waveCost,
|
|
104
|
+
});
|
|
105
|
+
projectedSpend += waveCost;
|
|
106
|
+
} else {
|
|
107
|
+
const affordable = Math.max(1, Math.floor(remaining / taskCost));
|
|
108
|
+
const actual = Math.min(affordable, taskCount);
|
|
109
|
+
plan.push({
|
|
110
|
+
waveIndex: i,
|
|
111
|
+
status: 'reduced',
|
|
112
|
+
tasks: taskCount,
|
|
113
|
+
parallelism: actual,
|
|
114
|
+
estimatedCost: actual * taskCost,
|
|
115
|
+
note: `Reduced from ${maxForWave} to ${actual} workers due to budget`,
|
|
116
|
+
});
|
|
117
|
+
projectedSpend += actual * taskCost;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
plan,
|
|
123
|
+
projectedTotalCost: projectedSpend,
|
|
124
|
+
remainingAfterPlan: totalBudget - projectedSpend,
|
|
125
|
+
budgetUtilization: projectedSpend / totalBudget,
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
shouldPause() {
|
|
130
|
+
return spent >= totalBudget;
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
formatSummary() {
|
|
134
|
+
const pct = ((spent / totalBudget) * 100).toFixed(1);
|
|
135
|
+
return `Budget: $${spent.toFixed(2)} / $${totalBudget.toFixed(2)} (${pct}% used), $${(totalBudget - spent).toFixed(2)} remaining`;
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function estimateGoalCost(goal, averageCostPerIteration) {
|
|
141
|
+
const remainingIterations = (goal.budget?.maxIterations || 15) - (goal.currentIteration || 0);
|
|
142
|
+
const avgCost = averageCostPerIteration || DEFAULT_COST_PER_WORKER;
|
|
143
|
+
return {
|
|
144
|
+
bestCase: avgCost,
|
|
145
|
+
expectedCase: avgCost * Math.min(3, remainingIterations),
|
|
146
|
+
worstCase: avgCost * remainingIterations,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = {
|
|
151
|
+
DEFAULT_COST_PER_WORKER,
|
|
152
|
+
DEFAULT_MAX_PARALLEL,
|
|
153
|
+
createCostAdvisor,
|
|
154
|
+
estimateGoalCost,
|
|
155
|
+
};
|