@goodandready/dsh-goal 0.2.8 → 0.2.10
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 +350 -13
- package/lib/engine-git-branch.js +105 -0
- package/lib/engine-intervention.js +150 -0
- package/lib/engine-issue-sync.js +66 -0
- package/lib/engine-milestones.js +42 -3
- package/lib/engine-prompt.js +28 -4
- package/lib/engine-retrospective.js +79 -0
- package/lib/engine-sessions.js +82 -0
- package/lib/engine-templates.js +167 -0
- package/lib/goal-engine-constants.js +30 -0
- package/lib/goal-engine.js +199 -233
- package/lib/index.js +36 -0
- package/lib/routes.js +139 -9
- package/lib/tools.js +11 -1
- package/package.json +1 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { MilestoneStatus } from './goal-engine-constants.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Apply human intervention to a paused goal
|
|
5
|
+
* @param {object} goal
|
|
6
|
+
* @param {object} intervention
|
|
7
|
+
* @returns {object} result
|
|
8
|
+
*/
|
|
9
|
+
export function applyIntervention(goal, intervention = {}) {
|
|
10
|
+
if (!goal) {
|
|
11
|
+
return { ok: false, error: 'No active goal' };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { directive, milestones, deletedMilestoneIds } = intervention;
|
|
15
|
+
|
|
16
|
+
if (typeof directive === 'string' && directive.trim()) {
|
|
17
|
+
goal.userDirective = directive.trim();
|
|
18
|
+
goal.userDirectiveConsumed = false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (Array.isArray(deletedMilestoneIds) && deletedMilestoneIds.length > 0) {
|
|
22
|
+
const toDelete = new Set(deletedMilestoneIds.map(String));
|
|
23
|
+
goal.milestones = goal.milestones.filter((m) => {
|
|
24
|
+
// Do not allow deleting completed milestones
|
|
25
|
+
if (m.status === MilestoneStatus.COMPLETED) return true;
|
|
26
|
+
return !toDelete.has(String(m.id));
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (Array.isArray(milestones) && milestones.length > 0) {
|
|
31
|
+
for (const update of milestones) {
|
|
32
|
+
if (!update || !update.id) continue;
|
|
33
|
+
const target = goal.milestones.find((m) => String(m.id) === String(update.id));
|
|
34
|
+
if (target) {
|
|
35
|
+
if (typeof update.title === 'string' && update.title.trim()) {
|
|
36
|
+
target.title = update.title.trim();
|
|
37
|
+
}
|
|
38
|
+
if (typeof update.notes === 'string') {
|
|
39
|
+
target.notes = update.notes.trim();
|
|
40
|
+
}
|
|
41
|
+
if (update.status && Object.values(MilestoneStatus).includes(update.status)) {
|
|
42
|
+
target.status = update.status;
|
|
43
|
+
}
|
|
44
|
+
if (Array.isArray(update.dependsOn)) {
|
|
45
|
+
target.dependsOn = update.dependsOn.map(String);
|
|
46
|
+
}
|
|
47
|
+
} else if (typeof update.title === 'string' && update.title.trim()) {
|
|
48
|
+
goal.milestones.push({
|
|
49
|
+
id: String(update.id),
|
|
50
|
+
title: update.title.trim(),
|
|
51
|
+
status: update.status || MilestoneStatus.PENDING,
|
|
52
|
+
notes: update.notes ? String(update.notes).trim() : '',
|
|
53
|
+
checklist: Array.isArray(update.checklist) ? update.checklist : [],
|
|
54
|
+
dependsOn: Array.isArray(update.dependsOn) ? update.dependsOn.map(String) : [],
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
recordActivity(goal, {
|
|
61
|
+
type: 'intervention',
|
|
62
|
+
tool: 'user_intervention',
|
|
63
|
+
status: 'success',
|
|
64
|
+
details: directive ? `Directive: ${directive.slice(0, 60)}` : 'Plan adjusted by user',
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return { ok: true, goal };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Record an activity entry in the goal's recent activity feed (max 10 entries)
|
|
72
|
+
* @param {object} goal
|
|
73
|
+
* @param {object} entry
|
|
74
|
+
*/
|
|
75
|
+
export function recordActivity(goal, entry = {}) {
|
|
76
|
+
if (!goal) return;
|
|
77
|
+
if (!Array.isArray(goal.activityFeed)) {
|
|
78
|
+
goal.activityFeed = [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const actEntry = {
|
|
82
|
+
id: `act-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
83
|
+
type: entry.type || 'tool',
|
|
84
|
+
tool: entry.tool || 'unknown',
|
|
85
|
+
status: entry.status || 'info',
|
|
86
|
+
details: entry.details ? String(entry.details).slice(0, 200) : '',
|
|
87
|
+
timestamp: entry.timestamp || Date.now(),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
goal.activityFeed.push(actEntry);
|
|
91
|
+
if (goal.activityFeed.length > 10) {
|
|
92
|
+
goal.activityFeed.shift();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Check and enforce token budget limits and auto-scaling
|
|
98
|
+
* @param {object} goal
|
|
99
|
+
* @param {object} options
|
|
100
|
+
* @returns {{ paused: boolean }}
|
|
101
|
+
*/
|
|
102
|
+
export function processTokenBudget(goal, options = {}) {
|
|
103
|
+
const { maxTokenBudget = 0, budgetWarningThreshold = 80, autoScaleBudgetNearCompletion = false, GoalState } = options;
|
|
104
|
+
const budget = goal.maxTokenBudget || maxTokenBudget || 0;
|
|
105
|
+
if (budget <= 0 || !goal.tokensUsage) return { paused: false };
|
|
106
|
+
|
|
107
|
+
const currentTotal = goal.tokensUsage.totalTokens;
|
|
108
|
+
const warnThreshold = (budget * budgetWarningThreshold) / 100;
|
|
109
|
+
|
|
110
|
+
if (currentTotal >= warnThreshold && !goal.budgetWarningTriggered) {
|
|
111
|
+
goal.budgetWarningTriggered = true;
|
|
112
|
+
goal.logs.push({
|
|
113
|
+
timestamp: Date.now(),
|
|
114
|
+
type: 'warning',
|
|
115
|
+
message: `Token budget warning: ${currentTotal.toLocaleString('en-US')}/${budget.toLocaleString('en-US')} tokens consumed (${Math.round((currentTotal / budget) * 100)}%)`,
|
|
116
|
+
});
|
|
117
|
+
if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (currentTotal >= budget && goal.state === (GoalState?.RUNNING || 'running')) {
|
|
121
|
+
const milestones = goal.milestones || [];
|
|
122
|
+
const completed = milestones.filter((m) => m.status === MilestoneStatus.COMPLETED).length;
|
|
123
|
+
const progress = milestones.length > 0 ? (completed / milestones.length) : 0;
|
|
124
|
+
|
|
125
|
+
if (autoScaleBudgetNearCompletion && !goal.budgetAutoScaled && (progress >= 0.8 || completed === milestones.length - 1)) {
|
|
126
|
+
const buffer = Math.round(budget * 0.2);
|
|
127
|
+
goal.maxTokenBudget = budget + buffer;
|
|
128
|
+
goal.budgetAutoScaled = true;
|
|
129
|
+
goal.logs.push({
|
|
130
|
+
timestamp: Date.now(),
|
|
131
|
+
type: 'info',
|
|
132
|
+
message: `Token budget auto-scaled by +${buffer.toLocaleString('en-US')} tokens near completion`,
|
|
133
|
+
});
|
|
134
|
+
if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
|
|
135
|
+
return { paused: false };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
goal.state = GoalState?.PAUSED || 'paused';
|
|
139
|
+
goal.pausedAt = Date.now();
|
|
140
|
+
goal.logs.push({
|
|
141
|
+
timestamp: Date.now(),
|
|
142
|
+
type: 'warning',
|
|
143
|
+
message: `Token budget reached: ${currentTotal.toLocaleString('en-US')}/${budget.toLocaleString('en-US')} tokens. Pausing autonomous loop.`,
|
|
144
|
+
});
|
|
145
|
+
if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
|
|
146
|
+
return { paused: true };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { paused: false };
|
|
150
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse issue identifier from goal title or string
|
|
3
|
+
* @param {string} input
|
|
4
|
+
* @returns {object|null} { issueNumber, issueUrl }
|
|
5
|
+
*/
|
|
6
|
+
export function parseIssueRef(input) {
|
|
7
|
+
if (!input || typeof input !== 'string') return null;
|
|
8
|
+
const urlMatch = input.match(/https?:\/\/[^\s]+\/issues\/(\d+)/i);
|
|
9
|
+
if (urlMatch) {
|
|
10
|
+
return { issueNumber: parseInt(urlMatch[1], 10), issueUrl: urlMatch[0] };
|
|
11
|
+
}
|
|
12
|
+
const numMatch = input.match(/(?:issue:?|#)(\d+)\b/i);
|
|
13
|
+
if (numMatch) {
|
|
14
|
+
return { issueNumber: parseInt(numMatch[1], 10), issueUrl: null };
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parse checklist items from markdown text
|
|
21
|
+
* @param {string} markdown
|
|
22
|
+
* @returns {Array<{ text: string, done: boolean, raw: string }>}
|
|
23
|
+
*/
|
|
24
|
+
export function parseIssueChecklist(markdown) {
|
|
25
|
+
if (!markdown || typeof markdown !== 'string') return [];
|
|
26
|
+
const items = [];
|
|
27
|
+
const lines = markdown.split('\n');
|
|
28
|
+
const regex = /^\s*-\s*\[([ xX])\]\s*(.+)$/;
|
|
29
|
+
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
const match = line.match(regex);
|
|
32
|
+
if (match) {
|
|
33
|
+
items.push({
|
|
34
|
+
done: match[1].toLowerCase() === 'x',
|
|
35
|
+
text: match[2].trim(),
|
|
36
|
+
raw: line,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return items;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Update checklist item status in markdown text
|
|
45
|
+
* @param {string} markdown
|
|
46
|
+
* @param {string} itemText
|
|
47
|
+
* @param {boolean} done
|
|
48
|
+
* @returns {string} updated markdown
|
|
49
|
+
*/
|
|
50
|
+
export function updateChecklistInMarkdown(markdown, itemText, done) {
|
|
51
|
+
if (!markdown || typeof markdown !== 'string') return markdown || '';
|
|
52
|
+
if (!itemText) return markdown;
|
|
53
|
+
|
|
54
|
+
const targetClean = itemText.trim().toLowerCase();
|
|
55
|
+
const lines = markdown.split('\n');
|
|
56
|
+
const regex = /^(\s*-\s*\[)([ xX])(\]\s*)(.+)$/;
|
|
57
|
+
|
|
58
|
+
return lines.map((line) => {
|
|
59
|
+
const match = line.match(regex);
|
|
60
|
+
if (match && match[4].trim().toLowerCase() === targetClean) {
|
|
61
|
+
const checkChar = done ? 'x' : ' ';
|
|
62
|
+
return `${match[1]}${checkChar}${match[3]}${match[4]}`;
|
|
63
|
+
}
|
|
64
|
+
return line;
|
|
65
|
+
}).join('\n');
|
|
66
|
+
}
|
package/lib/engine-milestones.js
CHANGED
|
@@ -32,6 +32,31 @@ export function parseChecklist(checklist) {
|
|
|
32
32
|
.filter((item) => item.text.length > 0);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Check if a milestone is blocked by uncompleted prerequisite dependencies
|
|
37
|
+
* @param {object} milestone
|
|
38
|
+
* @param {Array<object>} allMilestones
|
|
39
|
+
* @returns {{ blocked: boolean, missingDependencies: string[] }}
|
|
40
|
+
*/
|
|
41
|
+
export function isMilestoneBlocked(milestone, allMilestones = []) {
|
|
42
|
+
if (!milestone || !Array.isArray(milestone.dependsOn) || milestone.dependsOn.length === 0) {
|
|
43
|
+
return { blocked: false, missingDependencies: [] };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const missing = [];
|
|
47
|
+
for (const depId of milestone.dependsOn) {
|
|
48
|
+
const parent = allMilestones.find((m) => matchMilestone(m, depId));
|
|
49
|
+
if (!parent || parent.status !== MilestoneStatus.COMPLETED) {
|
|
50
|
+
missing.push(String(depId));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
blocked: missing.length > 0,
|
|
56
|
+
missingDependencies: missing,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
35
60
|
/**
|
|
36
61
|
* Build and normalize milestone objects from list
|
|
37
62
|
* @param {Array} milestonesList
|
|
@@ -54,6 +79,7 @@ export function parseMilestoneItems(milestonesList, startingCount = 0) {
|
|
|
54
79
|
title: itemTitle.trim(),
|
|
55
80
|
status: typeof item === 'object' && item?.status ? item.status : MilestoneStatus.PENDING,
|
|
56
81
|
notes: typeof item === 'object' && item?.notes ? String(item.notes) : '',
|
|
82
|
+
dependsOn: typeof item === 'object' && Array.isArray(item?.dependsOn) ? item.dependsOn.map(String) : [],
|
|
57
83
|
};
|
|
58
84
|
if (typeof item === 'object' && Array.isArray(item?.checklist)) {
|
|
59
85
|
mObj.checklist = parseChecklist(item.checklist);
|
|
@@ -71,12 +97,25 @@ export function parseMilestoneItems(milestonesList, startingCount = 0) {
|
|
|
71
97
|
* @param {string} [notes] New notes
|
|
72
98
|
* @param {Array} [checklist] New checklist
|
|
73
99
|
* @param {Array<string>} [validStatuses] Allowed statuses
|
|
74
|
-
* @
|
|
100
|
+
* @param {object} [options] Validation options
|
|
101
|
+
* @returns {{ prevStatus: string, updatedStatus: string, blocked?: boolean, missingDependencies?: string[] }}
|
|
75
102
|
*/
|
|
76
|
-
export function applyMilestoneUpdate(target, status, notes = '', checklist = null, validStatuses = []) {
|
|
103
|
+
export function applyMilestoneUpdate(target, status, notes = '', checklist = null, validStatuses = [], options = {}) {
|
|
77
104
|
if (!target) return { prevStatus: null, updatedStatus: null };
|
|
78
105
|
const prevStatus = target.status;
|
|
79
106
|
|
|
107
|
+
if (options.enforceDependencies && (status === MilestoneStatus.IN_PROGRESS || status === MilestoneStatus.COMPLETED)) {
|
|
108
|
+
const depCheck = isMilestoneBlocked(target, options.allMilestones || []);
|
|
109
|
+
if (depCheck.blocked) {
|
|
110
|
+
return {
|
|
111
|
+
prevStatus,
|
|
112
|
+
updatedStatus: prevStatus,
|
|
113
|
+
blocked: true,
|
|
114
|
+
missingDependencies: depCheck.missingDependencies,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
80
119
|
if (status && (validStatuses.length === 0 || validStatuses.includes(status))) {
|
|
81
120
|
target.status = status;
|
|
82
121
|
}
|
|
@@ -87,7 +126,7 @@ export function applyMilestoneUpdate(target, status, notes = '', checklist = nul
|
|
|
87
126
|
target.checklist = parseChecklist(checklist);
|
|
88
127
|
}
|
|
89
128
|
|
|
90
|
-
return { prevStatus, updatedStatus: target.status };
|
|
129
|
+
return { prevStatus, updatedStatus: target.status, blocked: false };
|
|
91
130
|
}
|
|
92
131
|
|
|
93
132
|
/**
|
package/lib/engine-prompt.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isMilestoneBlocked } from './engine-milestones.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Prompt generation for DSH Goal Mode
|
|
3
5
|
*/
|
|
@@ -6,6 +8,15 @@ export function buildStatePromptInjection(snapshot, pendingNudge) {
|
|
|
6
8
|
const hasMilestones = snapshot.milestones && snapshot.milestones.length > 0;
|
|
7
9
|
const etaText = snapshot.formattedETA ? ` (ETA: ${snapshot.formattedETA})` : '';
|
|
8
10
|
|
|
11
|
+
let directiveText = '';
|
|
12
|
+
if (snapshot.userDirective && !snapshot.userDirectiveConsumed) {
|
|
13
|
+
if (lang === 'zh') {
|
|
14
|
+
directiveText = `\n\n🛑 用户干预指令 (USER INTERVENTION DIRECTIVE):\n"${snapshot.userDirective}"\n你必须无条件优先遵循此指令,调整接下来的执行决策与步骤!\n`;
|
|
15
|
+
} else {
|
|
16
|
+
directiveText = `\n\n🛑 USER INTERVENTION DIRECTIVE:\n"${snapshot.userDirective}"\nYou must prioritize and follow this directive immediately in your next decisions and actions!\n`;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
9
20
|
let nudgeText = '';
|
|
10
21
|
if (pendingNudge) {
|
|
11
22
|
if (lang === 'zh') {
|
|
@@ -16,7 +27,13 @@ export function buildStatePromptInjection(snapshot, pendingNudge) {
|
|
|
16
27
|
}
|
|
17
28
|
|
|
18
29
|
let budgetWarningText = '';
|
|
19
|
-
if (snapshot.
|
|
30
|
+
if (snapshot.budgetAutoScaled) {
|
|
31
|
+
if (lang === 'zh') {
|
|
32
|
+
budgetWarningText = `\n\n💡 TOKEN 预算自动延展通知:\n检测到目标已接近尾声,系统已自动按策略扩充了缓冲预算。请集中精力尽快完成最后阶段,不要展开多余任务!\n`;
|
|
33
|
+
} else {
|
|
34
|
+
budgetWarningText = `\n\n💡 TOKEN BUDGET AUTO-SCALED:\nGoal is near completion; an automatic buffer extension was granted. Focus strictly on wrapping up the remaining milestones!\n`;
|
|
35
|
+
}
|
|
36
|
+
} else if (snapshot.budgetWarningTriggered) {
|
|
20
37
|
const total = snapshot.tokensUsage?.totalTokens || 0;
|
|
21
38
|
const max = snapshot.maxTokenBudget || 0;
|
|
22
39
|
const pct = max > 0 ? Math.round((total / max) * 100) : 0;
|
|
@@ -27,8 +44,13 @@ export function buildStatePromptInjection(snapshot, pendingNudge) {
|
|
|
27
44
|
}
|
|
28
45
|
}
|
|
29
46
|
|
|
47
|
+
const allMilestones = snapshot.milestones || [];
|
|
30
48
|
const formatMilestone = (m, i) => {
|
|
31
|
-
|
|
49
|
+
const depCheck = isMilestoneBlocked(m, allMilestones);
|
|
50
|
+
const blockedLabel = depCheck.blocked
|
|
51
|
+
? ` (BLOCKED by ${depCheck.missingDependencies.join(', ')})`
|
|
52
|
+
: '';
|
|
53
|
+
let line = ` ${i + 1}. [${m.status.toUpperCase()}] ${m.title}${blockedLabel}${m.notes ? ` (${m.notes})` : ''}`;
|
|
32
54
|
if (Array.isArray(m.checklist) && m.checklist.length > 0) {
|
|
33
55
|
const checkLines = m.checklist
|
|
34
56
|
.map((item) => ` [${item.done ? 'x' : ' '}] ${item.text}`)
|
|
@@ -44,6 +66,7 @@ export function buildStatePromptInjection(snapshot, pendingNudge) {
|
|
|
44
66
|
: ' (工作计划尚未建立 — 请立即调用 goal_set_milestones 设定初始里程碑!)';
|
|
45
67
|
|
|
46
68
|
return (
|
|
69
|
+
directiveText +
|
|
47
70
|
nudgeText +
|
|
48
71
|
budgetWarningText +
|
|
49
72
|
`\n\n[DSH GOAL MODE ACTIVE]\n` +
|
|
@@ -52,7 +75,7 @@ export function buildStatePromptInjection(snapshot, pendingNudge) {
|
|
|
52
75
|
`工作计划:\n` +
|
|
53
76
|
`${milestonesText}\n\n` +
|
|
54
77
|
`Goal Mode 执行契约 (必须严格遵循):\n` +
|
|
55
|
-
`1. ${hasMilestones ? '
|
|
78
|
+
`1. ${hasMilestones ? '按部就班推进当前进行中的里程碑。注意:标记为 [BLOCKED] 的里程碑在前置依赖完成前严禁推进。' : '第一步核心指令: 立即调用 goal_set_milestones 制定 3-7 个具体里程碑。在完成此工具调用前严禁执行其他操作!'}\n` +
|
|
56
79
|
`2. 推进里程碑时,必须通过 goal_update_progress 工具更新状态(开始前标为 in_progress,完成后标为 completed 并附简要说明)。如有细分子任务,可通过 checklist 字段同步进度。\n` +
|
|
57
80
|
`3. 当所有里程碑全部完成后,调用 goal_finish 工具提交详细成果总结。`
|
|
58
81
|
);
|
|
@@ -63,6 +86,7 @@ export function buildStatePromptInjection(snapshot, pendingNudge) {
|
|
|
63
86
|
: ' (Work plan is not yet established — call goal_set_milestones immediately with initial steps!)';
|
|
64
87
|
|
|
65
88
|
return (
|
|
89
|
+
directiveText +
|
|
66
90
|
nudgeText +
|
|
67
91
|
budgetWarningText +
|
|
68
92
|
`\n\n[DSH GOAL MODE ACTIVE]\n` +
|
|
@@ -71,7 +95,7 @@ export function buildStatePromptInjection(snapshot, pendingNudge) {
|
|
|
71
95
|
`Work Plan:\n` +
|
|
72
96
|
`${milestonesText}\n\n` +
|
|
73
97
|
`Goal Mode Instructions (MANDATORY TO FOLLOW):\n` +
|
|
74
|
-
`1. ${hasMilestones ? 'Execute the current active milestone from the work plan.' : 'YOUR FIRST STEP: Immediately call tool goal_set_milestones with the list of milestones (3-7 concrete steps). You must not execute work or finish turn without calling goal_set_milestones!'}\n` +
|
|
98
|
+
`1. ${hasMilestones ? 'Execute the current active milestone from the work plan. Note: Milestones marked [BLOCKED] must not be started until prerequisite milestones are COMPLETED.' : 'YOUR FIRST STEP: Immediately call tool goal_set_milestones with the list of milestones (3-7 concrete steps). You must not execute work or finish turn without calling goal_set_milestones!'}\n` +
|
|
75
99
|
`2. As each milestone progresses, update its status via tool goal_update_progress (status: "in_progress" before starting, status: "completed" upon completion with brief notes). If sub-tasks exist, pass checklist array to track items.\n` +
|
|
76
100
|
`3. When all milestones are completed, call tool goal_finish with a detailed summary of achieved results.`
|
|
77
101
|
);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { MilestoneStatus, calculateElapsedSeconds } from './goal-engine-constants.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generate post-goal retrospective analytics card
|
|
6
|
+
* @param {object} goal
|
|
7
|
+
* @param {string} cwd
|
|
8
|
+
* @returns {object} retrospective summary
|
|
9
|
+
*/
|
|
10
|
+
export function generateRetrospectiveData(goal, cwd = process.cwd()) {
|
|
11
|
+
if (!goal) return null;
|
|
12
|
+
|
|
13
|
+
const elapsedSeconds = calculateElapsedSeconds(goal);
|
|
14
|
+
const totalTokens = goal.tokensUsage?.totalTokens || 0;
|
|
15
|
+
// Estimated cost based on blended DeepSeek API pricing (~$0.0000015/token)
|
|
16
|
+
const estimatedCostUsd = Number(((totalTokens * 0.0000015)).toFixed(4));
|
|
17
|
+
|
|
18
|
+
const milestones = Array.isArray(goal.milestones) ? goal.milestones : [];
|
|
19
|
+
const totalMilestones = milestones.length;
|
|
20
|
+
const completedMilestones = milestones.filter((m) => m.status === MilestoneStatus.COMPLETED).length;
|
|
21
|
+
|
|
22
|
+
const milestoneVelocity = completedMilestones > 0
|
|
23
|
+
? Math.round(elapsedSeconds / completedMilestones)
|
|
24
|
+
: 0;
|
|
25
|
+
|
|
26
|
+
const activities = Array.isArray(goal.activityFeed) ? goal.activityFeed : [];
|
|
27
|
+
const totalToolCalls = activities.filter((a) => a.type === 'tool').length;
|
|
28
|
+
const failedToolCalls = activities.filter((a) => a.type === 'tool' && a.status === 'error').length;
|
|
29
|
+
const toolFailureRate = totalToolCalls > 0
|
|
30
|
+
? Math.round((failedToolCalls / totalToolCalls) * 100)
|
|
31
|
+
: 0;
|
|
32
|
+
|
|
33
|
+
let gitDiffStat = '';
|
|
34
|
+
let filesChangedCount = 0;
|
|
35
|
+
try {
|
|
36
|
+
const startRef = goal.startCommit || 'HEAD~1';
|
|
37
|
+
const rawStat = execFileSync('git', ['diff', '--stat', `${startRef}..HEAD`], {
|
|
38
|
+
cwd,
|
|
39
|
+
encoding: 'utf8',
|
|
40
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
41
|
+
}).trim();
|
|
42
|
+
gitDiffStat = rawStat;
|
|
43
|
+
const match = rawStat.match(/(\d+)\s+file[s]?\s+changed/i);
|
|
44
|
+
if (match) {
|
|
45
|
+
filesChangedCount = parseInt(match[1], 10);
|
|
46
|
+
}
|
|
47
|
+
} catch (err) {
|
|
48
|
+
gitDiffStat = 'No git diff available';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const recommendations = [];
|
|
52
|
+
if (toolFailureRate > 20) {
|
|
53
|
+
recommendations.push('High tool error rate detected; check command arguments or environment paths.');
|
|
54
|
+
}
|
|
55
|
+
if (totalTokens > 100000) {
|
|
56
|
+
recommendations.push('Consider setting maxTokenBudget or decomposing goal into smaller sub-tasks.');
|
|
57
|
+
}
|
|
58
|
+
if (completedMilestones === totalMilestones && totalMilestones > 0) {
|
|
59
|
+
recommendations.push('All milestones completed cleanly. Recommended next step: run full test suite and verify git status.');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
title: goal.title || 'Untitled Goal',
|
|
64
|
+
elapsedSeconds,
|
|
65
|
+
totalTokens,
|
|
66
|
+
estimatedCostUsd,
|
|
67
|
+
totalMilestones,
|
|
68
|
+
completedMilestones,
|
|
69
|
+
milestoneVelocity,
|
|
70
|
+
totalToolCalls,
|
|
71
|
+
failedToolCalls,
|
|
72
|
+
toolFailureRate,
|
|
73
|
+
gitDiffStat,
|
|
74
|
+
filesChangedCount,
|
|
75
|
+
recommendations,
|
|
76
|
+
gitBranch: goal.gitBranch || null,
|
|
77
|
+
generatedAt: Date.now(),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { GoalState } from './goal-engine-constants.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Prune inactive sessions when exceeding maxSessions threshold
|
|
5
|
+
*/
|
|
6
|
+
export function pruneInactiveSessions(goals, stallCounters, toolFailureCounters, maxSessions = 100) {
|
|
7
|
+
if (!goals || goals.size < maxSessions) return;
|
|
8
|
+
const inactive = [];
|
|
9
|
+
for (const [sid, goal] of goals.entries()) {
|
|
10
|
+
if (sid === 'default') continue;
|
|
11
|
+
if (goal.state === GoalState.COMPLETED || goal.state === GoalState.CANCELLED || goal.state === GoalState.FAILED) {
|
|
12
|
+
inactive.push({ sid, completedAt: goal.completedAt || goal.startedAt || 0 });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
inactive.sort((a, b) => a.completedAt - b.completedAt);
|
|
16
|
+
while (goals.size >= maxSessions && inactive.length > 0) {
|
|
17
|
+
const oldest = inactive.shift();
|
|
18
|
+
goals.delete(oldest.sid);
|
|
19
|
+
stallCounters?.delete(oldest.sid);
|
|
20
|
+
toolFailureCounters?.delete(oldest.sid);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Add a user steering nudge to a goal
|
|
26
|
+
*/
|
|
27
|
+
export function addNudgeToGoal(goal, text) {
|
|
28
|
+
if (!goal) return null;
|
|
29
|
+
const clean = String(text || '').trim();
|
|
30
|
+
if (!clean) return null;
|
|
31
|
+
|
|
32
|
+
if (!Array.isArray(goal.nudges)) goal.nudges = [];
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
goal.nudges.push({ text: clean, timestamp: now });
|
|
35
|
+
goal.pendingNudge = clean;
|
|
36
|
+
goal.logs.push({
|
|
37
|
+
timestamp: now,
|
|
38
|
+
type: 'info',
|
|
39
|
+
message: `User steering / clarification: "${clean}"`,
|
|
40
|
+
});
|
|
41
|
+
if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
|
|
42
|
+
return clean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Consume and clear pending nudge
|
|
47
|
+
*/
|
|
48
|
+
export function consumeGoalNudge(goal) {
|
|
49
|
+
if (!goal || !goal.pendingNudge) return null;
|
|
50
|
+
const nudge = goal.pendingNudge;
|
|
51
|
+
goal.pendingNudge = null;
|
|
52
|
+
return nudge;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Extend token budget for a goal
|
|
57
|
+
*/
|
|
58
|
+
export function extendGoalBudget(goal, addTokens = 50000, defaultBudget = 0) {
|
|
59
|
+
if (!goal) return null;
|
|
60
|
+
const currentTotal = goal.tokensUsage?.totalTokens || 0;
|
|
61
|
+
const currentMax = goal.maxTokenBudget || defaultBudget || 0;
|
|
62
|
+
const newBudget = Math.max(currentTotal, currentMax) + Math.max(1000, Number(addTokens) || 50000);
|
|
63
|
+
goal.maxTokenBudget = newBudget;
|
|
64
|
+
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
if (goal.state === GoalState.PAUSED) {
|
|
67
|
+
if (goal.pausedAt) {
|
|
68
|
+
goal.totalPausedDurationMs += now - goal.pausedAt;
|
|
69
|
+
goal.pausedAt = null;
|
|
70
|
+
}
|
|
71
|
+
goal.state = GoalState.RUNNING;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
goal.budgetWarningTriggered = false;
|
|
75
|
+
goal.logs.push({
|
|
76
|
+
timestamp: now,
|
|
77
|
+
type: 'info',
|
|
78
|
+
message: `Token budget extended by +${addTokens.toLocaleString('en-US')} (new limit: ${newBudget.toLocaleString('en-US')})`,
|
|
79
|
+
});
|
|
80
|
+
if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
|
|
81
|
+
return newBudget;
|
|
82
|
+
}
|