@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.
@@ -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 { GoalState, sessionIdOf } from './goal-engine-constants.js';
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
- * Register goal management tools into Cordis tools service
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 {any} options.engine
43
- * @param {Function} [options.resumeActiveAgent]
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, { engine, resumeActiveAgent, logger = console }) {
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
- // Tool 1: get_goal
73
- const handleGetGoal = async (_args, toolCtx) => {
74
- const sid = sessionIdOf(toolCtx, 'default');
75
- const snap = engine.getSnapshot(sid);
76
- return formatGoalValue(snap);
77
- };
78
-
79
- safeRegister({
80
- name: 'get_goal',
81
- description: 'Get current active goal and its execution phase.',
82
- parameters: { type: 'object', properties: {} },
83
- output: JSON_OUTPUT,
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
- safeRegister({
97
- name: 'create_goal',
98
- description: 'Initialize a new autonomous goal.',
99
- parameters: {
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
- output: JSON_OUTPUT,
108
- execute: handleCreateGoal,
109
- handler: handleCreateGoal,
110
- });
55
+ };
111
56
 
112
- // Tool 3: update_goal
113
- const handleUpdateGoal = async (args, toolCtx) => {
114
- const sid = sessionIdOf(toolCtx, 'default');
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 === 'complete') {
118
- const summary = args.blocked_reason || args.objective || 'Goal marked complete';
119
- const snap = engine.completeGoal(summary, sid);
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 === 'pause') {
134
- const reason = args.blocked_reason || 'Paused by model';
135
- const snap = engine.pause(reason, sid);
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 === 'resume') {
140
- const snap = engine.resume(sid);
141
- if (typeof resumeActiveAgent === 'function') {
142
- resumeActiveAgent(undefined, sid);
143
- }
144
- return formatGoalValue(snap);
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 === 'blocked') {
156
- const reason = args.blocked_reason || 'Goal blocked';
157
- const snap = engine.pause('Blocked: ' + reason, sid);
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
- return res;
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
- return formatGoalValue(engine.getSnapshot(sid));
92
+ throw new Error(`Unsupported action "${action}". Allowed: list, set, add, complete`);
177
93
  };
178
94
 
179
95
  safeRegister({
180
- name: 'update_goal',
181
- description: 'Update status or parameters of the current active goal.',
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: ['pause', 'resume', 'complete', 'edit', 'blocked'],
188
- description: 'Action to perform on the goal',
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', description: 'Short identifier, e.g. M1, step-1' },
227
- title: { type: 'string', description: 'Concise actionable title' },
228
- dependsOn: { type: 'array', items: { type: 'string' }, description: 'Prerequisite milestone IDs' },
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: ['id', 'title'],
126
+ required: ['title'],
231
127
  },
232
- description: 'List of milestones to achieve the goal',
128
+ description: 'Array of milestones when action is "set"',
233
129
  },
234
- },
235
- required: ['milestones'],
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
- type: 'object',
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: ['milestone_id', 'status'],
138
+ required: ['action'],
289
139
  },
290
140
  output: JSON_OUTPUT,
291
- execute: handleUpdateProgress,
292
- handler: handleUpdateProgress,
141
+ execute: handleGoalMilestones,
142
+ handler: handleGoalMilestones,
293
143
  });
294
144
 
295
- // Tool 6: goal_finish
296
- const handleGoalFinish = async ({ summary }, toolCtx) => {
297
- const sid = sessionIdOf(toolCtx, 'default');
298
- const snap = engine.completeGoal(summary, sid);
299
- if (typeof engine.recordToolActivity === 'function') {
300
- engine.recordToolActivity('goal_finish', 'success', summary ? summary.slice(0, 80) : 'Goal finished', sid);
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
- return {
303
- success: true,
304
- completed: true,
305
- summary,
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: 'goal_finish',
311
- description: 'Conclude the active goal successfully with a final summary and achievements.',
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
- summary: {
316
- type: 'string',
317
- description: 'Final summary of the goal outcome and deliverables.',
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: ['summary'],
178
+ required: ['action', 'milestoneId', 'index'],
321
179
  },
322
180
  output: JSON_OUTPUT,
323
- execute: handleGoalFinish,
324
- handler: handleGoalFinish,
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.12",
4
- "description": "Autonomous Goal Execution & Multi-Turn Task Tracking Engine with Sticky Header for DeepSeek Harness",
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
  }