@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.
Files changed (49) hide show
  1. package/commands/goal.md +7 -1
  2. package/commands/heartbeat.md +34 -1
  3. package/commands/loop-start.md +34 -6
  4. package/commands/triage.md +8 -1
  5. package/hooks/README.md +2 -2
  6. package/hooks/harness-statusline.js +9 -30
  7. package/hooks/strategic-compact/README.md +11 -12
  8. package/manifests/install-components.json +40 -0
  9. package/manifests/install-modules.json +43 -0
  10. package/manifests/install-profiles.json +2 -0
  11. package/package.json +1 -1
  12. package/schemas/loop-spec.schema.json +124 -0
  13. package/scripts/hooks/pre-compact.js +39 -8
  14. package/scripts/hooks/session-start-goal-resume.js +3 -20
  15. package/scripts/hooks/suggest-compact.js +9 -115
  16. package/scripts/lib/completion-oracle.js +4 -27
  17. package/scripts/lib/context-window-state.js +129 -0
  18. package/scripts/lib/context-window.js +294 -0
  19. package/scripts/lib/heartbeat-scheduler.js +40 -25
  20. package/scripts/lib/install-targets/registry.js +1 -1
  21. package/scripts/lib/loop-oracle.js +5 -0
  22. package/scripts/lib/loop-spec.js +168 -0
  23. package/scripts/lib/loop-state-store.js +221 -0
  24. package/scripts/lib/transcript-usage.js +11 -1
  25. package/skills/goframe-v2/examples/practices/quick-demo/manifest/config/config.yaml +14 -14
  26. package/skills/repo-scan/SKILL.md +63 -63
  27. package/skills/strategic-compact/SKILL.md +11 -2
  28. package/scripts/__pycache__/__init__.cpython-311.pyc +0 -0
  29. package/scripts/__pycache__/build_platform_artifacts.cpython-311.pyc +0 -0
  30. package/scripts/__pycache__/install_platform.cpython-311.pyc +0 -0
  31. package/scripts/__pycache__/langfuse_trace.cpython-311.pyc +0 -0
  32. package/scripts/__pycache__/query_audit_logs.cpython-311.pyc +0 -0
  33. package/scripts/__pycache__/scan_leaked_keys.cpython-311.pyc +0 -0
  34. package/scripts/__pycache__/team_skills_platform.cpython-311.pyc +0 -0
  35. package/scripts/__pycache__/team_skills_platform.cpython-313.pyc +0 -0
  36. package/scripts/__pycache__/validate_library.cpython-311.pyc +0 -0
  37. package/scripts/__pycache__/validate_workflow_state.cpython-311.pyc +0 -0
  38. package/scripts/evolution/__pycache__/__init__.cpython-311.pyc +0 -0
  39. package/scripts/evolution/__pycache__/store.cpython-311.pyc +0 -0
  40. package/scripts/hooks/__pycache__/__init__.cpython-311.pyc +0 -0
  41. package/scripts/hooks/__pycache__/mcp_health_check.cpython-311.pyc +0 -0
  42. package/scripts/hooks/__pycache__/observe.cpython-311.pyc +0 -0
  43. package/scripts/hooks/__pycache__/session_end.cpython-311.pyc +0 -0
  44. package/scripts/hooks/__pycache__/session_start.cpython-311.pyc +0 -0
  45. package/scripts/lib/__pycache__/audit_logger.cpython-311.pyc +0 -0
  46. package/scripts/lib/__pycache__/audit_query.cpython-311.pyc +0 -0
  47. package/scripts/lib/__pycache__/hook_contract.cpython-311.pyc +0 -0
  48. package/scripts/lib/__pycache__/memory_store.cpython-311.pyc +0 -0
  49. 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 ? undefined : undefined; // yaml parsing fallback below
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 configPath = path.join(projectRoot || process.cwd(), '.claude', 'heartbeat.yaml');
79
- if (!fs.existsSync(configPath)) return DEFAULT_CONFIG;
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
- const home = process.env.HOME || process.env.USERPROFILE || '';
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
- if (fs.existsSync(lastRunPath)) {
223
- try {
224
- lastRun = JSON.parse(fs.readFileSync(lastRunPath, 'utf-8'));
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
- const home = process.env.HOME || process.env.USERPROFILE || '';
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', 'cangming', 'codewhale']);
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,5 @@
1
+ 'use strict';
2
+
3
+ // Compatibility layer for the loop-engineering runtime. The existing goal
4
+ // oracle remains the implementation while callers migrate to loop naming.
5
+ module.exports = require('./completion-oracle');
@@ -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
- const contextTokens = inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens;
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"