@colin4k1024/tsp 2.5.0 → 2.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -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/README.md +1 -1
- package/hooks/harness-context-monitor.js +4 -0
- package/hooks/hooks.json +27 -26
- package/hooks/strategic-compact/README.md +7 -5
- package/hooks/strategic-compact/suggest-compact.js +9 -174
- package/marketplace.json +7 -8
- package/package.json +1 -1
- package/schemas/goal.schema.json +172 -0
- package/scripts/harness-audit.js +7 -4
- package/scripts/hooks/session-start-goal-resume.js +95 -0
- package/scripts/hooks/suggest-compact.js +392 -62
- package/scripts/install-platform.js +68 -85
- 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/opencode/convert-agents.js +273 -0
- package/scripts/lib/opencode/convert-hooks.js +286 -0
- package/scripts/lib/opencode/generate-agents-md.js +361 -0
- package/scripts/lib/wave-cost-advisor.js +155 -0
- package/scripts/test-opencode-install.js +151 -0
- package/skills/goal-convergence/SKILL.md +150 -0
- package/skills/goframe-v2/examples/practices/quick-demo/manifest/config/config.yaml +14 -14
- package/skills/loop-heartbeat/SKILL.md +120 -0
- package/skills/mcp-connector-bridge/SKILL.md +132 -0
- package/skills/repo-scan/SKILL.md +63 -63
- package/skills/rework-loop/SKILL.md +131 -0
- 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
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* blame-attribution.js
|
|
5
|
+
*
|
|
6
|
+
* Maps goal iteration failures to specific code changes using git blame/diff.
|
|
7
|
+
* Produces targeted rework briefs that constrain the next iteration's scope.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { execSync } = require('child_process');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
|
|
14
|
+
const MAX_REWORK_ATTEMPTS = 3;
|
|
15
|
+
|
|
16
|
+
function parseFailureLocations(failOutput) {
|
|
17
|
+
const locations = [];
|
|
18
|
+
const lines = failOutput.split('\n');
|
|
19
|
+
|
|
20
|
+
for (const line of lines) {
|
|
21
|
+
// Match common test failure patterns:
|
|
22
|
+
// "FAIL src/auth/refresh.test.ts"
|
|
23
|
+
// "● Auth > should refresh tokens"
|
|
24
|
+
// "at Object.<anonymous> (src/auth/refresh.ts:23:5)"
|
|
25
|
+
// "src/auth/refresh.ts(23,5): error TS2345"
|
|
26
|
+
const fileLineMatch = line.match(/([a-zA-Z0-9_\-./]+\.[a-z]{1,4})[:(](\d+)/);
|
|
27
|
+
if (fileLineMatch) {
|
|
28
|
+
const filePath = fileLineMatch[1];
|
|
29
|
+
const lineNumber = parseInt(fileLineMatch[2], 10);
|
|
30
|
+
if (!filePath.includes('node_modules') && !filePath.includes('.git')) {
|
|
31
|
+
locations.push({ file: filePath, line: lineNumber, context: line.trim() });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Match "FAIL" prefixed paths
|
|
36
|
+
const failMatch = line.match(/FAIL\s+(.+\.[a-z]{1,4})/);
|
|
37
|
+
if (failMatch) {
|
|
38
|
+
locations.push({ file: failMatch[1].trim(), line: null, context: line.trim() });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Deduplicate by file
|
|
43
|
+
const seen = new Set();
|
|
44
|
+
return locations.filter(loc => {
|
|
45
|
+
const key = `${loc.file}:${loc.line || 0}`;
|
|
46
|
+
if (seen.has(key)) return false;
|
|
47
|
+
seen.add(key);
|
|
48
|
+
return true;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getChangedFiles(cwd) {
|
|
53
|
+
try {
|
|
54
|
+
const output = execSync('git diff --name-only HEAD 2>/dev/null || git diff --name-only', {
|
|
55
|
+
encoding: 'utf-8',
|
|
56
|
+
cwd,
|
|
57
|
+
timeout: 10000,
|
|
58
|
+
}).trim();
|
|
59
|
+
return output ? output.split('\n').filter(Boolean) : [];
|
|
60
|
+
} catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getFileDiff(filePath, cwd) {
|
|
66
|
+
try {
|
|
67
|
+
return execSync(`git diff HEAD -- "${filePath}" 2>/dev/null || git diff -- "${filePath}"`, {
|
|
68
|
+
encoding: 'utf-8',
|
|
69
|
+
cwd,
|
|
70
|
+
timeout: 10000,
|
|
71
|
+
}).trim();
|
|
72
|
+
} catch {
|
|
73
|
+
return '';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function intersectFailuresWithChanges(failureLocations, changedFiles) {
|
|
78
|
+
return failureLocations.filter(loc => {
|
|
79
|
+
const normalizedFail = loc.file.replace(/^\.\//, '');
|
|
80
|
+
return changedFiles.some(changed => {
|
|
81
|
+
const normalizedChanged = changed.replace(/^\.\//, '');
|
|
82
|
+
return normalizedFail === normalizedChanged ||
|
|
83
|
+
normalizedFail.endsWith(normalizedChanged) ||
|
|
84
|
+
normalizedChanged.endsWith(normalizedFail);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function buildReworkBrief(goal, oracleResult, blameResult) {
|
|
90
|
+
const iteration = goal.currentIteration;
|
|
91
|
+
const failingFiles = blameResult.blamedLocations.map(l => l.file);
|
|
92
|
+
const uniqueFiles = [...new Set(failingFiles)];
|
|
93
|
+
|
|
94
|
+
const brief = [
|
|
95
|
+
'## REWORK BRIEF',
|
|
96
|
+
'',
|
|
97
|
+
`**Goal:** ${goal.objective}`,
|
|
98
|
+
`**Iteration:** ${iteration} (rework attempt ${blameResult.attemptCount} for this scope)`,
|
|
99
|
+
'',
|
|
100
|
+
'**Failing evidence:**',
|
|
101
|
+
...blameResult.blamedLocations.map(l =>
|
|
102
|
+
`- ${l.file}${l.line ? ':' + l.line : ''} — ${l.context.slice(0, 100)}`
|
|
103
|
+
),
|
|
104
|
+
'',
|
|
105
|
+
'**Root cause (blame):**',
|
|
106
|
+
...blameResult.changedFiles.slice(0, 5).map(f => `- Changed: ${f}`),
|
|
107
|
+
'',
|
|
108
|
+
'**Constraint:**',
|
|
109
|
+
...uniqueFiles.map(f => `- ONLY modify: ${f}`),
|
|
110
|
+
'- DO NOT touch test files unless they are the source of the bug',
|
|
111
|
+
'- DO NOT expand scope beyond the blamed files',
|
|
112
|
+
'',
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
if (oracleResult.nextHint) {
|
|
116
|
+
brief.push('**Oracle hint:**', oracleResult.nextHint, '');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return brief.join('\n');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function analyzeBlame(goal, oracleResult, cwd) {
|
|
123
|
+
const failOutput = (oracleResult.reasons || []).join('\n') +
|
|
124
|
+
'\n' + (oracleResult.conditionResults || [])
|
|
125
|
+
.filter(r => !r.passed)
|
|
126
|
+
.map(r => r.output)
|
|
127
|
+
.join('\n');
|
|
128
|
+
|
|
129
|
+
const failureLocations = parseFailureLocations(failOutput);
|
|
130
|
+
const changedFiles = getChangedFiles(cwd);
|
|
131
|
+
const blamedLocations = intersectFailuresWithChanges(failureLocations, changedFiles);
|
|
132
|
+
|
|
133
|
+
// Determine if escalation is needed
|
|
134
|
+
const shouldEscalate = blamedLocations.length === 0 && failureLocations.length > 0;
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
failureLocations,
|
|
138
|
+
changedFiles,
|
|
139
|
+
blamedLocations: blamedLocations.length > 0 ? blamedLocations : failureLocations.slice(0, 5),
|
|
140
|
+
intersection: blamedLocations.length > 0,
|
|
141
|
+
shouldEscalate,
|
|
142
|
+
escalationReason: shouldEscalate ? 'no_blame_intersection' : null,
|
|
143
|
+
attemptCount: 1, // caller should update from tracking store
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Rework tracking
|
|
148
|
+
|
|
149
|
+
function getReworkTrackingPath() {
|
|
150
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
151
|
+
return path.join(home, '.claude', 'rework-tracking.json');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function loadReworkTracking() {
|
|
155
|
+
const trackPath = getReworkTrackingPath();
|
|
156
|
+
if (!fs.existsSync(trackPath)) return {};
|
|
157
|
+
try {
|
|
158
|
+
return JSON.parse(fs.readFileSync(trackPath, 'utf-8'));
|
|
159
|
+
} catch {
|
|
160
|
+
return {};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function saveReworkTracking(tracking) {
|
|
165
|
+
const trackPath = getReworkTrackingPath();
|
|
166
|
+
const dir = path.dirname(trackPath);
|
|
167
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
168
|
+
fs.writeFileSync(trackPath, JSON.stringify(tracking, null, 2), 'utf-8');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function recordReworkAttempt(filePath, outcome) {
|
|
172
|
+
const tracking = loadReworkTracking();
|
|
173
|
+
if (!tracking[filePath]) {
|
|
174
|
+
tracking[filePath] = { attempts: 0, outcomes: [], lastAttempt: null, totalCost: 0 };
|
|
175
|
+
}
|
|
176
|
+
tracking[filePath].attempts += 1;
|
|
177
|
+
tracking[filePath].outcomes.push(outcome);
|
|
178
|
+
tracking[filePath].lastAttempt = new Date().toISOString();
|
|
179
|
+
saveReworkTracking(tracking);
|
|
180
|
+
return tracking[filePath];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function shouldEscalateFile(filePath) {
|
|
184
|
+
const tracking = loadReworkTracking();
|
|
185
|
+
const record = tracking[filePath];
|
|
186
|
+
if (!record) return false;
|
|
187
|
+
return record.attempts >= MAX_REWORK_ATTEMPTS;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function getPersistentTroubleSpots() {
|
|
191
|
+
const tracking = loadReworkTracking();
|
|
192
|
+
return Object.entries(tracking)
|
|
193
|
+
.filter(([, record]) => record.attempts >= MAX_REWORK_ATTEMPTS)
|
|
194
|
+
.map(([file, record]) => ({ file, ...record }));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = {
|
|
198
|
+
MAX_REWORK_ATTEMPTS,
|
|
199
|
+
parseFailureLocations,
|
|
200
|
+
getChangedFiles,
|
|
201
|
+
getFileDiff,
|
|
202
|
+
intersectFailuresWithChanges,
|
|
203
|
+
buildReworkBrief,
|
|
204
|
+
analyzeBlame,
|
|
205
|
+
loadReworkTracking,
|
|
206
|
+
saveReworkTracking,
|
|
207
|
+
recordReworkAttempt,
|
|
208
|
+
shouldEscalateFile,
|
|
209
|
+
getPersistentTroubleSpots,
|
|
210
|
+
};
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
|
|
8
|
+
const GOAL_STATES = {
|
|
9
|
+
active: 'active',
|
|
10
|
+
paused: 'paused',
|
|
11
|
+
converged: 'converged',
|
|
12
|
+
escalated: 'escalated',
|
|
13
|
+
failed: 'failed',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const ESCALATION_REASONS = {
|
|
17
|
+
budgetExhausted: 'budget_exhausted',
|
|
18
|
+
repeatedFailure: 'repeated_failure',
|
|
19
|
+
oracleUncertain: 'oracle_uncertain',
|
|
20
|
+
manual: 'manual',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function generateGoalId() {
|
|
24
|
+
return `goal-${crypto.randomBytes(4).toString('hex')}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function createGoal(objective, options = {}) {
|
|
28
|
+
const now = new Date().toISOString();
|
|
29
|
+
return {
|
|
30
|
+
goalId: generateGoalId(),
|
|
31
|
+
objective,
|
|
32
|
+
stoppingConditions: options.stoppingConditions || inferStoppingConditions(objective),
|
|
33
|
+
budget: {
|
|
34
|
+
maxIterations: options.maxIterations || 15,
|
|
35
|
+
maxDuration: options.maxDuration || '2h',
|
|
36
|
+
maxDollars: options.maxDollars || 10,
|
|
37
|
+
},
|
|
38
|
+
oracle: {
|
|
39
|
+
model: options.checkerModel || 'haiku',
|
|
40
|
+
allowedTools: ['Read', 'Bash'],
|
|
41
|
+
prompt: options.oraclePrompt || null,
|
|
42
|
+
},
|
|
43
|
+
state: GOAL_STATES.active,
|
|
44
|
+
currentIteration: 0,
|
|
45
|
+
createdAt: now,
|
|
46
|
+
updatedAt: now,
|
|
47
|
+
history: [],
|
|
48
|
+
escalation: null,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function inferStoppingConditions(objective) {
|
|
53
|
+
const lower = objective.toLowerCase();
|
|
54
|
+
const conditions = [];
|
|
55
|
+
|
|
56
|
+
if (lower.includes('test') && (lower.includes('pass') || lower.includes('fix'))) {
|
|
57
|
+
conditions.push({
|
|
58
|
+
type: 'test_pass',
|
|
59
|
+
command: 'npm test 2>&1; echo "EXIT:$?"',
|
|
60
|
+
description: 'All tests pass',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (lower.includes('lint') || lower.includes('eslint')) {
|
|
65
|
+
conditions.push({
|
|
66
|
+
type: 'lint_clean',
|
|
67
|
+
command: 'npm run lint -- --quiet 2>&1; echo "EXIT:$?"',
|
|
68
|
+
description: 'Linter reports no errors',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (lower.includes('coverage')) {
|
|
73
|
+
const match = lower.match(/(\d+)\s*%/);
|
|
74
|
+
const threshold = match ? parseInt(match[1], 10) : 80;
|
|
75
|
+
conditions.push({
|
|
76
|
+
type: 'coverage_threshold',
|
|
77
|
+
command: 'npm test -- --coverage --coverageReporters=text-summary 2>&1',
|
|
78
|
+
threshold,
|
|
79
|
+
description: `Test coverage >= ${threshold}%`,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (lower.includes('build') && (lower.includes('pass') || lower.includes('fix'))) {
|
|
84
|
+
conditions.push({
|
|
85
|
+
type: 'build_pass',
|
|
86
|
+
command: 'npm run build 2>&1; echo "EXIT:$?"',
|
|
87
|
+
description: 'Build succeeds',
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (conditions.length === 0) {
|
|
92
|
+
conditions.push({
|
|
93
|
+
type: 'custom_command',
|
|
94
|
+
command: 'echo "Manual verification required"; exit 1',
|
|
95
|
+
description: 'Requires explicit stopping condition — use --condition flag',
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return conditions;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function evaluateCondition(condition) {
|
|
103
|
+
try {
|
|
104
|
+
const output = execSync(condition.command, {
|
|
105
|
+
encoding: 'utf-8',
|
|
106
|
+
timeout: 60000,
|
|
107
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
108
|
+
}).trim();
|
|
109
|
+
|
|
110
|
+
let passed = true;
|
|
111
|
+
|
|
112
|
+
if (condition.type === 'coverage_threshold' && condition.threshold) {
|
|
113
|
+
const coverageMatch = output.match(/(?:All files|Statements)\s*[:|]\s*([\d.]+)%/);
|
|
114
|
+
if (coverageMatch) {
|
|
115
|
+
passed = parseFloat(coverageMatch[1]) >= condition.threshold;
|
|
116
|
+
} else {
|
|
117
|
+
passed = false;
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
const exitMatch = output.match(/EXIT:(\d+)$/);
|
|
121
|
+
if (exitMatch) {
|
|
122
|
+
passed = exitMatch[1] === '0';
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { type: condition.type, passed, output: output.slice(0, 500) };
|
|
127
|
+
} catch (error) {
|
|
128
|
+
return {
|
|
129
|
+
type: condition.type,
|
|
130
|
+
passed: false,
|
|
131
|
+
output: (error.stderr || error.message || '').slice(0, 500),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function buildOraclePrompt(goal, conditionResults, iterationSummary) {
|
|
137
|
+
const recentHistory = goal.history.slice(-3);
|
|
138
|
+
|
|
139
|
+
return `You are a completion oracle. Your job is to evaluate whether a goal has been achieved.
|
|
140
|
+
You CANNOT modify code — you can only read and evaluate.
|
|
141
|
+
|
|
142
|
+
## Goal
|
|
143
|
+
Objective: ${goal.objective}
|
|
144
|
+
|
|
145
|
+
## Stopping Conditions Results
|
|
146
|
+
${conditionResults.map(r => `- [${r.passed ? 'PASS' : 'FAIL'}] ${r.type}: ${r.output.slice(0, 200)}`).join('\n')}
|
|
147
|
+
|
|
148
|
+
## Maker's Iteration Summary
|
|
149
|
+
${iterationSummary || '(no summary provided)'}
|
|
150
|
+
|
|
151
|
+
## Recent History
|
|
152
|
+
${recentHistory.map(h => `Iteration ${h.iteration}: ${h.oracleVerdict} — ${(h.failReasons || []).join(', ')}`).join('\n') || '(first iteration)'}
|
|
153
|
+
|
|
154
|
+
## Your Task
|
|
155
|
+
Evaluate whether ALL stopping conditions are met. Respond with EXACTLY this JSON:
|
|
156
|
+
{
|
|
157
|
+
"converged": <true if ALL conditions pass, false otherwise>,
|
|
158
|
+
"conditionResults": [{"type": "...", "passed": true/false, "output": "brief"}],
|
|
159
|
+
"reasons": ["why not converged, if applicable"],
|
|
160
|
+
"nextHint": "specific guidance for the maker's next iteration (if not converged)",
|
|
161
|
+
"confidence": <0.0 to 1.0>
|
|
162
|
+
}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function checkBudget(goal) {
|
|
166
|
+
if (goal.currentIteration >= goal.budget.maxIterations) {
|
|
167
|
+
return { exhausted: true, reason: ESCALATION_REASONS.budgetExhausted, detail: 'Max iterations reached' };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const durationMs = parseDuration(goal.budget.maxDuration);
|
|
171
|
+
const elapsed = Date.now() - new Date(goal.createdAt).getTime();
|
|
172
|
+
if (elapsed >= durationMs) {
|
|
173
|
+
return { exhausted: true, reason: ESCALATION_REASONS.budgetExhausted, detail: 'Max duration reached' };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const totalCost = goal.history.reduce((sum, h) => sum + (h.costDollars || 0), 0);
|
|
177
|
+
if (totalCost >= goal.budget.maxDollars) {
|
|
178
|
+
return { exhausted: true, reason: ESCALATION_REASONS.budgetExhausted, detail: 'Max cost reached' };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const recentFails = goal.history.slice(-5).filter(h => h.oracleVerdict === 'fail');
|
|
182
|
+
if (recentFails.length >= 5) {
|
|
183
|
+
return { exhausted: true, reason: ESCALATION_REASONS.repeatedFailure, detail: '5 consecutive failures' };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return { exhausted: false };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function parseDuration(duration) {
|
|
190
|
+
const match = duration.match(/^(\d+)(m|h)$/);
|
|
191
|
+
if (!match) return 2 * 60 * 60 * 1000;
|
|
192
|
+
const value = parseInt(match[1], 10);
|
|
193
|
+
return match[2] === 'h' ? value * 60 * 60 * 1000 : value * 60 * 1000;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function recordIteration(goal, verdict, costDollars = 0) {
|
|
197
|
+
const entry = {
|
|
198
|
+
iteration: goal.currentIteration,
|
|
199
|
+
makerSummary: verdict.makerSummary || null,
|
|
200
|
+
oracleVerdict: verdict.converged ? 'pass' : 'fail',
|
|
201
|
+
failReasons: verdict.reasons || [],
|
|
202
|
+
nextHint: verdict.nextHint || null,
|
|
203
|
+
timestamp: new Date().toISOString(),
|
|
204
|
+
costDollars,
|
|
205
|
+
};
|
|
206
|
+
goal.history.push(entry);
|
|
207
|
+
goal.currentIteration += 1;
|
|
208
|
+
goal.updatedAt = new Date().toISOString();
|
|
209
|
+
|
|
210
|
+
if (verdict.converged) {
|
|
211
|
+
goal.state = GOAL_STATES.converged;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return entry;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function escalateGoal(goal, reason, detail) {
|
|
218
|
+
goal.state = GOAL_STATES.escalated;
|
|
219
|
+
goal.escalation = {
|
|
220
|
+
reason,
|
|
221
|
+
details: detail,
|
|
222
|
+
escalatedAt: new Date().toISOString(),
|
|
223
|
+
};
|
|
224
|
+
goal.updatedAt = new Date().toISOString();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function pauseGoal(goal) {
|
|
228
|
+
goal.state = GOAL_STATES.paused;
|
|
229
|
+
goal.updatedAt = new Date().toISOString();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function resumeGoal(goal) {
|
|
233
|
+
if (goal.state === GOAL_STATES.paused || goal.state === GOAL_STATES.escalated) {
|
|
234
|
+
goal.state = GOAL_STATES.active;
|
|
235
|
+
goal.updatedAt = new Date().toISOString();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Goal persistence
|
|
240
|
+
|
|
241
|
+
function getGoalsDir() {
|
|
242
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
243
|
+
const dir = path.join(home, '.claude', 'goals');
|
|
244
|
+
if (!fs.existsSync(dir)) {
|
|
245
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
246
|
+
}
|
|
247
|
+
return dir;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function saveGoal(goal) {
|
|
251
|
+
const filePath = path.join(getGoalsDir(), `${goal.goalId}.json`);
|
|
252
|
+
fs.writeFileSync(filePath, JSON.stringify(goal, null, 2), 'utf-8');
|
|
253
|
+
return filePath;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function loadGoal(goalId) {
|
|
257
|
+
const filePath = path.join(getGoalsDir(), `${goalId}.json`);
|
|
258
|
+
if (!fs.existsSync(filePath)) return null;
|
|
259
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function listGoals(filter) {
|
|
263
|
+
const dir = getGoalsDir();
|
|
264
|
+
if (!fs.existsSync(dir)) return [];
|
|
265
|
+
|
|
266
|
+
return fs.readdirSync(dir)
|
|
267
|
+
.filter(f => f.endsWith('.json'))
|
|
268
|
+
.map(f => JSON.parse(fs.readFileSync(path.join(dir, f), 'utf-8')))
|
|
269
|
+
.filter(g => !filter || g.state === filter)
|
|
270
|
+
.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function getActiveGoals() {
|
|
274
|
+
return listGoals(GOAL_STATES.active);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Oracle execution (integration point for claude -p or sub-agent)
|
|
278
|
+
|
|
279
|
+
function runOracleEvaluation(goal, iterationSummary) {
|
|
280
|
+
const conditionResults = goal.stoppingConditions.map(evaluateCondition);
|
|
281
|
+
const allPassed = conditionResults.every(r => r.passed);
|
|
282
|
+
|
|
283
|
+
if (allPassed) {
|
|
284
|
+
return {
|
|
285
|
+
converged: true,
|
|
286
|
+
conditionResults,
|
|
287
|
+
reasons: [],
|
|
288
|
+
nextHint: null,
|
|
289
|
+
confidence: 1.0,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const failedConditions = conditionResults.filter(r => !r.passed);
|
|
294
|
+
return {
|
|
295
|
+
converged: false,
|
|
296
|
+
conditionResults,
|
|
297
|
+
reasons: failedConditions.map(r => `${r.type}: ${r.output.slice(0, 100)}`),
|
|
298
|
+
nextHint: `Focus on: ${failedConditions.map(r => r.type).join(', ')}. ${failedConditions[0].output.slice(0, 200)}`,
|
|
299
|
+
confidence: 0.7,
|
|
300
|
+
oraclePrompt: buildOraclePrompt(goal, conditionResults, iterationSummary),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Main loop driver (called by the goal command handler)
|
|
305
|
+
|
|
306
|
+
async function runGoalIteration(goal, makerFn) {
|
|
307
|
+
const budgetCheck = checkBudget(goal);
|
|
308
|
+
if (budgetCheck.exhausted) {
|
|
309
|
+
escalateGoal(goal, budgetCheck.reason, budgetCheck.detail);
|
|
310
|
+
saveGoal(goal);
|
|
311
|
+
return { action: 'escalated', goal };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const makerResult = await makerFn(goal);
|
|
315
|
+
|
|
316
|
+
const oracleResult = runOracleEvaluation(goal, makerResult.summary);
|
|
317
|
+
|
|
318
|
+
const entry = recordIteration(goal, {
|
|
319
|
+
...oracleResult,
|
|
320
|
+
makerSummary: makerResult.summary,
|
|
321
|
+
}, makerResult.costDollars || 0);
|
|
322
|
+
|
|
323
|
+
saveGoal(goal);
|
|
324
|
+
|
|
325
|
+
if (oracleResult.converged) {
|
|
326
|
+
return { action: 'converged', goal, entry };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return { action: 'continue', goal, entry, nextHint: oracleResult.nextHint };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
module.exports = {
|
|
333
|
+
GOAL_STATES,
|
|
334
|
+
ESCALATION_REASONS,
|
|
335
|
+
generateGoalId,
|
|
336
|
+
createGoal,
|
|
337
|
+
inferStoppingConditions,
|
|
338
|
+
evaluateCondition,
|
|
339
|
+
buildOraclePrompt,
|
|
340
|
+
checkBudget,
|
|
341
|
+
recordIteration,
|
|
342
|
+
escalateGoal,
|
|
343
|
+
pauseGoal,
|
|
344
|
+
resumeGoal,
|
|
345
|
+
saveGoal,
|
|
346
|
+
loadGoal,
|
|
347
|
+
listGoals,
|
|
348
|
+
getActiveGoals,
|
|
349
|
+
runOracleEvaluation,
|
|
350
|
+
runGoalIteration,
|
|
351
|
+
};
|