@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,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
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
|
|
6
|
+
const OPENCODE_HOME = path.join(os.homedir(), '.config', 'opencode');
|
|
7
|
+
const PLUGIN_DIR = path.join(OPENCODE_HOME, 'plugins', 'team-skills-platform');
|
|
8
|
+
|
|
9
|
+
console.log('=== OpenCode 安装验证 ===\n');
|
|
10
|
+
|
|
11
|
+
let passed = 0;
|
|
12
|
+
let failed = 0;
|
|
13
|
+
|
|
14
|
+
function check(description, condition) {
|
|
15
|
+
if (condition) {
|
|
16
|
+
console.log(`✅ ${description}`);
|
|
17
|
+
passed++;
|
|
18
|
+
} else {
|
|
19
|
+
console.log(`❌ ${description}`);
|
|
20
|
+
failed++;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 检查目录结构
|
|
25
|
+
console.log('📁 目录结构检查:');
|
|
26
|
+
check('OPENCODE_HOME 目录存在', fs.existsSync(OPENCODE_HOME));
|
|
27
|
+
check('AGENTS.md 文件存在', fs.existsSync(path.join(OPENCODE_HOME, 'AGENTS.md')));
|
|
28
|
+
check('opencode.json 文件存在', fs.existsSync(path.join(OPENCODE_HOME, 'opencode.json')));
|
|
29
|
+
check('agents 目录存在', fs.existsSync(path.join(OPENCODE_HOME, 'agents')));
|
|
30
|
+
check('command 目录存在', fs.existsSync(path.join(OPENCODE_HOME, 'command')));
|
|
31
|
+
check('plugins 目录存在', fs.existsSync(path.join(OPENCODE_HOME, 'plugins')));
|
|
32
|
+
check('team-skills-platform 插件存在', fs.existsSync(PLUGIN_DIR));
|
|
33
|
+
check('tsp-hooks.js 插件存在', fs.existsSync(path.join(OPENCODE_HOME, 'plugins', 'tsp-hooks.js')));
|
|
34
|
+
|
|
35
|
+
console.log('\n📄 文件内容检查:');
|
|
36
|
+
|
|
37
|
+
// 检查 AGENTS.md
|
|
38
|
+
const agentsMdPath = path.join(OPENCODE_HOME, 'AGENTS.md');
|
|
39
|
+
if (fs.existsSync(agentsMdPath)) {
|
|
40
|
+
const content = fs.readFileSync(agentsMdPath, 'utf8');
|
|
41
|
+
check('AGENTS.md 包含团队技能平台标记', content.includes('<!-- team-skills-platform -->'));
|
|
42
|
+
check('AGENTS.md 包含规则索引', content.includes('## 规则索引'));
|
|
43
|
+
check('AGENTS.md 包含角色索引', content.includes('## 可用角色'));
|
|
44
|
+
check('AGENTS.md 包含命令索引', content.includes('## 核心团队命令'));
|
|
45
|
+
check('AGENTS.md 包含技能索引', content.includes('## 可用技能'));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 检查 opencode.json
|
|
49
|
+
const configPath = path.join(OPENCODE_HOME, 'opencode.json');
|
|
50
|
+
if (fs.existsSync(configPath)) {
|
|
51
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
52
|
+
check('opencode.json 包含 instructions', Array.isArray(config.instructions));
|
|
53
|
+
check('opencode.json 包含 plugin', Array.isArray(config.plugin));
|
|
54
|
+
check('opencode.json 包含 permission', typeof config.permission === 'object');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log('\n👥 Agents 检查:');
|
|
58
|
+
const agentsDir = path.join(OPENCODE_HOME, 'agents');
|
|
59
|
+
if (fs.existsSync(agentsDir)) {
|
|
60
|
+
const agentFiles = fs.readdirSync(agentsDir).filter(f => f.endsWith('.md'));
|
|
61
|
+
check(`agents 目录包含文件 (${agentFiles.length})`, agentFiles.length > 0);
|
|
62
|
+
|
|
63
|
+
// 检查是否有角色 agents
|
|
64
|
+
const roleAgents = ['tech-lead.md', 'product-manager.md', 'architect.md', 'frontend-engineer.md',
|
|
65
|
+
'backend-engineer.md', 'qa-engineer.md', 'devops-engineer.md'];
|
|
66
|
+
for (const agent of roleAgents) {
|
|
67
|
+
check(`角色 agent ${agent} 存在`, fs.existsSync(path.join(agentsDir, agent)));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 检查是否有 specialist agents
|
|
71
|
+
const specialistAgents = agentFiles.filter(f => f.startsWith('specialist-'));
|
|
72
|
+
check(`specialist agents 存在 (${specialistAgents.length})`, specialistAgents.length > 0);
|
|
73
|
+
|
|
74
|
+
// 检查 agent 文件格式
|
|
75
|
+
if (agentFiles.length > 0) {
|
|
76
|
+
const sampleAgent = fs.readFileSync(path.join(agentsDir, agentFiles[0]), 'utf8');
|
|
77
|
+
check('agent 文件包含 YAML front matter', sampleAgent.startsWith('---'));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log('\n📝 Commands 检查:');
|
|
82
|
+
const commandsDir = path.join(OPENCODE_HOME, 'command');
|
|
83
|
+
if (fs.existsSync(commandsDir)) {
|
|
84
|
+
const commandFiles = fs.readdirSync(commandsDir).filter(f => f.endsWith('.md'));
|
|
85
|
+
check(`commands 目录包含文件 (${commandFiles.length})`, commandFiles.length > 0);
|
|
86
|
+
|
|
87
|
+
// 检查核心命令
|
|
88
|
+
const coreCommands = ['team-intake.md', 'team-plan.md', 'team-execute.md', 'team-review.md',
|
|
89
|
+
'team-release.md', 'handoff.md'];
|
|
90
|
+
for (const cmd of coreCommands) {
|
|
91
|
+
check(`核心命令 ${cmd} 存在`, fs.existsSync(path.join(commandsDir, cmd)));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log('\n🎯 Skills 检查:');
|
|
96
|
+
const skillsDir = path.join(PLUGIN_DIR, 'skills');
|
|
97
|
+
if (fs.existsSync(skillsDir)) {
|
|
98
|
+
const skillDirs = fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
99
|
+
.filter(d => d.isDirectory())
|
|
100
|
+
.map(d => d.name);
|
|
101
|
+
check(`skills 目录包含目录 (${skillDirs.length})`, skillDirs.length > 0);
|
|
102
|
+
|
|
103
|
+
// 检查是否有 SKILL.md 文件
|
|
104
|
+
let skillsWithMd = 0;
|
|
105
|
+
for (const dir of skillDirs.slice(0, 10)) {
|
|
106
|
+
if (fs.existsSync(path.join(skillsDir, dir, 'SKILL.md'))) {
|
|
107
|
+
skillsWithMd++;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
check(`skills 包含 SKILL.md 文件 (${skillsWithMd})`, skillsWithMd > 0);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log('\n📜 Rules 检查:');
|
|
114
|
+
const rulesDir = path.join(PLUGIN_DIR, 'rules');
|
|
115
|
+
if (fs.existsSync(rulesDir)) {
|
|
116
|
+
const ruleItems = fs.readdirSync(rulesDir);
|
|
117
|
+
check(`rules 目录包含内容 (${ruleItems.length})`, ruleItems.length > 0);
|
|
118
|
+
|
|
119
|
+
// 检查通用规则
|
|
120
|
+
check('common 规则目录存在', fs.existsSync(path.join(rulesDir, 'common')));
|
|
121
|
+
check('zh 规则目录存在', fs.existsSync(path.join(rulesDir, 'zh')));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log('\n⚡ Hooks 检查:');
|
|
125
|
+
const hooksPath = path.join(OPENCODE_HOME, 'plugins', 'tsp-hooks.js');
|
|
126
|
+
if (fs.existsSync(hooksPath)) {
|
|
127
|
+
const hooksContent = fs.readFileSync(hooksPath, 'utf8');
|
|
128
|
+
check('tsp-hooks.js 包含插件函数', hooksContent.includes('module.exports = function tspHooks'));
|
|
129
|
+
check('tsp-hooks.js 包含 tool.execute.before', hooksContent.includes("'tool.execute.before'"));
|
|
130
|
+
check('tsp-hooks.js 包含 tool.execute.after', hooksContent.includes("'tool.execute.after'"));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 总结
|
|
134
|
+
console.log('\n=== 测试总结 ===');
|
|
135
|
+
console.log(`通过: ${passed}`);
|
|
136
|
+
console.log(`失败: ${failed}`);
|
|
137
|
+
console.log(`总计: ${passed + failed}`);
|
|
138
|
+
|
|
139
|
+
if (failed === 0) {
|
|
140
|
+
console.log('\n🎉 所有测试通过!OpenCode 安装成功。');
|
|
141
|
+
console.log('\n下一步:');
|
|
142
|
+
console.log('1. 启动 OpenCode: opencode');
|
|
143
|
+
console.log('2. 查看可用角色: AGENTS.md 中包含所有角色索引');
|
|
144
|
+
console.log('3. 执行团队命令: /team-intake, /team-plan 等');
|
|
145
|
+
console.log('4. 加载技能: skill frontend-engineering');
|
|
146
|
+
console.log('5. 引用规则: @rules/common/coding-style.md');
|
|
147
|
+
process.exit(0);
|
|
148
|
+
} else {
|
|
149
|
+
console.log('\n⚠️ 部分测试失败,请检查安装。');
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: goal-convergence
|
|
3
|
+
description: "Goal-oriented autonomous loop with external completion oracle. Keeps iterating until verifiable stopping conditions are met, checked by a separate model."
|
|
4
|
+
origin: ECC
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Goal Convergence
|
|
8
|
+
|
|
9
|
+
Goal-oriented autonomous loop with external completion oracle.
|
|
10
|
+
Based on Addy Osmani's Loop Engineering principle: "Keep going until a verifiable
|
|
11
|
+
stopping condition holds, with a separate small model checking completion."
|
|
12
|
+
|
|
13
|
+
## When to Activate
|
|
14
|
+
|
|
15
|
+
- User invokes `/goal` command
|
|
16
|
+
- Heartbeat auto-creates a goal from scan failures
|
|
17
|
+
- Triage item is promoted to a goal via `/triage act <id> goal`
|
|
18
|
+
- Session resumes with an active goal state file
|
|
19
|
+
|
|
20
|
+
## Core Concept: Maker-Oracle Separation
|
|
21
|
+
|
|
22
|
+
The model that wrote the code is too nice grading its own homework.
|
|
23
|
+
A second model (the oracle) with different instructions and READ-ONLY tools
|
|
24
|
+
catches what the first talked itself into.
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
Maker (primary model, full tools)
|
|
28
|
+
│
|
|
29
|
+
▼ produces iteration output
|
|
30
|
+
│
|
|
31
|
+
Oracle (different model, read-only)
|
|
32
|
+
│
|
|
33
|
+
├─ ALL conditions pass → CONVERGE (goal done)
|
|
34
|
+
├─ Some fail + budget remains → nextHint → LOOP (maker iterates)
|
|
35
|
+
└─ Budget exhausted → ESCALATE (triage inbox)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Goal Lifecycle
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
CREATE → ACTIVE → [iterate] → CONVERGED
|
|
42
|
+
│
|
|
43
|
+
├──── PAUSED (manual /goal pause)
|
|
44
|
+
└──── ESCALATED (budget exhausted or repeated failure)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
States:
|
|
48
|
+
- `active`: Maker-oracle loop is running
|
|
49
|
+
- `paused`: Manually paused, resumes with `/goal resume`
|
|
50
|
+
- `converged`: All stopping conditions met, goal complete
|
|
51
|
+
- `escalated`: Cannot converge within budget, needs human triage
|
|
52
|
+
- `failed`: Explicitly abandoned
|
|
53
|
+
|
|
54
|
+
## Stopping Conditions
|
|
55
|
+
|
|
56
|
+
Each goal has one or more stopping conditions. ALL must pass for convergence.
|
|
57
|
+
|
|
58
|
+
| Type | Command Pattern | Example |
|
|
59
|
+
|------|----------------|---------|
|
|
60
|
+
| `test_pass` | Test runner exits 0 | `npm test` |
|
|
61
|
+
| `lint_clean` | Linter exits 0 | `npm run lint -- --quiet` |
|
|
62
|
+
| `coverage_threshold` | Coverage >= N% | `npm test -- --coverage` |
|
|
63
|
+
| `build_pass` | Build succeeds | `npm run build` |
|
|
64
|
+
| `custom_command` | Any command exits 0 | `grep -r "TODO" src/ \| wc -l` |
|
|
65
|
+
|
|
66
|
+
## Oracle Protocol
|
|
67
|
+
|
|
68
|
+
The oracle receives:
|
|
69
|
+
1. The goal objective (natural language)
|
|
70
|
+
2. All stopping condition commands and their latest output
|
|
71
|
+
3. The maker's iteration summary
|
|
72
|
+
4. Previous iteration history (last 3)
|
|
73
|
+
|
|
74
|
+
The oracle returns:
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"converged": false,
|
|
78
|
+
"conditionResults": [
|
|
79
|
+
{"type": "test_pass", "passed": false, "output": "2 tests failing"},
|
|
80
|
+
{"type": "lint_clean", "passed": true, "output": ""}
|
|
81
|
+
],
|
|
82
|
+
"reasons": ["2 tests still failing in auth module"],
|
|
83
|
+
"nextHint": "Focus on src/auth/refresh.test.ts — the token expiry mock is stale",
|
|
84
|
+
"confidence": 0.85
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Budget Management
|
|
89
|
+
|
|
90
|
+
Three budget dimensions, any exhaustion triggers escalation:
|
|
91
|
+
|
|
92
|
+
| Dimension | Default | Rationale |
|
|
93
|
+
|-----------|---------|-----------|
|
|
94
|
+
| Iterations | 15 | Prevents infinite loops |
|
|
95
|
+
| Wall time | 2h | Bounds real-world duration |
|
|
96
|
+
| Cost (USD) | $10 | Prevents runaway API spend |
|
|
97
|
+
|
|
98
|
+
On escalation, the goal:
|
|
99
|
+
1. Persists full state to `~/.claude/goals/{goalId}.json`
|
|
100
|
+
2. Creates a triage inbox item with context
|
|
101
|
+
3. Marks state as `escalated`
|
|
102
|
+
4. Reports final status to user
|
|
103
|
+
|
|
104
|
+
## Inter-Session Persistence
|
|
105
|
+
|
|
106
|
+
Goals survive session restarts:
|
|
107
|
+
1. `SessionEnd` hook serializes active goals
|
|
108
|
+
2. `SessionStart` hook detects active goals and notifies user
|
|
109
|
+
3. `/goal resume` re-enters the loop with oracle's last `nextHint`
|
|
110
|
+
|
|
111
|
+
State file: `~/.claude/goals/{goalId}.json` (follows `schemas/goal.schema.json`)
|
|
112
|
+
|
|
113
|
+
## Integration Points
|
|
114
|
+
|
|
115
|
+
- **`/heartbeat`**: Auto-creates goals from scan failures
|
|
116
|
+
- **`/triage`**: Escalated goals land in triage; triage items can become goals
|
|
117
|
+
- **`/checkpoint`**: Each iteration implicitly checkpoints
|
|
118
|
+
- **`/verify`**: Oracle internally uses verification patterns
|
|
119
|
+
- **`wave-execution`**: Multiple goals can run in parallel waves
|
|
120
|
+
- **`rework-loop`**: Failed iterations trigger blame-attributed rework
|
|
121
|
+
|
|
122
|
+
## Hard Bans
|
|
123
|
+
|
|
124
|
+
- Oracle MUST be a different model than maker (eliminates self-grading bias)
|
|
125
|
+
- Oracle MUST NOT have write tools (prevents it from "helping" fix issues)
|
|
126
|
+
- Goals MUST have at least one stopping condition (no open-ended loops)
|
|
127
|
+
- Budget MUST have at least one limit set (no unbounded execution)
|
|
128
|
+
- State MUST persist to disk (no goals lost on crash)
|
|
129
|
+
|
|
130
|
+
## Example
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
User: /goal "make all tests pass"
|
|
134
|
+
|
|
135
|
+
System infers:
|
|
136
|
+
objective: "make all tests pass"
|
|
137
|
+
stoppingConditions: [{type: "test_pass", command: "npm test"}]
|
|
138
|
+
budget: {maxIterations: 15, maxDuration: "2h", maxDollars: 10}
|
|
139
|
+
oracle: {model: "haiku"}
|
|
140
|
+
|
|
141
|
+
Iteration 1:
|
|
142
|
+
Maker: reads test output, fixes obvious import error in auth.ts
|
|
143
|
+
Oracle: runs `npm test` → 1 test still failing → {converged: false, nextHint: "..."}
|
|
144
|
+
|
|
145
|
+
Iteration 2:
|
|
146
|
+
Maker: fixes token refresh logic based on oracle hint
|
|
147
|
+
Oracle: runs `npm test` → all pass → {converged: true}
|
|
148
|
+
|
|
149
|
+
Result: CONVERGED in 2 iterations, $0.45 spent
|
|
150
|
+
```
|
|
@@ -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"
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: loop-heartbeat
|
|
3
|
+
description: "Scheduled discovery automation that runs scans on a heartbeat interval, classifies findings, and routes them to goals or triage inbox."
|
|
4
|
+
origin: ECC
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Loop Heartbeat
|
|
8
|
+
|
|
9
|
+
Scheduled discovery automation that runs scans, finds issues, and routes findings
|
|
10
|
+
to goals or triage. The heartbeat is what makes a loop a loop — not a one-shot run.
|
|
11
|
+
|
|
12
|
+
Based on Addy Osmani's Loop Engineering: "Automations are the thing that makes
|
|
13
|
+
a loop an actual loop. Run prompts on a schedule. Findings go to a triage inbox;
|
|
14
|
+
empty runs archive themselves."
|
|
15
|
+
|
|
16
|
+
## When to Activate
|
|
17
|
+
|
|
18
|
+
- User invokes `/heartbeat start` to begin scheduled scans
|
|
19
|
+
- User invokes `/heartbeat run` for a one-shot scan
|
|
20
|
+
- A goal converges and follow-up scanning is needed
|
|
21
|
+
- Project onboarding includes quality monitoring setup
|
|
22
|
+
|
|
23
|
+
## Core Concept
|
|
24
|
+
|
|
25
|
+
The heartbeat is the "discovery layer" of a loop. It answers: "What's broken right
|
|
26
|
+
now?" on a recurring basis without human prompting.
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
Every 30 minutes:
|
|
30
|
+
┌─ Run test suite ─── PASS → skip
|
|
31
|
+
├─ Run linter ─────── FAIL → create goal (auto-fix)
|
|
32
|
+
├─ Audit deps ─────── FAIL → triage inbox (human decision)
|
|
33
|
+
└─ Type check ─────── PASS → skip
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Configuration
|
|
37
|
+
|
|
38
|
+
Create `.claude/heartbeat.yaml` in the project root:
|
|
39
|
+
|
|
40
|
+
```yaml
|
|
41
|
+
heartbeat:
|
|
42
|
+
interval: "30m"
|
|
43
|
+
scans:
|
|
44
|
+
- name: "test-health"
|
|
45
|
+
command: "npm test 2>&1 | tail -10"
|
|
46
|
+
onFailure: "auto-goal"
|
|
47
|
+
description: "Test suite health"
|
|
48
|
+
- name: "lint-drift"
|
|
49
|
+
command: "npm run lint -- --quiet 2>&1 | wc -l"
|
|
50
|
+
threshold: 0
|
|
51
|
+
onFailure: "triage"
|
|
52
|
+
description: "Lint error count"
|
|
53
|
+
- name: "type-check"
|
|
54
|
+
command: "npx tsc --noEmit 2>&1; echo EXIT:$?"
|
|
55
|
+
onFailure: "auto-goal"
|
|
56
|
+
description: "TypeScript type errors"
|
|
57
|
+
budget:
|
|
58
|
+
maxDollarsPerHour: 2.0
|
|
59
|
+
pauseOnExhaust: true
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Scan Result Classification
|
|
63
|
+
|
|
64
|
+
| onFailure | Behavior | Use When |
|
|
65
|
+
|-----------|----------|----------|
|
|
66
|
+
| `auto-goal` | Creates `/goal` automatically | Machine can fix it (tests, lint, types) |
|
|
67
|
+
| `triage` | Adds to `/triage` inbox | Human judgment needed (deps, security) |
|
|
68
|
+
| `notify` | Desktop notification only | Informational, no action required |
|
|
69
|
+
| `ignore` | Log silently | Monitoring only, aggregate later |
|
|
70
|
+
|
|
71
|
+
## Budget Controls
|
|
72
|
+
|
|
73
|
+
- `maxDollarsPerHour`: Pause heartbeat if scanning costs exceed this
|
|
74
|
+
- `pauseOnExhaust`: If true, pause (resumable); if false, stop entirely
|
|
75
|
+
- Goal creation inherits the default goal budget from `/goal` settings
|
|
76
|
+
|
|
77
|
+
## Empty Run Handling
|
|
78
|
+
|
|
79
|
+
When all scans pass:
|
|
80
|
+
- No goals or triage items are created
|
|
81
|
+
- Run is logged to `~/.claude/heartbeat-last-run.json`
|
|
82
|
+
- Next run proceeds on schedule
|
|
83
|
+
- This is the "archive themselves silently" behavior
|
|
84
|
+
|
|
85
|
+
## Integration Points
|
|
86
|
+
|
|
87
|
+
- **`/goal`**: `auto-goal` failures create goals via completion-oracle.js
|
|
88
|
+
- **`/triage`**: `triage` failures append to `~/.claude/triage/inbox.jsonl`
|
|
89
|
+
- **CronCreate**: Scheduling uses the Claude Code CronCreate primitive
|
|
90
|
+
- **ScheduleWakeup**: Alternative for dynamic-interval loops
|
|
91
|
+
- **Hooks**: Runs as a scheduled task, not a synchronous hook
|
|
92
|
+
|
|
93
|
+
## Hard Bans
|
|
94
|
+
|
|
95
|
+
- Heartbeat MUST NOT modify code (it's discovery-only)
|
|
96
|
+
- Heartbeat MUST NOT create goals without a stopping condition
|
|
97
|
+
- Budget limits MUST be enforced (no unbounded scan spending)
|
|
98
|
+
- Failed scans MUST be classified (no silent failures)
|
|
99
|
+
- Empty runs MUST NOT create noise (pass = skip)
|
|
100
|
+
|
|
101
|
+
## Example Session
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
User: /heartbeat start
|
|
105
|
+
|
|
106
|
+
System:
|
|
107
|
+
Loaded .claude/heartbeat.yaml (3 scans configured)
|
|
108
|
+
Registered CronCreate: every 30m
|
|
109
|
+
Next run: 30m from now
|
|
110
|
+
Budget: $2.00/hour
|
|
111
|
+
|
|
112
|
+
[30 minutes later, heartbeat fires]
|
|
113
|
+
|
|
114
|
+
Scan results:
|
|
115
|
+
✓ test-health: all tests pass
|
|
116
|
+
✗ lint-drift: 4 lint errors found → created goal-a1b2c3d4
|
|
117
|
+
✓ type-check: no type errors
|
|
118
|
+
|
|
119
|
+
Summary: 2/3 scans passed, 1 goal created
|
|
120
|
+
```
|