openplanr 2.5.0 → 2.6.0

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.
Files changed (68) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/lib/host-packages/adapter-registry.json +5 -5
  3. package/lib/host-packages/capability-map.json +1 -1
  4. package/lib/host-packages/claude/.claude-plugin/marketplace.json +2 -2
  5. package/lib/host-packages/claude/openplanr/.claude-plugin/plugin.json +1 -1
  6. package/lib/host-packages/claude/openplanr/.openplanr-content.json +46 -14
  7. package/lib/host-packages/claude/openplanr/skills/design/openplanr.skill.json +22 -0
  8. package/lib/host-packages/claude/openplanr/skills/design/scripts/design.mjs +69 -16
  9. package/lib/host-packages/claude/openplanr/skills/design/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
  10. package/lib/host-packages/claude/openplanr/skills/design/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
  11. package/lib/host-packages/claude/openplanr/skills/design/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
  12. package/lib/host-packages/claude/openplanr/skills/design-loop/openplanr.skill.json +22 -0
  13. package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/design.mjs +69 -16
  14. package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
  15. package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
  16. package/lib/host-packages/claude/openplanr/skills/design-loop/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
  17. package/lib/host-packages/claude/openplanr/skills/design-review/openplanr.skill.json +22 -0
  18. package/lib/host-packages/claude/openplanr/skills/design-review/scripts/design.mjs +69 -16
  19. package/lib/host-packages/claude/openplanr/skills/design-review/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
  20. package/lib/host-packages/claude/openplanr/skills/design-review/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
  21. package/lib/host-packages/claude/openplanr/skills/design-review/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
  22. package/lib/host-packages/claude/openplanr/skills/plan/openplanr.skill.json +22 -0
  23. package/lib/host-packages/claude/openplanr/skills/plan/scripts/design.mjs +69 -16
  24. package/lib/host-packages/claude/openplanr/skills/plan/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
  25. package/lib/host-packages/claude/openplanr/skills/plan/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
  26. package/lib/host-packages/claude/openplanr/skills/plan/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
  27. package/lib/host-packages/cursor/openplanr/.openplanr-content.json +42 -10
  28. package/lib/host-packages/cursor/openplanr/manifest.json +1 -1
  29. package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/design.mjs +69 -16
  30. package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
  31. package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
  32. package/lib/host-packages/cursor/openplanr/rules/planr-design/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
  33. package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/design.mjs +69 -16
  34. package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
  35. package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
  36. package/lib/host-packages/cursor/openplanr/rules/planr-design-loop/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
  37. package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/design.mjs +69 -16
  38. package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
  39. package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
  40. package/lib/host-packages/cursor/openplanr/rules/planr-design-review/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
  41. package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/design.mjs +69 -16
  42. package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
  43. package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
  44. package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
  45. package/lib/host-packages/openai/.claude-plugin/marketplace.json +2 -2
  46. package/lib/host-packages/openai/openplanr/.codex-plugin/plugin.json +1 -1
  47. package/lib/host-packages/openai/openplanr/.openplanr-content.json +46 -14
  48. package/lib/host-packages/openai/openplanr/skills/design/openplanr.skill.json +22 -0
  49. package/lib/host-packages/openai/openplanr/skills/design/scripts/design.mjs +69 -16
  50. package/lib/host-packages/openai/openplanr/skills/design/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
  51. package/lib/host-packages/openai/openplanr/skills/design/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
  52. package/lib/host-packages/openai/openplanr/skills/design/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
  53. package/lib/host-packages/openai/openplanr/skills/design-loop/openplanr.skill.json +22 -0
  54. package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/design.mjs +69 -16
  55. package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
  56. package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
  57. package/lib/host-packages/openai/openplanr/skills/design-loop/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
  58. package/lib/host-packages/openai/openplanr/skills/design-review/openplanr.skill.json +22 -0
  59. package/lib/host-packages/openai/openplanr/skills/design-review/scripts/design.mjs +69 -16
  60. package/lib/host-packages/openai/openplanr/skills/design-review/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
  61. package/lib/host-packages/openai/openplanr/skills/design-review/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
  62. package/lib/host-packages/openai/openplanr/skills/design-review/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
  63. package/lib/host-packages/openai/openplanr/skills/plan/openplanr.skill.json +22 -0
  64. package/lib/host-packages/openai/openplanr/skills/plan/scripts/design.mjs +69 -16
  65. package/lib/host-packages/openai/openplanr/skills/plan/scripts/runtime/packages/design/templates/studio/enhancements.js +19 -2
  66. package/lib/host-packages/openai/openplanr/skills/plan/scripts/runtime/packages/design/templates/studio/handoff-center.css +81 -0
  67. package/lib/host-packages/openai/openplanr/skills/plan/scripts/runtime/packages/design/templates/studio/handoff-center.js +400 -0
  68. package/package.json +2 -2
