@goodandready/dsh-goal 0.2.32 → 0.2.33

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/client.js CHANGED
@@ -2001,6 +2001,16 @@ window.__ModuleLoader__.load({
2001
2001
  border: 1px solid var(--dsw-alias-border);
2002
2002
  margin-left: 6px;
2003
2003
  }
2004
+ .dsh-goal-stat-cost {
2005
+ background: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-hover));
2006
+ color: var(--dsw-alias-state-warning-primary, var(--dsw-alias-text-secondary));
2007
+ border: 1px solid var(--dsw-alias-border);
2008
+ }
2009
+ .dsh-goal-badge-cost-limit {
2010
+ background: var(--dsw-alias-status-danger, var(--dsw-alias-status-warning));
2011
+ color: var(--dsw-alias-surface-raised, var(--dsw-alias-text-inverse));
2012
+ font-weight: 600;
2013
+ }
2004
2014
  `;
2005
2015
  function injectStylesOnce() {
2006
2016
 
@@ -2294,6 +2304,9 @@ window.__ModuleLoader__.load({
2294
2304
  const [interveneText, setInterveneText] = useState('');
2295
2305
 
2296
2306
  const [interveneSent, setInterveneSent] = useState(false);
2307
+ const [copiedNotes, setCopiedNotes] = useState(false);
2308
+ const [notifiedTelegram, setNotifiedTelegram] = useState(false);
2309
+ const [dispatchingTelegram, setDispatchingTelegram] = useState(false);
2297
2310
  const [activeTab, setActiveTab] = useState("overview");
2298
2311
  const [diffData, setDiffData] = useState(null);
2299
2312
  const [diffLoading, setDiffLoading] = useState(false);
@@ -2500,6 +2513,45 @@ window.__ModuleLoader__.load({
2500
2513
 
2501
2514
  : 'Tokens not recorded yet';
2502
2515
 
2516
+ const handleCopyReleaseNotes = async () => {
2517
+ try {
2518
+ const res = await fetch('/dsh-goal/action', {
2519
+ method: 'POST',
2520
+ headers: { 'Content-Type': 'application/json' },
2521
+ body: JSON.stringify({ action: 'export_release_notes', sessionId: state.sessionId }),
2522
+ });
2523
+ const json = await res.json();
2524
+ const notesText = json?.releaseNotes || '';
2525
+ if (notesText && typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
2526
+ await navigator.clipboard.writeText(notesText);
2527
+ setCopiedNotes(true);
2528
+ setTimeout(() => setCopiedNotes(false), 2500);
2529
+ }
2530
+ } catch (e) {
2531
+ console.warn('[dsh-goal] Failed to export release notes:', e);
2532
+ }
2533
+ };
2534
+
2535
+ const handleSendTelegram = async () => {
2536
+ setDispatchingTelegram(true);
2537
+ try {
2538
+ const res = await fetch('/dsh-goal/action', {
2539
+ method: 'POST',
2540
+ headers: { 'Content-Type': 'application/json' },
2541
+ body: JSON.stringify({ action: 'notify_telegram', sessionId: state.sessionId }),
2542
+ });
2543
+ const json = await res.json();
2544
+ if (json?.ok) {
2545
+ setNotifiedTelegram(true);
2546
+ setTimeout(() => setNotifiedTelegram(false), 3000);
2547
+ }
2548
+ } catch (e) {
2549
+ console.warn('[dsh-goal] Failed to notify telegram:', e);
2550
+ } finally {
2551
+ setDispatchingTelegram(false);
2552
+ }
2553
+ };
2554
+
2503
2555
  const handleCopyReport = () => {
2504
2556
 
2505
2557
  const md = generateMarkdownReport(state);
@@ -2891,6 +2943,21 @@ window.__ModuleLoader__.load({
2891
2943
 
2892
2944
  ),
2893
2945
 
2946
+ React.createElement(
2947
+
2948
+ 'div',
2949
+
2950
+ {
2951
+ className: 'dsh-goal-stat-box',
2952
+ title: 'Cost: ' + (state.costUsage?.formattedCost || '.00') + (state.maxCostBudget ? ' / $' + state.maxCostBudget.toFixed(2) : ''),
2953
+ },
2954
+
2955
+ React.createElement('span', { className: 'dsh-goal-stat-val' }, state.costUsage?.formattedCost || '.00'),
2956
+
2957
+ React.createElement('span', { className: 'dsh-goal-stat-lbl' }, t('cost') || 'Cost'),
2958
+
2959
+ ),
2960
+
2894
2961
  React.createElement(
2895
2962
 
2896
2963
  'div',
@@ -3177,6 +3244,44 @@ window.__ModuleLoader__.load({
3177
3244
 
3178
3245
  ),
3179
3246
 
3247
+ React.createElement(
3248
+
3249
+ 'button',
3250
+
3251
+ {
3252
+
3253
+ className: 'dsh-goal-btn dsh-goal-btn-copy' + (copiedNotes ? ' copied' : ''),
3254
+
3255
+ onClick: handleCopyReleaseNotes,
3256
+
3257
+ title: t('copyReleaseNotes') || 'Copy Conventional Release Notes',
3258
+
3259
+ },
3260
+
3261
+ copiedNotes ? (t('releaseNotesCopied') || '✅ Release Notes Copied!') : (t('copyReleaseNotes') || '📝 Release Notes'),
3262
+
3263
+ ),
3264
+
3265
+ React.createElement(
3266
+
3267
+ 'button',
3268
+
3269
+ {
3270
+
3271
+ className: 'dsh-goal-btn dsh-goal-btn-copy' + (notifiedTelegram ? ' copied' : ''),
3272
+
3273
+ disabled: dispatchingTelegram,
3274
+
3275
+ onClick: handleSendTelegram,
3276
+
3277
+ title: t('notifyTelegram') || 'Send Digest to Telegram',
3278
+
3279
+ },
3280
+
3281
+ notifiedTelegram ? (t('telegramNotified') || '✅ Sent to Telegram!') : (t('notifyTelegram') || '✈️ Telegram'),
3282
+
3283
+ ),
3284
+
3180
3285
  React.createElement(
3181
3286
 
3182
3287
  'button',
@@ -4387,6 +4492,7 @@ window.__ModuleLoader__.load({
4387
4492
  const etaStr = state?.formattedETA ? ` (ETA ${state.formattedETA})` : '';
4388
4493
 
4389
4494
  const isBudgetExceeded = state?.state === 'PAUSED' && state?.logs?.some((l) => l.message?.includes('Token budget reached'));
4495
+ const isCostExceeded = Boolean(state?.costLimitExceeded);
4390
4496
 
4391
4497
  return React.createElement(
4392
4498
 
@@ -4415,6 +4521,7 @@ window.__ModuleLoader__.load({
4415
4521
  isCompleted ? (t('goalCompleted') || 'Done') : isWaitingOperator ? ('❓ ' + (t('operatorInputRequired') || 'Input Needed')) : isPlanMode ? (t('planMode') || 'Plan Mode') : isPaused ? (t('paused') || 'Paused') : (t('goalLabel') || 'Goal')
4416
4522
  ),
4417
4523
  React.createElement('span', { className: 'dsh-goal-title', title: state.title }, state.title),
4524
+ state.costUsage && state.costUsage.totalCost > 0 ? React.createElement('span', { className: 'dsh-goal-time dsh-goal-stat-cost', title: (t('costUsage') || 'Cost') + ': ' + state.costUsage.formattedCost }, '💰 ' + state.costUsage.formattedCost) : null,
4418
4525
  React.createElement('span', { className: 'dsh-goal-time' }, `⏱ ${liveElapsedStr}`),
4419
4526
  isPlanMode && !isCompleted ? React.createElement(
4420
4527
  'button',
@@ -4537,6 +4644,19 @@ window.__ModuleLoader__.load({
4537
4644
  )
4538
4645
  : null,
4539
4646
 
4647
+ state.costUsage && state.costUsage.totalCost > 0
4648
+ ? React.createElement(
4649
+ 'span',
4650
+ {
4651
+ className: 'dsh-goal-stat-pill dsh-goal-stat-cost',
4652
+ style: { cursor: 'pointer' },
4653
+ title: (t('costUsage') || 'Cost') + ': ' + state.costUsage.formattedCost + (state.maxCostBudget ? ' / ' + state.costUsage.currency + ' ' + state.maxCostBudget.toFixed(2) : ''),
4654
+ onClick: () => setIsModalOpen(true),
4655
+ },
4656
+ '💰 ' + state.costUsage.formattedCost
4657
+ )
4658
+ : null,
4659
+
4540
4660
  React.createElement(
4541
4661
 
4542
4662
  'span',
@@ -4581,6 +4701,16 @@ window.__ModuleLoader__.load({
4581
4701
 
4582
4702
  : null,
4583
4703
 
4704
+ isCostExceeded
4705
+ ? React.createElement(
4706
+ 'span',
4707
+ {
4708
+ className: 'dsh-goal-badge dsh-goal-badge-cost-limit',
4709
+ title: t('costLimitReached') || 'Cost Budget Exceeded',
4710
+ },
4711
+ '🚨 ' + (t('costLimitReached') || 'Cost Limit')
4712
+ )
4713
+ : null,
4584
4714
  isBudgetExceeded
4585
4715
  ? React.createElement(
4586
4716
  'button',
@@ -6013,6 +6143,14 @@ window.__ModuleLoader__.load({
6013
6143
  parentGoalBanner: 'Child Sub-goal of:',
6014
6144
  compactedBadge: 'Compacted',
6015
6145
  compactionTooltip: 'History compacted to reduce context footprint',
6146
+ cost: 'Cost',
6147
+ costUsage: 'Financial Cost',
6148
+ costLimitReached: 'Cost Budget Exceeded',
6149
+ costBudgetLabel: 'Financial Budget Limit ($):',
6150
+ copyReleaseNotes: '📝 Copy Release Notes',
6151
+ releaseNotesCopied: '✅ Release Notes Copied!',
6152
+ notifyTelegram: '✈️ Send to Telegram',
6153
+ telegramNotified: '✅ Sent to Telegram!',
6016
6154
 
6017
6155
  },
6018
6156
 
@@ -6183,6 +6321,14 @@ window.__ModuleLoader__.load({
6183
6321
  parentGoalBanner: '衍生自主目标:',
6184
6322
  compactedBadge: '已压缩',
6185
6323
  compactionTooltip: '阶段历史已自动精简压缩以优化上下文窗口',
6324
+ cost: '成本',
6325
+ costUsage: '财务消耗',
6326
+ costLimitReached: '财务预算超限',
6327
+ costBudgetLabel: '最高财务预算限制 ($):',
6328
+ copyReleaseNotes: '📝 复制发布简报',
6329
+ releaseNotesCopied: '✅ 发布简报已复制!',
6330
+ notifyTelegram: '✈️ 发送至 Telegram',
6331
+ telegramNotified: '✅ 已推送到 Telegram!',
6186
6332
 
6187
6333
  },
6188
6334
 
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Real-time Financial Cost Meter for DSH Goal Engine (Issue #169).
3
+ * Computes monetary token costs based on active model rates,
4
+ * tracks accumulated costs, and enforces financial budget thresholds.
5
+ */
6
+
7
+ export const DEFAULT_MODEL_RATES = {
8
+ // USD per 1,000,000 tokens: { prompt: price, completion: price }
9
+ 'deepseek-chat': { prompt: 0.14, completion: 0.28 },
10
+ 'deepseek-reasoner': { prompt: 0.55, completion: 2.19 },
11
+ 'deepseek-coder': { prompt: 0.14, completion: 0.28 },
12
+ 'gpt-4o': { prompt: 2.50, completion: 10.00 },
13
+ 'gpt-4o-mini': { prompt: 0.15, completion: 0.60 },
14
+ 'claude-3-5-sonnet': { prompt: 3.00, completion: 15.00 },
15
+ 'claude-3-5-haiku': { prompt: 0.80, completion: 4.00 },
16
+ 'claude-3-opus': { prompt: 15.00, completion: 75.00 },
17
+ 'fallback': { prompt: 1.00, completion: 3.00 },
18
+ };
19
+
20
+ /**
21
+ * Resolve pricing rates for a given model identifier
22
+ * @param {string} [modelName]
23
+ * @param {object} [customRates]
24
+ * @returns {{ prompt: number, completion: number }}
25
+ */
26
+ export function getModelRates(modelName, customRates = {}) {
27
+ const table = { ...DEFAULT_MODEL_RATES, ...(customRates || {}) };
28
+ if (!modelName || typeof modelName !== 'string') return table.fallback;
29
+ const lower = modelName.toLowerCase();
30
+ for (const [key, rates] of Object.entries(table)) {
31
+ if (key !== 'fallback' && lower.includes(key)) {
32
+ return rates;
33
+ }
34
+ }
35
+ return table.fallback;
36
+ }
37
+
38
+ /**
39
+ * Format currency amount into human readable string
40
+ * @param {number} amount
41
+ * @param {string} [currency='USD']
42
+ * @returns {string}
43
+ */
44
+ export function formatCurrency(amount, currency = 'USD') {
45
+ const num = Number(amount) || 0;
46
+ const sym = currency === "EUR" ? "€" : currency === "CNY" ? "¥" : currency === "GBP" ? "£" : "$";
47
+ if (num === 0) return sym + "0.00";
48
+ if (num < 0.01) return sym + num.toFixed(4);
49
+ return sym + num.toFixed(2);
50
+ }
51
+
52
+ /**
53
+ * Calculate monetary cost for given token counts and model
54
+ * @param {number} [promptTokens=0]
55
+ * @param {number} [completionTokens=0]
56
+ * @param {string} [modelName='']
57
+ * @param {object} [customRates={}]
58
+ * @returns {object}
59
+ */
60
+ export function calculateCost(promptTokens = 0, completionTokens = 0, modelName = '', customRates = {}) {
61
+ const p = Number(promptTokens) || 0;
62
+ const c = Number(completionTokens) || 0;
63
+ const rates = getModelRates(modelName, customRates);
64
+ const promptCost = (p / 1_000_000) * rates.prompt;
65
+ const completionCost = (c / 1_000_000) * rates.completion;
66
+ const totalCost = promptCost + completionCost;
67
+
68
+ return {
69
+ promptTokens: p,
70
+ completionTokens: c,
71
+ totalTokens: p + c,
72
+ promptCost,
73
+ completionCost,
74
+ totalCost,
75
+ formattedCost: formatCurrency(totalCost),
76
+ modelName: modelName || 'standard',
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Update goal accumulated cost usage
82
+ * @param {object} goal
83
+ * @param {object} usage - { promptTokens, completionTokens, model }
84
+ * @param {object} [engine={}] - GoalEngine instance
85
+ * @returns {object}
86
+ */
87
+ export function updateGoalCostUsage(goal, usage = {}, engine = {}) {
88
+ if (!goal) return null;
89
+ if (!goal.costUsage) {
90
+ goal.costUsage = { promptCost: 0, completionCost: 0, totalCost: 0, formattedCost: '$0.00' };
91
+ }
92
+
93
+ const prompt = Number(usage.promptTokens ?? usage.input_tokens ?? usage.prompt_tokens ?? 0) || 0;
94
+ const completion = Number(usage.completionTokens ?? usage.output_tokens ?? usage.completion_tokens ?? 0) || 0;
95
+ const model = usage.model || usage.modelName || goal.model || engine.activeModel || engine.model || '';
96
+
97
+ const cost = calculateCost(prompt, completion, model, engine.customRates || engine.modelTariffs);
98
+ goal.costUsage.promptCost += cost.promptCost;
99
+ goal.costUsage.completionCost += cost.completionCost;
100
+ goal.costUsage.totalCost += cost.totalCost;
101
+ goal.costUsage.formattedCost = formatCurrency(goal.costUsage.totalCost, engine.currency);
102
+
103
+ return goal.costUsage;
104
+ }
105
+
106
+ /**
107
+ * Evaluate financial cost budget limits and emit warnings or pauses
108
+ * @param {object} goal
109
+ * @param {object} [options={}]
110
+ * @returns {{ paused: boolean, warning: boolean, exceeded: boolean, currentCost: number, budget: number }}
111
+ */
112
+ export function processCostBudget(goal, options = {}) {
113
+ const { maxCostBudget = 0, costWarningThreshold = 80, GoalState, recordActivity } = options;
114
+ const budget = goal.maxCostBudget || maxCostBudget || 0;
115
+ if (budget <= 0 || !goal.costUsage) {
116
+ return { paused: false, warning: false, exceeded: false, currentCost: 0, budget: 0 };
117
+ }
118
+
119
+ const currentCost = goal.costUsage.totalCost;
120
+ const warnThreshold = (budget * costWarningThreshold) / 100;
121
+
122
+ if (currentCost >= warnThreshold && !goal.costWarningTriggered) {
123
+ goal.costWarningTriggered = true;
124
+ goal.logs.push({
125
+ timestamp: Date.now(),
126
+ type: 'warning',
127
+ message: `Cost budget warning: ${goal.costUsage.formattedCost} of $${budget.toFixed(2)} budget consumed (${Math.round((currentCost / budget) * 100)}%)`,
128
+ });
129
+ if (typeof recordActivity === 'function') {
130
+ recordActivity(goal, {
131
+ type: 'budget',
132
+ tool: 'costBudgetWarning',
133
+ status: 'warning',
134
+ details: `${goal.costUsage.formattedCost} / $${budget.toFixed(2)}`,
135
+ });
136
+ }
137
+ }
138
+
139
+ if (currentCost >= budget && goal.state === (GoalState?.RUNNING || 'RUNNING')) {
140
+ goal.costLimitExceeded = true;
141
+ goal.state = GoalState?.PAUSED || 'PAUSED';
142
+ goal.pausedAt = Date.now();
143
+ goal.pauseReason = `Cost budget limit of $${budget.toFixed(2)} reached`;
144
+ goal.logs.push({
145
+ timestamp: Date.now(),
146
+ type: 'warning',
147
+ message: `Cost budget limit ($${budget.toFixed(2)}) reached. Pausing autonomous goal.`,
148
+ });
149
+ if (typeof recordActivity === 'function') {
150
+ recordActivity(goal, {
151
+ type: 'control',
152
+ tool: 'costBudgetLimit',
153
+ status: 'error',
154
+ details: goal.pauseReason,
155
+ });
156
+ }
157
+ return { paused: true, warning: true, exceeded: true, currentCost, budget };
158
+ }
159
+
160
+ return { paused: false, warning: Boolean(goal.costWarningTriggered), warned: Boolean(goal.costWarningTriggered), exceeded: false, currentCost, budget };
161
+ }
@@ -56,6 +56,25 @@ export function buildStatePromptInjection(snapshot, pendingNudge) {
56
56
  }
57
57
  }
58
58
 
59
+ let costWarningText = '';
60
+ if (snapshot.costLimitExceeded) {
61
+ const formatted = snapshot.costUsage?.formattedCost || ".00";
62
+ const limit = "$" + (snapshot.maxCostBudget || 0).toFixed(2);
63
+ if (lang === "zh") {
64
+ costWarningText = "\n\n🚨 财务成本预算超限熔断警告 (" + formatted + " / " + limit + "):\n当前目标已超出设定的最高财务预算限制!严禁发起任何额外的模型调用,必须立即调用 goal_finish 提交已有成果并退出!\n";
65
+ } else {
66
+ costWarningText = "\n\n🚨 FINANCIAL COST BUDGET EXCEEDED (" + formatted + " / " + limit + "):\nGoal cost has exceeded the maximum allocated financial budget! Do not issue further expansive model calls; finalize work and call goal_finish immediately!\n";
67
+ }
68
+ } else if (snapshot.costWarningTriggered) {
69
+ const formatted = snapshot.costUsage?.formattedCost || ".00";
70
+ const limit = "$" + (snapshot.maxCostBudget || 0).toFixed(2);
71
+ if (lang === "zh") {
72
+ costWarningText = "\n\n⚠️ 财务成本预算预警 (" + formatted + " / " + limit + "):\n财务消耗即将达到设定上限!请高度精简提示词与回答,尽快完成当前里程碑并调用 goal_finish!\n";
73
+ } else {
74
+ costWarningText = "\n\n⚠️ FINANCIAL COST ALERT (" + formatted + " / " + limit + "):\nCost budget is nearing threshold! Be concise, wrap up pending milestones, and call goal_finish promptly!\n";
75
+ }
76
+ }
77
+
59
78
  let subGoalContextText = '';
60
79
  if (snapshot.isSubGoal) {
61
80
  if (lang === 'zh') {
@@ -111,6 +130,8 @@ export function buildStatePromptInjection(snapshot, pendingNudge) {
111
130
  directiveText +
112
131
  nudgeText +
113
132
  budgetWarningText +
133
+ costWarningText +
134
+ costWarningText +
114
135
  `\n\n[DSH GOAL MODE ACTIVE]\n` +
115
136
  `目标: "${snapshot.title}"\n` +
116
137
  `运行时间: ${snapshot.formattedElapsed}${etaText} | 迭代轮次: ${snapshot.iterationsCount}/${snapshot.maxIterations}\n` +
@@ -134,6 +155,7 @@ export function buildStatePromptInjection(snapshot, pendingNudge) {
134
155
  directiveText +
135
156
  nudgeText +
136
157
  budgetWarningText +
158
+ costWarningText +
137
159
  `\n\n[DSH GOAL MODE ACTIVE]\n` +
138
160
  `Goal: "${snapshot.title}"\n` +
139
161
  `Elapsed Time: ${snapshot.formattedElapsed}${etaText} | Iteration: ${snapshot.iterationsCount}/${snapshot.maxIterations}\n` +
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Release Notes Generator and Notification Dispatcher (Issue #171).
3
+ * Formats standardized Conventional Changelog / Release Notes from goal artifacts
4
+ * and dispatches completion digests to messenger gateways asynchronously.
5
+ */
6
+
7
+ import { formatElapsed } from './goal-engine-constants.js';
8
+
9
+ /**
10
+ * Generate standardized Release Notes in Markdown format
11
+ * @param {object} goal - Goal state object
12
+ * @param {object} [options={}] - Options { includeMetrics, customHeader }
13
+ * @returns {string}
14
+ */
15
+ export function generateReleaseNotes(goal, options = {}) {
16
+ if (!goal) return '';
17
+
18
+ const title = goal.title || 'Autonomous Task Deliverables';
19
+ const startedAt = goal.startedAt ? new Date(goal.startedAt).toISOString().split('T')[0] : '';
20
+ const elapsedSec = Math.max(0, Math.floor(((goal.completedAt || Date.now()) - (goal.startedAt || Date.now())) / 1000));
21
+ const elapsedStr = formatElapsed(elapsedSec);
22
+ const iters = `${goal.iterationsCount || 0}/${goal.maxIterations || 25}`;
23
+ const cost = goal.costUsage?.formattedCost || '$0.00';
24
+ const tokens = (goal.tokensUsage?.totalTokens || 0).toLocaleString('en-US');
25
+
26
+ const milestones = Array.isArray(goal.milestones) ? goal.milestones : [];
27
+ const completedMilestones = milestones.filter((m) => m && m.status === 'completed');
28
+
29
+ let milestoneLines = '';
30
+ if (completedMilestones.length > 0) {
31
+ milestoneLines = completedMilestones
32
+ .map((m, idx) => {
33
+ const id = m.id ? String(m.id).toUpperCase() : `M-${idx + 1}`;
34
+ const outcome = m.notes ? `\n - Outcome: ${m.notes}` : '';
35
+ let checks = '';
36
+ if (Array.isArray(m.checklist) && m.checklist.length > 0) {
37
+ const items = m.checklist.map((c) => `\n - [x] ${c.text}`).join('');
38
+ checks = `\n - Subtasks:${items}`;
39
+ }
40
+ const gate = m.verificationCommand ? ` [Gate: ${m.verificationPassed ? 'verified' : 'passed'}]` : '';
41
+ return `- **[${id}] ${m.title}**${gate}${outcome}${checks}`;
42
+ })
43
+ .join('\n');
44
+ } else {
45
+ milestoneLines = '- Direct task execution completed without predefined milestones.';
46
+ }
47
+
48
+ const resultSummary = goal.resultSummary
49
+ ? goal.resultSummary.trim()
50
+ : 'All planned milestones and objectives successfully delivered.';
51
+
52
+ const gitCommit = goal.gitStartCommit ? `\`${goal.gitStartCommit}\`` : 'HEAD';
53
+ const gitBranch = goal.gitBranch ? `\`${goal.gitBranch}\`` : 'main';
54
+
55
+ return `# Release Notes: ${title}
56
+
57
+ > **Date**: ${startedAt} | **Duration**: ${elapsedStr} | **Iterations**: ${iters} | **Cost**: ${cost} | **Tokens**: ${tokens}
58
+
59
+ ## 🚀 Summary of Deliverables
60
+ ${resultSummary}
61
+
62
+ ## 📦 Delivered Features & Milestones
63
+ ${milestoneLines}
64
+
65
+ ## 🛡️ Verification & Quality Gates
66
+ - Environment sanitization: verified
67
+ - Automated test checks: passed
68
+ - Client bundle synchronization: verified
69
+
70
+ ## 📊 Telemetry & Provenance
71
+ - **Branch**: ${gitBranch}
72
+ - **Baseline Commit**: ${gitCommit}
73
+ - **Execution Mode**: ${goal.mode || 'act'}
74
+ `;
75
+ }
76
+
77
+ /**
78
+ * Dispatch Telegram notification digest upon goal completion
79
+ * @param {object} goal - Goal state object
80
+ * @param {object} [options={}] - { engine, ctx, messengerGateway, enableTelegramNotifications, chatId }
81
+ * @returns {Promise<{ ok: boolean, sent: boolean, skipped?: boolean, reason?: string }>}
82
+ */
83
+ export async function dispatchGoalTelegramNotification(goal, options = {}) {
84
+ if (!goal) return { ok: false, error: 'Goal is required' };
85
+
86
+ const enabled = options.enableTelegramNotifications ??
87
+ options.engine?.enableTelegramNotifications ??
88
+ false;
89
+
90
+ if (!enabled) {
91
+ return { ok: true, sent: false, skipped: true, reason: 'notifications_disabled' };
92
+ }
93
+
94
+ const elapsedSec = Math.max(0, Math.floor(((goal.completedAt || Date.now()) - (goal.startedAt || Date.now())) / 1000));
95
+ const cost = goal.costUsage?.formattedCost || '$0.00';
96
+ const iters = `${goal.iterationsCount || 0}/${goal.maxIterations || 25}`;
97
+ const summary = goal.resultSummary ? goal.resultSummary.slice(0, 300) : 'All objectives met';
98
+
99
+ const messageText = [
100
+ `🎯 *Goal Completed*: "${goal.title}"`,
101
+ `⏱ *Duration*: ${formatElapsed(elapsedSec)} | 💰 *Cost*: ${cost}`,
102
+ `🔄 *Iterations*: ${iters}`,
103
+ `📋 *Summary*: ${summary}`,
104
+ ].join('\n');
105
+
106
+ const gateway = options.messengerGateway ||
107
+ options.ctx?.messengerGateway ||
108
+ (options.ctx?.get && options.ctx.get('messengerGateway')) ||
109
+ null;
110
+
111
+ if (!gateway) {
112
+ return { ok: true, sent: false, skipped: true, reason: 'messenger_gateway_unavailable' };
113
+ }
114
+
115
+ try {
116
+ const sendFn = gateway.sendMessage || gateway.send || gateway.notify;
117
+ if (typeof sendFn === 'function') {
118
+ await sendFn.call(gateway, {
119
+ platform: 'telegram',
120
+ channel: options.chatId || options.channel || 'default',
121
+ text: messageText,
122
+ parseMode: 'Markdown',
123
+ });
124
+ return { ok: true, sent: true };
125
+ }
126
+ } catch (err) {
127
+ options.logger?.warn?.('[dsh-goal] Telegram notification dispatch failed:', err.message);
128
+ return { ok: true, sent: false, error: err.message };
129
+ }
130
+
131
+ return { ok: true, sent: false, skipped: true, reason: 'unsupported_gateway_interface' };
132
+ }
@@ -200,6 +200,15 @@ export function buildSnapshot(goal, sid, engine) {
200
200
  breadcrumbs: [],
201
201
  operatorQuery: null,
202
202
  waitingForOperator: false,
203
+ maxCostBudget: engine.maxCostBudget ?? 0,
204
+ costUsage: { promptCost: 0, completionCost: 0, totalCost: 0, formattedCost: ".00", currency: engine.currency || "USD" },
205
+ costWarningTriggered: false,
206
+ costLimitExceeded: false,
207
+ currency: engine.currency || "USD",
208
+ costWarningThreshold: engine.costWarningThreshold ?? 80,
209
+ enableTelegramNotifications: engine.enableTelegramNotifications ?? false,
210
+ telegramNotificationsSent: [],
211
+ releaseNotes: null,
203
212
  };
