@useshifu/coding-harness 0.3.36 → 0.3.37
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/README.md +43 -42
- package/bin/continuity-preparation.js +4 -2
- package/bin/continuity-state.js +101 -9
- package/bin/cursor-sessions.js +2 -1
- package/bin/native-preparation.js +104 -22
- package/bin/onboarding-ui.js +6 -4
- package/bin/onboarding.js +44 -17
- package/bin/recovery-state.js +132 -0
- package/bin/run-deadline.js +11 -0
- package/bin/secret-check.js +32 -0
- package/bin/setup-options.js +3 -4
- package/bin/shifu-harness.js +862 -149
- package/bin/sync-controls.js +97 -0
- package/bin/v5-payload.js +2 -0
- package/commands/shifu-sync.md +9 -7
- package/package.json +1 -1
- package/skills/shifu-context/SKILL.md +3 -3
- package/skills/shifu-onboarding/SKILL.md +7 -3
- package/skills/shifu-sync/SKILL.md +17 -32
- package/wizard/index.html +45 -52
- package/wizard/wizard.css +8 -3
- package/wizard/wizard.js +223 -45
package/wizard/wizard.js
CHANGED
|
@@ -1,49 +1,227 @@
|
|
|
1
1
|
(() => {
|
|
2
|
-
const token=location.hash.slice(1);
|
|
3
|
-
const el=(id)=>document.getElementById(id);
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
el(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
el('
|
|
33
|
-
|
|
2
|
+
const token = location.hash.slice(1);
|
|
3
|
+
const el = (id) => document.getElementById(id);
|
|
4
|
+
const names = { codex: 'Codex', claude_code: 'Claude Code', opencode: 'OpenCode', cursor: 'Cursor' };
|
|
5
|
+
const completed = ['complete', 'skipped', 'empty'];
|
|
6
|
+
let state, choices, busy = false;
|
|
7
|
+
|
|
8
|
+
async function api(path, body) {
|
|
9
|
+
const response = await fetch('/api/' + path, {
|
|
10
|
+
method: body ? 'POST' : 'GET',
|
|
11
|
+
headers: { 'X-Shifu-Wizard': token, ...(body ? { 'Content-Type': 'application/json' } : {}) },
|
|
12
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
13
|
+
});
|
|
14
|
+
const result = await response.json();
|
|
15
|
+
if (!response.ok) throw new Error(result.error || 'Setup is unavailable.');
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function facts(target, items) {
|
|
20
|
+
el(target).replaceChildren(...items.map(([label, value]) => {
|
|
21
|
+
const row = document.createElement('div'), dt = document.createElement('dt'), dd = document.createElement('dd');
|
|
22
|
+
dt.textContent = label;
|
|
23
|
+
dd.textContent = value;
|
|
24
|
+
row.append(dt, dd);
|
|
25
|
+
return row;
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function projectScope() {
|
|
30
|
+
const all = document.querySelector('input[name=project-scope][value=all]').checked;
|
|
31
|
+
el('project-list').hidden = all;
|
|
32
|
+
el('project-note').textContent = all ? 'All projects in this coding tool are included, including new projects.' : 'Select the projects whose history and new work you want Shifu to use.';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function settings(value) {
|
|
36
|
+
el('history').value = ['0', '15', '30'].includes(String(value.historyDays)) ? String(value.historyDays) : 'custom';
|
|
37
|
+
el('custom').value = value.historyDays || 7;
|
|
38
|
+
el('custom-label').hidden = el('history').value !== 'custom';
|
|
39
|
+
document.querySelector(`input[name=growth][value=${value.growthFeedback ? 'on' : 'off'}]`).checked = true;
|
|
40
|
+
document.querySelector(`input[name=alignment][value=${value.decisionAlignment ? 'on' : 'off'}]`).checked = true;
|
|
41
|
+
document.querySelector(`input[name=project-scope][value=${value.projectScope === 'all' ? 'all' : 'selected'}]`).checked = true;
|
|
42
|
+
const selected = new Set(value.projectRefs || []);
|
|
43
|
+
const projects = (value.projects || []).map((project) => {
|
|
44
|
+
const label = document.createElement('label'), checkbox = document.createElement('input'), text = document.createElement('span');
|
|
45
|
+
checkbox.type = 'checkbox';
|
|
46
|
+
checkbox.name = 'project';
|
|
47
|
+
checkbox.value = project.ref;
|
|
48
|
+
checkbox.checked = selected.has(project.ref);
|
|
49
|
+
text.textContent = `${project.label} · ${project.sessionCount} sessions`;
|
|
50
|
+
label.append(checkbox, text);
|
|
51
|
+
return label;
|
|
52
|
+
});
|
|
53
|
+
if (!projects.length) {
|
|
54
|
+
const empty = document.createElement('p');
|
|
55
|
+
empty.className = 'note';
|
|
56
|
+
empty.textContent = 'No projects found yet. Select All projects to include new work as it appears.';
|
|
57
|
+
projects.push(empty);
|
|
58
|
+
}
|
|
59
|
+
el('project-list').replaceChildren(...projects);
|
|
60
|
+
projectScope();
|
|
61
|
+
el('schedule-enabled').checked = value.scheduleEnabled ?? value.syncPolicy?.scheduleEnabled ?? true;
|
|
62
|
+
el('files-enabled').checked = value.filesEnabled === true;
|
|
63
|
+
el('input-folder').value = value.inputFolder || '';
|
|
64
|
+
el('folder-choice').hidden = !el('files-enabled').checked;
|
|
65
|
+
el('key-entry').hidden = value.configured;
|
|
66
|
+
el('saved-key').hidden = !value.configured;
|
|
67
|
+
el('harness').textContent = names[value.harness] || value.harness;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function syncLabel(value) {
|
|
71
|
+
const policy = value.syncPolicy;
|
|
72
|
+
if ((value.scheduleEnabled ?? policy?.scheduleEnabled) === false) return 'Scheduled work sync off';
|
|
73
|
+
if (!policy) return 'Every hour · automatic after Start sync';
|
|
74
|
+
return policy.scheduleEnabled ? `Every ${policy.intervalHours} hours · ${policy.approvalMode}` : 'Scheduled work sync off';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function projectLabel(value) {
|
|
78
|
+
if (value.projectScope === 'all') return 'All projects in this coding tool';
|
|
79
|
+
const selected = new Set(value.projectRefs || []);
|
|
80
|
+
return (value.projects || state.projects || []).filter((project) => selected.has(project.ref)).map((project) => project.label).join(', ') || 'Selected projects';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function renderProgress() {
|
|
84
|
+
el('welcome').hidden = true;
|
|
85
|
+
el('settings').hidden = true;
|
|
86
|
+
el('review').hidden = true;
|
|
87
|
+
el('progress').hidden = false;
|
|
88
|
+
const labels = { pending: 'Waiting to prepare history', running: 'Preparing selected history', partial: 'Part of your selected history is saved', blocked: 'Older unsynced turns need your direction', failed: 'History import needs attention', complete: 'Selected history saved', skipped: 'Initial history skipped', empty: 'No eligible history found', paused: 'Processing paused' };
|
|
89
|
+
el('progress-title').textContent = state.paused ? 'Local preparation and uploads are paused.' : state.contextAvailable ? 'Your first context is ready.' : labels[state.backfillStatus] || 'Setup is continuing.';
|
|
90
|
+
facts('progress-facts', [
|
|
91
|
+
['Connection', state.connectionStatus === 'reload_required' ? 'Verified · reload assistant required' : state.connectionStatus],
|
|
92
|
+
['Projects', projectLabel(state)],
|
|
93
|
+
['History', state.historyDays ? `Last ${state.historyDays} days` : 'Start from now'],
|
|
94
|
+
['Files', state.filesEnabled ? state.inputFolder : 'Off'],
|
|
95
|
+
['Ongoing sync', state.paused ? 'Paused' : syncLabel(state)],
|
|
96
|
+
['History import', labels[state.backfillStatus] || state.backfillStatus],
|
|
97
|
+
['Accepted', String(state.acceptedSegments || 0) + ' segments'],
|
|
98
|
+
['Remaining', String(state.pendingSegments || 0) + ' segments'],
|
|
99
|
+
['Blocked', String(state.blockedSegments || 0) + ' sessions'],
|
|
100
|
+
]);
|
|
101
|
+
el('progress-note').textContent = state.paused ? 'No new local preparation or upload requests will start. Work already submitted to Shifu or your model provider may finish; saved work and import progress are preserved.' : state.lastErrorCode === 'preparation_deferred' ? 'This batch reached its model-request limit. Remaining selected history stays pending for the next scheduled run or Retry unfinished history.' : state.lastErrorCode ? 'Accepted work is preserved. Retry the connection or ask your assistant for help; the selected scope will not be expanded automatically.' : state.backfillStatus === 'skipped' ? 'Initial history was skipped. New work in the selected scope can become context for future questions.' : 'You can continue working. The import resumes from saved progress; first context availability is separate from full completion.';
|
|
102
|
+
el('pause').hidden = state.paused === true;
|
|
103
|
+
el('resume-all').hidden = state.paused !== true;
|
|
104
|
+
el('resume').hidden = state.paused === true || !['failed', 'partial', 'blocked', 'pending'].includes(state.backfillStatus);
|
|
105
|
+
el('copy').hidden = !state.contextAvailable;
|
|
106
|
+
el('handoff').hidden = !state.contextAvailable;
|
|
107
|
+
el('prompt').value = state.resumePrompt || '';
|
|
108
|
+
el('change').hidden = state.backfillStatus === 'running' && !state.paused;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
el('history').addEventListener('change', () => { el('custom-label').hidden = el('history').value !== 'custom'; });
|
|
112
|
+
el('files-enabled').addEventListener('change', () => { el('folder-choice').hidden = !el('files-enabled').checked; });
|
|
113
|
+
document.querySelectorAll('input[name=project-scope]').forEach((input) => input.addEventListener('change', projectScope));
|
|
114
|
+
el('settings').addEventListener('submit', async (event) => {
|
|
115
|
+
event.preventDefault();
|
|
116
|
+
if (busy) return;
|
|
117
|
+
const historyDays = el('history').value === 'custom' ? Number(el('custom').value) : Number(el('history').value);
|
|
118
|
+
if (!Number.isSafeInteger(historyDays) || historyDays < 0 || historyDays > 36500 || (el('history').value === 'custom' && historyDays === 0)) {
|
|
119
|
+
el('message').textContent = 'Choose a positive whole number of custom days, or Start from now.';
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const projectScope = document.querySelector('input[name=project-scope]:checked').value;
|
|
123
|
+
const projectRefs = projectScope === 'all' ? [] : [...document.querySelectorAll('input[name=project]:checked')].map((input) => input.value);
|
|
124
|
+
if (projectScope === 'selected' && !projectRefs.length) {
|
|
125
|
+
el('message').textContent = 'Select at least one project, or choose All projects.';
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const filesEnabled = el('files-enabled').checked;
|
|
129
|
+
const inputFolder = el('input-folder').value.trim();
|
|
130
|
+
if (filesEnabled && !inputFolder) {
|
|
131
|
+
el('message').textContent = 'Choose the folder whose files you want to upload.';
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
choices = { historyDays, projectScope, projectRefs, scheduleEnabled: el('schedule-enabled').checked, filesEnabled, ...(filesEnabled ? { inputFolder } : {}), growthFeedback: document.querySelector('input[name=growth]:checked').value === 'on', decisionAlignment: document.querySelector('input[name=alignment]:checked').value === 'on' };
|
|
135
|
+
busy = true;
|
|
136
|
+
el('review-button').disabled = true;
|
|
137
|
+
el('message').textContent = 'Checking your selection locally…';
|
|
138
|
+
try {
|
|
139
|
+
const preview = await api('preview', choices);
|
|
140
|
+
if (!Number.isSafeInteger(preview.scopeRevision) || preview.scopeRevision < 1) throw new Error('The selection could not be reviewed. Try again before starting sync.');
|
|
141
|
+
choices.scopeRevision = preview.scopeRevision;
|
|
142
|
+
facts('summary', [
|
|
143
|
+
['Destination', preview.destination],
|
|
144
|
+
['Projects', projectLabel(preview)],
|
|
145
|
+
['History', historyDays ? `Last ${historyDays} days · ${preview.sessionCount} sessions` : 'Start from now'],
|
|
146
|
+
['Files', filesEnabled ? `${preview.fileCount} files · ${preview.totalFileBytes} bytes` : 'Off'],
|
|
147
|
+
...(filesEnabled ? [['Folder', preview.inputFolder], ['Skipped', `${preview.skippedSymlinks || 0} links · ${preview.skippedDirectories || 0} subfolders`]] : []),
|
|
148
|
+
['Ongoing sync', syncLabel(preview)],
|
|
149
|
+
['Growth feedback', choices.growthFeedback ? 'On' : 'Off'],
|
|
150
|
+
['Decision alignment', choices.decisionAlignment ? 'On' : 'Off'],
|
|
151
|
+
]);
|
|
152
|
+
el('scope').textContent = preview.quotaNotice || 'Preparing selected coding text uses your configured model account’s allowance. Review is local; processing begins after Start sync.';
|
|
153
|
+
el('welcome').hidden = true;
|
|
154
|
+
el('settings').hidden = true;
|
|
155
|
+
el('review').hidden = false;
|
|
156
|
+
el('message').textContent = '';
|
|
157
|
+
} catch (error) { el('message').textContent = error.message; }
|
|
158
|
+
finally { busy = false; el('review-button').disabled = false; }
|
|
159
|
+
});
|
|
160
|
+
el('edit').addEventListener('click', () => { choices = undefined; el('review').hidden = true; el('welcome').hidden = false; el('settings').hidden = false; });
|
|
161
|
+
el('apply').addEventListener('click', async () => {
|
|
162
|
+
if (busy || !choices?.scopeRevision) return;
|
|
163
|
+
busy = true;
|
|
164
|
+
el('apply').disabled = true;
|
|
165
|
+
el('message').textContent = 'Verifying your connection and starting the reviewed selection…';
|
|
166
|
+
const key = el('key').value.trim();
|
|
167
|
+
el('key').value = '';
|
|
168
|
+
try {
|
|
169
|
+
state = await api('apply', { ...choices, ...(!state.configured ? { key } : {}) });
|
|
170
|
+
renderProgress();
|
|
171
|
+
el('message').textContent = '';
|
|
172
|
+
} catch (error) {
|
|
173
|
+
choices = undefined;
|
|
174
|
+
el('message').textContent = error.message;
|
|
175
|
+
el('review').hidden = true;
|
|
176
|
+
el('welcome').hidden = false; el('settings').hidden = false;
|
|
177
|
+
} finally { busy = false; el('apply').disabled = false; }
|
|
34
178
|
});
|
|
35
|
-
el('
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
179
|
+
el('change').addEventListener('click', async () => {
|
|
180
|
+
try {
|
|
181
|
+
const abandon = !completed.includes(state.backfillStatus);
|
|
182
|
+
if (abandon && !window.confirm('Leave the unfinished history selection and review new settings? Saved records and import progress will remain.')) return;
|
|
183
|
+
state = await api('new', { abandon });
|
|
184
|
+
choices = undefined;
|
|
185
|
+
settings(state);
|
|
186
|
+
el('progress').hidden = true;
|
|
187
|
+
el('welcome').hidden = false; el('settings').hidden = false;
|
|
188
|
+
} catch (error) { el('message').textContent = error.message; }
|
|
42
189
|
});
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
190
|
+
async function processingAction(path) {
|
|
191
|
+
if (busy) return;
|
|
192
|
+
busy = true;
|
|
193
|
+
for (const id of ['pause', 'resume-all', 'resume']) el(id).disabled = true;
|
|
194
|
+
try {
|
|
195
|
+
state = await api(path, {});
|
|
196
|
+
renderProgress();
|
|
197
|
+
el('message').textContent = path === 'pause' ? 'Local preparation and uploads paused. Accepted work is preserved.' : 'Local preparation and uploads resumed from the saved selection and import progress.';
|
|
198
|
+
} catch (error) { el('message').textContent = error.message; }
|
|
199
|
+
finally { busy = false; for (const id of ['pause', 'resume-all', 'resume']) el(id).disabled = false; }
|
|
200
|
+
}
|
|
201
|
+
el('pause').addEventListener('click', () => processingAction('pause'));
|
|
202
|
+
el('resume-all').addEventListener('click', () => processingAction('resume'));
|
|
203
|
+
el('resume').addEventListener('click', () => processingAction('resume'));
|
|
204
|
+
el('copy').addEventListener('click', async () => {
|
|
205
|
+
try { await navigator.clipboard.writeText(state.resumePrompt); el('message').textContent = 'Copied. Reload your assistant if needed, then paste the prompt.'; }
|
|
206
|
+
catch { el('prompt').hidden = false; el('prompt').focus(); el('prompt').select(); el('message').textContent = 'Copy this prompt and paste it into your assistant.'; }
|
|
207
|
+
});
|
|
208
|
+
async function refresh() {
|
|
209
|
+
if (busy || document.hidden) return;
|
|
210
|
+
try {
|
|
211
|
+
const previous = state;
|
|
212
|
+
state = await api('status');
|
|
213
|
+
if (state.scopeReviewRequired && !previous?.scopeReviewRequired) {
|
|
214
|
+
choices = undefined;
|
|
215
|
+
settings(state);
|
|
216
|
+
el('progress').hidden = true;
|
|
217
|
+
el('review').hidden = true;
|
|
218
|
+
el('welcome').hidden = false; el('settings').hidden = false;
|
|
219
|
+
el('message').textContent = 'The source selection changed. Review it before processing continues.';
|
|
220
|
+
} else if (state.authorized && !state.scopeReviewRequired) renderProgress();
|
|
221
|
+
}
|
|
222
|
+
catch (error) { el('message').textContent = error.message; }
|
|
223
|
+
}
|
|
224
|
+
api('status').then((value) => { state = value; settings(value); if (value.authorized && !value.scopeReviewRequired) renderProgress(); if (value.scopeReviewRequired) el('message').textContent = 'Review your sources and select Start sync to continue. Saved work and import progress are preserved.'; }).catch((error) => { el('settings').hidden = true; el('message').textContent = error.message; });
|
|
225
|
+
setInterval(refresh, 5000);
|
|
226
|
+
document.addEventListener('visibilitychange', () => { if (!document.hidden) refresh(); });
|
|
49
227
|
})();
|