@@ -0,0 +1,400 @@
1
+ /* Five-step Design Handoff Center. Domain decisions stay in server projections. */
2
+ (() => {
3
+ 'use strict';
4
+ if (document.currentScript && !document.currentScript.isConnected) return;
5
+
6
+ const q = (selector, parent = document) => parent.querySelector(selector);
7
+ const node = (tag, text = '', attrs = {}) => {
8
+ const value = document.createElement(tag);
9
+ if (text) value.textContent = text;
10
+ for (const [key, entry] of Object.entries(attrs)) if (entry !== undefined && entry !== null && entry !== false) value.setAttribute(key, entry);
11
+ return value;
12
+ };
13
+ const action = (label, run, attrs = {}) => {
14
+ const value = node('button', label, { type: 'button', ...attrs });
15
+ value.addEventListener('click', run);
16
+ return value;
17
+ };
18
+ const plainPackage = value => ({
19
+ id: value.id,
20
+ title: value.title,
21
+ sources: structuredClone(value.sources || []),
22
+ requirements: (value.requirements || []).map(({ id: _id, ...requirement }) => structuredClone(requirement)),
23
+ });
24
+ const requestId = prefix => `${prefix}-${globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random().toString(16).slice(2)}`}`;
25
+ const labels = { ready: 'Ready', pass: 'Complete', attention: 'Needs attention', blocked: 'Blocked', stale: 'Out of date', absent: 'Not available', approved: 'Approved', draft: 'Draft', superseded: 'Superseded', revoked: 'Revoked' };
26
+ const steps = [
27
+ ['readiness', 'Readiness'],
28
+ ['review', 'Review resolution'],
29
+ ['package', 'Implementation package'],
30
+ ['approval', 'Owner approval'],
31
+ ['plan', 'Continue to Plan'],
32
+ ];
33
+
34
+ let bridge, root, panel, opener, activeStep = 'readiness', serial = 0;
35
+ const state = { readiness: null, review: null, implementation: null, errors: {} };
36
+
37
+ const readiness = () => state.readiness?.readiness || state.readiness;
38
+ const approvedReview = () => Boolean(state.review?.current && state.review?.draft?.status === 'approved');
39
+ const packageDraft = () => state.implementation?.draft || null;
40
+ const currentPackage = () => state.implementation?.current || null;
41
+ const approvedPackage = () => currentPackage()?.status === 'approved';
42
+ const statusFor = id => {
43
+ if (state.errors[id === 'readiness' ? 'readiness' : id === 'review' ? 'review' : 'implementation']) return 'attention';
44
+ if (id === 'readiness') return readiness()?.status || 'absent';
45
+ if (id === 'review') return approvedReview() ? 'pass' : state.review?.draft ? (state.review.current ? 'attention' : 'stale') : 'blocked';
46
+ if (id === 'package') return packageDraft() ? 'pass' : readiness()?.status === 'ready' ? 'attention' : 'blocked';
47
+ if (id === 'approval') return approvedPackage() ? 'pass' : currentPackage()?.status || (packageDraft() ? 'attention' : 'blocked');
48
+ return approvedPackage() ? 'ready' : 'blocked';
49
+ };
50
+ const badge = value => node('span', labels[value] || value, { class: 'design-handoff-badge', 'data-status': value });
51
+ const paragraph = (text, className = '') => node('p', text, className ? { class: className } : {});
52
+ const empty = (title, detail) => {
53
+ const value = node('div', '', { class: 'design-handoff-empty' });
54
+ value.append(node('strong', title), paragraph(detail));
55
+ return value;
56
+ };
57
+ const sectionTitle = (eyebrow, title, detail) => {
58
+ const block = node('header', '', { class: 'design-handoff-step-header' });
59
+ block.append(node('span', eyebrow, { class: 'design-handoff-eyebrow' }), node('h3', title), paragraph(detail));
60
+ return block;
61
+ };
62
+ const setStatus = (message, error = false) => {
63
+ const live = q('[data-design-handoff-status]', panel);
64
+ if (!live) return;
65
+ live.textContent = message;
66
+ live.dataset.error = String(error);
67
+ };
68
+ const setBusy = busy => {
69
+ panel?.toggleAttribute('aria-busy', busy);
70
+ panel && q('[data-design-handoff-refresh]', panel)?.toggleAttribute('disabled', busy);
71
+ };
72
+ const perform = async (message, work, success) => {
73
+ setBusy(true); setStatus(message);
74
+ try {
75
+ await work();
76
+ await refresh();
77
+ setStatus(success);
78
+ } catch (error) {
79
+ setStatus(error?.message || 'The handoff action could not be completed. Refresh and try again.', true);
80
+ } finally { setBusy(false); }
81
+ };
82
+
83
+ function renderProgress() {
84
+ const progress = q('[data-design-handoff-progress]', panel);
85
+ progress.replaceChildren();
86
+ for (const [index, [id, label]] of steps.entries()) {
87
+ const status = statusFor(id);
88
+ const button = action(label, () => { activeStep = id; render(); q(`[data-handoff-step="${id}"]`, panel)?.focus(); }, {
89
+ class: 'design-handoff-step',
90
+ 'data-handoff-step': id,
91
+ 'data-status': status,
92
+ 'aria-current': activeStep === id ? 'step' : undefined,
93
+ 'aria-label': `${index + 1}. ${label}: ${labels[status] || status}`,
94
+ });
95
+ button.prepend(node('span', String(index + 1).padStart(2, '0'), { 'aria-hidden': 'true' }));
96
+ button.append(badge(status));
97
+ progress.append(button);
98
+ }
99
+ }
100
+
101
+ function recoveryAction(check) {
102
+ const label = check.recoveryAction?.label || 'Return to the design';
103
+ return action(label, () => {
104
+ if (['resolve-review-decisions', 'resolve-review-blockers', 'approve-review-handoff'].includes(check.recoveryAction?.id)) {
105
+ activeStep = 'review'; render();
106
+ } else {
107
+ close(); bridge.announce(`${check.message} ${label}.`);
108
+ }
109
+ }, { class: 'design-handoff-link' });
110
+ }
111
+
112
+ function renderReadiness(content) {
113
+ const value = readiness();
114
+ content.append(sectionTitle('Step 1 of 5', 'Readiness', 'Confirm the current design, verification and review evidence before preparing implementation work.'));
115
+ if (state.errors.readiness) {
116
+ content.append(empty('Readiness is temporarily unavailable', state.errors.readiness));
117
+ content.append(action('Retry readiness', refresh, { class: 'design-primary' }));
118
+ return;
119
+ }
120
+ if (!value || value.status === 'absent') {
121
+ content.append(empty('No readiness projection yet', value?.message || 'Complete a local design render to compute handoff readiness.'));
122
+ content.append(action(value?.nextAction?.label || 'Return to the design', close, { class: 'design-primary' }));
123
+ return;
124
+ }
125
+ const summary = node('div', '', { class: 'design-handoff-summary' });
126
+ summary.append(badge(value.status), node('strong', value.status === 'ready' ? 'This design can move into implementation packaging.' : `${value.blockers?.length || 0} blocking checks remain.`));
127
+ content.append(summary);
128
+ const list = node('ol', '', { class: 'design-handoff-checks' });
129
+ for (const check of value.checks || []) {
130
+ const item = node('li', '', { 'data-status': check.status });
131
+ const body = node('div'); body.append(node('strong', check.message), badge(check.status));
132
+ item.append(body);
133
+ if (check.status !== 'pass') item.append(recoveryAction(check));
134
+ list.append(item);
135
+ }
136
+ content.append(list);
137
+ if (value.status === 'ready') content.append(action('Review resolved feedback', () => { activeStep = 'review'; render(); }, { class: 'design-primary' }));
138
+ else if (value.nextActions?.[0]) content.append(recoveryAction({ message: 'Readiness needs attention.', recoveryAction: value.nextActions[0] }));
139
+ }
140
+
141
+ function reviewGroup(title, items) {
142
+ const section = node('section', '', { class: 'design-handoff-review-group' });
143
+ section.append(node('h4', `${title} · ${items?.length || 0}`));
144
+ if (!items?.length) section.append(paragraph('None recorded.', 'design-muted'));
145
+ for (const item of items || []) {
146
+ const card = node('article');
147
+ card.append(paragraph(item.text || item.statement || 'Review decision'));
148
+ if (item.refinement) card.append(paragraph(item.refinement, 'design-handoff-refinement'));
149
+ section.append(card);
150
+ }
151
+ return section;
152
+ }
153
+
154
+ function renderReview(content) {
155
+ content.append(sectionTitle('Step 2 of 5', 'Review resolution', 'Turn current feedback into explicit accepted, open, deferred and declined decisions.'));
156
+ if (state.errors.review) {
157
+ content.append(empty('Review decisions could not be loaded', state.errors.review));
158
+ content.append(action('Retry review decisions', refresh, { class: 'design-primary' }));
159
+ return;
160
+ }
161
+ const response = state.review, draft = response?.draft;
162
+ if (!draft) {
163
+ content.append(empty('No review handoff has been prepared', 'Generate a proposal from the current comments. You will review every decision before approval.'));
164
+ content.append(action('Generate review handoff', () => perform('Generating review decisions…', async () => {
165
+ state.review = await bridge.updateReviewHandoff({ action: 'draft', version: 0 });
166
+ }, 'Review decisions generated.'), { class: 'design-primary' }));
167
+ return;
168
+ }
169
+ const status = response.current ? draft.status : 'stale';
170
+ const summary = node('div', '', { class: 'design-handoff-summary' });
171
+ summary.append(badge(status), node('strong', draft.content?.summary || (response.current ? 'Review decisions match this design.' : 'The design or feedback changed after this handoff was prepared.')));
172
+ content.append(summary);
173
+ const decisions = node('div', '', { class: 'design-handoff-review-groups' });
174
+ decisions.append(
175
+ reviewGroup('Accepted changes', draft.content?.agreedChanges),
176
+ reviewGroup('Open questions', draft.content?.openQuestions),
177
+ reviewGroup('Deferred', draft.content?.deferred),
178
+ reviewGroup('Declined', draft.content?.rejected),
179
+ );
180
+ content.append(decisions);
181
+ const label = approvedReview() ? 'Continue to implementation package' : response.current ? 'Review and approve decisions' : 'Regenerate current decisions';
182
+ content.append(action(label, () => {
183
+ if (approvedReview()) { activeStep = 'package'; render(); }
184
+ else bridge.openReviewHandoff();
185
+ }, { class: 'design-primary' }));
186
+ }
187
+
188
+ function packageEditor(source, { regenerate = false } = {}) {
189
+ const form = node('form', '', { class: 'design-handoff-package-editor' });
190
+ const editable = plainPackage(source);
191
+ const title = node('input', '', { type: 'text', required: '', maxlength: '256', 'aria-label': 'Implementation package title' });
192
+ title.value = editable.title;
193
+ const titleLabel = node('label', 'Package title'); titleLabel.append(title); form.append(titleLabel);
194
+ const requirements = node('div', '', { class: 'design-handoff-requirements' });
195
+ const editors = editable.requirements.map((requirement, index) => {
196
+ const card = node('fieldset'); card.append(node('legend', `${requirement.kind.replaceAll('-', ' ')} requirement ${index + 1}`));
197
+ const statement = node('textarea', '', { required: '', maxlength: '16384', 'aria-label': `Requirement ${index + 1} statement` }); statement.value = requirement.statement;
198
+ const verification = node('textarea', '', { required: '', maxlength: '16384', 'aria-label': `Requirement ${index + 1} verification` }); verification.value = requirement.verification.join('\n');
199
+ const statementLabel = node('label', 'Requirement'); statementLabel.append(statement);
200
+ const verificationLabel = node('label', 'Verification, one expectation per line'); verificationLabel.append(verification);
201
+ card.append(statementLabel, verificationLabel); requirements.append(card);
202
+ return { requirement, statement, verification };
203
+ });
204
+ form.append(requirements);
205
+ const save = node('button', regenerate ? 'Create next package version' : 'Save implementation package', { type: 'submit', class: 'design-primary' });
206
+ form.append(save);
207
+ form.addEventListener('submit', event => {
208
+ event.preventDefault();
209
+ if (!form.reportValidity()) return;
210
+ editable.title = title.value.trim();
211
+ editable.requirements = editors.map(({ requirement, statement, verification }) => ({
212
+ ...requirement,
213
+ statement: statement.value.trim(),
214
+ verification: verification.value.split('\n').map(value => value.trim()).filter(Boolean),
215
+ }));
216
+ const input = regenerate
217
+ ? { action: 'regenerate', requestId: requestId('regenerate'), package: editable }
218
+ : { action: 'draft', package: editable };
219
+ void perform(regenerate ? 'Creating a new package version…' : 'Saving the implementation package…', async () => {
220
+ await bridge.updateImplementationHandoff(input);
221
+ }, regenerate ? 'A new package version is ready for approval.' : 'Implementation package saved.');
222
+ });
223
+ return form;
224
+ }
225
+
226
+ function renderPackage(content) {
227
+ content.append(sectionTitle('Step 3 of 5', 'Implementation package', 'Review traceable requirements and observable verification expectations. Integrity values stay in the contract and are hidden here.'));
228
+ if (state.errors.implementation) {
229
+ content.append(empty('Implementation package could not be loaded', state.errors.implementation));
230
+ content.append(action('Retry implementation package', refresh, { class: 'design-primary' }));
231
+ return;
232
+ }
233
+ const response = state.implementation, draft = response?.draft;
234
+ if (!approvedReview() || readiness()?.status !== 'ready') {
235
+ content.append(empty('Readiness must be complete first', 'Approve current review decisions and resolve every blocking readiness check before composing the implementation package.'));
236
+ content.append(action('Return to readiness', () => { activeStep = 'readiness'; render(); }, { class: 'design-primary' }));
237
+ return;
238
+ }
239
+ if (!draft) {
240
+ const proposal = response?.proposal;
241
+ if (!proposal) {
242
+ content.append(empty('No package proposal is available', 'Refresh the current design render, then return here to prepare the implementation scope.'));
243
+ content.append(action('Refresh package proposal', refresh, { class: 'design-primary' }));
244
+ return;
245
+ }
246
+ content.append(paragraph(`${proposal.sources.length} approved sources · ${proposal.requirements.length} proposed requirements`, 'design-muted'));
247
+ content.append(packageEditor(proposal));
248
+ return;
249
+ }
250
+ const summary = node('div', '', { class: 'design-handoff-summary' });
251
+ summary.append(badge(draft.status), node('strong', `${draft.title} · version ${draft.version}`));
252
+ content.append(summary, paragraph(`${draft.sources.length} sources · ${draft.requirements.length} requirements`, 'design-muted'));
253
+ const list = node('ol', '', { class: 'design-handoff-requirement-list' });
254
+ for (const requirement of draft.requirements) {
255
+ const item = node('li');
256
+ item.append(node('strong', requirement.statement), paragraph(requirement.verification.join(' · '), 'design-muted'));
257
+ list.append(item);
258
+ }
259
+ content.append(list);
260
+ if (approvedPackage()) {
261
+ const edit = node('details'); edit.append(node('summary', 'Prepare a revised package version'), packageEditor(draft, { regenerate: true })); content.append(edit);
262
+ }
263
+ content.append(action('Review owner approval', () => { activeStep = 'approval'; render(); }, { class: 'design-primary' }));
264
+ }
265
+
266
+ function renderApproval(content) {
267
+ content.append(sectionTitle('Step 4 of 5', 'Owner approval', 'Approval records the exact package version and permits a later, separate Plan invocation.'));
268
+ if (state.errors.implementation) {
269
+ content.append(empty('Approval state could not be loaded', state.errors.implementation));
270
+ content.append(action('Retry approval state', refresh, { class: 'design-primary' }));
271
+ return;
272
+ }
273
+ const response = state.implementation, draft = response?.draft, current = response?.current, preview = response?.approvalPreview;
274
+ if (!draft) {
275
+ content.append(empty('No package is ready for approval', 'Complete the implementation package before requesting owner approval.'));
276
+ content.append(action('Prepare implementation package', () => { activeStep = 'package'; render(); }, { class: 'design-primary' }));
277
+ return;
278
+ }
279
+ if (approvedPackage()) {
280
+ const approved = response.history?.find(item => item.id === current.id && item.version === current.version);
281
+ content.append(node('div', '', { class: 'design-handoff-approval-card' }));
282
+ const card = content.lastElementChild;
283
+ card.append(badge('approved'), node('h4', approved?.title || draft.title), paragraph(`Version ${current.version} was approved${approved?.approval?.actorId ? ` by ${approved.approval.actorId}` : ''}${approved?.approval?.approvedAt ? ` on ${new Date(approved.approval.approvedAt).toLocaleString()}` : ''}.`));
284
+ content.append(action('Continue to Plan', () => { activeStep = 'plan'; render(); }, { class: 'design-primary' }));
285
+ const details = node('details'); details.append(node('summary', 'Revoke this approval'));
286
+ const reason = node('textarea', '', { 'aria-label': 'Revocation reason', maxlength: '16384', placeholder: 'Why is this package no longer approved?' });
287
+ details.append(reason, action('Revoke approval', () => {
288
+ if (!reason.value.trim()) { reason.setCustomValidity('Enter a revocation reason.'); reason.reportValidity(); return; }
289
+ reason.setCustomValidity('');
290
+ void perform('Revoking approval…', async () => bridge.updateImplementationHandoff({ action: 'revoke', requestId: requestId('revoke'), expectedVersion: current.version, expectedContentDigest: current.contentDigest, reason: reason.value.trim() }), 'Approval revoked.');
291
+ }, { class: 'design-danger' }));
292
+ content.append(details);
293
+ return;
294
+ }
295
+ if (!preview?.available) {
296
+ content.append(empty(current?.status === 'revoked' ? 'The current approval was revoked' : 'This package cannot be approved yet', 'Refresh the package against the current ready design, then review approval again.'));
297
+ content.append(action('Review implementation package', () => { activeStep = 'package'; render(); }, { class: 'design-primary' }));
298
+ return;
299
+ }
300
+ const card = node('div', '', { class: 'design-handoff-approval-card' });
301
+ card.append(node('h4', preview.summary.title), paragraph(`Version ${preview.summary.packageVersion} · ${preview.summary.requirementCount} requirements · direction ${preview.summary.selectedVariant}`), paragraph(preview.summary.description, 'design-muted'));
302
+ content.append(card, action('Approve exact package', () => perform('Recording owner approval…', async () => {
303
+ await bridge.updateImplementationHandoff({ action: 'approve', requestId: requestId('approve'), ...preview.approvalRequest });
304
+ }, 'Implementation package approved.'), { class: 'design-primary' }));
305
+ }
306
+
307
+ async function copyText(value, output) {
308
+ try { await navigator.clipboard.writeText(value); output.textContent = 'Invocation copied. Paste it into your active coding agent when you are ready.'; }
309
+ catch {
310
+ const field = node('textarea', '', { readonly: '', 'aria-label': 'Plan invocation' }); field.value = value; output.before(field); field.focus(); field.select(); output.textContent = 'Copy the selected invocation.';
311
+ }
312
+ }
313
+
314
+ function renderPlan(content) {
315
+ content.append(sectionTitle('Step 5 of 5', 'Continue to Plan', 'Prepare an explicit host handoff. Plan remains a separate action; Ship is never started here.'));
316
+ if (!approvedPackage()) {
317
+ content.append(empty('An approved current package is required', currentPackage()?.status === 'revoked' ? 'This package was revoked. Prepare and approve a current version before planning.' : 'Complete owner approval before preparing the Plan invocation.'));
318
+ content.append(action('Review owner approval', () => { activeStep = 'approval'; render(); }, { class: 'design-primary' }));
319
+ return;
320
+ }
321
+ const context = bridge.context();
322
+ const subject = node('input', '', { type: 'text', required: '', maxlength: '256', value: context.designId, 'aria-label': 'Plan subject' });
323
+ const label = node('label', 'Plan subject'); label.append(subject); content.append(label);
324
+ const result = node('div', '', { class: 'design-handoff-plan-result', 'aria-live': 'polite' });
325
+ content.append(action('Prepare Plan handoff', async () => {
326
+ setBusy(true); setStatus('Preparing the Plan handoff…');
327
+ try {
328
+ const response = await bridge.updateImplementationHandoff({ action: 'continue-to-plan', subject: subject.value.trim() });
329
+ const handoff = response.handoff;
330
+ if (typeof bridge.continueToActiveHost === 'function') {
331
+ await bridge.continueToActiveHost(handoff);
332
+ result.textContent = 'The approved package was handed to the active host. Invoke Plan there to continue.';
333
+ } else {
334
+ const host = String(context.host || '').toLowerCase();
335
+ const key = host.includes('claude') ? 'claudeCode' : host.includes('codex') ? 'codex' : host.includes('cursor') ? 'cursor' : 'fallback';
336
+ const invocation = handoff.invocations[key] || handoff.invocations.fallback;
337
+ result.replaceChildren(node('code', invocation));
338
+ result.append(action('Copy invocation', () => void copyText(invocation, result), { class: 'design-handoff-copy' }));
339
+ }
340
+ setStatus('Plan handoff prepared. No planning files, agents, Git state or Ship run were changed.');
341
+ } catch (error) { setStatus(error?.message || 'The Plan handoff could not be prepared.', true); }
342
+ finally { setBusy(false); }
343
+ }, { class: 'design-primary' }), result);
344
+ }
345
+
346
+ function render() {
347
+ if (!panel) return;
348
+ renderProgress();
349
+ const content = q('[data-design-handoff-content]', panel); content.replaceChildren();
350
+ ({ readiness: renderReadiness, review: renderReview, package: renderPackage, approval: renderApproval, plan: renderPlan })[activeStep](content);
351
+ }
352
+
353
+ async function refresh() {
354
+ const attempt = ++serial;
355
+ setBusy(true); setStatus('Refreshing handoff state…');
356
+ const results = await Promise.allSettled([bridge.loadReadiness(), bridge.loadReviewHandoff(), bridge.loadImplementationHandoff()]);
357
+ if (attempt !== serial || !panel) return;
358
+ for (const [index, key] of ['readiness', 'review', 'implementation'].entries()) {
359
+ const result = results[index];
360
+ if (result.status === 'fulfilled') { state[key] = result.value; delete state.errors[key]; }
361
+ else state.errors[key] = result.reason?.message || 'This state is temporarily unavailable.';
362
+ }
363
+ setBusy(false); setStatus(Object.keys(state.errors).length ? 'Some handoff state is unavailable. The current design remains usable.' : 'Handoff state is current.', Boolean(Object.keys(state.errors).length));
364
+ render();
365
+ }
366
+
367
+ function close() {
368
+ if (!panel) return;
369
+ const returnTo = opener; panel.remove(); panel = null; delete root.dataset.designHandoffOpen;
370
+ returnTo?.isConnected && returnTo.focus({ preventScroll: true });
371
+ }
372
+
373
+ function open(step = activeStep) {
374
+ if (!bridge.owner()) return bridge.announce('Only a design owner can prepare an implementation handoff.', true);
375
+ activeStep = steps.some(([id]) => id === step) ? step : 'readiness';
376
+ if (panel) { render(); q(`[data-handoff-step="${activeStep}"]`, panel)?.focus(); return; }
377
+ opener = document.activeElement; root.dataset.designHandoffOpen = 'true';
378
+ panel = node('aside', '', { class: 'design-handoff-center', role: 'dialog', 'aria-modal': 'false', 'aria-labelledby': 'design-handoff-center-title' });
379
+ const header = node('header');
380
+ const heading = node('div'); heading.append(node('span', 'Owner workspace', { class: 'design-handoff-eyebrow' }), node('h2', 'Handoff Center', { id: 'design-handoff-center-title' }));
381
+ header.append(heading, action('×', close, { 'aria-label': 'Close Handoff Center', class: 'design-handoff-close' }));
382
+ const progress = node('nav', '', { class: 'design-handoff-progress', 'data-design-handoff-progress': '', 'aria-label': 'Design handoff progress' });
383
+ const content = node('section', '', { class: 'design-handoff-content', 'data-design-handoff-content': '', tabindex: '-1' });
384
+ const footer = node('footer'); footer.append(node('p', '', { 'data-design-handoff-status': '', role: 'status', 'aria-live': 'polite' }), action('Refresh', refresh, { 'data-design-handoff-refresh': '' }));
385
+ panel.append(header, progress, content, footer); root.append(panel);
386
+ panel.addEventListener('keydown', event => { if (event.key === 'Escape') { event.preventDefault(); close(); } });
387
+ render(); q(`[data-handoff-step="${activeStep}"]`, panel)?.focus({ preventScroll: true }); void refresh();
388
+ }
389
+
390
+ function mount() {
391
+ bridge = window.__openPlanrDesignHandoffBridge;
392
+ root = q('.planr-shell');
393
+ if (!bridge || !root) return false;
394
+ window.__openPlanrDesignHandoffCenter = Object.freeze({ open, close, refresh, getState: () => ({ open: Boolean(panel), step: activeStep, statuses: Object.fromEntries(steps.map(([id]) => [id, statusFor(id)])) }) });
395
+ return true;
396
+ }
397
+ const started = Date.now();
398
+ function boot() { if (!mount() && Date.now() - started < 15000) setTimeout(boot, 30); }
399
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot, { once: true }); else boot();
400
+ })();
@@ -198,6 +198,28 @@
198
198
  "cursor"
199
199
  ]
200
200
  },
201
+ {
202
+ "path": "scripts/runtime/packages/design/templates/studio/handoff-center.css",
203
+ "kind": "asset",
204
+ "executable": false,
205
+ "hosts": [
206
+ "claude-code",
207
+ "codex",
208
+ "chatgpt",
209
+ "cursor"
210
+ ]
211
+ },
212
+ {
213
+ "path": "scripts/runtime/packages/design/templates/studio/handoff-center.js",
214
+ "kind": "asset",
215
+ "executable": false,
216
+ "hosts": [
217
+ "claude-code",
218
+ "codex",
219
+ "chatgpt",
220
+ "cursor"
221
+ ]
222
+ },
201
223
  {
202
224
  "path": "scripts/runtime/packages/design/templates/studio/share.css",
203
225
  "kind": "asset",
@@ -3,7 +3,7 @@
3
3
  // packages/design/lib/design/utility.mjs
4
4
  import { spawn } from "node:child_process";
5
5
  import { existsSync as existsSync12, mkdirSync as mkdirSync9, readFileSync as readFileSync17, realpathSync as realpathSync7, writeFileSync as writeFileSync9 } from "node:fs";
6
- import { dirname as dirname14, join as join13, resolve as resolve7 } from "node:path";
6
+ import { dirname as dirname14, join as join13, resolve as resolve8 } from "node:path";
7
7
  import { fileURLToPath as fileURLToPath3 } from "node:url";
8
8
 
9
9
  // packages/design/lib/design/document.mjs
@@ -354,11 +354,11 @@ function createArtifactBridgeTools(document2, window2) {
354
354
  const outputWidth = Math.max(1, Math.round(width * scale)), outputHeight = Math.max(1, Math.round(height * scale));
355
355
  const svg = '<svg xmlns="http://www.w3.org/2000/svg" width="' + outputWidth + '" height="' + outputHeight + '" viewBox="0 0 ' + width + " " + height + '"><foreignObject width="' + width + '" height="' + height + '">' + markup + "</foreignObject></svg>";
356
356
  image = new Image();
357
- await new Promise((resolve8, reject) => {
357
+ await new Promise((resolve9, reject) => {
358
358
  const timer = setTimeout(() => reject(new Error("Thumbnail capture timed out.")), Math.max(1, deadline - Date.now()));
359
359
  image.onload = () => {
360
360
  clearTimeout(timer);
361
- resolve8();
361
+ resolve9();
362
362
  };
363
363
  image.onerror = () => {
364
364
  clearTimeout(timer);
@@ -13081,12 +13081,14 @@ ${shellMarkup}
13081
13081
  }
13082
13082
 
13083
13083
  // packages/design/lib/design/studio-render.mjs
13084
- var DESIGN_STUDIO_VERSION = "1.8.5";
13084
+ var DESIGN_STUDIO_VERSION = "1.9.0";
13085
13085
  var DESIGN_STUDIO_ASSETS = Object.freeze({
13086
13086
  style: "templates/studio/studio.css",
13087
13087
  runtime: "templates/studio/studio.js",
13088
13088
  enhancementsStyle: "templates/studio/enhancements.css",
13089
- enhancementsRuntime: "templates/studio/enhancements.js"
13089
+ enhancementsRuntime: "templates/studio/enhancements.js",
13090
+ handoffCenterStyle: "templates/studio/handoff-center.css",
13091
+ handoffCenterRuntime: "templates/studio/handoff-center.js"
13090
13092
  });
13091
13093
  function applyDesignStudioTheme(preference) {
13092
13094
  if (!preference) {
@@ -13543,8 +13545,8 @@ function renderDesignStudio(input = {}, options = {}) {
13543
13545
  return renderDesignStudioMarkup({ ...input, entries: input.entries ?? createDesignStudioEntries(input.document, input.envelope) }, {
13544
13546
  ...options,
13545
13547
  renderShell: renderArtifactShellDocument,
13546
- style: ["studio.css", "enhancements.css"].map((file) => readFileSync7(new URL(file, templateRoot), "utf8")).join("\n"),
13547
- runtime: renderDesignReviewExportSource() + "\n" + ["studio.js", "enhancements.js"].map((file) => readFileSync7(new URL(file, templateRoot), "utf8")).join("\n")
13548
+ style: ["studio.css", "enhancements.css", "handoff-center.css"].map((file) => readFileSync7(new URL(file, templateRoot), "utf8")).join("\n"),
13549
+ runtime: renderDesignReviewExportSource() + "\n" + ["studio.js", "enhancements.js", "handoff-center.js"].map((file) => readFileSync7(new URL(file, templateRoot), "utf8")).join("\n")
13548
13550
  });
13549
13551
  }
13550
13552
 
@@ -13827,7 +13829,7 @@ function designRendererRevision() {
13827
13829
  json({
13828
13830
  version: DESIGN_RENDERER_VERSION,
13829
13831
  stage: hash(stageRuntimeBytes()),
13830
- assets: ["studio.css", "studio.js", "enhancements.css", "enhancements.js"].filter((name) => existsSync3(new URL(`../../templates/studio/${name}`, new URL("./runtime/packages/design/lib/design/document.mjs", import.meta.url).href))).map(
13832
+ assets: ["studio.css", "studio.js", "enhancements.css", "enhancements.js", "handoff-center.css", "handoff-center.js"].filter((name) => existsSync3(new URL(`../../templates/studio/${name}`, new URL("./runtime/packages/design/lib/design/document.mjs", import.meta.url).href))).map(
13831
13833
  (name) => hash(
13832
13834
  readFileSync8(
13833
13835
  new URL(`../../templates/studio/${name}`, new URL("./runtime/packages/design/lib/design/document.mjs", import.meta.url).href)
@@ -13994,7 +13996,7 @@ ${errors.join("\n")}`);
13994
13996
 