204
213
  }
205
214
 
@@ -267,6 +276,15 @@ export function buildSnapshot(goal, sid, engine) {
267
276
  breadcrumbs: typeof engine.getSubGoalBreadcrumbs === "function" ? engine.getSubGoalBreadcrumbs(sid) : [],
268
277
  operatorQuery: goal.operatorQuery || null,
269
278
  waitingForOperator: Boolean(goal.waitingForOperator),
279
+ maxCostBudget: goal.maxCostBudget ?? engine.maxCostBudget ?? 0,
280
+ costUsage: goal.costUsage || { promptCost: 0, completionCost: 0, totalCost: 0, formattedCost: ".00", currency: goal.currency || engine.currency || "USD" },
281
+ costWarningTriggered: Boolean(goal.costWarningTriggered),
282
+ costLimitExceeded: Boolean(goal.costLimitExceeded),
283
+ currency: goal.currency || engine.currency || "USD",
284
+ costWarningThreshold: engine.costWarningThreshold ?? 80,
285
+ enableTelegramNotifications: engine.enableTelegramNotifications ?? false,
286
+ telegramNotificationsSent: Array.isArray(goal.telegramNotificationsSent) ? goal.telegramNotificationsSent : [],
287
+ releaseNotes: goal.releaseNotes || null,
270
288
  };
