@goodandready/dsh-goal 0.2.6 → 0.2.7
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 +150 -136
- package/lib/engine-milestones.js +106 -0
- package/lib/engine-reports.js +7 -7
- package/lib/goal-engine-constants.js +116 -0
- package/lib/goal-engine.js +20 -140
- package/lib/routes.js +30 -2
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -420,7 +420,7 @@ window.__ModuleLoader__.load({
|
|
|
420
420
|
border: 1px solid var(--dsw-alias-border-subtle);
|
|
421
421
|
border-radius: 9999px;
|
|
422
422
|
padding: 4px 12px;
|
|
423
|
-
box-shadow:
|
|
423
|
+
box-shadow: var(--dsw-alias-shadow-md);
|
|
424
424
|
backdrop-filter: blur(8px);
|
|
425
425
|
font-size: 13px;
|
|
426
426
|
max-width: 100%;
|
|
@@ -453,7 +453,7 @@ window.__ModuleLoader__.load({
|
|
|
453
453
|
margin: 0;
|
|
454
454
|
}
|
|
455
455
|
.dsh-goal-budget-warn {
|
|
456
|
-
color:
|
|
456
|
+
color: var(--dsw-alias-status-warning);
|
|
457
457
|
font-weight: 600;
|
|
458
458
|
display: inline-flex;
|
|
459
459
|
align-items: center;
|
|
@@ -2580,7 +2580,9 @@ window.__ModuleLoader__.load({
|
|
|
2580
2580
|
const next = !prev;
|
|
2581
2581
|
try {
|
|
2582
2582
|
localStorage.setItem('dsh_goal_dock_collapsed', String(next));
|
|
2583
|
-
} catch (e) {
|
|
2583
|
+
} catch (e) {
|
|
2584
|
+
// Handled defensively: localStorage might be unavailable or quota exceeded
|
|
2585
|
+
}
|
|
2584
2586
|
return next;
|
|
2585
2587
|
});
|
|
2586
2588
|
};
|
|
@@ -2637,7 +2639,7 @@ window.__ModuleLoader__.load({
|
|
|
2637
2639
|
|
|
2638
2640
|
const active = resolveLocale();
|
|
2639
2641
|
|
|
2640
|
-
return LOCALES[active]?.[key] || LOCALES.en?.[key] ||
|
|
2642
|
+
return LOCALES[active]?.[key] || LOCALES.en?.[key] || key;
|
|
2641
2643
|
|
|
2642
2644
|
};
|
|
2643
2645
|
|
|
@@ -3247,7 +3249,7 @@ window.__ModuleLoader__.load({
|
|
|
3247
3249
|
'button',
|
|
3248
3250
|
{
|
|
3249
3251
|
className: 'dsh-goal-btn',
|
|
3250
|
-
style: { background: '
|
|
3252
|
+
style: { background: 'var(--dsw-alias-status-warning)', color: 'var(--dsw-alias-surface-raised)', fontWeight: 600, fontSize: 12, padding: '2px 8px' },
|
|
3251
3253
|
title: t('extendBudgetBtn') || '+50k Tokens & Resume',
|
|
3252
3254
|
onClick: () => handleAction('extend_budget', { addTokens: 50000 }),
|
|
3253
3255
|
},
|
|
@@ -4686,146 +4688,158 @@ window.__ModuleLoader__.load({
|
|
|
4686
4688
|
module.exports.inject = ['slots', 'locale', 'settingsScope'];
|
|
4687
4689
|
|
|
4688
4690
|
module.exports.apply = function apply(ctx) {
|
|
4691
|
+
if (typeof ctx.effect === 'function') {
|
|
4692
|
+
ctx.effect(() => {
|
|
4693
|
+
try {
|
|
4694
|
+
ctx.locale?.register?.(NS, LOCALES);
|
|
4695
|
+
} catch (err) { /* handled defensively */ }
|
|
4696
|
+
|
|
4697
|
+
const settingsScope =
|
|
4698
|
+
ctx.settingsScope && typeof ctx.settingsScope.bind === 'function'
|
|
4699
|
+
? ctx.settingsScope.bind({ namespace: NS })
|
|
4700
|
+
: null;
|
|
4701
|
+
|
|
4702
|
+
const disposers = [];
|
|
4703
|
+
const registerSlotSafe = (name, entry, comp) => {
|
|
4704
|
+
try {
|
|
4705
|
+
if (typeof ctx.slots?.inject === 'function') {
|
|
4706
|
+
const unreg = ctx.slots.inject(name, () => {
|
|
4707
|
+
try {
|
|
4708
|
+
return ctx.slots.register(entry, comp);
|
|
4709
|
+
} catch (err) { /* handled defensively */ }
|
|
4710
|
+
});
|
|
4711
|
+
if (typeof unreg === 'function') disposers.push(unreg);
|
|
4712
|
+
} else if (typeof ctx.slots?.register === 'function') {
|
|
4713
|
+
const unreg = ctx.slots.register(entry, comp);
|
|
4714
|
+
if (typeof unreg === 'function') disposers.push(unreg);
|
|
4715
|
+
}
|
|
4716
|
+
} catch (err) { /* handled defensively */ }
|
|
4717
|
+
};
|
|
4718
|
+
|
|
4719
|
+
registerSlotSafe(
|
|
4720
|
+
'conversation.input.dock',
|
|
4721
|
+
{
|
|
4722
|
+
name: 'conversation.input.dock',
|
|
4723
|
+
id: '@goodandready/dsh-goal',
|
|
4724
|
+
order: 10,
|
|
4725
|
+
locale: NS,
|
|
4726
|
+
inject: (slotProps) => ({ ctx, ...slotProps }),
|
|
4727
|
+
},
|
|
4728
|
+
GoalTopBanner,
|
|
4729
|
+
);
|
|
4730
|
+
|
|
4731
|
+
registerSlotSafe(
|
|
4732
|
+
'plugins.item',
|
|
4733
|
+
{
|
|
4734
|
+
name: 'plugins.item',
|
|
4735
|
+
id: ROW_ID,
|
|
4736
|
+
order: 60,
|
|
4737
|
+
label: () => 'Goal',
|
|
4738
|
+
locale: NS,
|
|
4739
|
+
inject: () => ({ ctx, scope: settingsScope }),
|
|
4740
|
+
},
|
|
4741
|
+
GoalSettingsCard,
|
|
4742
|
+
);
|
|
4743
|
+
|
|
4744
|
+
registerSlotSafe(
|
|
4745
|
+
'plugins.row.config',
|
|
4746
|
+
{
|
|
4747
|
+
name: 'plugins.row.config',
|
|
4748
|
+
key: ROW_CONFIG_KEY,
|
|
4749
|
+
locale: NS,
|
|
4750
|
+
inject: () => ({ ctx, scope: settingsScope }),
|
|
4751
|
+
},
|
|
4752
|
+
GoalSettingsCard,
|
|
4753
|
+
);
|
|
4754
|
+
|
|
4755
|
+
registerSlotSafe(
|
|
4756
|
+
'settings.plugin.item',
|
|
4757
|
+
{
|
|
4758
|
+
name: 'settings.plugin.item',
|
|
4759
|
+
key: NS,
|
|
4760
|
+
locale: NS,
|
|
4761
|
+
inject: () => ({ ctx, scope: settingsScope }),
|
|
4762
|
+
},
|
|
4763
|
+
GoalSettingsCard,
|
|
4764
|
+
);
|
|
4689
4765
|
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
}
|
|
4697
|
-
|
|
4698
|
-
// Plugin settings scope
|
|
4699
|
-
|
|
4700
|
-
const settingsScope =
|
|
4701
|
-
|
|
4702
|
-
ctx.settingsScope && typeof ctx.settingsScope.bind === 'function'
|
|
4703
|
-
|
|
4704
|
-
? ctx.settingsScope.bind({ namespace: NS })
|
|
4705
|
-
|
|
4706
|
-
: null;
|
|
4707
|
-
|
|
4708
|
-
const registerSlotSafe = (name, entry, comp) => {
|
|
4709
|
-
|
|
4766
|
+
return () => {
|
|
4767
|
+
for (const dispose of disposers) {
|
|
4768
|
+
try { dispose(); } catch (e) { /* handled defensively */ }
|
|
4769
|
+
}
|
|
4770
|
+
};
|
|
4771
|
+
}, 'dsh-goal: client slots & locale');
|
|
4772
|
+
} else {
|
|
4710
4773
|
try {
|
|
4711
|
-
|
|
4712
|
-
if (typeof ctx.slots?.inject === 'function') {
|
|
4713
|
-
|
|
4714
|
-
ctx.slots.inject(name, () => {
|
|
4715
|
-
|
|
4716
|
-
try {
|
|
4717
|
-
|
|
4718
|
-
return ctx.slots.register(entry, comp);
|
|
4719
|
-
|
|
4720
|
-
} catch (err) { /* handled defensively */ }
|
|
4721
|
-
|
|
4722
|
-
});
|
|
4723
|
-
|
|
4724
|
-
} else if (typeof ctx.slots?.register === 'function') {
|
|
4725
|
-
|
|
4726
|
-
ctx.slots.register(entry, comp);
|
|
4727
|
-
|
|
4728
|
-
}
|
|
4729
|
-
|
|
4774
|
+
ctx.locale?.register?.(NS, LOCALES);
|
|
4730
4775
|
} catch (err) { /* handled defensively */ }
|
|
4731
4776
|
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
registerSlotSafe(
|
|
4737
|
-
|
|
4738
|
-
'conversation.input.dock',
|
|
4739
|
-
|
|
4740
|
-
{
|
|
4741
|
-
|
|
4742
|
-
name: 'conversation.input.dock',
|
|
4743
|
-
|
|
4744
|
-
id: '@goodandready/dsh-goal',
|
|
4745
|
-
|
|
4746
|
-
order: 10,
|
|
4747
|
-
|
|
4748
|
-
locale: NS,
|
|
4749
|
-
|
|
4750
|
-
inject: (slotProps) => ({ ctx, ...slotProps }),
|
|
4751
|
-
|
|
4752
|
-
},
|
|
4753
|
-
|
|
4754
|
-
GoalTopBanner,
|
|
4755
|
-
|
|
4756
|
-
);
|
|
4757
|
-
|
|
4758
|
-
// 2. Register settings card.
|
|
4759
|
-
// List seat (plugins.item): the seat the Plugins page renders as the plugin's own
|
|
4760
|
-
// page with its configuration. The label is a static string on purpose — it is
|
|
4761
|
-
// resolved while the page renders, and a locale lookup there would take the whole
|
|
4762
|
-
// client batch down with it.
|
|
4763
|
-
registerSlotSafe(
|
|
4764
|
-
|
|
4765
|
-
'plugins.item',
|
|
4766
|
-
|
|
4767
|
-
{
|
|
4777
|
+
const settingsScope =
|
|
4778
|
+
ctx.settingsScope && typeof ctx.settingsScope.bind === 'function'
|
|
4779
|
+
? ctx.settingsScope.bind({ namespace: NS })
|
|
4780
|
+
: null;
|
|
4768
4781
|
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
}
|
|
4782
|
-
|
|
4783
|
-
GoalSettingsCard,
|
|
4784
|
-
|
|
4785
|
-
);
|
|
4786
|
-
|
|
4787
|
-
// Row seat and the legacy seat kept as fallbacks.
|
|
4788
|
-
|
|
4789
|
-
registerSlotSafe(
|
|
4790
|
-
|
|
4791
|
-
'plugins.row.config',
|
|
4792
|
-
|
|
4793
|
-
{
|
|
4794
|
-
|
|
4795
|
-
name: 'plugins.row.config',
|
|
4796
|
-
|
|
4797
|
-
key: ROW_CONFIG_KEY,
|
|
4798
|
-
|
|
4799
|
-
locale: NS,
|
|
4800
|
-
|
|
4801
|
-
inject: () => ({ ctx, scope: settingsScope }),
|
|
4802
|
-
|
|
4803
|
-
},
|
|
4804
|
-
|
|
4805
|
-
GoalSettingsCard,
|
|
4806
|
-
|
|
4807
|
-
);
|
|
4808
|
-
|
|
4809
|
-
registerSlotSafe(
|
|
4810
|
-
|
|
4811
|
-
'settings.plugin.item',
|
|
4812
|
-
|
|
4813
|
-
{
|
|
4814
|
-
|
|
4815
|
-
name: 'settings.plugin.item',
|
|
4816
|
-
|
|
4817
|
-
key: NS,
|
|
4818
|
-
|
|
4819
|
-
locale: NS,
|
|
4820
|
-
|
|
4821
|
-
inject: () => ({ ctx, scope: settingsScope }),
|
|
4782
|
+
const registerSlotSafe = (name, entry, comp) => {
|
|
4783
|
+
try {
|
|
4784
|
+
if (typeof ctx.slots?.inject === 'function') {
|
|
4785
|
+
ctx.slots.inject(name, () => {
|
|
4786
|
+
try {
|
|
4787
|
+
return ctx.slots.register(entry, comp);
|
|
4788
|
+
} catch (err) { /* handled defensively */ }
|
|
4789
|
+
});
|
|
4790
|
+
} else if (typeof ctx.slots?.register === 'function') {
|
|
4791
|
+
ctx.slots.register(entry, comp);
|
|
4792
|
+
}
|
|
4793
|
+
} catch (err) { /* handled defensively */ }
|
|
4794
|
+
};
|
|
4822
4795
|
|
|
4823
|
-
|
|
4796
|
+
registerSlotSafe(
|
|
4797
|
+
'conversation.input.dock',
|
|
4798
|
+
{
|
|
4799
|
+
name: 'conversation.input.dock',
|
|
4800
|
+
id: '@goodandready/dsh-goal',
|
|
4801
|
+
order: 10,
|
|
4802
|
+
locale: NS,
|
|
4803
|
+
inject: (slotProps) => ({ ctx, ...slotProps }),
|
|
4804
|
+
},
|
|
4805
|
+
GoalTopBanner,
|
|
4806
|
+
);
|
|
4824
4807
|
|
|
4825
|
-
|
|
4808
|
+
registerSlotSafe(
|
|
4809
|
+
'plugins.item',
|
|
4810
|
+
{
|
|
4811
|
+
name: 'plugins.item',
|
|
4812
|
+
id: ROW_ID,
|
|
4813
|
+
order: 60,
|
|
4814
|
+
label: () => 'Goal',
|
|
4815
|
+
locale: NS,
|
|
4816
|
+
inject: () => ({ ctx, scope: settingsScope }),
|
|
4817
|
+
},
|
|
4818
|
+
GoalSettingsCard,
|
|
4819
|
+
);
|
|
4826
4820
|
|
|
4827
|
-
|
|
4821
|
+
registerSlotSafe(
|
|
4822
|
+
'plugins.row.config',
|
|
4823
|
+
{
|
|
4824
|
+
name: 'plugins.row.config',
|
|
4825
|
+
key: ROW_CONFIG_KEY,
|
|
4826
|
+
locale: NS,
|
|
4827
|
+
inject: () => ({ ctx, scope: settingsScope }),
|
|
4828
|
+
},
|
|
4829
|
+
GoalSettingsCard,
|
|
4830
|
+
);
|
|
4828
4831
|
|
|
4832
|
+
registerSlotSafe(
|
|
4833
|
+
'settings.plugin.item',
|
|
4834
|
+
{
|
|
4835
|
+
name: 'settings.plugin.item',
|
|
4836
|
+
key: NS,
|
|
4837
|
+
locale: NS,
|
|
4838
|
+
inject: () => ({ ctx, scope: settingsScope }),
|
|
4839
|
+
},
|
|
4840
|
+
GoalSettingsCard,
|
|
4841
|
+
);
|
|
4842
|
+
}
|
|
4829
4843
|
};
|
|
4830
4844
|
|
|
4831
4845
|
return module.exports;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { MilestoneStatus } from './goal-engine-constants.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Match a milestone object by exact ID or normalized ID (e.g. m-1 vs m1 vs 1)
|
|
5
|
+
* @param {object} m Milestone object
|
|
6
|
+
* @param {string|number} id Milestone ID to match
|
|
7
|
+
* @returns {boolean}
|
|
8
|
+
*/
|
|
9
|
+
export function matchMilestone(m, id) {
|
|
10
|
+
if (!m || id === undefined || id === null) return false;
|
|
11
|
+
if (m.id === String(id)) return true;
|
|
12
|
+
const s1 = String(m.id).toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
13
|
+
const s2 = String(id).toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
14
|
+
if (s1 && s1 === s2) return true;
|
|
15
|
+
const n1 = s1.replace(/^m+/, '');
|
|
16
|
+
const n2 = s2.replace(/^m+/, '');
|
|
17
|
+
return Boolean(n1 && n1 === n2);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Safely parse checklist items
|
|
22
|
+
* @param {Array} checklist
|
|
23
|
+
* @returns {Array<{ text: string, done: boolean }>}
|
|
24
|
+
*/
|
|
25
|
+
export function parseChecklist(checklist) {
|
|
26
|
+
if (!Array.isArray(checklist)) return [];
|
|
27
|
+
return checklist
|
|
28
|
+
.map((item) => ({
|
|
29
|
+
text: String(item?.text || item?.title || '').trim(),
|
|
30
|
+
done: Boolean(item?.done || item?.completed),
|
|
31
|
+
}))
|
|
32
|
+
.filter((item) => item.text.length > 0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Build and normalize milestone objects from list
|
|
37
|
+
* @param {Array} milestonesList
|
|
38
|
+
* @param {number} startingCount
|
|
39
|
+
* @returns {Array<object>}
|
|
40
|
+
*/
|
|
41
|
+
export function parseMilestoneItems(milestonesList, startingCount = 0) {
|
|
42
|
+
if (!Array.isArray(milestonesList)) return [];
|
|
43
|
+
const result = [];
|
|
44
|
+
let count = startingCount;
|
|
45
|
+
|
|
46
|
+
for (const item of milestonesList) {
|
|
47
|
+
const itemTitle = typeof item === 'string' ? item : item?.title;
|
|
48
|
+
if (!itemTitle || !itemTitle.trim()) continue;
|
|
49
|
+
|
|
50
|
+
count += 1;
|
|
51
|
+
const mId = typeof item === 'object' && item?.id ? item.id : ('m-' + count);
|
|
52
|
+
const mObj = {
|
|
53
|
+
id: String(mId),
|
|
54
|
+
title: itemTitle.trim(),
|
|
55
|
+
status: typeof item === 'object' && item?.status ? item.status : MilestoneStatus.PENDING,
|
|
56
|
+
notes: typeof item === 'object' && item?.notes ? String(item.notes) : '',
|
|
57
|
+
};
|
|
58
|
+
if (typeof item === 'object' && Array.isArray(item?.checklist)) {
|
|
59
|
+
mObj.checklist = parseChecklist(item.checklist);
|
|
60
|
+
}
|
|
61
|
+
result.push(mObj);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Apply updates to a milestone target
|
|
69
|
+
* @param {object} target Milestone object
|
|
70
|
+
* @param {string} [status] New status
|
|
71
|
+
* @param {string} [notes] New notes
|
|
72
|
+
* @param {Array} [checklist] New checklist
|
|
73
|
+
* @param {Array<string>} [validStatuses] Allowed statuses
|
|
74
|
+
* @returns {{ prevStatus: string, updatedStatus: string }}
|
|
75
|
+
*/
|
|
76
|
+
export function applyMilestoneUpdate(target, status, notes = '', checklist = null, validStatuses = []) {
|
|
77
|
+
if (!target) return { prevStatus: null, updatedStatus: null };
|
|
78
|
+
const prevStatus = target.status;
|
|
79
|
+
|
|
80
|
+
if (status && (validStatuses.length === 0 || validStatuses.includes(status))) {
|
|
81
|
+
target.status = status;
|
|
82
|
+
}
|
|
83
|
+
if (notes !== undefined && notes !== null && notes !== '') {
|
|
84
|
+
target.notes = String(notes);
|
|
85
|
+
}
|
|
86
|
+
if (Array.isArray(checklist)) {
|
|
87
|
+
target.checklist = parseChecklist(checklist);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { prevStatus, updatedStatus: target.status };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Toggle a checklist item in milestone target
|
|
95
|
+
* @param {object} target Milestone object
|
|
96
|
+
* @param {number} itemIndex Index in checklist
|
|
97
|
+
* @param {boolean} [done] Optional explicit done boolean
|
|
98
|
+
* @returns {boolean}
|
|
99
|
+
*/
|
|
100
|
+
export function toggleMilestoneChecklistItem(target, itemIndex, done) {
|
|
101
|
+
if (!target || !Array.isArray(target.checklist) || !target.checklist[itemIndex]) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
target.checklist[itemIndex].done = done !== undefined ? Boolean(done) : !target.checklist[itemIndex].done;
|
|
105
|
+
return true;
|
|
106
|
+
}
|
package/lib/engine-reports.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Safely get current short git commit hash
|
|
@@ -7,7 +7,7 @@ import { execSync } from 'node:child_process';
|
|
|
7
7
|
*/
|
|
8
8
|
export function getGitCurrentCommit(cwd) {
|
|
9
9
|
try {
|
|
10
|
-
return
|
|
10
|
+
return execFileSync('git', ['rev-parse', '--short', 'HEAD'], {
|
|
11
11
|
encoding: 'utf8',
|
|
12
12
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
13
13
|
timeout: 1000,
|
|
@@ -126,7 +126,7 @@ import path from 'node:path';
|
|
|
126
126
|
export function createMilestoneCheckpoint(milestone, sessionId = 'default', cwd) {
|
|
127
127
|
if (!milestone || !milestone.id) return null;
|
|
128
128
|
try {
|
|
129
|
-
const status =
|
|
129
|
+
const status = execFileSync('git', ['status', '--porcelain'], {
|
|
130
130
|
encoding: 'utf8',
|
|
131
131
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
132
132
|
timeout: 2000,
|
|
@@ -135,21 +135,21 @@ export function createMilestoneCheckpoint(milestone, sessionId = 'default', cwd)
|
|
|
135
135
|
|
|
136
136
|
// If there are changes, auto-commit checkpoint
|
|
137
137
|
if (status) {
|
|
138
|
-
|
|
138
|
+
execFileSync('git', ['add', '-A'], {
|
|
139
139
|
stdio: ['ignore', 'ignore', 'ignore'],
|
|
140
140
|
timeout: 3000,
|
|
141
141
|
cwd: cwd || undefined,
|
|
142
142
|
});
|
|
143
143
|
const cleanTitle = (milestone.title || '').replace(/[\"\`\$]/g, '');
|
|
144
144
|
const msg = `checkpoint(goal): [${milestone.id}] ${cleanTitle}`;
|
|
145
|
-
|
|
145
|
+
execFileSync('git', ['commit', '-m', msg, '--no-verify'], {
|
|
146
146
|
stdio: ['ignore', 'ignore', 'ignore'],
|
|
147
147
|
timeout: 5000,
|
|
148
148
|
cwd: cwd || undefined,
|
|
149
149
|
});
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
const hash =
|
|
152
|
+
const hash = execFileSync('git', ['rev-parse', '--short', 'HEAD'], {
|
|
153
153
|
encoding: 'utf8',
|
|
154
154
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
155
155
|
timeout: 1000,
|
|
@@ -173,7 +173,7 @@ export function rollbackToCheckpoint(commitHash, cwd) {
|
|
|
173
173
|
try {
|
|
174
174
|
const cleanHash = commitHash.trim().replace(/[^a-zA-Z0-9_-]/g, '');
|
|
175
175
|
if (!cleanHash) return false;
|
|
176
|
-
|
|
176
|
+
execFileSync('git', ['checkout', cleanHash, '--', '.'], {
|
|
177
177
|
stdio: ['ignore', 'ignore', 'ignore'],
|
|
178
178
|
timeout: 5000,
|
|
179
179
|
cwd: cwd || undefined,
|
|
@@ -98,3 +98,119 @@ export function sessionIdOf(invocationOrReq, fallback = 'default') {
|
|
|
98
98
|
}
|
|
99
99
|
return fallback;
|
|
100
100
|
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Calculate elapsed seconds from goal timing properties
|
|
104
|
+
* @param {object} goal
|
|
105
|
+
* @returns {number}
|
|
106
|
+
*/
|
|
107
|
+
export function calculateElapsedSeconds(goal) {
|
|
108
|
+
if (!goal) return 0;
|
|
109
|
+
const { startedAt, pausedAt, totalPausedDurationMs, completedAt } = goal;
|
|
110
|
+
const endTime = completedAt || (pausedAt || Date.now());
|
|
111
|
+
const elapsedMs = Math.max(0, endTime - startedAt - (totalPausedDurationMs || 0));
|
|
112
|
+
return Math.floor(elapsedMs / 1000);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Calculate estimated remaining seconds based on completed milestones
|
|
117
|
+
* @param {object} goal
|
|
118
|
+
* @param {number} elapsed
|
|
119
|
+
* @returns {number|null}
|
|
120
|
+
*/
|
|
121
|
+
export function calculateRemainingSeconds(goal, elapsed) {
|
|
122
|
+
if (!goal || goal.state !== GoalState.RUNNING) return null;
|
|
123
|
+
const total = goal.milestones?.length || 0;
|
|
124
|
+
if (total === 0) return null;
|
|
125
|
+
const completedCount = goal.milestones.filter((m) => m.status === MilestoneStatus.COMPLETED).length;
|
|
126
|
+
if (completedCount === 0 || completedCount >= total) return null;
|
|
127
|
+
if (elapsed <= 0) return null;
|
|
128
|
+
const avgSecPerMilestone = elapsed / completedCount;
|
|
129
|
+
const remainingCount = total - completedCount;
|
|
130
|
+
return Math.max(1, Math.round(avgSecPerMilestone * remainingCount));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Build snapshot object for a given goal or idle state
|
|
135
|
+
* @param {object|null} goal
|
|
136
|
+
* @param {string} sid
|
|
137
|
+
* @param {object} engine
|
|
138
|
+
* @returns {object}
|
|
139
|
+
*/
|
|
140
|
+
export function buildSnapshot(goal, sid, engine) {
|
|
141
|
+
if (!goal) {
|
|
142
|
+
return {
|
|
143
|
+
sessionId: sid,
|
|
144
|
+
hasActiveGoal: false,
|
|
145
|
+
state: GoalState.IDLE,
|
|
146
|
+
title: '',
|
|
147
|
+
startedAt: null,
|
|
148
|
+
pausedAt: null,
|
|
149
|
+
totalPausedDurationMs: 0,
|
|
150
|
+
completedAt: null,
|
|
151
|
+
elapsedSeconds: 0,
|
|
152
|
+
formattedElapsed: '0s',
|
|
153
|
+
estimatedRemainingSeconds: null,
|
|
154
|
+
formattedETA: null,
|
|
155
|
+
lang: 'en',
|
|
156
|
+
tokensUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
157
|
+
milestones: [],
|
|
158
|
+
progressPercent: 0,
|
|
159
|
+
iterationsCount: 0,
|
|
160
|
+
maxIterations: engine.defaultMaxIterations,
|
|
161
|
+
autoDrive: engine.autoDrive,
|
|
162
|
+
enableSound: engine.enableSound,
|
|
163
|
+
showQuickLaunchButton: engine.showQuickLaunchButton,
|
|
164
|
+
gitStartCommit: null,
|
|
165
|
+
pendingNudge: null,
|
|
166
|
+
toolFailureCount: 0,
|
|
167
|
+
consecutiveToolFailureLimit: engine.consecutiveToolFailureLimit,
|
|
168
|
+
maxTokenBudget: engine.maxTokenBudget,
|
|
169
|
+
budgetWarningThreshold: engine.budgetWarningThreshold,
|
|
170
|
+
budgetWarningTriggered: false,
|
|
171
|
+
autoCheckpointOnMilestone: engine.autoCheckpointOnMilestone,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const elapsed = calculateElapsedSeconds(goal);
|
|
176
|
+
const milestones = goal.milestones || [];
|
|
177
|
+
const completedCount = milestones.filter((m) => m.status === MilestoneStatus.COMPLETED).length;
|
|
178
|
+
const progressPercent = milestones.length > 0 ? Math.round((completedCount / milestones.length) * 100) : 0;
|
|
179
|
+
const estSec = calculateRemainingSeconds(goal, elapsed);
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
sessionId: sid,
|
|
183
|
+
hasActiveGoal: true,
|
|
184
|
+
id: goal.id,
|
|
185
|
+
state: goal.state,
|
|
186
|
+
title: goal.title,
|
|
187
|
+
description: goal.description,
|
|
188
|
+
lang: goal.lang || detectLanguage(goal.title),
|
|
189
|
+
startedAt: goal.startedAt,
|
|
190
|
+
pausedAt: goal.pausedAt,
|
|
191
|
+
totalPausedDurationMs: goal.totalPausedDurationMs,
|
|
192
|
+
completedAt: goal.completedAt,
|
|
193
|
+
elapsedSeconds: elapsed,
|
|
194
|
+
formattedElapsed: formatElapsed(elapsed),
|
|
195
|
+
estimatedRemainingSeconds: estSec,
|
|
196
|
+
formattedETA: formatETA(estSec),
|
|
197
|
+
tokensUsage: goal.tokensUsage || { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
198
|
+
iterationsCount: goal.iterationsCount,
|
|
199
|
+
maxIterations: goal.maxIterations,
|
|
200
|
+
milestones,
|
|
201
|
+
progressPercent,
|
|
202
|
+
logs: goal.logs,
|
|
203
|
+
resultSummary: goal.resultSummary,
|
|
204
|
+
autoDrive: engine.autoDrive,
|
|
205
|
+
enableSound: engine.enableSound,
|
|
206
|
+
showQuickLaunchButton: engine.showQuickLaunchButton,
|
|
207
|
+
gitStartCommit: goal.gitStartCommit || null,
|
|
208
|
+
pendingNudge: goal.pendingNudge || null,
|
|
209
|
+
toolFailureCount: engine.getToolFailureCount(sid),
|
|
210
|
+
consecutiveToolFailureLimit: engine.consecutiveToolFailureLimit,
|
|
211
|
+
maxTokenBudget: goal.maxTokenBudget ?? engine.maxTokenBudget,
|
|
212
|
+
budgetWarningThreshold: engine.budgetWarningThreshold,
|
|
213
|
+
budgetWarningTriggered: Boolean(goal.budgetWarningTriggered),
|
|
214
|
+
autoCheckpointOnMilestone: engine.autoCheckpointOnMilestone,
|
|
215
|
+
};
|
|
216
|
+
}
|
package/lib/goal-engine.js
CHANGED
|
@@ -1,20 +1,10 @@
|
|
|
1
|
-
import { GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf } from './goal-engine-constants.js';
|
|
1
|
+
import { GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf, calculateElapsedSeconds, calculateRemainingSeconds, buildSnapshot } from './goal-engine-constants.js';
|
|
2
2
|
import { getGitCurrentCommit, exportReportMarkdown, exportReportGitHubPR, createMilestoneCheckpoint, rollbackToCheckpoint, saveGoalArtifact } from './engine-reports.js';
|
|
3
3
|
import { EngineStore } from './engine-store.js';
|
|
4
4
|
import { buildStatePromptInjection } from './engine-prompt.js';
|
|
5
|
-
|
|
6
|
-
if (!m || id === undefined || id === null) return false;
|
|
7
|
-
if (m.id === String(id)) return true;
|
|
8
|
-
const s1 = String(m.id).toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
9
|
-
const s2 = String(id).toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
10
|
-
if (s1 && s1 === s2) return true;
|
|
11
|
-
const n1 = s1.replace(/^m+/, '');
|
|
12
|
-
const n2 = s2.replace(/^m+/, '');
|
|
13
|
-
return Boolean(n1 && n1 === n2);
|
|
14
|
-
}
|
|
15
|
-
|
|
5
|
+
import { matchMilestone, parseMilestoneItems, applyMilestoneUpdate, toggleMilestoneChecklistItem } from './engine-milestones.js';
|
|
16
6
|
|
|
17
|
-
export { GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf };
|
|
7
|
+
export { GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf, matchMilestone };
|
|
18
8
|
export { getGitCurrentCommit, exportReportMarkdown, exportReportGitHubPR, createMilestoneCheckpoint, rollbackToCheckpoint, saveGoalArtifact };
|
|
19
9
|
|
|
20
10
|
/**
|
|
@@ -198,6 +188,7 @@ export class GoalEngine {
|
|
|
198
188
|
const oldest = inactive.shift();
|
|
199
189
|
this.goals.delete(oldest.sid);
|
|
200
190
|
this.stallCounters.delete(oldest.sid);
|
|
191
|
+
this.toolFailureCounters.delete(oldest.sid);
|
|
201
192
|
}
|
|
202
193
|
}
|
|
203
194
|
|
|
@@ -346,6 +337,7 @@ export class GoalEngine {
|
|
|
346
337
|
const sid = sessionId || 'default';
|
|
347
338
|
this.goals.delete(sid);
|
|
348
339
|
this.stallCounters.delete(sid);
|
|
340
|
+
this.toolFailureCounters.delete(sid);
|
|
349
341
|
this.emit(sid, true);
|
|
350
342
|
return this.getSnapshot(sid);
|
|
351
343
|
}
|
|
@@ -376,6 +368,7 @@ export class GoalEngine {
|
|
|
376
368
|
}
|
|
377
369
|
|
|
378
370
|
this.stallCounters.delete(sid);
|
|
371
|
+
this.toolFailureCounters.delete(sid);
|
|
379
372
|
this.emit(sid, true);
|
|
380
373
|
return this.getSnapshot(sid);
|
|
381
374
|
}
|
|
@@ -385,18 +378,8 @@ export class GoalEngine {
|
|
|
385
378
|
const goal = this.goals.get(sid);
|
|
386
379
|
if (!goal || !Array.isArray(milestonesList)) return;
|
|
387
380
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
if (!itemTitle || !itemTitle.trim()) continue;
|
|
391
|
-
|
|
392
|
-
const mId = typeof item === 'object' && item.id ? item.id : `m-${goal.milestones.length + 1}`;
|
|
393
|
-
goal.milestones.push({
|
|
394
|
-
id: String(mId),
|
|
395
|
-
title: itemTitle.trim(),
|
|
396
|
-
status: typeof item === 'object' && item.status ? item.status : MilestoneStatus.PENDING,
|
|
397
|
-
notes: typeof item === 'object' && item.notes ? item.notes : '',
|
|
398
|
-
});
|
|
399
|
-
}
|
|
381
|
+
const newItems = parseMilestoneItems(milestonesList, goal.milestones.length);
|
|
382
|
+
goal.milestones.push(...newItems);
|
|
400
383
|
|
|
401
384
|
this.recordProgress(sid);
|
|
402
385
|
if (shouldEmit) this.emit(sid);
|
|
@@ -410,20 +393,13 @@ export class GoalEngine {
|
|
|
410
393
|
const target = goal.milestones.find((m) => matchMilestone(m, id));
|
|
411
394
|
if (!target) return false;
|
|
412
395
|
|
|
413
|
-
const prevStatus =
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
if (Array.isArray(checklist)) {
|
|
422
|
-
target.checklist = checklist.map((item) => ({
|
|
423
|
-
text: String(item.text || item.title || '').trim(),
|
|
424
|
-
done: Boolean(item.done || item.completed),
|
|
425
|
-
})).filter((item) => item.text.length > 0);
|
|
426
|
-
}
|
|
396
|
+
const { prevStatus } = applyMilestoneUpdate(
|
|
397
|
+
target,
|
|
398
|
+
status,
|
|
399
|
+
notes,
|
|
400
|
+
checklist,
|
|
401
|
+
Object.values(MilestoneStatus)
|
|
402
|
+
);
|
|
427
403
|
|
|
428
404
|
// Auto Git Checkpoint on milestone completion
|
|
429
405
|
if (target.status === MilestoneStatus.COMPLETED && prevStatus !== MilestoneStatus.COMPLETED && this.autoCheckpointOnMilestone) {
|
|
@@ -459,11 +435,9 @@ export class GoalEngine {
|
|
|
459
435
|
if (!goal) return false;
|
|
460
436
|
|
|
461
437
|
const target = goal.milestones.find((m) => matchMilestone(m, milestoneId));
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
}
|
|
438
|
+
const ok = toggleMilestoneChecklistItem(target, itemIndex, done);
|
|
439
|
+
if (!ok) return false;
|
|
465
440
|
|
|
466
|
-
target.checklist[itemIndex].done = done !== undefined ? Boolean(done) : !target.checklist[itemIndex].done;
|
|
467
441
|
this.recordProgress(sid);
|
|
468
442
|
this.emit(sid);
|
|
469
443
|
return true;
|
|
@@ -525,13 +499,7 @@ export class GoalEngine {
|
|
|
525
499
|
}
|
|
526
500
|
|
|
527
501
|
getElapsedSeconds(sessionId = 'default') {
|
|
528
|
-
|
|
529
|
-
const goal = this.goals.get(sid);
|
|
530
|
-
if (!goal) return 0;
|
|
531
|
-
const { startedAt, pausedAt, totalPausedDurationMs, completedAt } = goal;
|
|
532
|
-
const endTime = completedAt || (pausedAt || Date.now());
|
|
533
|
-
const elapsedMs = Math.max(0, endTime - startedAt - totalPausedDurationMs);
|
|
534
|
-
return Math.floor(elapsedMs / 1000);
|
|
502
|
+
return calculateElapsedSeconds(this.goals.get(sessionId || 'default'));
|
|
535
503
|
}
|
|
536
504
|
|
|
537
505
|
addTokenUsage(usage, sessionId = 'default') {
|
|
@@ -588,100 +556,12 @@ export class GoalEngine {
|
|
|
588
556
|
getEstimatedRemainingSeconds(sessionId = 'default') {
|
|
589
557
|
const sid = sessionId || 'default';
|
|
590
558
|
const goal = this.goals.get(sid);
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
const total = goal.milestones.length;
|
|
594
|
-
if (total === 0) return null;
|
|
595
|
-
|
|
596
|
-
const completedCount = goal.milestones.filter((m) => m.status === MilestoneStatus.COMPLETED).length;
|
|
597
|
-
if (completedCount === 0 || completedCount >= total) return null;
|
|
598
|
-
|
|
599
|
-
const elapsed = this.getElapsedSeconds(sid);
|
|
600
|
-
if (elapsed <= 0) return null;
|
|
601
|
-
|
|
602
|
-
const avgSecPerMilestone = elapsed / completedCount;
|
|
603
|
-
const remainingCount = total - completedCount;
|
|
604
|
-
return Math.max(1, Math.round(avgSecPerMilestone * remainingCount));
|
|
559
|
+
return calculateRemainingSeconds(goal, this.getElapsedSeconds(sid));
|
|
605
560
|
}
|
|
606
561
|
|
|
607
562
|
getSnapshot(sessionId = 'default') {
|
|
608
563
|
const sid = sessionId || 'default';
|
|
609
|
-
|
|
610
|
-
if (!goal) {
|
|
611
|
-
return {
|
|
612
|
-
sessionId: sid,
|
|
613
|
-
hasActiveGoal: false,
|
|
614
|
-
state: GoalState.IDLE,
|
|
615
|
-
title: '',
|
|
616
|
-
startedAt: null,
|
|
617
|
-
pausedAt: null,
|
|
618
|
-
totalPausedDurationMs: 0,
|
|
619
|
-
completedAt: null,
|
|
620
|
-
elapsedSeconds: 0,
|
|
621
|
-
formattedElapsed: '0s',
|
|
622
|
-
estimatedRemainingSeconds: null,
|
|
623
|
-
formattedETA: null,
|
|
624
|
-
lang: 'en',
|
|
625
|
-
tokensUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
626
|
-
milestones: [],
|
|
627
|
-
progressPercent: 0,
|
|
628
|
-
iterationsCount: 0,
|
|
629
|
-
maxIterations: this.defaultMaxIterations,
|
|
630
|
-
autoDrive: this.autoDrive,
|
|
631
|
-
enableSound: this.enableSound,
|
|
632
|
-
showQuickLaunchButton: this.showQuickLaunchButton,
|
|
633
|
-
gitStartCommit: null,
|
|
634
|
-
pendingNudge: null,
|
|
635
|
-
toolFailureCount: 0,
|
|
636
|
-
consecutiveToolFailureLimit: this.consecutiveToolFailureLimit,
|
|
637
|
-
maxTokenBudget: this.maxTokenBudget,
|
|
638
|
-
budgetWarningThreshold: this.budgetWarningThreshold,
|
|
639
|
-
budgetWarningTriggered: false,
|
|
640
|
-
autoCheckpointOnMilestone: this.autoCheckpointOnMilestone,
|
|
641
|
-
};
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
const elapsed = this.getElapsedSeconds(sid);
|
|
645
|
-
const milestones = goal.milestones;
|
|
646
|
-
const completedCount = milestones.filter((m) => m.status === MilestoneStatus.COMPLETED).length;
|
|
647
|
-
const progressPercent = milestones.length > 0 ? Math.round((completedCount / milestones.length) * 100) : 0;
|
|
648
|
-
const estSec = this.getEstimatedRemainingSeconds(sid);
|
|
649
|
-
|
|
650
|
-
return {
|
|
651
|
-
sessionId: sid,
|
|
652
|
-
hasActiveGoal: true,
|
|
653
|
-
id: goal.id,
|
|
654
|
-
state: goal.state,
|
|
655
|
-
title: goal.title,
|
|
656
|
-
description: goal.description,
|
|
657
|
-
lang: goal.lang || detectLanguage(goal.title),
|
|
658
|
-
startedAt: goal.startedAt,
|
|
659
|
-
pausedAt: goal.pausedAt,
|
|
660
|
-
totalPausedDurationMs: goal.totalPausedDurationMs,
|
|
661
|
-
completedAt: goal.completedAt,
|
|
662
|
-
elapsedSeconds: elapsed,
|
|
663
|
-
formattedElapsed: formatElapsed(elapsed),
|
|
664
|
-
estimatedRemainingSeconds: estSec,
|
|
665
|
-
formattedETA: formatETA(estSec),
|
|
666
|
-
tokensUsage: goal.tokensUsage || { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
667
|
-
iterationsCount: goal.iterationsCount,
|
|
668
|
-
maxIterations: goal.maxIterations,
|
|
669
|
-
milestones,
|
|
670
|
-
progressPercent,
|
|
671
|
-
logs: goal.logs,
|
|
672
|
-
resultSummary: goal.resultSummary,
|
|
673
|
-
autoDrive: this.autoDrive,
|
|
674
|
-
enableSound: this.enableSound,
|
|
675
|
-
showQuickLaunchButton: this.showQuickLaunchButton,
|
|
676
|
-
gitStartCommit: goal.gitStartCommit || null,
|
|
677
|
-
pendingNudge: goal.pendingNudge || null,
|
|
678
|
-
toolFailureCount: this.getToolFailureCount(sid),
|
|
679
|
-
consecutiveToolFailureLimit: this.consecutiveToolFailureLimit,
|
|
680
|
-
maxTokenBudget: goal.maxTokenBudget ?? this.maxTokenBudget,
|
|
681
|
-
budgetWarningThreshold: this.budgetWarningThreshold,
|
|
682
|
-
budgetWarningTriggered: Boolean(goal.budgetWarningTriggered),
|
|
683
|
-
autoCheckpointOnMilestone: this.autoCheckpointOnMilestone,
|
|
684
|
-
};
|
|
564
|
+
return buildSnapshot(this.goals.get(sid), sid, this);
|
|
685
565
|
}
|
|
686
566
|
|
|
687
567
|
getGoalSnapshot(sessionId = 'default') {
|
package/lib/routes.js
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
1
3
|
import { MilestoneStatus, detectLanguage, sessionIdOf } from './goal-engine-constants.js';
|
|
2
4
|
import { saveGoalArtifact, rollbackToCheckpoint } from './engine-reports.js';
|
|
3
5
|
|
|
6
|
+
function getSafeCwd(requestedCwd) {
|
|
7
|
+
if (!requestedCwd || typeof requestedCwd !== 'string') {
|
|
8
|
+
return process.cwd();
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
const resolved = path.resolve(requestedCwd.trim());
|
|
12
|
+
const st = fs.statSync(resolved);
|
|
13
|
+
if (!st.isDirectory()) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
return resolved;
|
|
17
|
+
} catch (err) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
4
22
|
/**
|
|
5
23
|
* Register webServer HTTP routes and SSE stream for DSH Goal
|
|
6
24
|
* @param {any} ctx Cordis context
|
|
@@ -291,7 +309,12 @@ export function registerRoutes(ctx, {
|
|
|
291
309
|
res.statusCode = 400;
|
|
292
310
|
return res.end(JSON.stringify({ error: 'commit hash is required for rollback' }));
|
|
293
311
|
}
|
|
294
|
-
const
|
|
312
|
+
const safeCwd = getSafeCwd(data.cwd);
|
|
313
|
+
if (data.cwd && !safeCwd) {
|
|
314
|
+
res.statusCode = 400;
|
|
315
|
+
return res.end(JSON.stringify({ error: 'Invalid or non-existent working directory' }));
|
|
316
|
+
}
|
|
317
|
+
const ok = rollbackToCheckpoint(commit, safeCwd);
|
|
295
318
|
if (!ok) {
|
|
296
319
|
res.statusCode = 500;
|
|
297
320
|
return res.end(JSON.stringify({ error: `Failed to rollback to checkpoint ${commit}` }));
|
|
@@ -306,7 +329,12 @@ export function registerRoutes(ctx, {
|
|
|
306
329
|
res.statusCode = 404;
|
|
307
330
|
return res.end(JSON.stringify({ error: 'No active goal to save artifact for' }));
|
|
308
331
|
}
|
|
309
|
-
const
|
|
332
|
+
const safeCwd = getSafeCwd(data.cwd);
|
|
333
|
+
if (data.cwd && !safeCwd) {
|
|
334
|
+
res.statusCode = 400;
|
|
335
|
+
return res.end(JSON.stringify({ error: 'Invalid or non-existent working directory' }));
|
|
336
|
+
}
|
|
337
|
+
const artifact = saveGoalArtifact(snap, safeCwd);
|
|
310
338
|
if (!artifact) {
|
|
311
339
|
res.statusCode = 500;
|
|
312
340
|
return res.end(JSON.stringify({ error: 'Failed to write goal artifact to disk' }));
|
package/package.json
CHANGED