create-claude-cabinet 0.34.1 → 0.35.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.
package/package.json
CHANGED
|
@@ -187,6 +187,14 @@ One or more "why this matters" lines for the client.
|
|
|
187
187
|
blocks dispatch until it is supplied (the internal `text` is never
|
|
188
188
|
shown to a client verbatim).
|
|
189
189
|
|
|
190
|
+
**Literal `\n` normalization.** When notes are written via MCP tools
|
|
191
|
+
that JSON-encode the string, newlines sometimes arrive as literal
|
|
192
|
+
two-character `\n` sequences (backslash + n) instead of real newlines.
|
|
193
|
+
`parseClientCopy` normalizes these before parsing — titles won't contain
|
|
194
|
+
stray `\n` characters. Status suffixes like "— complete" or "— shipped"
|
|
195
|
+
are also stripped from titles since the bucket placement already
|
|
196
|
+
communicates status.
|
|
197
|
+
|
|
190
198
|
The copy block may be human-written or generated-then-approved.
|
|
191
199
|
`/engagement-sync` step 3.5 can auto-draft copy from structured action
|
|
192
200
|
metadata (via `buildGenerationSource`), with the consultant reviewing
|
|
@@ -240,6 +248,9 @@ transport:
|
|
|
240
248
|
type: email # the CHANNEL (email | mcp | file)
|
|
241
249
|
consultant: "oren@example.com" # where client feedback is sent
|
|
242
250
|
|
|
251
|
+
engagement_notes: # persistent notes shown at top of every packet
|
|
252
|
+
- "All email logic is built and tested on staging — real delivery activates once Postmark is configured."
|
|
253
|
+
|
|
243
254
|
sections: # the credential checklist = one section
|
|
244
255
|
- key: go_live
|
|
245
256
|
title: "Go-Live Credentials"
|
|
@@ -274,8 +285,10 @@ A **packet** is a per-recipient projection of engagement state. Fields:
|
|
|
274
285
|
engagement, packet_id, recipient, role, generated_at,
|
|
275
286
|
needs_you: [ { ref, title, why, kind, options } ],
|
|
276
287
|
in_flight, shipped, delegated,
|
|
277
|
-
billing,
|
|
278
|
-
messages, new_since_last, generated_label
|
|
288
|
+
billing, # present ONLY if roles[role].billing === true
|
|
289
|
+
messages, new_since_last, generated_label,
|
|
290
|
+
engagement_notes, # persistent consultant-authored notes (from config)
|
|
291
|
+
checklist_summary # [ { type, count } ] when engagement.yaml has sections
|
|
279
292
|
```
|
|
280
293
|
|
|
281
294
|
- The client **never** sees `action_fid` — items are referenced by an
|
|
@@ -306,13 +319,15 @@ The pure render engine (`engagement.mjs`, imports nothing — the adapter
|
|
|
306
319
|
boundary) pins these behaviors so Phase 3/4 can build on them without
|
|
307
320
|
reopening Phase 1:
|
|
308
321
|
|
|
309
|
-
- **`renderPacket` returns `{ packet, refmap, notClientReady }`.**
|
|
322
|
+
- **`renderPacket` returns `{ packet, refmap, notClientReady, allDropped }`.**
|
|
310
323
|
`packet` is the client projection, built without `action_fid`/`_refmap`
|
|
311
324
|
ever added (leaking is impossible by construction, not by a strip step).
|
|
312
325
|
`refmap` is the consultant-side `ref → action_fid` map. The caller writes
|
|
313
326
|
`{ ...packet, _refmap: refmap }` to `<recipient>-sent.json` and delivers
|
|
314
327
|
`packet` alone. `notClientReady` lists fids of client-visible actions
|
|
315
|
-
dropped because they lacked a client-facing copy block.
|
|
328
|
+
dropped because they lacked a client-facing copy block. `allDropped` is
|
|
329
|
+
`true` when ALL visible items were dropped (likely a missing-notes-column
|
|
330
|
+
query bug) — the caller should halt dispatch, not send an empty packet.
|
|
316
331
|
- **Not-client-ready items are dropped, never rendered with internal text.**
|
|
317
332
|
A `client-visible` action with no `<!-- client-facing -->` block is
|
|
318
333
|
excluded from the packet (and reported in `notClientReady`) — the engine
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
const KNOWN_TAG_PREFIXES = ['audience', 'scope', 'assignee', 'needs'];
|
|
17
17
|
const KNOWN_BARE_TAGS = ['client-visible'];
|
|
18
18
|
|
|
19
|
+
// Trailing status phrases that are redundant with bucket placement.
|
|
20
|
+
const STATUS_SUFFIX_RE = /\s*[—–-]\s*(?:complete|completed|shipped|done|planned|in progress|started|delivered)\s*(?:\(.*\))?\s*$/i;
|
|
21
|
+
|
|
19
22
|
// pib-db status → plain English shown to clients (never the raw enum).
|
|
20
23
|
const STATUS_LABELS = {
|
|
21
24
|
open: 'Not started',
|
|
@@ -82,8 +85,9 @@ export function loadEngagement(config) {
|
|
|
82
85
|
|
|
83
86
|
const transport = config.transport || { type: 'email', consultant: null };
|
|
84
87
|
const sections = Array.isArray(config.sections) ? config.sections : [];
|
|
88
|
+
const engagement_notes = config.engagement_notes || null;
|
|
85
89
|
|
|
86
|
-
return { engagement, billing, recipients, roles, transport, sections };
|
|
90
|
+
return { engagement, billing, recipients, roles, transport, sections, engagement_notes };
|
|
87
91
|
}
|
|
88
92
|
|
|
89
93
|
export function normalizeRecipient(r) {
|
|
@@ -171,8 +175,11 @@ export function parseClientCopy(notes) {
|
|
|
171
175
|
const close = notes.indexOf('-->', bodyStart);
|
|
172
176
|
if (close === -1) return null;
|
|
173
177
|
|
|
174
|
-
const
|
|
175
|
-
if (!
|
|
178
|
+
const raw = notes.slice(bodyStart, close).trim();
|
|
179
|
+
if (!raw) return null;
|
|
180
|
+
// Normalize literal two-char \n sequences (from double-escaping during
|
|
181
|
+
// JSON-encoded MCP writes) to real newlines before splitting.
|
|
182
|
+
const inner = raw.replace(/\\n/g, '\n');
|
|
176
183
|
const lines = inner.split('\n').map(l => l.trim()).filter(Boolean);
|
|
177
184
|
if (lines.length === 0) return null;
|
|
178
185
|
|
|
@@ -361,7 +368,8 @@ export function renderPacket(config, actions, events, billing, recipient, prevPa
|
|
|
361
368
|
throw new Error(`renderPacket: stableRef collision — ${refmap[ref]} and ${a.fid} both hash to ${ref}`);
|
|
362
369
|
}
|
|
363
370
|
refmap[ref] = a.fid;
|
|
364
|
-
const
|
|
371
|
+
const title = copy.client_title.replace(STATUS_SUFFIX_RE, '');
|
|
372
|
+
const item = { ref, title, why: copy.client_why };
|
|
365
373
|
|
|
366
374
|
if (a.completed || a.status === 'done') {
|
|
367
375
|
// Completed wins over needs: a done item is never shown as pending.
|
|
@@ -408,7 +416,8 @@ export function renderPacket(config, actions, events, billing, recipient, prevPa
|
|
|
408
416
|
}
|
|
409
417
|
// Construction-not-strip: explicit field listing only. Never spread from
|
|
410
418
|
// project row. fid/project_fid/name/notes/tags are never assigned.
|
|
411
|
-
const
|
|
419
|
+
const rollupTitle = copy.client_title.replace(STATUS_SUFFIX_RE, '');
|
|
420
|
+
const rollupItem = { title: rollupTitle, why: copy.client_why, status: rollupStatus, feedbackable: false };
|
|
412
421
|
const parsedProjectTags = parseActionTags(p.tags);
|
|
413
422
|
if (p.status === 'done' || (children.length > 0 && children.every(c => c.completed || c.status === 'done'))) {
|
|
414
423
|
shipped.push(rollupItem);
|
|
@@ -419,6 +428,29 @@ export function renderPacket(config, actions, events, billing, recipient, prevPa
|
|
|
419
428
|
}
|
|
420
429
|
}
|
|
421
430
|
|
|
431
|
+
// Silent-empty signal: if there were visible actions but ALL of them landed
|
|
432
|
+
// in notClientReady, the caller likely used a query that omits the notes
|
|
433
|
+
// column (e.g. pib_list_actions instead of pib_query). Signal so the
|
|
434
|
+
// caller can halt dispatch instead of silently sending an empty packet.
|
|
435
|
+
const allDropped = visible.length > 0 && notClientReady.length === visible.length;
|
|
436
|
+
|
|
437
|
+
// Checklist summary: sections parsed from engagement.yaml but NOT rendered as
|
|
438
|
+
// bucket items (those are interactive via /engagement). Include a summary so
|
|
439
|
+
// the preview shows checklist coverage.
|
|
440
|
+
const sections = config.sections || [];
|
|
441
|
+
const checklistSummary = sections.length > 0
|
|
442
|
+
? sections.map(s => {
|
|
443
|
+
const items = Array.isArray(s.items) ? s.items : [];
|
|
444
|
+
return { type: s.type || s.label || 'unknown', count: items.length };
|
|
445
|
+
})
|
|
446
|
+
: null;
|
|
447
|
+
|
|
448
|
+
// Engagement-wide notes: persistent messages from the consultant that appear
|
|
449
|
+
// at the top of every packet (e.g. caveats, disclaimers, contextual notes).
|
|
450
|
+
const engagementNotes = Array.isArray(config.engagement_notes) ? config.engagement_notes
|
|
451
|
+
: typeof config.engagement_notes === 'string' ? [config.engagement_notes]
|
|
452
|
+
: null;
|
|
453
|
+
|
|
422
454
|
// Messages: engagement-level events (target_fid null) plus events whose
|
|
423
455
|
// target action is still live. Events for soft-deleted / unknown actions
|
|
424
456
|
// are dropped (defensive — even if the caller passed them through).
|
|
@@ -452,6 +484,9 @@ export function renderPacket(config, actions, events, billing, recipient, prevPa
|
|
|
452
484
|
new_since_last,
|
|
453
485
|
};
|
|
454
486
|
|
|
487
|
+
if (engagementNotes) packet.engagement_notes = engagementNotes;
|
|
488
|
+
if (checklistSummary) packet.checklist_summary = checklistSummary;
|
|
489
|
+
|
|
455
490
|
// billing only for roles configured to receive it, and only when billing is
|
|
456
491
|
// actually enabled (a config.billing with enabled:false must not produce a
|
|
457
492
|
// "$0.00" section; a computed renderBilling block has no `enabled` field, so
|
|
@@ -465,7 +500,7 @@ export function renderPacket(config, actions, events, billing, recipient, prevPa
|
|
|
465
500
|
// roles, defined role). Defense-in-depth so the dispatch path can't skip it.
|
|
466
501
|
assertPacketInvariants(packet, recipient, roles);
|
|
467
502
|
|
|
468
|
-
return { packet, refmap, notClientReady };
|
|
503
|
+
return { packet, refmap, notClientReady, allDropped };
|
|
469
504
|
}
|
|
470
505
|
|
|
471
506
|
function extractOptions(action) {
|
|
@@ -66,7 +66,14 @@ note before anything else:
|
|
|
66
66
|
Always show the `generated_label` ("as of <date>") as a header so
|
|
67
67
|
staleness is visible even under the threshold.
|
|
68
68
|
|
|
69
|
-
### 3.
|
|
69
|
+
### 3. Engagement notes, messages, then work
|
|
70
|
+
|
|
71
|
+
If the packet carries `engagement_notes` (persistent consultant-authored
|
|
72
|
+
notes), render them first in a distinct block before any messages or items.
|
|
73
|
+
|
|
74
|
+
If the packet carries `checklist_summary`, show a single summary line
|
|
75
|
+
after engagement notes: "Plus N checklist items (X decisions, Y credentials,
|
|
76
|
+
...) — these appear when you run `/engagement` interactively."
|
|
70
77
|
|
|
71
78
|
Render the **messages** feed distinctly from action items — each with an
|
|
72
79
|
explicit sender and timestamp, in order:
|
|
@@ -74,16 +81,23 @@ explicit sender and timestamp, in order:
|
|
|
74
81
|
> **Oren · May 30** — "Quick note: the hosting migration is scheduled
|
|
75
82
|
> for next week."
|
|
76
83
|
|
|
77
|
-
Then render the work
|
|
78
|
-
|
|
79
|
-
- **
|
|
80
|
-
|
|
84
|
+
Then render the work with **visual structure**:
|
|
85
|
+
|
|
86
|
+
- **Needs your attention (N)** (`needs_you`) — lead with these. Use a
|
|
87
|
+
clear heading with the count.
|
|
88
|
+
- **In progress (N)** (`in_flight`) — each item gets an arrow indicator.
|
|
89
|
+
- **Done (N)** (`shipped`) — each item gets a checkmark indicator.
|
|
81
90
|
- **Handled for you** (`delegated`) — place LAST, collapsed to a one-line
|
|
82
91
|
summary ("Sydney is handling 3 items (marketing)"); offer to expand on
|
|
83
92
|
request. It's reassurance, not a to-do.
|
|
84
93
|
|
|
85
|
-
|
|
86
|
-
|
|
94
|
+
**Formatting rules:**
|
|
95
|
+
- Every section gets a header with a count: "**Done (12)**" not just a
|
|
96
|
+
bare list.
|
|
97
|
+
- Items in `new_since_last` are marked with "(new)" after the title.
|
|
98
|
+
- Show `billing` only if the packet carries it (principals only). Format
|
|
99
|
+
billing as a clear table (Date | Hours | Work), with rate and total at
|
|
100
|
+
the bottom — not a raw JSON dump.
|
|
87
101
|
|
|
88
102
|
### 4. Collect responses (respectful batch review)
|
|
89
103
|
|
|
@@ -180,12 +180,19 @@ For each recipient: load their previous packet from
|
|
|
180
180
|
`engagement-packets/<recipient>-sent.json` (if any) as `prevPacket`,
|
|
181
181
|
then:
|
|
182
182
|
```
|
|
183
|
-
const { packet, refmap, notClientReady } = renderPacket(config, actions, events, billing, recipient, prevPacket, projects)
|
|
183
|
+
const { packet, refmap, notClientReady, allDropped } = renderPacket(config, actions, events, billing, recipient, prevPacket, projects)
|
|
184
184
|
```
|
|
185
185
|
`notClientReady` reconfirms (defensively) the not-client-ready items the
|
|
186
186
|
engine dropped — they never reach the client (no internal text leaks).
|
|
187
187
|
Fold these into the step-3 block report.
|
|
188
188
|
|
|
189
|
+
**`allDropped` guard:** If `allDropped` is true, **STOP** — do not
|
|
190
|
+
proceed to dispatch. Every visible item was dropped because it lacked
|
|
191
|
+
client-facing copy. This almost always means the query used
|
|
192
|
+
`pib_list_actions` (which omits `notes`) instead of `pib_query` with
|
|
193
|
+
`SELECT *`. Surface: "Empty packet — all N client-visible items are
|
|
194
|
+
missing client-facing copy. Did you use pib_query with SELECT *?"
|
|
195
|
+
|
|
189
196
|
### 5. Hard gate
|
|
190
197
|
|
|
191
198
|
`assertPacketInvariants(packet, recipient, config.roles)`. If it throws
|