@colin4k1024/tsp 2.5.3 → 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.
- package/commands/goal.md +7 -1
- package/commands/heartbeat.md +34 -1
- package/commands/loop-start.md +34 -6
- package/commands/triage.md +8 -1
- package/hooks/README.md +2 -2
- package/hooks/harness-statusline.js +9 -30
- package/hooks/strategic-compact/README.md +11 -12
- package/manifests/install-components.json +40 -0
- package/manifests/install-modules.json +43 -0
- package/manifests/install-profiles.json +2 -0
- package/package.json +1 -1
- package/schemas/loop-spec.schema.json +124 -0
- package/scripts/hooks/pre-compact.js +39 -8
- package/scripts/hooks/session-start-goal-resume.js +3 -20
- package/scripts/hooks/suggest-compact.js +9 -115
- package/scripts/lib/completion-oracle.js +4 -27
- package/scripts/lib/context-window-state.js +129 -0
- package/scripts/lib/context-window.js +294 -0
- package/scripts/lib/heartbeat-scheduler.js +40 -25
- package/scripts/lib/install-targets/registry.js +1 -1
- package/scripts/lib/loop-oracle.js +5 -0
- package/scripts/lib/loop-spec.js +168 -0
- package/scripts/lib/loop-state-store.js +221 -0
- package/scripts/lib/transcript-usage.js +11 -1
- package/skills/goframe-v2/examples/practices/quick-demo/manifest/config/config.yaml +14 -14
- package/skills/repo-scan/SKILL.md +63 -63
- package/skills/strategic-compact/SKILL.md +11 -2
- package/scripts/__pycache__/__init__.cpython-311.pyc +0 -0
- package/scripts/__pycache__/build_platform_artifacts.cpython-311.pyc +0 -0
- package/scripts/__pycache__/install_platform.cpython-311.pyc +0 -0
- package/scripts/__pycache__/langfuse_trace.cpython-311.pyc +0 -0
- package/scripts/__pycache__/query_audit_logs.cpython-311.pyc +0 -0
- package/scripts/__pycache__/scan_leaked_keys.cpython-311.pyc +0 -0
- package/scripts/__pycache__/team_skills_platform.cpython-311.pyc +0 -0
- package/scripts/__pycache__/team_skills_platform.cpython-313.pyc +0 -0
- package/scripts/__pycache__/validate_library.cpython-311.pyc +0 -0
- package/scripts/__pycache__/validate_workflow_state.cpython-311.pyc +0 -0
- package/scripts/evolution/__pycache__/__init__.cpython-311.pyc +0 -0
- package/scripts/evolution/__pycache__/store.cpython-311.pyc +0 -0
- package/scripts/hooks/__pycache__/__init__.cpython-311.pyc +0 -0
- package/scripts/hooks/__pycache__/mcp_health_check.cpython-311.pyc +0 -0
- package/scripts/hooks/__pycache__/observe.cpython-311.pyc +0 -0
- package/scripts/hooks/__pycache__/session_end.cpython-311.pyc +0 -0
- package/scripts/hooks/__pycache__/session_start.cpython-311.pyc +0 -0
- package/scripts/lib/__pycache__/audit_logger.cpython-311.pyc +0 -0
- package/scripts/lib/__pycache__/audit_query.cpython-311.pyc +0 -0
- package/scripts/lib/__pycache__/hook_contract.cpython-311.pyc +0 -0
- package/scripts/lib/__pycache__/memory_store.cpython-311.pyc +0 -0
- package/scripts/lib/__pycache__/utils.cpython-311.pyc +0 -0
|
@@ -14,9 +14,11 @@
|
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const path = require('path');
|
|
16
16
|
const { execSync } = require('child_process');
|
|
17
|
-
const yaml = require
|
|
17
|
+
const yaml = require('js-yaml');
|
|
18
18
|
|
|
19
19
|
const { createGoal, saveGoal } = require('./completion-oracle');
|
|
20
|
+
const loopStateStore = require('./loop-state-store');
|
|
21
|
+
const { parseLoopSpecContent } = require('./loop-spec');
|
|
20
22
|
|
|
21
23
|
const DEFAULT_CONFIG = {
|
|
22
24
|
interval: '30m',
|
|
@@ -75,12 +77,40 @@ function parseSimpleYaml(content) {
|
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
function loadConfig(projectRoot) {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
+
const root = projectRoot || process.cwd();
|
|
81
|
+
const loopSpecPath = path.join(root, '.tsp', 'loop.yaml');
|
|
82
|
+
if (fs.existsSync(loopSpecPath)) {
|
|
83
|
+
try {
|
|
84
|
+
const loopSpec = parseLoopSpecContent(fs.readFileSync(loopSpecPath, 'utf-8'), loopSpecPath);
|
|
85
|
+
return {
|
|
86
|
+
...DEFAULT_CONFIG,
|
|
87
|
+
interval: loopSpec.cadence,
|
|
88
|
+
scans: loopSpec.gates.map(gate => ({
|
|
89
|
+
name: gate.name,
|
|
90
|
+
command: `${gate.command} 2>&1; echo EXIT:$?`,
|
|
91
|
+
onFailure: SCAN_ACTIONS.autoGoal,
|
|
92
|
+
description: gate.description || gate.name,
|
|
93
|
+
})),
|
|
94
|
+
budget: {
|
|
95
|
+
...DEFAULT_CONFIG.budget,
|
|
96
|
+
maxDollarsPerHour: loopSpec.budget.maxDollars,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
} catch {
|
|
100
|
+
return DEFAULT_CONFIG;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const configPath = [
|
|
105
|
+
path.join(root, '.tsp', 'heartbeat.yaml'),
|
|
106
|
+
path.join(root, '.claude', 'heartbeat.yaml'),
|
|
107
|
+
].find(candidate => fs.existsSync(candidate));
|
|
108
|
+
|
|
109
|
+
if (!configPath) return DEFAULT_CONFIG;
|
|
80
110
|
|
|
81
111
|
try {
|
|
82
112
|
const content = fs.readFileSync(configPath, 'utf-8');
|
|
83
|
-
const parsed = parseSimpleYaml(content);
|
|
113
|
+
const parsed = yaml.load(content, { schema: yaml.JSON_SCHEMA }) || parseSimpleYaml(content);
|
|
84
114
|
return { ...DEFAULT_CONFIG, ...parsed.heartbeat };
|
|
85
115
|
} catch {
|
|
86
116
|
return DEFAULT_CONFIG;
|
|
@@ -147,14 +177,7 @@ function createTriageItem(scan, result) {
|
|
|
147
177
|
}
|
|
148
178
|
|
|
149
179
|
function appendToTriageInbox(item) {
|
|
150
|
-
|
|
151
|
-
const inboxDir = path.join(home, '.claude', 'triage');
|
|
152
|
-
if (!fs.existsSync(inboxDir)) {
|
|
153
|
-
fs.mkdirSync(inboxDir, { recursive: true });
|
|
154
|
-
}
|
|
155
|
-
const inboxPath = path.join(inboxDir, 'inbox.jsonl');
|
|
156
|
-
fs.appendFileSync(inboxPath, JSON.stringify(item) + '\n', 'utf-8');
|
|
157
|
-
return inboxPath;
|
|
180
|
+
return loopStateStore.appendTriageItem(item);
|
|
158
181
|
}
|
|
159
182
|
|
|
160
183
|
function createGoalFromScanFailure(scan, result) {
|
|
@@ -181,7 +204,7 @@ function runHeartbeat(projectRoot) {
|
|
|
181
204
|
if (config.scans.length === 0) {
|
|
182
205
|
return {
|
|
183
206
|
status: 'no_scans',
|
|
184
|
-
message: 'No scans configured in .claude/heartbeat.yaml',
|
|
207
|
+
message: 'No scans configured in .tsp/loop.yaml, .tsp/heartbeat.yaml, or .claude/heartbeat.yaml',
|
|
185
208
|
results: [],
|
|
186
209
|
};
|
|
187
210
|
}
|
|
@@ -216,14 +239,10 @@ function runHeartbeat(projectRoot) {
|
|
|
216
239
|
function getHeartbeatStatus(projectRoot) {
|
|
217
240
|
const config = loadConfig(projectRoot);
|
|
218
241
|
|
|
219
|
-
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
220
|
-
const lastRunPath = path.join(home, '.claude', 'heartbeat-last-run.json');
|
|
221
242
|
let lastRun = null;
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
} catch { /* ignore */ }
|
|
226
|
-
}
|
|
243
|
+
try {
|
|
244
|
+
lastRun = loopStateStore.loadHeartbeat('last-run');
|
|
245
|
+
} catch { /* ignore */ }
|
|
227
246
|
|
|
228
247
|
return {
|
|
229
248
|
configured: config.scans.length > 0,
|
|
@@ -236,11 +255,7 @@ function getHeartbeatStatus(projectRoot) {
|
|
|
236
255
|
}
|
|
237
256
|
|
|
238
257
|
function saveLastRun(result) {
|
|
239
|
-
|
|
240
|
-
const lastRunPath = path.join(home, '.claude', 'heartbeat-last-run.json');
|
|
241
|
-
const dir = path.dirname(lastRunPath);
|
|
242
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
243
|
-
fs.writeFileSync(lastRunPath, JSON.stringify(result, null, 2), 'utf-8');
|
|
258
|
+
return loopStateStore.saveHeartbeat('last-run', result);
|
|
244
259
|
}
|
|
245
260
|
|
|
246
261
|
function parseInterval(interval) {
|
|
@@ -11,7 +11,7 @@ const geminiProject = require('./gemini-project');
|
|
|
11
11
|
const opencodeHome = require('./opencode-home');
|
|
12
12
|
const windsurfProject = require('./windsurf-project');
|
|
13
13
|
|
|
14
|
-
const PUBLIC_INSTALL_TARGETS = Object.freeze(['claude', 'codex', 'opencode'
|
|
14
|
+
const PUBLIC_INSTALL_TARGETS = Object.freeze(['claude', 'codex', 'opencode']);
|
|
15
15
|
const TARGET_ALIASES = Object.freeze({
|
|
16
16
|
'claude-code': 'claude',
|
|
17
17
|
claudecode: 'claude',
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const yaml = require('js-yaml');
|
|
6
|
+
|
|
7
|
+
class LoopSpecError extends Error {
|
|
8
|
+
constructor(message, details = {}) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'LoopSpecError';
|
|
11
|
+
this.details = details;
|
|
12
|
+
this.code = details.code || 'loop_spec_error';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function ensureObject(value, label, filePath) {
|
|
17
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
18
|
+
throw new LoopSpecError(`${label} must be an object`, {
|
|
19
|
+
filePath,
|
|
20
|
+
code: 'invalid_payload',
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function ensureNonEmptyString(value, label, filePath) {
|
|
26
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
27
|
+
throw new LoopSpecError(`${label} must be a non-empty string`, {
|
|
28
|
+
filePath,
|
|
29
|
+
code: 'invalid_field',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function ensurePositiveNumber(value, label, filePath) {
|
|
35
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
|
36
|
+
throw new LoopSpecError(`${label} must be a positive number`, {
|
|
37
|
+
filePath,
|
|
38
|
+
code: 'invalid_budget',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeGate(gate, index, filePath) {
|
|
44
|
+
ensureObject(gate, `loop.gates[${index}]`, filePath);
|
|
45
|
+
ensureNonEmptyString(gate.name, `loop.gates[${index}].name`, filePath);
|
|
46
|
+
ensureNonEmptyString(gate.command, `loop.gates[${index}].command`, filePath);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
name: gate.name.trim(),
|
|
50
|
+
command: gate.command.trim(),
|
|
51
|
+
description: typeof gate.description === 'string' && gate.description.trim()
|
|
52
|
+
? gate.description.trim()
|
|
53
|
+
: null,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normalizeBudget(budget, filePath) {
|
|
58
|
+
ensureObject(budget, 'loop.budget', filePath);
|
|
59
|
+
ensurePositiveNumber(budget.maxIterations, 'loop.budget.maxIterations', filePath);
|
|
60
|
+
ensurePositiveNumber(budget.maxDollars, 'loop.budget.maxDollars', filePath);
|
|
61
|
+
ensureNonEmptyString(budget.maxDuration, 'loop.budget.maxDuration', filePath);
|
|
62
|
+
|
|
63
|
+
if (!/^(\d+)(m|h)$/.test(budget.maxDuration)) {
|
|
64
|
+
throw new LoopSpecError('loop.budget.maxDuration must use Nm or Nh format', {
|
|
65
|
+
filePath,
|
|
66
|
+
code: 'invalid_budget_duration',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
maxIterations: budget.maxIterations,
|
|
72
|
+
maxDuration: budget.maxDuration,
|
|
73
|
+
maxDollars: budget.maxDollars,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizeActor(actor, label, filePath) {
|
|
78
|
+
ensureObject(actor, label, filePath);
|
|
79
|
+
ensureNonEmptyString(actor.role, `${label}.role`, filePath);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
role: actor.role.trim(),
|
|
83
|
+
writeAccess: actor.writeAccess === true,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function normalizeLoopSpec(payload, filePath = '<inline>') {
|
|
88
|
+
ensureObject(payload, 'loop spec', filePath);
|
|
89
|
+
ensureObject(payload.loop, 'loop', filePath);
|
|
90
|
+
|
|
91
|
+
const loop = payload.loop;
|
|
92
|
+
ensureNonEmptyString(loop.id, 'loop.id', filePath);
|
|
93
|
+
ensureNonEmptyString(loop.description, 'loop.description', filePath);
|
|
94
|
+
ensureNonEmptyString(loop.cadence, 'loop.cadence', filePath);
|
|
95
|
+
ensureNonEmptyString(loop.skill, 'loop.skill', filePath);
|
|
96
|
+
ensureNonEmptyString(loop.stateFile, 'loop.stateFile', filePath);
|
|
97
|
+
|
|
98
|
+
if (!/^(\d+)(m|h|d)$/.test(loop.cadence)) {
|
|
99
|
+
throw new LoopSpecError('loop.cadence must use Nm, Nh, or Nd format', {
|
|
100
|
+
filePath,
|
|
101
|
+
code: 'invalid_cadence',
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!Array.isArray(loop.gates) || loop.gates.length === 0) {
|
|
106
|
+
throw new LoopSpecError('loop.gates must include at least one hard verification gate', {
|
|
107
|
+
filePath,
|
|
108
|
+
code: 'missing_gates',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
id: loop.id.trim(),
|
|
114
|
+
description: loop.description.trim(),
|
|
115
|
+
cadence: loop.cadence.trim(),
|
|
116
|
+
skill: loop.skill.trim(),
|
|
117
|
+
stateFile: loop.stateFile.trim(),
|
|
118
|
+
gates: loop.gates.map((gate, index) => normalizeGate(gate, index, filePath)),
|
|
119
|
+
maker: normalizeActor(loop.maker, 'loop.maker', filePath),
|
|
120
|
+
checker: normalizeActor(loop.checker, 'loop.checker', filePath),
|
|
121
|
+
budget: normalizeBudget(loop.budget, filePath),
|
|
122
|
+
escalation: {
|
|
123
|
+
onBudgetExhausted: loop.escalation?.onBudgetExhausted || 'triage',
|
|
124
|
+
onSecurityFinding: loop.escalation?.onSecurityFinding || 'human',
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function parseLoopSpecContent(content, filePath = '<inline>') {
|
|
130
|
+
let payload;
|
|
131
|
+
try {
|
|
132
|
+
payload = yaml.load(content, {
|
|
133
|
+
filename: path.basename(filePath),
|
|
134
|
+
schema: yaml.JSON_SCHEMA,
|
|
135
|
+
});
|
|
136
|
+
} catch (error) {
|
|
137
|
+
throw new LoopSpecError(`Failed to parse loop spec YAML: ${error.message}`, {
|
|
138
|
+
filePath,
|
|
139
|
+
code: 'yaml_parse_error',
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return normalizeLoopSpec(payload, filePath);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function loadLoopSpecFile(filePath) {
|
|
147
|
+
let content;
|
|
148
|
+
try {
|
|
149
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
150
|
+
} catch (error) {
|
|
151
|
+
throw new LoopSpecError(`Failed to read loop spec file: ${error.message}`, {
|
|
152
|
+
filePath,
|
|
153
|
+
code: 'read_error',
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
path: filePath,
|
|
159
|
+
loop: parseLoopSpecContent(content, filePath),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = {
|
|
164
|
+
LoopSpecError,
|
|
165
|
+
normalizeLoopSpec,
|
|
166
|
+
parseLoopSpecContent,
|
|
167
|
+
loadLoopSpecFile,
|
|
168
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -54,8 +54,13 @@ function normalizeUsage(raw) {
|
|
|
54
54
|
const outputTokens = Number(raw.output_tokens || raw.completion_tokens || 0) || 0;
|
|
55
55
|
const cacheCreationTokens = Number(raw.cache_creation_input_tokens || raw.cache_creation_prompt_tokens || 0) || 0;
|
|
56
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;
|
|
57
58
|
|
|
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);
|
|
59
64
|
if (contextTokens === 0) return null;
|
|
60
65
|
|
|
61
66
|
return {
|
|
@@ -63,6 +68,7 @@ function normalizeUsage(raw) {
|
|
|
63
68
|
outputTokens,
|
|
64
69
|
cacheCreationTokens,
|
|
65
70
|
cacheReadTokens,
|
|
71
|
+
totalTokens,
|
|
66
72
|
contextTokens,
|
|
67
73
|
};
|
|
68
74
|
}
|
|
@@ -163,11 +169,15 @@ function resolveTranscriptMetrics(transcriptPath, modelId) {
|
|
|
163
169
|
|
|
164
170
|
const contextLimit = resolveContextLimit(modelId);
|
|
165
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)));
|
|
166
174
|
|
|
167
175
|
return {
|
|
168
176
|
usagePct,
|
|
169
177
|
contextTokens: usage.contextTokens,
|
|
170
178
|
contextLimit,
|
|
179
|
+
remainingTokens,
|
|
180
|
+
remainingPct,
|
|
171
181
|
source: 'transcript_usage',
|
|
172
182
|
};
|
|
173
183
|
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
# https://goframe.org/docs/web/server-config-file-template
|
|
2
|
-
server:
|
|
3
|
-
address: ":8000"
|
|
4
|
-
openapiPath: "/api.json"
|
|
5
|
-
swaggerPath: "/swagger"
|
|
6
|
-
|
|
7
|
-
# https://goframe.org/docs/core/glog-config
|
|
8
|
-
logger:
|
|
9
|
-
level : "all"
|
|
10
|
-
stdout: true
|
|
11
|
-
|
|
12
|
-
# https://goframe.org/docs/core/gdb-config-file
|
|
13
|
-
database:
|
|
14
|
-
default:
|
|
1
|
+
# https://goframe.org/docs/web/server-config-file-template
|
|
2
|
+
server:
|
|
3
|
+
address: ":8000"
|
|
4
|
+
openapiPath: "/api.json"
|
|
5
|
+
swaggerPath: "/swagger"
|
|
6
|
+
|
|
7
|
+
# https://goframe.org/docs/core/glog-config
|
|
8
|
+
logger:
|
|
9
|
+
level : "all"
|
|
10
|
+
stdout: true
|
|
11
|
+
|
|
12
|
+
# https://goframe.org/docs/core/gdb-config-file
|
|
13
|
+
database:
|
|
14
|
+
default:
|
|
15
15
|
link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test"
|