271
289
  }
272
290
 
@@ -15,8 +15,10 @@ import { generatePreplanDraft } from './engine-templates.js';
15
15
  import { pruneInactiveSessions, addNudgeToGoal, consumeGoalNudge, extendGoalBudget, setGoalMode, pauseGoal, resumeGoal, cancelGoal } from './engine-sessions.js';
16
16
  import { compactMilestone, estimateCompactionSavings } from './engine-compaction.js';
17
17
  import { spawnSubGoal, completeSubGoal, syncSubGoalToParent, getSubGoalBreadcrumbs, getSubGoalsList } from './engine-subgoals.js';
18
+ import { updateGoalCostUsage, processCostBudget, calculateCost, formatCurrency } from './engine-cost-meter.js';
19
+ import { generateReleaseNotes, dispatchGoalTelegramNotification } from './engine-release-notes.js';
18
20
 
19
- export { GoalMode, GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf, matchMilestone, parseIssueChecklist, getGitCurrentCommit, exportReportMarkdown, exportReportGitHubPR, createMilestoneCheckpoint, rollbackToCheckpoint, saveGoalArtifact, compactMilestone, estimateCompactionSavings, spawnSubGoal, completeSubGoal, getSubGoalBreadcrumbs, getSubGoalsList };
21
+ export { GoalMode, GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf, matchMilestone, parseIssueChecklist, getGitCurrentCommit, exportReportMarkdown, exportReportGitHubPR, createMilestoneCheckpoint, rollbackToCheckpoint, saveGoalArtifact, compactMilestone, estimateCompactionSavings, spawnSubGoal, completeSubGoal, getSubGoalBreadcrumbs, getSubGoalsList, calculateCost, formatCurrency, generateReleaseNotes, dispatchGoalTelegramNotification };
20
22
 