13995
13997
  // packages/design/lib/design/review.mjs
13996
13998
  import { existsSync as existsSync11, readFileSync as readFileSync16 } from "node:fs";
13997
- import { dirname as dirname13, join as join12 } from "node:path";
13999
+ import { dirname as dirname13, join as join12, relative as relative4, resolve as resolve7 } from "node:path";
13998
14000
 
13999
14001
  // packages/artifact/lib/artifact/import.mjs
14000
14002
  import {
@@ -18363,6 +18365,52 @@ function currentImplementationBasis(file, env) {
18363
18365
  }
18364
18366
  };
18365
18367
  }
18368
+ function proposeImplementationPackage(file, env) {
18369
+ const current = currentDesign(file);
18370
+ const basis = currentImplementationBasis(file, env);
18371
+ const repository = projectRoot(current.root);
18372
+ const paths = [.../* @__PURE__ */ new Set([
18373
+ designSpecPath(current.root),
18374
+ ...(current.sourceFiles ?? []).map((path) => resolve7(current.root, path))
18375
+ ])].filter((path) => existsSync11(path));
18376
+ const sources = paths.map((path, index) => {
18377
+ const logicalPath2 = relative4(repository, path).replaceAll("\\", "/");
18378
+ const extension = logicalPath2.split(".").pop()?.toLowerCase();
18379
+ return {
18380
+ id: `SRC-${String(index + 1).padStart(3, "0")}`,
18381
+ kind: path === designSpecPath(current.root) ? "design-specification" : extension === "html" ? "screen" : ["css", "json"].includes(extension) ? "token" : "component",
18382
+ path: logicalPath2,
18383
+ revision: basis.sourceRevision,
18384
+ digest: `sha256:${hash(readFileSync16(path))}`
18385
+ };
18386
+ });
18387
+ const sourceByPath = new Map(sources.map((source) => [source.path, source.id]));
18388
+ const sourceId = (path) => sourceByPath.get(relative4(repository, resolve7(current.root, path)).replaceAll("\\", "/"));
18389
+ const selected = current.document.variants.find((variant) => variant.id === current.document.selectedVariant);
18390
+ const requirements = current.document.screens.map((screen) => {
18391
+ const authored = selected?.sources?.[screen.id] ?? screen.source;
18392
+ const refs = [authored?.html, ...authored?.styles ?? [], ...authored?.scripts ?? []].map(sourceId).filter(Boolean);
18393
+ return {
18394
+ kind: "behavior",
18395
+ statement: `${screen.title}: ${screen.description || "Implement the approved screen behavior and states."}`,
18396
+ sourceRefs: refs.length ? refs : [sources[0].id],
18397
+ verification: current.document.frames.map((frame) => `${screen.title} matches the approved ${frame.label} frame at ${frame.width} \xD7 ${frame.height}.`)
18398
+ };
18399
+ });
18400
+ const allRefs = sources.map((source) => source.id);
18401
+ requirements.push({
18402
+ kind: "accessibility",
18403
+ statement: "Preserve the approved interaction semantics, keyboard path, focus behavior and readable status communication.",
18404
+ sourceRefs: allRefs,
18405
+ verification: ["Keyboard-only use, visible focus, screen-reader labels and status announcements pass on every implemented screen."]
18406
+ });
18407
+ return {
18408
+ id: `${current.document.id}-implementation`,
18409
+ title: `${current.document.title} implementation package`,
18410
+ sources,
18411
+ requirements
18412
+ };
18413
+ }
18366
18414
  async function persistDesignTaste(current, state) {
18367
18415
  const path = join12(
18368
18416
  projectRoot(current.root),
@@ -18585,7 +18633,12 @@ ${renderArtifactParentRuntime({ ...options, adapterRuntimeUrl: `${base}api/desig
18585
18633
  const unlock = await acquireStartLock(join12(design.root, ".design/render.lock"));
18586
18634
  try {
18587
18635
  const root = dirname13(designSpecPath(design.root));
18588
- respond(res, 200, { ok: true, draft: readImplementationHandoffDraft(root), ...readImplementationHandoffLifecycle(root), approvalPreview: previewImplementationHandoffApproval(root) });
18636
+ let proposal = null;
18637
+ try {
18638
+ proposal = proposeImplementationPackage(file, env);
18639
+ } catch {
18640
+ }
18641
+ respond(res, 200, { ok: true, draft: readImplementationHandoffDraft(root), ...readImplementationHandoffLifecycle(root), approvalPreview: previewImplementationHandoffApproval(root), proposal });
18589
18642
  } finally {
18590
18643
  unlock();
18591
18644
  }
@@ -19066,7 +19119,7 @@ function verifyDesignDocument(file, report) {
19066
19119
  const images = (Array.isArray(report.screenshots) ? report.screenshots : []).filter((item2) => {
19067
19120
  const path = typeof item2 === "string" ? item2 : item2?.path;
19068
19121
  try {
19069
- return typeof path === "string" && isPng(readFileSync17(resolve7(path)));
19122
+ return typeof path === "string" && isPng(readFileSync17(resolve8(path)));
19070
19123
  } catch {
19071
19124
  return false;
19072
19125
  }
@@ -19095,14 +19148,14 @@ async function designUtility(argv, { stdout = (value) => process.stdout.write(`$
19095
19148
  flags[key] = ["json", "no-open"].includes(key) ? true : args[++i];
19096
19149
  }
19097
19150
  if (flags.view && !["canvas", "prototype", "walkthrough"].includes(flags.view)) throw new Error("View must be canvas, prototype, or walkthrough.");
19098
- const file = resolve7(input);
19151
+ const file = resolve8(input);
19099
19152
  let result;
19100
19153
  if (command === "handoff") {
19101
19154
  const action3 = flags.action ?? "inspect";
19102
19155
  if (action3 === "inspect") result = readDesignHandoff(file);
19103
19156
  else {
19104
19157
  const snapshot2 = readDesignHandoff(file);
19105
- const input2 = flags.input ? readJson2(resolve7(flags.input)) : {};
19158
+ const input2 = flags.input ? readJson2(resolve8(flags.input)) : {};
19106
19159
  result = await updateDesignHandoff(file, { ...input2, action: action3, revision: input2.revision ?? snapshot2.revision, version: input2.version ?? (snapshot2.draft?.version ?? 0) });
19107
19160
  }
19108
19161
  }
@@ -19137,7 +19190,7 @@ async function designUtility(argv, { stdout = (value) => process.stdout.write(`$
19137
19190
  const current = currentDesign(file), view = flags.view ?? current.document.defaultView;
19138
19191
  const state = readJson2(join13(current.root, ".design/studio-state.json"), { state: {} }).state;
19139
19192
  if (flags.format && flags.format !== "html") throw new Error("Portable export supports HTML. Use the studio PNG action for browser-rendered captures.");
19140
- const output = resolve7(flags.output ?? join13(current.root, `${view}-export.html`));
19193
+ const output = resolve8(flags.output ?? join13(current.root, `${view}-export.html`));
19141
19194
  if (existsSync12(output)) throw new Error(`Export already exists: ${output}. Choose a new --output path.`);
19142
19195
  mkdirSync9(dirname14(output), { recursive: true });
19143
19196
  writeFileSync9(output, standaloneDesignHtml({ ...current, state }, view), { flag: "wx" });
@@ -19145,7 +19198,7 @@ async function designUtility(argv, { stdout = (value) => process.stdout.write(`$
19145
19198
  }
19146
19199
  if (command === "verify") {
19147
19200
  if (!flags.report) throw new Error("verify requires --report <browser-report.json>.");
19148
- result = verifyDesignDocument(file, readJson2(resolve7(flags.report)));
19201
+ result = verifyDesignDocument(file, readJson2(resolve8(flags.report)));
19149
19202
  }
19150
19203
  if (command === "feedback") {
19151
19204
  await syncDesignShare(file, { env, fetchImpl });
@@ -19156,7 +19209,7 @@ async function designUtility(argv, { stdout = (value) => process.stdout.write(`$
19156
19209
  if (!["json", "markdown"].includes(format)) throw new Error("Feedback export format must be json or markdown.");
19157
19210
  if (!["current", "all"].includes(scope)) throw new Error("Feedback export scope must be current or all.");
19158
19211
  if (!flags.output) throw new Error("Feedback export requires --output <path>.");
19159
- const output = resolve7(flags.output);
19212
+ const output = resolve8(flags.output);
19160
19213
  if (existsSync12(output)) throw new Error("Feedback export already exists. Choose a new --output path.");
19161
19214
  const snapshot2 = exportDesignReview(file, { scope, env });
19162
19215
  mkdirSync9(dirname14(output), { recursive: true });