@goodandready/dsh-goal 0.2.12 → 0.2.14
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/lib/addon-store.js +213 -0
- package/lib/engine-reports.js +112 -33
- package/lib/git-snapshot-manager.js +99 -0
- package/lib/index.js +226 -395
- package/lib/milestone-manager.js +141 -0
- package/lib/routes.js +249 -138
- package/lib/token-budget-guard.js +128 -0
- package/lib/tools.js +115 -257
- package/package.json +9 -3
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token budget guard: monitors goal session tokens via ctx.tokenMeter,
|
|
3
|
+
* emits warning at threshold (default 80%), and calls ctx.goals.block() at 100%.
|
|
4
|
+
*/
|
|
5
|
+
export class TokenBudgetGuard {
|
|
6
|
+
constructor({
|
|
7
|
+
maxTokenBudget = 0,
|
|
8
|
+
warningThreshold = 80,
|
|
9
|
+
autoScaleNearCompletion = false,
|
|
10
|
+
logger = console,
|
|
11
|
+
} = {}) {
|
|
12
|
+
this.maxTokenBudget = maxTokenBudget;
|
|
13
|
+
this.warningThreshold = warningThreshold;
|
|
14
|
+
this.autoScaleNearCompletion = autoScaleNearCompletion;
|
|
15
|
+
this.logger = logger;
|
|
16
|
+
this.warnedGoals = new Set();
|
|
17
|
+
this.blockedGoals = new Set();
|
|
18
|
+
this.autoScaledGoals = new Set();
|
|
19
|
+
this.customBudgets = new Map(); // goalId -> maxTokenBudget override
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
updateConfig({ maxTokenBudget, warningThreshold, autoScaleNearCompletion }) {
|
|
23
|
+
if (typeof maxTokenBudget === 'number') this.maxTokenBudget = maxTokenBudget;
|
|
24
|
+
if (typeof warningThreshold === 'number') this.warningThreshold = warningThreshold;
|
|
25
|
+
if (typeof autoScaleNearCompletion === 'boolean') this.autoScaleNearCompletion = autoScaleNearCompletion;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
setGoalBudget(goalId, limit) {
|
|
29
|
+
if (goalId && typeof limit === 'number') {
|
|
30
|
+
this.customBudgets.set(goalId, limit);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
getEffectiveBudget(goalId) {
|
|
35
|
+
if (goalId && this.customBudgets.has(goalId)) {
|
|
36
|
+
return this.customBudgets.get(goalId);
|
|
37
|
+
}
|
|
38
|
+
return this.maxTokenBudget;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Evaluate token consumption and trigger warnings or goal blocking
|
|
43
|
+
* @param {object} params
|
|
44
|
+
* @param {object} params.agent
|
|
45
|
+
* @param {object} params.goal - current goal snapshot { id, revision, phase }
|
|
46
|
+
* @param {object} [params.tokenMeter] - ctx.tokenMeter
|
|
47
|
+
* @param {object} [params.goalsService] - ctx.goals
|
|
48
|
+
* @param {Array} [params.milestones] - milestone list to test near-completion
|
|
49
|
+
* @returns {{ status: 'ok'|'warning'|'blocked'|'auto_scaled', message?: string, tokensUsed: number, budget: number, pct: number }}
|
|
50
|
+
*/
|
|
51
|
+
checkBudget({ agent, goal, tokenMeter, goalsService, milestones = [] }) {
|
|
52
|
+
if (!goal || !goal.id) return { status: 'ok', tokensUsed: 0, budget: 0, pct: 0 };
|
|
53
|
+
const goalId = goal.id;
|
|
54
|
+
const limit = this.getEffectiveBudget(goalId);
|
|
55
|
+
if (limit <= 0) return { status: 'ok', tokensUsed: 0, budget: 0, pct: 0 };
|
|
56
|
+
|
|
57
|
+
let tokensUsed = 0;
|
|
58
|
+
if (tokenMeter && agent?.session) {
|
|
59
|
+
try {
|
|
60
|
+
const measurement = tokenMeter.measure(agent.session);
|
|
61
|
+
tokensUsed = measurement?.totalTokens || 0;
|
|
62
|
+
} catch (err) {
|
|
63
|
+
this.logger.warn?.('[TokenBudgetGuard] Failed to measure tokens:', err.message);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const pct = Math.round((tokensUsed / limit) * 100);
|
|
68
|
+
|
|
69
|
+
// 1. Check if budget exceeded (>= 100%)
|
|
70
|
+
if (tokensUsed >= limit) {
|
|
71
|
+
// Check auto-scale buffer near completion (>= 75% milestones done)
|
|
72
|
+
if (this.autoScaleNearCompletion && !this.autoScaledGoals.has(goalId)) {
|
|
73
|
+
const total = milestones.length;
|
|
74
|
+
const done = milestones.filter(m => m.completed).length;
|
|
75
|
+
const nearDone = total > 0 && (done / total >= 0.75 || done === total - 1);
|
|
76
|
+
if (nearDone) {
|
|
77
|
+
const extension = Math.round(limit * 0.2); // +20%
|
|
78
|
+
const newLimit = limit + extension;
|
|
79
|
+
this.setGoalBudget(goalId, newLimit);
|
|
80
|
+
this.autoScaledGoals.add(goalId);
|
|
81
|
+
const msg = `⚡ Token budget auto-scaled: added +${extension.toLocaleString()} tokens buffer near goal completion (${newLimit.toLocaleString()} total).`;
|
|
82
|
+
this.logger.info?.(`[TokenBudgetGuard] ${msg}`);
|
|
83
|
+
return { status: 'auto_scaled', message: msg, tokensUsed, budget: newLimit, pct: Math.round((tokensUsed / newLimit) * 100) };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Block goal via core GoalService
|
|
88
|
+
const msg = `Token budget limit (${limit.toLocaleString()}) reached (${tokensUsed.toLocaleString()} tokens used, ${pct}%). Goal execution is blocked.`;
|
|
89
|
+
if (!this.blockedGoals.has(goalId)) {
|
|
90
|
+
this.blockedGoals.add(goalId);
|
|
91
|
+
if (goalsService && typeof goalsService.block === 'function' && agent) {
|
|
92
|
+
try {
|
|
93
|
+
goalsService.block(agent, { id: goal.id, revision: goal.revision }, {
|
|
94
|
+
code: 'budget',
|
|
95
|
+
message: msg,
|
|
96
|
+
});
|
|
97
|
+
this.logger.info?.(`[TokenBudgetGuard] Blocked goal "${goalId}" via ctx.goals.block()`);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
this.logger.warn?.('[TokenBudgetGuard] ctx.goals.block() failed:', err.message);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { status: 'blocked', message: msg, tokensUsed, budget: limit, pct };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 2. Check warning threshold (e.g. >= 80%)
|
|
107
|
+
if (pct >= this.warningThreshold) {
|
|
108
|
+
const msg = `⚠️ Token budget warning: ${tokensUsed.toLocaleString()} of ${limit.toLocaleString()} tokens used (${pct}%). Goal will block at 100%.`;
|
|
109
|
+
const isNewWarning = !this.warnedGoals.has(goalId);
|
|
110
|
+
if (isNewWarning) {
|
|
111
|
+
this.warnedGoals.add(goalId);
|
|
112
|
+
this.logger.warn?.(`[TokenBudgetGuard] Goal "${goalId}" reached ${pct}% of token budget.`);
|
|
113
|
+
}
|
|
114
|
+
return { status: 'warning', message: msg, tokensUsed, budget: limit, pct, isNewWarning };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { status: 'ok', tokensUsed, budget: limit, pct };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
resetGoal(goalId) {
|
|
121
|
+
if (goalId) {
|
|
122
|
+
this.warnedGoals.delete(goalId);
|
|
123
|
+
this.blockedGoals.delete(goalId);
|
|
124
|
+
this.autoScaledGoals.delete(goalId);
|
|
125
|
+
this.customBudgets.delete(goalId);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
package/lib/tools.js
CHANGED
|
@@ -1,327 +1,185 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export const JSON_OUTPUT = {
|
|
4
|
-
schema: { type: 'object', additionalProperties: true },
|
|
5
|
-
render: (_args, val) => [{ type: 'text', text: JSON.stringify(val) }],
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Format goal snapshot into DSH core-compatible goal representation
|
|
10
|
-
* @param {any} snap
|
|
11
|
-
* @returns {object}
|
|
12
|
-
*/
|
|
13
|
-
export function formatGoalValue(snap) {
|
|
14
|
-
if (!snap || !snap.hasActiveGoal) {
|
|
15
|
-
return { goal: null };
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
let phase = 'active';
|
|
19
|
-
if (snap.state === GoalState.PAUSED) phase = 'paused';
|
|
20
|
-
else if (snap.state === GoalState.COMPLETED) phase = 'complete';
|
|
21
|
-
|
|
22
|
-
const roundsStarted = Number.isInteger(snap.iterations) ? snap.iterations : 0;
|
|
23
|
-
const maxGoalRounds = Number.isInteger(snap.maxIterations) ? snap.maxIterations : 25;
|
|
24
|
-
|
|
25
|
-
return {
|
|
26
|
-
goal: {
|
|
27
|
-
id: snap.id || 'goal-active',
|
|
28
|
-
revision: 1,
|
|
29
|
-
objective: snap.title || 'Goal',
|
|
30
|
-
phase,
|
|
31
|
-
roundsStarted,
|
|
32
|
-
maxGoalRounds,
|
|
33
|
-
},
|
|
34
|
-
activation: snap.state === GoalState.RUNNING ? 'armed' : 'disarmed',
|
|
35
|
-
};
|
|
36
|
-
}
|
|
1
|
+
import { MilestoneManager } from './milestone-manager.js';
|
|
2
|
+
import { AddonStore } from './addon-store.js';
|
|
37
3
|
|
|
38
4
|
/**
|
|
39
|
-
*
|
|
5
|
+
* Tools module for @goodandready/dsh-goal (Core Addon).
|
|
6
|
+
*
|
|
7
|
+
* Register addon goal tools: goal_milestones and goal_checklist.
|
|
8
|
+
* Does NOT register create_goal, get_goal, update_goal (core provides them).
|
|
9
|
+
*
|
|
40
10
|
* @param {any} ctx
|
|
41
|
-
* @param {object} options
|
|
42
|
-
* @param {
|
|
43
|
-
* @param {
|
|
11
|
+
* @param {object} [options]
|
|
12
|
+
* @param {import('./milestone-manager.js').MilestoneManager} [options.milestoneManager]
|
|
13
|
+
* @param {object} [options.goalsService] - core GoalService (ctx.goals)
|
|
14
|
+
* @param {object} [options.logger]
|
|
44
15
|
*/
|
|
45
|
-
export function registerTools(ctx, {
|
|
16
|
+
export function registerTools(ctx, { milestoneManager, goalsService, logger = console } = {}) {
|
|
17
|
+
const mm = milestoneManager || new MilestoneManager(new AddonStore(null, logger), { logger });
|
|
18
|
+
|
|
46
19
|
ctx.inject(['tools'], (tctx) => {
|
|
47
20
|
if (!tctx.tools?.register) return;
|
|
48
21
|
|
|
49
|
-
const displacedTools = new Map();
|
|
50
22
|
const safeRegister = (definition) => {
|
|
51
23
|
try {
|
|
52
24
|
const name = definition.name;
|
|
53
|
-
const globalTools = tctx.tools.layers?.global?.tools;
|
|
54
|
-
if (globalTools?.data instanceof Map && globalTools.data.has(name)) {
|
|
55
|
-
displacedTools.set(name, globalTools.data.get(name));
|
|
56
|
-
globalTools.data.delete(name);
|
|
57
|
-
}
|
|
58
25
|
const unregister = tctx.tools.register(definition);
|
|
59
26
|
if (typeof unregister === 'function') {
|
|
60
|
-
tctx.effect(() => () => {
|
|
61
|
-
unregister();
|
|
62
|
-
if (displacedTools.has(name) && globalTools?.data instanceof Map) {
|
|
63
|
-
globalTools.data.set(name, displacedTools.get(name));
|
|
64
|
-
}
|
|
27
|
+
tctx.effect?.(() => () => {
|
|
28
|
+
try { unregister(); } catch (e) { /* unregister error non-fatal */ }
|
|
65
29
|
}, `dsh-goal: tool ${name}`);
|
|
66
30
|
}
|
|
67
31
|
} catch (err) {
|
|
68
|
-
logger.warn(`[dsh-goal] Tool ${definition.name} registration skipped:`, err.message);
|
|
32
|
+
logger.warn?.(`[dsh-goal] Tool ${definition.name} registration skipped:`, err.message);
|
|
69
33
|
}
|
|
70
34
|
};
|
|
71
35
|
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
execute: handleGetGoal,
|
|
85
|
-
handler: handleGetGoal,
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
// Tool 2: create_goal
|
|
89
|
-
const handleCreateGoal = async (args, toolCtx) => {
|
|
90
|
-
const sid = sessionIdOf(toolCtx, 'default');
|
|
91
|
-
const maxIterations = Number(args.max_goal_rounds) || 25;
|
|
92
|
-
const snap = engine.startGoal(args.objective, { maxIterations }, sid);
|
|
93
|
-
return formatGoalValue(snap);
|
|
36
|
+
/** Helper to resolve current goal ID for a tool execution */
|
|
37
|
+
const resolveGoalId = (toolCtx) => {
|
|
38
|
+
if (goalsService) {
|
|
39
|
+
try {
|
|
40
|
+
const agent = toolCtx?.agent || (tctx.agents && toolCtx?.agentId ? tctx.agents.get(toolCtx.agentId) : null);
|
|
41
|
+
if (agent && typeof goalsService.get === 'function') {
|
|
42
|
+
const current = goalsService.get(agent);
|
|
43
|
+
if (current?.id) return current.id;
|
|
44
|
+
}
|
|
45
|
+
} catch (e) { /* ignore */ }
|
|
46
|
+
}
|
|
47
|
+
return toolCtx?.sessionId || toolCtx?.session?.id || 'default';
|
|
94
48
|
};
|
|
95
49
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
type: 'object',
|
|
101
|
-
properties: {
|
|
102
|
-
objective: { type: 'string', description: 'The overarching objective to achieve' },
|
|
103
|
-
max_goal_rounds: { type: 'number', description: 'Maximum iterations allowed' },
|
|
104
|
-
},
|
|
105
|
-
required: ['objective'],
|
|
50
|
+
const JSON_OUTPUT = {
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties: {
|
|
53
|
+
ok: { type: 'boolean' },
|
|
106
54
|
},
|
|
107
|
-
|
|
108
|
-
execute: handleCreateGoal,
|
|
109
|
-
handler: handleCreateGoal,
|
|
110
|
-
});
|
|
55
|
+
};
|
|
111
56
|
|
|
112
|
-
// Tool
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
const action = args.action;
|
|
57
|
+
// Tool 1: goal_milestones
|
|
58
|
+
const handleGoalMilestones = async (args = {}, toolCtx = {}) => {
|
|
59
|
+
const goalId = args.goalId || args.goal_id || resolveGoalId(toolCtx);
|
|
60
|
+
const action = args.action || 'list';
|
|
61
|
+
const milestoneId = args.milestoneId || args.milestone_id;
|
|
116
62
|
|
|
117
|
-
if (action === '
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
if (toolCtx?.deferContext) {
|
|
121
|
-
try {
|
|
122
|
-
toolCtx.deferContext({
|
|
123
|
-
type: 'text',
|
|
124
|
-
text: `<goal_complete>\nObjective: ${JSON.stringify(snap.title || summary)}\nThe goal is marked complete. Summarize what was accomplished for the user.\n</goal_complete>`,
|
|
125
|
-
});
|
|
126
|
-
} catch (err) {
|
|
127
|
-
toolCtx?.logger?.debug?.('[dsh-goal] Failed to defer complete context:', err);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return formatGoalValue(snap);
|
|
63
|
+
if (action === 'list') {
|
|
64
|
+
const list = mm.getMilestones(goalId);
|
|
65
|
+
return { ok: true, goalId, milestones: list };
|
|
131
66
|
}
|
|
132
67
|
|
|
133
|
-
if (action === '
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
return formatGoalValue(snap);
|
|
68
|
+
if (action === 'set') {
|
|
69
|
+
const list = mm.setMilestones(goalId, args.milestones);
|
|
70
|
+
return { ok: true, goalId, count: list.length, milestones: list };
|
|
137
71
|
}
|
|
138
72
|
|
|
139
|
-
if (action === '
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (action === 'edit') {
|
|
148
|
-
const snap = engine.getSnapshot(sid);
|
|
149
|
-
if (args.objective) snap.title = args.objective;
|
|
150
|
-
if (args.max_goal_rounds) snap.maxIterations = Number(args.max_goal_rounds);
|
|
151
|
-
engine.emit(sid, true);
|
|
152
|
-
return formatGoalValue(snap);
|
|
73
|
+
if (action === 'add') {
|
|
74
|
+
const item = mm.addMilestone(goalId, {
|
|
75
|
+
title: args.title,
|
|
76
|
+
checklist: args.checklist,
|
|
77
|
+
dependsOn: args.dependsOn || args.depends_on,
|
|
78
|
+
});
|
|
79
|
+
return { ok: true, goalId, milestone: item };
|
|
153
80
|
}
|
|
154
81
|
|
|
155
|
-
if (action === '
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (toolCtx?.deferContext) {
|
|
159
|
-
try {
|
|
160
|
-
toolCtx.deferContext({
|
|
161
|
-
type: 'text',
|
|
162
|
-
text: `<goal_blocked>\nObjective: ${JSON.stringify(snap.title || 'Goal')}\nBlocked: ${JSON.stringify(reason)}\nExplain to the user what blocked progress.\n</goal_blocked>`,
|
|
163
|
-
});
|
|
164
|
-
} catch (err) {
|
|
165
|
-
toolCtx?.logger?.debug?.('[dsh-goal] Failed to defer blocked context:', err);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
const res = formatGoalValue(snap);
|
|
169
|
-
if (res.goal) {
|
|
170
|
-
res.goal.phase = 'blocked';
|
|
171
|
-
res.goal.blockedReason = { code: 'model-reported', message: reason };
|
|
82
|
+
if (action === 'complete') {
|
|
83
|
+
if (!milestoneId) {
|
|
84
|
+
throw new Error('milestoneId is required for complete action');
|
|
172
85
|
}
|
|
173
|
-
|
|
86
|
+
const completed = await mm.completeMilestone(goalId, milestoneId, {
|
|
87
|
+
cwd: toolCtx?.session?.cwd || toolCtx?.cwd || process.cwd(),
|
|
88
|
+
});
|
|
89
|
+
return { ok: true, goalId, milestoneId, milestone: completed };
|
|
174
90
|
}
|
|
175
91
|
|
|
176
|
-
|
|
92
|
+
throw new Error(`Unsupported action "${action}". Allowed: list, set, add, complete`);
|
|
177
93
|
};
|
|
178
94
|
|
|
179
95
|
safeRegister({
|
|
180
|
-
name: '
|
|
181
|
-
description: '
|
|
96
|
+
name: 'goal_milestones',
|
|
97
|
+
description: 'Manage milestones and work phases for the active goal. Supports list, set, add, and complete.',
|
|
182
98
|
parameters: {
|
|
183
99
|
type: 'object',
|
|
184
100
|
properties: {
|
|
185
101
|
action: {
|
|
186
102
|
type: 'string',
|
|
187
|
-
enum: ['
|
|
188
|
-
description: '
|
|
103
|
+
enum: ['list', 'set', 'add', 'complete'],
|
|
104
|
+
description: 'Milestone action to perform',
|
|
189
105
|
},
|
|
190
|
-
objective: { type: 'string', description: 'Updated goal title (for edit)' },
|
|
191
|
-
max_goal_rounds: { type: 'number', description: 'Updated iterations limit (for edit)' },
|
|
192
|
-
blocked_reason: { type: 'string', description: 'Reason for pause, blocked, or completion summary' },
|
|
193
|
-
},
|
|
194
|
-
required: ['action'],
|
|
195
|
-
},
|
|
196
|
-
output: JSON_OUTPUT,
|
|
197
|
-
execute: handleUpdateGoal,
|
|
198
|
-
handler: handleUpdateGoal,
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
// Tool 4: goal_set_milestones
|
|
202
|
-
const handleSetMilestones = async ({ milestones }, toolCtx) => {
|
|
203
|
-
const sid = sessionIdOf(toolCtx, 'default');
|
|
204
|
-
const snap = engine.setMilestones(milestones, sid);
|
|
205
|
-
if (typeof engine.recordToolActivity === 'function') {
|
|
206
|
-
engine.recordToolActivity('goal_set_milestones', 'success', 'Defined milestones', sid);
|
|
207
|
-
}
|
|
208
|
-
return {
|
|
209
|
-
success: true,
|
|
210
|
-
count: snap.milestones?.length || 0,
|
|
211
|
-
milestones: snap.milestones,
|
|
212
|
-
};
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
safeRegister({
|
|
216
|
-
name: 'goal_set_milestones',
|
|
217
|
-
description: 'Decompose the goal into 3-7 verifiable milestones.',
|
|
218
|
-
parameters: {
|
|
219
|
-
type: 'object',
|
|
220
|
-
properties: {
|
|
221
106
|
milestones: {
|
|
222
107
|
type: 'array',
|
|
223
108
|
items: {
|
|
224
109
|
type: 'object',
|
|
225
110
|
properties: {
|
|
226
|
-
id: { type: 'string'
|
|
227
|
-
title: { type: 'string'
|
|
228
|
-
|
|
111
|
+
id: { type: 'string' },
|
|
112
|
+
title: { type: 'string' },
|
|
113
|
+
checklist: {
|
|
114
|
+
type: 'array',
|
|
115
|
+
items: {
|
|
116
|
+
type: 'object',
|
|
117
|
+
properties: {
|
|
118
|
+
text: { type: 'string' },
|
|
119
|
+
done: { type: 'boolean' },
|
|
120
|
+
},
|
|
121
|
+
required: ['text'],
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
dependsOn: { type: 'array', items: { type: 'string' } },
|
|
229
125
|
},
|
|
230
|
-
required: ['
|
|
126
|
+
required: ['title'],
|
|
231
127
|
},
|
|
232
|
-
description: '
|
|
128
|
+
description: 'Array of milestones when action is "set"',
|
|
233
129
|
},
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
},
|
|
237
|
-
output: JSON_OUTPUT,
|
|
238
|
-
execute: handleSetMilestones,
|
|
239
|
-
handler: handleSetMilestones,
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
// Tool 5: goal_update_progress
|
|
243
|
-
const handleUpdateProgress = async ({ milestone_id, status, notes, checklist }, toolCtx) => {
|
|
244
|
-
const sid = sessionIdOf(toolCtx, 'default');
|
|
245
|
-
const ok = engine.updateMilestone(milestone_id, status, notes, sid, checklist);
|
|
246
|
-
if (typeof engine.recordToolActivity === 'function') {
|
|
247
|
-
engine.recordToolActivity('goal_update_progress', ok ? 'success' : 'error', String(milestone_id) + ' -> ' + String(status), sid);
|
|
248
|
-
}
|
|
249
|
-
const snap = engine.getSnapshot(sid);
|
|
250
|
-
const targetMilestone = snap.milestones?.find((m) => m.id === String(milestone_id) || m.id.toLowerCase().replace(/[^a-z0-9]/g, '') === String(milestone_id).toLowerCase().replace(/[^a-z0-9]/g, '') || m.id.toLowerCase().replace(/[^0-9]/g, '') === String(milestone_id).toLowerCase().replace(/[^0-9]/g, ''));
|
|
251
|
-
return {
|
|
252
|
-
success: true,
|
|
253
|
-
milestone_id,
|
|
254
|
-
status,
|
|
255
|
-
checklist: targetMilestone?.checklist || null,
|
|
256
|
-
checkpointCommit: targetMilestone?.checkpointCommit || null,
|
|
257
|
-
total_milestones: snap.milestones?.length,
|
|
258
|
-
completed: snap.milestones?.filter((m) => m.status === 'completed').length,
|
|
259
|
-
};
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
safeRegister({
|
|
263
|
-
name: 'goal_update_progress',
|
|
264
|
-
description: 'Update the progress of a specific milestone, optionally attaching a sub-tasks checklist.',
|
|
265
|
-
parameters: {
|
|
266
|
-
type: 'object',
|
|
267
|
-
properties: {
|
|
268
|
-
milestone_id: { type: 'string', description: 'Milestone ID (e.g. M1)' },
|
|
269
|
-
status: {
|
|
270
|
-
type: 'string',
|
|
271
|
-
enum: ['pending', 'in_progress', 'completed', 'failed'],
|
|
272
|
-
description: 'New milestone status',
|
|
273
|
-
},
|
|
274
|
-
notes: { type: 'string', description: 'Summary of actions completed or blocking issue' },
|
|
130
|
+
title: { type: 'string', description: 'Milestone title when action is "add"' },
|
|
131
|
+
milestoneId: { type: 'string', description: 'Target milestone ID when action is "complete"' },
|
|
275
132
|
checklist: {
|
|
276
133
|
type: 'array',
|
|
277
|
-
items: {
|
|
278
|
-
|
|
279
|
-
properties: {
|
|
280
|
-
text: { type: 'string', description: 'Checklist task item description' },
|
|
281
|
-
done: { type: 'boolean', description: 'Whether the item is completed' },
|
|
282
|
-
},
|
|
283
|
-
required: ['text', 'done'],
|
|
284
|
-
},
|
|
285
|
-
description: 'Optional sub-tasks / checklist items within this milestone',
|
|
134
|
+
items: { type: 'string' },
|
|
135
|
+
description: 'Initial checklist items for added milestone',
|
|
286
136
|
},
|
|
287
137
|
},
|
|
288
|
-
required: ['
|
|
138
|
+
required: ['action'],
|
|
289
139
|
},
|
|
290
140
|
output: JSON_OUTPUT,
|
|
291
|
-
execute:
|
|
292
|
-
handler:
|
|
141
|
+
execute: handleGoalMilestones,
|
|
142
|
+
handler: handleGoalMilestones,
|
|
293
143
|
});
|
|
294
144
|
|
|
295
|
-
// Tool
|
|
296
|
-
const
|
|
297
|
-
const
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
145
|
+
// Tool 2: goal_checklist
|
|
146
|
+
const handleGoalChecklist = async (args = {}, toolCtx = {}) => {
|
|
147
|
+
const goalId = args.goalId || args.goal_id || resolveGoalId(toolCtx);
|
|
148
|
+
const milestoneId = args.milestoneId || args.milestone_id;
|
|
149
|
+
const index = args.index !== undefined ? args.index : args.item_index;
|
|
150
|
+
const action = args.action || 'toggle';
|
|
151
|
+
|
|
152
|
+
if (!milestoneId) {
|
|
153
|
+
throw new Error('milestoneId is required for goal_checklist');
|
|
301
154
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
155
|
+
if (index === undefined || index === null) {
|
|
156
|
+
throw new Error('index is required for goal_checklist');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (action === 'toggle') {
|
|
160
|
+
const item = mm.toggleChecklist(goalId, milestoneId, index, args.done);
|
|
161
|
+
return { ok: true, goalId, milestoneId, index, item };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
throw new Error(`Unsupported action "${action}". Allowed: toggle`);
|
|
307
165
|
};
|
|
308
166
|
|
|
309
167
|
safeRegister({
|
|
310
|
-
name: '
|
|
311
|
-
description: '
|
|
168
|
+
name: 'goal_checklist',
|
|
169
|
+
description: 'Toggle sub-task checklist items within a goal milestone.',
|
|
312
170
|
parameters: {
|
|
313
171
|
type: 'object',
|
|
314
172
|
properties: {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
},
|
|
173
|
+
action: { type: 'string', enum: ['toggle'], description: 'Action to perform' },
|
|
174
|
+
milestoneId: { type: 'string', description: 'Target milestone ID' },
|
|
175
|
+
index: { type: 'number', description: 'Zero-based index of checklist item' },
|
|
176
|
+
done: { type: 'boolean', description: 'Explicit completion flag (optional, toggles if omitted)' },
|
|
319
177
|
},
|
|
320
|
-
required: ['
|
|
178
|
+
required: ['action', 'milestoneId', 'index'],
|
|
321
179
|
},
|
|
322
180
|
output: JSON_OUTPUT,
|
|
323
|
-
execute:
|
|
324
|
-
handler:
|
|
181
|
+
execute: handleGoalChecklist,
|
|
182
|
+
handler: handleGoalChecklist,
|
|
325
183
|
});
|
|
326
184
|
});
|
|
327
185
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-goal",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.14",
|
|
4
|
+
"description": "Core Goal Addon (Token Budget, Milestones, Git Snapshots & Retrospective Reports) for DeepSeek Harness",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -55,12 +55,18 @@
|
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
57
57
|
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
|
|
58
|
-
"@deepseek-ai/schemastery": "^3.18.1"
|
|
58
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
59
|
+
"@deepseek-ai/dsh-goal": ">=0.1.5-rc.3"
|
|
59
60
|
},
|
|
60
61
|
"devDependencies": {
|
|
61
62
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
62
63
|
},
|
|
63
64
|
"dependencies": {
|
|
64
65
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
66
|
+
},
|
|
67
|
+
"peerDependenciesMeta": {
|
|
68
|
+
"@deepseek-ai/dsh-goal": {
|
|
69
|
+
"optional": true
|
|
70
|
+
}
|
|
65
71
|
}
|
|
66
72
|
}
|