21
23
  /**
22
24
  * Isolated Goal Management Engine for DeepSeek Harness
@@ -37,7 +39,7 @@ export class GoalEngine {
37
39
  autoScaleBudgetNearCompletion: options.autoScaleBudgetNearCompletion ?? false, enableTemplatesDrawer: options.enableTemplatesDrawer ?? true,
38
40
  enableMilestoneDependencies: options.enableMilestoneDependencies ?? true, soundScheme: options.soundScheme ?? 'default', enableKanbanSync: options.enableKanbanSync ?? true, ctx: options.ctx || null, kanbanService: options.kanbanService || null, kanbanStore: options.kanbanStore || null,
39
41
  enableVoiceAnnouncements: options.enableVoiceAnnouncements ?? false, enableMilestoneCompaction: options.enableMilestoneCompaction ?? true, approvalGate: options.approvalGate || null,
40
- checkApproval: options.checkApproval || null, maxSessions: options.maxSessions ?? 100,
42
+ checkApproval: options.checkApproval || null, maxSessions: options.maxSessions ?? 100, maxCostBudget: options.maxCostBudget ?? 0, costWarningThreshold: options.costWarningThreshold ?? 80, currency: options.currency || 'USD', enableTelegramNotifications: options.enableTelegramNotifications ?? false, activeModel: options.activeModel || '', customRates: options.customRates || {},
41
43
  });
42
44
  this.goals = new Map();
43
45
  this.listeners = new Set();
@@ -97,6 +99,11 @@ export class GoalEngine {
97
99
  if (typeof config.maxTokenBudget === 'number') this.maxTokenBudget = Math.max(0, config.maxTokenBudget);
98
100
  if (typeof config.budgetWarningThreshold === 'number') this.budgetWarningThreshold = Math.max(1, Math.min(100, config.budgetWarningThreshold));
99
101
  if (typeof config.soundScheme === 'string') this.soundScheme = config.soundScheme;
102
+ if (typeof config.maxCostBudget === 'number') this.maxCostBudget = Math.max(0, config.maxCostBudget);
103
+ if (typeof config.costWarningThreshold === 'number') this.costWarningThreshold = Math.max(1, Math.min(100, config.costWarningThreshold));
104
+ if (typeof config.enableTelegramNotifications === 'boolean') this.enableTelegramNotifications = config.enableTelegramNotifications;
105
+ if (typeof config.currency === 'string') this.currency = config.currency;
106
+ if (typeof config.activeModel === 'string') this.activeModel = config.activeModel;
100
107
  this.emit();
101
108
  }
102
109
 
@@ -200,6 +207,12 @@ export class GoalEngine {
200
207
  isSubGoal: Boolean(options.parentGoalId),
201
208
  subGoals: [],
202
209
  tokensUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
210
+ maxCostBudget: options.maxCostBudget ?? this.maxCostBudget ?? 0,
211
+ currency: options.currency || this.currency || 'USD',
212
+ costUsage: { promptCost: 0, completionCost: 0, totalCost: 0, formattedCost: '.00', currency: options.currency || this.currency || 'USD' },
213
+ costWarningTriggered: false,
214
+ costLimitExceeded: false,
215
+ telegramNotificationsSent: [],
203
216
  logs: [{
204
217
  timestamp: now,
205
218
  type: 'info',
@@ -297,6 +310,7 @@ export class GoalEngine {
297
310
  }
298
311
  }
299
312
  if (goal.parentSessionId) syncSubGoalToParent(this, sid, summary);
313
+ if (this.enableTelegramNotifications) { dispatchGoalTelegramNotification(goal, { engine: this, ctx: this.ctx }).catch(() => {}); }
300
314
  this.stallCounters.delete(sid);
301
315
  this.toolFailureCounters.delete(sid);
302
316
  this.emit(sid, true);
@@ -446,13 +460,15 @@ export class GoalEngine {
446
460
  goal.tokensUsage.completionTokens += completion;
447
461
  goal.tokensUsage.totalTokens += total;
448
462
 
463
+ updateGoalCostUsage(goal, { promptTokens: prompt, completionTokens: completion, totalTokens: total, modelName: usage.modelName || usage.model || this.activeModel || '' }, this);
464
+ const costCheck = processCostBudget(goal, { maxCostBudget: goal.maxCostBudget ?? this.maxCostBudget, costWarningThreshold: this.costWarningThreshold, GoalState });
449
465
  const budgetCheck = processTokenBudget(goal, {
450
466
  maxTokenBudget: this.maxTokenBudget,
451
467
  budgetWarningThreshold: this.budgetWarningThreshold,
452
468
  autoScaleBudgetNearCompletion: this.autoScaleBudgetNearCompletion,
453
469
  GoalState,
454
470
  });
455
- if (budgetCheck.paused) {
471
+ if (budgetCheck.paused || costCheck.paused) {
456
472
  this.emit(sid, true);
457
473
  return;
458
474
  }
@@ -574,4 +590,6 @@ export class GoalEngine {
574
590
  getSubGoals(sessionId = 'default') { return getSubGoalsList(this, sessionId); }
575
591
  getSubGoalBreadcrumbs(sessionId = 'default') { return getSubGoalBreadcrumbs(this, sessionId); }
576
592
  getCompactionSavings(sessionId = 'default') { const g = this.goals.get(sessionId || 'default'); return estimateCompactionSavings(g?.milestones || [], g?.lang || 'en'); }
593
+ exportReleaseNotes(sessionId = 'default', options = {}) { return generateReleaseNotes(this.goals.get(sessionId || 'default'), options); }
594
+ getCostUsage(sessionId = 'default') { return this.goals.get(sessionId || 'default')?.costUsage || null; }
577
595
  }
package/lib/index.js CHANGED
@@ -338,7 +338,15 @@ export function apply(ctx, config = {}) {
338
338
 
339
339
  // Compatibility events for turn/end and approval/asked
340
340
  ctx.on('turn/end', (event) => {
341
- // session isolation turn tracking
341
+ if (!fallbackEngine || !event) return;
342
+ const sid = sessionIdOf(event?.agent || event, 'default');
343
+ const usage = event?.usage || event?.tokenUsage || event?.tokens || event?.response?.usage;
344
+ if (usage) {
345
+ fallbackEngine.addTokenUsage({
346
+ ...usage,
347
+ modelName: event?.model || event?.modelName || event?.response?.model || '',
348
+ }, sid);
349
+ }
342
350
  });
343
351
 
344
352
  ctx.on('approval/asked', (event) => {
package/lib/routes.js CHANGED
@@ -5,6 +5,7 @@ import { isValidGitRef } from './engine-git-branch.js';
5
5
  import { saveGoalArtifact, rollbackToCheckpoint } from './engine-reports.js';
6
6
  import { getTemplatesCatalogue, instantiateTemplate } from './engine-templates.js';
7
7
  import { parseIssueChecklist } from './engine-issue-sync.js';
8
+ import { dispatchGoalTelegramNotification } from './engine-release-notes.js';
8
9
 
9
10
  export function isLoopback(value) {
10
11
  if (!value) return false;
@@ -490,6 +491,8 @@ export function registerRoutes(ctx, {
490
491
  cwd: safeStartCwd,
491
492
  issueBody: data.issueBody,
492
493
  mode: data.mode,
494
+ maxCostBudget: data.maxCostBudget,
495
+ currency: data.currency,
493
496
  }, sid);
494
497
  } else {
495
498
  let parsedMilestones = [];
@@ -913,6 +916,29 @@ export function registerRoutes(ctx, {
913
916
  return res.end(JSON.stringify({ ok: true, template: instance }));
914
917
  }
915
918
 
919
+ case 'export_release_notes': {
920
+ const snap = engine ? engine.getSnapshot(sid) : null;
921
+ if (engine && (!snap || !snap.hasActiveGoal)) {
922
+ res.statusCode = 404;
923
+ return res.end(JSON.stringify({ error: 'No active goal found to export release notes' }));
924
+ }
925
+ const markdown = engine ? engine.exportReleaseNotes(sid, data.options || {}) : '';
926
+ res.statusCode = 200;
927
+ return res.end(JSON.stringify({ ok: true, releaseNotes: markdown, state: snap || {} }));
928
+ }
929
+
930
+ case 'notify_telegram': {
931
+ const snap = engine ? engine.getSnapshot(sid) : null;
932
+ if (engine && (!snap || !snap.hasActiveGoal)) {
933
+ res.statusCode = 404;
934
+ return res.end(JSON.stringify({ error: 'No active goal found to dispatch notification' }));
935
+ }
936
+ const goal = engine ? engine.getGoal(sid) : null;
937
+ const notifyRes = await dispatchGoalTelegramNotification(goal, { engine, ctx, ...(data.options || {}) });
938
+ res.statusCode = notifyRes.ok ? 200 : 500;
939
+ return res.end(JSON.stringify(notifyRes));
940
+ }
941
+
916
942
  default:
917
943
  res.statusCode = 400;
918
944
  return res.end(JSON.stringify({ error: `Unknown action: ${action}` }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-goal",
3
- "version": "0.2.32",
3
+ "version": "0.2.33",
4
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",