create-claude-cabinet 0.45.0 → 0.47.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/README.md +6 -10
- package/lib/cli.js +374 -91
- package/lib/copy.js +108 -6
- package/lib/db-setup.js +122 -17
- package/lib/engagement-server-setup.js +34 -9
- package/lib/metadata.js +51 -2
- package/lib/migrate-from-omega.js +13 -1
- package/lib/mux-setup.js +33 -9
- package/lib/settings-merge.js +52 -10
- package/lib/watchtower-setup.js +230 -0
- package/package.json +5 -1
- package/templates/cabinet/_cabinet-member-template.md +8 -3
- package/templates/cabinet/advisories-state-schema.md +34 -7
- package/templates/cabinet/checklist-stats-schema.md +15 -1
- package/templates/cabinet/composition-patterns.md +4 -3
- package/templates/cabinet/memory-lifecycle-contract.md +135 -0
- package/templates/cabinet/pib-db-access.md +13 -0
- package/templates/cabinet/skill-output-conventions.md +35 -1
- package/templates/cabinet/watchtower-contracts.md +503 -1
- package/templates/cabinet/worktree-invocation-contract.md +87 -0
- package/templates/engagement/OVERVIEW.md +12 -0
- package/templates/engagement/__tests__/engagement.test.mjs +177 -2
- package/templates/engagement/__tests__/invoice-doc.test.mjs +195 -0
- package/templates/engagement/engagement-preview.mjs +100 -0
- package/templates/engagement/engagement-schema.md +240 -0
- package/templates/engagement/engagement.mjs +391 -11
- package/templates/engagement/pib-db-patches/pib-db-lib.mjs +10 -1
- package/templates/engagement/pib-db-patches/pib-db-mcp-server.mjs +5 -44
- package/templates/engagement/pib-db-patches/pib-db.mjs +5 -2
- package/templates/hooks/action-completion-gate.sh +9 -3
- package/templates/hooks/memory-index-guard.sh +17 -11
- package/templates/hooks/watchtower-session-start.sh +24 -2
- package/templates/mcp/pib-db.json +1 -4
- package/templates/mux/__tests__/mux-fail-loud.fixture.sh +44 -0
- package/templates/mux/__tests__/station-liveness.fixture.sh +234 -0
- package/templates/mux/__tests__/station-liveness.test.mjs +47 -0
- package/templates/mux/bin/mux +289 -56
- package/templates/mux/config/help.txt +1 -0
- package/templates/rules/acknowledge-when-corrected.md +33 -0
- package/templates/rules/maintainability.md +11 -0
- package/templates/rules/memory-capture.md +26 -4
- package/templates/rules/plan-before-bulk-or-irreversible-actions.md +48 -0
- package/templates/rules/verify-before-asserting.md +79 -0
- package/templates/scripts/__tests__/advisor-pass.test.mjs +238 -0
- package/templates/scripts/__tests__/advisories.test.mjs +262 -0
- package/templates/scripts/__tests__/ahead-check-origin.test.mjs +355 -0
- package/templates/scripts/__tests__/batch-disposition.test.mjs +235 -0
- package/templates/scripts/__tests__/claude-churn-authored.test.mjs +90 -0
- package/templates/scripts/__tests__/cross-ring-reader.test.mjs +505 -0
- package/templates/scripts/__tests__/dx-captures-briefing.test.mjs +169 -0
- package/templates/scripts/__tests__/feedback-outbox-flush.test.mjs +232 -0
- package/templates/scripts/__tests__/hook-runner.test.mjs +247 -0
- package/templates/scripts/__tests__/narrative-corpus.test.mjs +247 -0
- package/templates/scripts/__tests__/phase-shim.test.mjs +112 -0
- package/templates/scripts/__tests__/qa-handoff-gate.test.mjs +68 -0
- package/templates/scripts/__tests__/qa-handoff-merge-state.test.mjs +162 -0
- package/templates/scripts/__tests__/resolve-cli.test.mjs +314 -0
- package/templates/scripts/__tests__/ring-state-ownership.test.mjs +228 -3
- package/templates/scripts/__tests__/ring1-content-detector.test.mjs +168 -0
- package/templates/scripts/__tests__/ring1-flagged-actions.test.mjs +135 -0
- package/templates/scripts/__tests__/ring1-script-drift.test.mjs +126 -0
- package/templates/scripts/__tests__/ring2-queue-reader.test.mjs +80 -0
- package/templates/scripts/__tests__/ring2-recall-canary.test.mjs +150 -0
- package/templates/scripts/__tests__/ring2-roster-review.test.mjs +311 -0
- package/templates/scripts/__tests__/ring2-thread-context.test.mjs +189 -0
- package/templates/scripts/__tests__/ring3-chunk-merge.test.mjs +122 -0
- package/templates/scripts/__tests__/ring3-close-lenses.test.mjs +359 -0
- package/templates/scripts/__tests__/ring3-dedup.test.mjs +467 -0
- package/templates/scripts/__tests__/ring3-memory-titles.test.mjs +187 -0
- package/templates/scripts/__tests__/ring3-novelty-rescue.test.mjs +148 -0
- package/templates/scripts/__tests__/ring3-recent-slice.test.mjs +101 -0
- package/templates/scripts/__tests__/ring4-reconcile.test.mjs +393 -0
- package/templates/scripts/__tests__/routine-dispatch.test.mjs +312 -0
- package/templates/scripts/__tests__/shared-thread-reader.test.mjs +187 -0
- package/templates/scripts/__tests__/suppression-ledger.test.mjs +148 -0
- package/templates/scripts/__tests__/verify-backfill.test.mjs +103 -0
- package/templates/scripts/__tests__/verify-coverage.test.mjs +80 -0
- package/templates/scripts/__tests__/watchtower-snapshot.test.mjs +261 -0
- package/templates/scripts/__tests__/watchtower-sync.test.mjs +311 -0
- package/templates/scripts/load-triage-history.js +5 -2
- package/templates/scripts/pib-db-mcp-server.mjs +5 -44
- package/templates/scripts/pib-db-path.mjs +61 -0
- package/templates/scripts/pib-db.mjs +5 -2
- package/templates/scripts/validate-memory.mjs +214 -16
- package/templates/scripts/watchtower-advisories.mjs +309 -0
- package/templates/scripts/watchtower-build-context.mjs +301 -18
- package/templates/scripts/watchtower-cross-ring-reader.mjs +700 -0
- package/templates/scripts/watchtower-hook-runner.mjs +422 -0
- package/templates/scripts/watchtower-lib.mjs +447 -2
- package/templates/scripts/watchtower-narrative-corpus.mjs +385 -0
- package/templates/scripts/watchtower-phase-shim.mjs +171 -0
- package/templates/scripts/watchtower-queue.mjs +484 -1
- package/templates/scripts/watchtower-ring1.mjs +533 -53
- package/templates/scripts/watchtower-ring2.mjs +787 -48
- package/templates/scripts/watchtower-ring3-close.mjs +1357 -99
- package/templates/scripts/watchtower-ring4-runner.sh +85 -0
- package/templates/scripts/watchtower-ring4.mjs +753 -0
- package/templates/scripts/watchtower-routines.mjs +358 -0
- package/templates/scripts/watchtower-snapshot.mjs +452 -0
- package/templates/scripts/watchtower-status.sh +1 -1
- package/templates/scripts/watchtower-sync.mjs +393 -0
- package/templates/skills/audit/SKILL.md +5 -1
- package/templates/skills/briefing/SKILL.md +584 -236
- package/templates/skills/cabinet-anthropic-insider/SKILL.md +14 -6
- package/templates/skills/cabinet-historian/SKILL.md +14 -11
- package/templates/skills/cabinet-process-therapist/SKILL.md +28 -0
- package/templates/skills/cabinet-security/SKILL.md +11 -0
- package/templates/skills/cabinet-system-advocate/SKILL.md +22 -21
- package/templates/skills/cabinet-user-advocate/SKILL.md +13 -7
- package/templates/skills/catch-up/SKILL.md +113 -0
- package/templates/skills/cc-publish/SKILL.md +148 -25
- package/templates/skills/cc-remember/SKILL.md +45 -0
- package/templates/skills/close/SKILL.md +107 -0
- package/templates/skills/collab-client/SKILL.md +22 -5
- package/templates/skills/collab-consultant/SKILL.md +110 -2
- package/templates/skills/debrief/SKILL.md +148 -16
- package/templates/skills/debrief-classic/SKILL.md +696 -0
- package/templates/skills/debrief-classic/calibration.md +44 -0
- package/templates/skills/debrief-classic/phases/audit-pattern-capture.md +78 -0
- package/templates/skills/debrief-classic/phases/auto-maintenance.md +48 -0
- package/templates/skills/debrief-classic/phases/checklist-feedback.md +123 -0
- package/templates/skills/debrief-classic/phases/close-work.md +163 -0
- package/templates/skills/debrief-classic/phases/health-checks.md +54 -0
- package/templates/skills/debrief-classic/phases/inventory.md +40 -0
- package/templates/skills/debrief-classic/phases/loose-ends.md +52 -0
- package/templates/skills/debrief-classic/phases/methodology-capture.md +223 -0
- package/templates/skills/debrief-classic/phases/qa-handoff-sweep.md +78 -0
- package/templates/skills/debrief-classic/phases/record-lessons.md +177 -0
- package/templates/skills/debrief-classic/phases/report.md +59 -0
- package/templates/skills/debrief-classic/phases/update-state.md +48 -0
- package/templates/skills/debrief-classic/phases/upstream-feedback.md +185 -0
- package/templates/skills/debrief-classic/phases/verify-coverage.md +101 -0
- package/templates/skills/execute/SKILL.md +77 -6
- package/templates/skills/execute/phases/commit-and-deploy.md +8 -0
- package/templates/skills/execute-group/SKILL.md +23 -26
- package/templates/skills/generate-plan-groups/SKILL.md +20 -0
- package/templates/skills/inbox/SKILL.md +124 -7
- package/templates/skills/memory/SKILL.md +22 -6
- package/templates/skills/orient/SKILL.md +140 -52
- package/templates/skills/orient-classic/SKILL.md +770 -0
- package/templates/skills/orient-classic/phases/auto-maintenance.md +52 -0
- package/templates/skills/orient-classic/phases/briefing.md +53 -0
- package/templates/skills/orient-classic/phases/cabinet.md +46 -0
- package/templates/skills/orient-classic/phases/checklist-status.md +54 -0
- package/templates/skills/orient-classic/phases/context.md +88 -0
- package/templates/skills/orient-classic/phases/data-sync.md +35 -0
- package/templates/skills/orient-classic/phases/deferred-check.md +55 -0
- package/templates/skills/orient-classic/phases/dx-captures.md +53 -0
- package/templates/skills/orient-classic/phases/health-checks.md +50 -0
- package/templates/skills/orient-classic/phases/verify-backfill.md +109 -0
- package/templates/skills/orient-classic/phases/work-scan.md +69 -0
- package/templates/skills/plan/SKILL.md +8 -0
- package/templates/skills/qa-drain/SKILL.md +157 -0
- package/templates/skills/qa-handoff/SKILL.md +287 -46
- package/templates/skills/session-handoff/SKILL.md +175 -6
- package/templates/skills/setup-accounts/SKILL.md +38 -16
- package/templates/skills/threads/SKILL.md +4 -0
- package/templates/skills/triage-audit/SKILL.md +6 -0
- package/templates/skills/validate/phases/validators.md +41 -0
- package/templates/skills/watchtower/SKILL.md +199 -2
- package/templates/verify-runtime/CONVENTIONS.md +9 -0
- package/templates/verify-runtime/README.md +37 -0
- package/templates/watchtower/config.json.template +10 -2
- package/templates/watchtower/queue/items/item.json.schema +1 -1
- package/templates/workflows/execute-group-complete.js +10 -1
|
@@ -84,11 +84,24 @@ export function loadEngagement(config) {
|
|
|
84
84
|
: { enabled: false, timelog: null, rate: 190, currency: 'USD', period: 'engagement' };
|
|
85
85
|
|
|
86
86
|
const transport = config.transport || { type: 'email', consultant: null };
|
|
87
|
+
|
|
88
|
+
// invoicing: optional block controlling the local invoice document
|
|
89
|
+
// (`/collab-consultant invoice`). NO bank/remit/From fields live here —
|
|
90
|
+
// Relay supplies the sender from the operator's account and assigns the
|
|
91
|
+
// invoice number; the document only carries bill-to + line items + total.
|
|
92
|
+
const invoicing = config.invoicing
|
|
93
|
+
? {
|
|
94
|
+
mode: config.invoicing.mode === 'itemized' ? 'itemized' : 'consolidated',
|
|
95
|
+
scope_label: config.invoicing.scope_label || 'Consulting services',
|
|
96
|
+
terms_days: Number.isFinite(config.invoicing.terms_days) ? config.invoicing.terms_days : 30,
|
|
97
|
+
}
|
|
98
|
+
: { mode: 'consolidated', scope_label: 'Consulting services', terms_days: 30 };
|
|
99
|
+
|
|
87
100
|
const sections = Array.isArray(config.sections) ? config.sections : [];
|
|
88
101
|
const engagement_notes = config.engagement_notes || null;
|
|
89
102
|
const scoped_notes = Array.isArray(config.scoped_notes) ? config.scoped_notes : [];
|
|
90
103
|
|
|
91
|
-
return { engagement, billing, recipients, roles, transport, sections, engagement_notes, scoped_notes };
|
|
104
|
+
return { engagement, billing, recipients, roles, transport, invoicing, sections, engagement_notes, scoped_notes };
|
|
92
105
|
}
|
|
93
106
|
|
|
94
107
|
export function normalizeRecipient(r) {
|
|
@@ -112,6 +125,8 @@ export function normalizeRecipient(r) {
|
|
|
112
125
|
id: r.id,
|
|
113
126
|
name: r.name || r.id,
|
|
114
127
|
email: r.email || null,
|
|
128
|
+
// org (optional) rides through so the invoice bill-to block can show it.
|
|
129
|
+
org: r.org || r.organization || null,
|
|
115
130
|
role,
|
|
116
131
|
scopes: Array.isArray(r.scopes) ? r.scopes : [],
|
|
117
132
|
has_claude_code,
|
|
@@ -327,6 +342,217 @@ function extractDate(cell) {
|
|
|
327
342
|
return m ? m[0] : null;
|
|
328
343
|
}
|
|
329
344
|
|
|
345
|
+
// ---------------------------------------------------------------------------
|
|
346
|
+
// renderInvoiceDoc — PURE. A clean, print-friendly invoice HTML document
|
|
347
|
+
// the operator uploads to Relay's "Create Invoice → Autofill from document".
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
// Relay (the operator's bank) does the actual invoicing — it has no API but
|
|
350
|
+
// extracts fields from an uploaded PDF/PNG. So this is the ONLY job: render a
|
|
351
|
+
// flat, self-contained invoice document. Validated against Relay's autofill
|
|
352
|
+
// (live experiment, 2026-06-16):
|
|
353
|
+
// - Relay parses line items as Description / Quantity / Unit price / Amount
|
|
354
|
+
// and computes its OWN totals (Quantity = hours, Unit price = the rate).
|
|
355
|
+
// - Relay assigns its OWN invoice number and pulls the From/sender (business
|
|
356
|
+
// name, address, email) from the operator's account → the document must
|
|
357
|
+
// NOT include an invoice number or any From/remit/bank block (ignored or
|
|
358
|
+
// overridden). No bank/ACH details anywhere.
|
|
359
|
+
// - Relay autofills the customer (bill-to name + email) and the line items
|
|
360
|
+
// + total correctly.
|
|
361
|
+
// - Relay does NOT map a net-30 due date — it defaults the due date to
|
|
362
|
+
// today. The terms only survive in Relay's free-text Memo → terms go in a
|
|
363
|
+
// memo-friendly footer line, and the skill tells the operator to set the
|
|
364
|
+
// due date in Relay by hand.
|
|
365
|
+
//
|
|
366
|
+
// Signature: renderInvoiceDoc(config, recipient, billingResult,
|
|
367
|
+
// { mode, invoiceDate, dueDate, period }) → HTML string.
|
|
368
|
+
// - PURE: invoiceDate/dueDate/period are passed in. A default for missing
|
|
369
|
+
// dates uses the `config.__now` injectable-time seam (never new Date()).
|
|
370
|
+
// - mode 'consolidated' (default): ONE line item — description
|
|
371
|
+
// "<scope_label> — <Month YYYY>", Quantity = total hours, Unit price =
|
|
372
|
+
// rate, Amount = billingResult.total.
|
|
373
|
+
// - mode 'itemized': one line per billingResult.lineItems row.
|
|
374
|
+
// - Total due is billingResult.total VERBATIM (the honest figure; for
|
|
375
|
+
// itemized it is NOT a re-sum of the per-line amounts).
|
|
376
|
+
export function renderInvoiceDoc(config, recipient, billingResult, { mode = 'consolidated', invoiceDate = null, dueDate = null, period = null } = {}) {
|
|
377
|
+
if (!billingResult || !Array.isArray(billingResult.lineItems) || billingResult.lineItems.length === 0) {
|
|
378
|
+
throw new Error('renderInvoiceDoc: billingResult has no lineItems (nothing to invoice)');
|
|
379
|
+
}
|
|
380
|
+
if (!(billingResult.hours > 0)) {
|
|
381
|
+
throw new Error('renderInvoiceDoc: billingResult has zero hours (nothing to invoice)');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const now = config?.__now || isoNow();
|
|
385
|
+
const issued = invoiceDate || now.slice(0, 10);
|
|
386
|
+
const due = dueDate || issued;
|
|
387
|
+
const rate = billingResult.rate;
|
|
388
|
+
const currency = billingResult.currency || 'USD';
|
|
389
|
+
|
|
390
|
+
// Period label: prefer the explicit period (YYYY-MM), else the issue month.
|
|
391
|
+
const periodMonth = (period || issued).slice(0, 7);
|
|
392
|
+
const periodLabel = formatMonthYear(periodMonth);
|
|
393
|
+
|
|
394
|
+
const invoicing = config?.invoicing || {};
|
|
395
|
+
const scopeLabel = invoicing.scope_label || 'Consulting services';
|
|
396
|
+
|
|
397
|
+
// Build the line rows. Each: { description, quantity (hours), unitPrice, amount }.
|
|
398
|
+
let rows;
|
|
399
|
+
if (mode === 'itemized') {
|
|
400
|
+
rows = billingResult.lineItems.map(li => {
|
|
401
|
+
const qty = Math.round((li.hours || 0) * 100) / 100;
|
|
402
|
+
return {
|
|
403
|
+
description: (li.work && li.work.trim()) || li.date || scopeLabel,
|
|
404
|
+
quantity: qty,
|
|
405
|
+
unitPrice: rate,
|
|
406
|
+
amount: Math.round(qty * rate * 100) / 100,
|
|
407
|
+
};
|
|
408
|
+
});
|
|
409
|
+
} else {
|
|
410
|
+
rows = [{
|
|
411
|
+
description: `${scopeLabel} — ${periodLabel}`,
|
|
412
|
+
quantity: billingResult.hours,
|
|
413
|
+
unitPrice: rate,
|
|
414
|
+
amount: billingResult.total,
|
|
415
|
+
}];
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// The stated Total is billingResult.total VERBATIM — never a re-sum.
|
|
419
|
+
const total = billingResult.total;
|
|
420
|
+
|
|
421
|
+
// Bill-to from the recipient object (autofilled by Relay as the customer).
|
|
422
|
+
const billToName = recipient?.name || recipient?.id || '';
|
|
423
|
+
const billToOrg = recipient?.org || recipient?.organization || null;
|
|
424
|
+
const billToEmail = recipient?.email || null;
|
|
425
|
+
|
|
426
|
+
const billToLines = [esc(billToName)];
|
|
427
|
+
if (billToOrg) billToLines.push(esc(billToOrg));
|
|
428
|
+
if (billToEmail) billToLines.push(esc(billToEmail));
|
|
429
|
+
|
|
430
|
+
const rowHtml = rows.map(r => ` <tr>
|
|
431
|
+
<td class="desc">${esc(r.description)}</td>
|
|
432
|
+
<td class="num">${fmtQty(r.quantity)}</td>
|
|
433
|
+
<td class="num">${fmtMoney(r.unitPrice, currency)}</td>
|
|
434
|
+
<td class="num">${fmtMoney(r.amount, currency)}</td>
|
|
435
|
+
</tr>`).join('\n');
|
|
436
|
+
|
|
437
|
+
const memoLine = `Payment terms: Net ${daysBetween(issued, due)} — due ${due}. Period: ${periodLabel}.`;
|
|
438
|
+
|
|
439
|
+
return `<!DOCTYPE html>
|
|
440
|
+
<html lang="en">
|
|
441
|
+
<head>
|
|
442
|
+
<meta charset="utf-8">
|
|
443
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
444
|
+
<title>Invoice — ${esc(periodLabel)}</title>
|
|
445
|
+
<style>
|
|
446
|
+
* { box-sizing: border-box; }
|
|
447
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; color: #1a1a1a; margin: 0; padding: 48px; font-size: 14px; line-height: 1.5; }
|
|
448
|
+
.invoice { max-width: 720px; margin: 0 auto; }
|
|
449
|
+
h1 { font-size: 28px; letter-spacing: 0.06em; margin: 0 0 24px; font-weight: 600; }
|
|
450
|
+
.meta { display: flex; justify-content: space-between; gap: 32px; margin-bottom: 32px; }
|
|
451
|
+
.meta .block { flex: 1; }
|
|
452
|
+
.label { text-transform: uppercase; font-size: 11px; letter-spacing: 0.08em; color: #888; margin-bottom: 4px; }
|
|
453
|
+
.billto-name { font-weight: 600; }
|
|
454
|
+
table { width: 100%; border-collapse: collapse; margin: 8px 0 0; }
|
|
455
|
+
thead th { text-align: left; border-bottom: 2px solid #1a1a1a; padding: 8px 10px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: #555; }
|
|
456
|
+
thead th.num { text-align: right; }
|
|
457
|
+
tbody td { padding: 10px; border-bottom: 1px solid #e2e2e2; vertical-align: top; }
|
|
458
|
+
tbody td.num { text-align: right; white-space: nowrap; }
|
|
459
|
+
tbody td.desc { width: 55%; }
|
|
460
|
+
.totals { margin-top: 16px; display: flex; justify-content: flex-end; }
|
|
461
|
+
.totals table { width: auto; min-width: 280px; }
|
|
462
|
+
.totals td { padding: 6px 10px; border: none; }
|
|
463
|
+
.totals td.num { text-align: right; white-space: nowrap; }
|
|
464
|
+
.totals tr.grand td { border-top: 2px solid #1a1a1a; font-weight: 700; font-size: 16px; padding-top: 12px; }
|
|
465
|
+
.memo { margin-top: 40px; padding-top: 16px; border-top: 1px solid #e2e2e2; color: #555; font-size: 13px; }
|
|
466
|
+
@media print { body { padding: 0; } }
|
|
467
|
+
</style>
|
|
468
|
+
</head>
|
|
469
|
+
<body>
|
|
470
|
+
<div class="invoice">
|
|
471
|
+
<h1>INVOICE</h1>
|
|
472
|
+
<div class="meta">
|
|
473
|
+
<div class="block">
|
|
474
|
+
<div class="label">Bill to</div>
|
|
475
|
+
<div class="billto-name">${billToLines.join('</div>\n <div>')}</div>
|
|
476
|
+
</div>
|
|
477
|
+
<div class="block">
|
|
478
|
+
<div class="label">Issued</div>
|
|
479
|
+
<div>${esc(issued)}</div>
|
|
480
|
+
<div class="label" style="margin-top:12px;">Period</div>
|
|
481
|
+
<div>${esc(periodLabel)}</div>
|
|
482
|
+
</div>
|
|
483
|
+
</div>
|
|
484
|
+
<table>
|
|
485
|
+
<thead>
|
|
486
|
+
<tr>
|
|
487
|
+
<th class="desc">Description</th>
|
|
488
|
+
<th class="num">Quantity (hrs)</th>
|
|
489
|
+
<th class="num">Unit price</th>
|
|
490
|
+
<th class="num">Amount</th>
|
|
491
|
+
</tr>
|
|
492
|
+
</thead>
|
|
493
|
+
<tbody>
|
|
494
|
+
${rowHtml}
|
|
495
|
+
</tbody>
|
|
496
|
+
</table>
|
|
497
|
+
<div class="totals">
|
|
498
|
+
<table>
|
|
499
|
+
<tr class="grand">
|
|
500
|
+
<td>Total due</td>
|
|
501
|
+
<td class="num">${fmtMoney(total, currency)}</td>
|
|
502
|
+
</tr>
|
|
503
|
+
</table>
|
|
504
|
+
</div>
|
|
505
|
+
<div class="memo">${esc(memoLine)}</div>
|
|
506
|
+
</div>
|
|
507
|
+
</body>
|
|
508
|
+
</html>
|
|
509
|
+
`;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// HTML-escape a value for safe interpolation into the invoice document.
|
|
513
|
+
function esc(v) {
|
|
514
|
+
return String(v == null ? '' : v)
|
|
515
|
+
.replace(/&/g, '&')
|
|
516
|
+
.replace(/</g, '<')
|
|
517
|
+
.replace(/>/g, '>')
|
|
518
|
+
.replace(/"/g, '"');
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// "2026-06" → "June 2026". Falls back to the raw input on a malformed value.
|
|
522
|
+
function formatMonthYear(yyyymm) {
|
|
523
|
+
const m = String(yyyymm || '').match(/^(\d{4})-(\d{2})/);
|
|
524
|
+
if (!m) return String(yyyymm || '');
|
|
525
|
+
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
|
526
|
+
const idx = parseInt(m[2], 10) - 1;
|
|
527
|
+
if (idx < 0 || idx > 11) return String(yyyymm);
|
|
528
|
+
return `${months[idx]} ${m[1]}`;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Whole-day gap between two ISO dates (used to label the memo's "Net N").
|
|
532
|
+
// Defaults to 30 if either date is unparseable.
|
|
533
|
+
function daysBetween(fromIso, toIso) {
|
|
534
|
+
const a = Date.parse(`${fromIso}T00:00:00Z`);
|
|
535
|
+
const b = Date.parse(`${toIso}T00:00:00Z`);
|
|
536
|
+
if (!Number.isFinite(a) || !Number.isFinite(b)) return 30;
|
|
537
|
+
return Math.round((b - a) / 86400000);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function fmtQty(n) {
|
|
541
|
+
// Trim trailing zeros: 2.50 → "2.5", 3.00 → "3".
|
|
542
|
+
const r = Math.round((Number(n) || 0) * 100) / 100;
|
|
543
|
+
return String(r);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function fmtMoney(n, currency = 'USD') {
|
|
547
|
+
const amount = (Number(n) || 0).toFixed(2);
|
|
548
|
+
const symbols = { USD: '$', CAD: '$', AUD: '$', GBP: '£', EUR: '€' };
|
|
549
|
+
const sym = symbols[currency];
|
|
550
|
+
// Group thousands with commas.
|
|
551
|
+
const [whole, frac] = amount.split('.');
|
|
552
|
+
const grouped = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
553
|
+
return sym ? `${sym}${grouped}.${frac}` : `${grouped}.${frac} ${currency}`;
|
|
554
|
+
}
|
|
555
|
+
|
|
330
556
|
// ---------------------------------------------------------------------------
|
|
331
557
|
// renderPacket — build a per-recipient projection + consultant-side refmap
|
|
332
558
|
// ---------------------------------------------------------------------------
|
|
@@ -403,7 +629,14 @@ export function renderPacket(config, actions, events, billing, recipient, prevPa
|
|
|
403
629
|
// Completed wins over needs: a done item is never shown as pending.
|
|
404
630
|
shipped.push({ ...item, status: STATUS_LABELS.done });
|
|
405
631
|
} else if (parsed.needs.length > 0) {
|
|
406
|
-
|
|
632
|
+
const kind = parsed.needs[0];
|
|
633
|
+
if (kind === 'decision') {
|
|
634
|
+
// Structured options + decision_type (§5.1). A real (projected) action
|
|
635
|
+
// declares free-text by carrying no options.
|
|
636
|
+
needs_you.push({ ...item, kind, ...buildDecisionFields(a) });
|
|
637
|
+
} else {
|
|
638
|
+
needs_you.push({ ...item, kind, options: extractClientOptions(a) });
|
|
639
|
+
}
|
|
407
640
|
} else if (parsed.assignees.length > 0 && !parsed.assignees.includes(recipient.id)) {
|
|
408
641
|
// Delegated = assigned to others and NOT to this recipient. An item the
|
|
409
642
|
// recipient is also an assignee on is their own work (in_flight), not delegated.
|
|
@@ -458,22 +691,51 @@ export function renderPacket(config, actions, events, billing, recipient, prevPa
|
|
|
458
691
|
}
|
|
459
692
|
}
|
|
460
693
|
|
|
694
|
+
// Internalized decision projection (§5.1, P1.1): synthesize a needs_you
|
|
695
|
+
// decision per engagement.yaml `decide` item that no LIVE action already
|
|
696
|
+
// covers. The synthesized item carries a deterministic backing fid so its
|
|
697
|
+
// ref/refmap entry is stable and an inbound answer resolves to it — exactly
|
|
698
|
+
// as for a real action. Structural dedup (invariant #3): a live, visible
|
|
699
|
+
// needs:decision action declaring `engagement_key: <key>` in its notes
|
|
700
|
+
// renders that decision instead, so it is never emitted twice.
|
|
701
|
+
const coveredKeys = new Set();
|
|
702
|
+
for (const a of visible) {
|
|
703
|
+
// Only a projected action that ACTUALLY RENDERS as a decision (i.e. has
|
|
704
|
+
// client copy) covers its key. A marker-bearing action with no copy is
|
|
705
|
+
// dropped to notClientReady and renders nowhere; if it also suppressed the
|
|
706
|
+
// synthesis the decision would vanish silently. Require copy so synthesis
|
|
707
|
+
// still fills that gap.
|
|
708
|
+
if (parseActionTags(a.tags).needs.includes('decision') && parseClientCopy(a) !== null) {
|
|
709
|
+
const k = parseEngagementKey(a.notes);
|
|
710
|
+
if (k) coveredKeys.add(k);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
for (const synth of synthesizeDecisions(config, recipient, roles, coveredKeys, refmap, liveFids)) {
|
|
714
|
+
needs_you.push(synth);
|
|
715
|
+
}
|
|
716
|
+
|
|
461
717
|
// Silent-empty signal: if there were visible actions but ALL of them landed
|
|
462
718
|
// in notClientReady, the caller likely used a query that omits the notes
|
|
463
719
|
// column (e.g. pib_list_actions instead of pib_query). Signal so the
|
|
464
720
|
// caller can halt dispatch instead of silently sending an empty packet.
|
|
465
721
|
const allDropped = visible.length > 0 && notClientReady.length === visible.length;
|
|
466
722
|
|
|
467
|
-
// Checklist summary:
|
|
468
|
-
//
|
|
469
|
-
// the
|
|
723
|
+
// Checklist summary: NON-decide checklist items only (credentials/confirms
|
|
724
|
+
// handled interactively via /setup-accounts). `decide` items now render as
|
|
725
|
+
// needs_you decisions (the internalized projection, §5.1), so counting them
|
|
726
|
+
// here too would double-surface every decision — they are EXCLUDED. The type
|
|
727
|
+
// label is derived from a real section field (type → kind → label → key),
|
|
728
|
+
// never the literal 'unknown'; a section with zero non-decide items is
|
|
729
|
+
// omitted entirely (no empty / 'unknown' rows leak into the packet).
|
|
470
730
|
const sections = config.sections || [];
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
:
|
|
731
|
+
const checklistRows = [];
|
|
732
|
+
for (const s of sections) {
|
|
733
|
+
const items = Array.isArray(s.items) ? s.items : [];
|
|
734
|
+
const nonDecide = items.filter(it => it && it.kind !== 'decide');
|
|
735
|
+
if (nonDecide.length === 0) continue;
|
|
736
|
+
checklistRows.push({ type: s.type || s.kind || s.label || s.key || 'checklist', count: nonDecide.length });
|
|
737
|
+
}
|
|
738
|
+
const checklistSummary = checklistRows.length > 0 ? checklistRows : null;
|
|
477
739
|
|
|
478
740
|
// Engagement-wide notes: persistent messages from the consultant that appear
|
|
479
741
|
// at the top of every packet (e.g. caveats, disclaimers, contextual notes).
|
|
@@ -553,6 +815,111 @@ export function extractClientOptions(item) {
|
|
|
553
815
|
return extractOptions(item);
|
|
554
816
|
}
|
|
555
817
|
|
|
818
|
+
// ---------------------------------------------------------------------------
|
|
819
|
+
// Decision items — structured options & the internalized projection (§5.1)
|
|
820
|
+
// ---------------------------------------------------------------------------
|
|
821
|
+
|
|
822
|
+
// Deterministic backing fid for a yaml-authored decision. MUST satisfy the
|
|
823
|
+
// pib-db FID_PATTERN (`act:<8 lowercase hex>`) so an inbound answer's
|
|
824
|
+
// target_fid passes addEngagementEvent's validateFid — the production ingest
|
|
825
|
+
// path the loop test's in-memory refmap can't exercise. Deterministic FNV-1a
|
|
826
|
+
// over (slug, section, key) → 8 hex: a stable ref across renders, the same
|
|
827
|
+
// identity any materialized backing row derives. A collision with a real
|
|
828
|
+
// pib-db-generated fid is ~1e-6 and is hard-guarded in synthesizeDecisions
|
|
829
|
+
// (an unrelated live-fid match throws loudly rather than mis-attributing).
|
|
830
|
+
export function decisionFid(engagementSlug, sectionKey, itemKey) {
|
|
831
|
+
let h = 0x811c9dc5;
|
|
832
|
+
const s = `${engagementSlug}${sectionKey}${itemKey}`;
|
|
833
|
+
for (let i = 0; i < s.length; i++) {
|
|
834
|
+
h ^= s.charCodeAt(i);
|
|
835
|
+
h = Math.imul(h, 0x01000193) >>> 0;
|
|
836
|
+
}
|
|
837
|
+
return 'act:' + (h >>> 0).toString(16).padStart(8, '0');
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Normalize one option (yaml entry OR a legacy comma-split string) → { value,
|
|
841
|
+
// label }. `value` is the canonical, comma-safe answer text; `label` defaults
|
|
842
|
+
// to it. Empty/whitespace-only values drop out.
|
|
843
|
+
export function normalizeOption(opt) {
|
|
844
|
+
if (opt == null) return null;
|
|
845
|
+
if (typeof opt === 'object' && !Array.isArray(opt)) {
|
|
846
|
+
const value = String(opt.value ?? '').trim();
|
|
847
|
+
if (!value) return null;
|
|
848
|
+
const lab = opt.label != null ? String(opt.label).trim() : '';
|
|
849
|
+
return { value, label: lab || value };
|
|
850
|
+
}
|
|
851
|
+
const value = String(opt).trim();
|
|
852
|
+
return value ? { value, label: value } : null;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// { decision_type, options } for a yaml decide item. free_text when the item
|
|
856
|
+
// declares free_text:true OR carries no options. Choice options are the
|
|
857
|
+
// structured array — never a comma-joined string.
|
|
858
|
+
function decisionShape(item) {
|
|
859
|
+
const options = Array.isArray(item.options) ? item.options.map(normalizeOption).filter(Boolean) : [];
|
|
860
|
+
const freeText = item.free_text === true || options.length === 0;
|
|
861
|
+
return { decision_type: freeText ? 'free_text' : 'choice', options: freeText ? [] : options };
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// { decision_type, options } for a REAL (projected) needs:decision action.
|
|
865
|
+
// Sources options from the legacy comma-split path and normalizes them to the
|
|
866
|
+
// structured shape so every decision item is uniform. No options ⇒ free_text.
|
|
867
|
+
function buildDecisionFields(action) {
|
|
868
|
+
const options = (extractClientOptions(action) || []).map(normalizeOption).filter(Boolean);
|
|
869
|
+
const freeText = options.length === 0;
|
|
870
|
+
return { decision_type: freeText ? 'free_text' : 'choice', options: freeText ? [] : options };
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// The dedup key: a backing/projected action declares which decide item it
|
|
874
|
+
// covers with an `engagement_key: <itemKey>` marker on its own notes line.
|
|
875
|
+
function parseEngagementKey(notes) {
|
|
876
|
+
const m = (notes || '').match(/^engagement_key:\s*(\S+)\s*$/m);
|
|
877
|
+
return m ? m[1] : null;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Synthesize needs_you decision items from the config's `decide` sections,
|
|
881
|
+
// skipping any key a live action already covers (coveredKeys). Pure: builds an
|
|
882
|
+
// in-memory action shape per decide item and reuses visibleTo (audience-aware)
|
|
883
|
+
// + stableRef, then emits the structured decision item and records its refmap
|
|
884
|
+
// entry. Returns the items to append to needs_you.
|
|
885
|
+
function synthesizeDecisions(config, recipient, roles, coveredKeys, refmap, liveFids) {
|
|
886
|
+
const out = [];
|
|
887
|
+
const slug = config.engagement?.slug || config.engagement?.project_fid || 'engagement';
|
|
888
|
+
for (const sec of config.sections || []) {
|
|
889
|
+
const sectionKey = sec.key || sec.label || 'section';
|
|
890
|
+
for (const it of (sec.items || [])) {
|
|
891
|
+
if (!it || it.kind !== 'decide' || !it.key) continue;
|
|
892
|
+
if (coveredKeys.has(it.key)) continue; // a live action renders this one
|
|
893
|
+
const audience = Array.isArray(it.audience) ? it.audience : (it.audience ? [it.audience] : []);
|
|
894
|
+
const tags = ['client-visible', 'needs:decision', ...audience.map(a => `audience:${a}`)].join(',');
|
|
895
|
+
const synthAction = { fid: decisionFid(slug, sectionKey, it.key), tags, status: 'open', notes: `engagement_key: ${it.key}` };
|
|
896
|
+
if (!visibleTo(synthAction, recipient, roles)) continue;
|
|
897
|
+
// Collision guard: this key was NOT covered by a marker-bearing action
|
|
898
|
+
// (coveredKeys skip above), so a live-fid match here is an UNRELATED
|
|
899
|
+
// action. Throw loudly rather than mis-attribute the answer to it.
|
|
900
|
+
if (liveFids && liveFids.has(synthAction.fid)) {
|
|
901
|
+
throw new Error(`renderPacket: synthesized decision fid ${synthAction.fid} (key "${it.key}") collides with an unrelated live action fid`);
|
|
902
|
+
}
|
|
903
|
+
const ref = stableRef(synthAction.fid);
|
|
904
|
+
if (refmap[ref] && refmap[ref] !== synthAction.fid) {
|
|
905
|
+
throw new Error(`renderPacket: stableRef collision (synthesized decision) — ${refmap[ref]} and ${synthAction.fid} both hash to ${ref}`);
|
|
906
|
+
}
|
|
907
|
+
refmap[ref] = synthAction.fid;
|
|
908
|
+
const title = String(it.prompt || it.key).replace(STATUS_SUFFIX_RE, '').trim();
|
|
909
|
+
const why = [it.help, it.tradeoff ? `Trade-off: ${it.tradeoff}` : '']
|
|
910
|
+
.map(x => (x == null ? '' : String(x).trim())).filter(Boolean).join('\n\n');
|
|
911
|
+
const { decision_type, options } = decisionShape(it);
|
|
912
|
+
const item = { ref, title, why, kind: 'decision', decision_type, options };
|
|
913
|
+
const itemTags = tags.split(',').map(t => t.trim()).filter(Boolean);
|
|
914
|
+
const scopedNotes = (config.scoped_notes || [])
|
|
915
|
+
.filter(sn => sn.tag && itemTags.includes(sn.tag)).map(sn => sn.note).filter(Boolean);
|
|
916
|
+
if (scopedNotes.length) item.context_notes = scopedNotes;
|
|
917
|
+
out.push(item);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return out;
|
|
921
|
+
}
|
|
922
|
+
|
|
556
923
|
function collectRefs(packet) {
|
|
557
924
|
if (!packet) return [];
|
|
558
925
|
const buckets = ['needs_you', 'in_flight', 'shipped', 'delegated'];
|
|
@@ -596,6 +963,19 @@ export function assertPacketInvariants(packet, recipient, roles) {
|
|
|
596
963
|
}
|
|
597
964
|
}
|
|
598
965
|
|
|
966
|
+
// Fail-closed (§5.1, P1.2): an answerable choice decision must carry >=2
|
|
967
|
+
// options. An unanswerable choice can never ship as an empty client dialog —
|
|
968
|
+
// the failure is loud at render time. free_text decisions (0 options) and
|
|
969
|
+
// display-only roll-ups (no ref) are exempt.
|
|
970
|
+
for (const item of packet.needs_you || []) {
|
|
971
|
+
if (item.kind === 'decision' && item.ref && item.decision_type === 'choice') {
|
|
972
|
+
const n = Array.isArray(item.options) ? item.options.length : 0;
|
|
973
|
+
if (n < 2) {
|
|
974
|
+
throw new Error(`assertPacketInvariants: decision "${item.title}" (${item.ref}) is a choice with ${n} option(s) — a choice decision needs >=2 options. Fix: add a second option; OR declare free_text (a yaml decide item); OR drop the needs:decision tag (a projected action). The whole packet is held until this is resolved.`);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
599
979
|
const hasBilling = Object.prototype.hasOwnProperty.call(packet, 'billing') && packet.billing != null;
|
|
600
980
|
if (hasBilling && role.billing !== true) {
|
|
601
981
|
throw new Error(`assertPacketInvariants: role "${recipient.role}" must not receive billing`);
|
|
@@ -8,10 +8,19 @@
|
|
|
8
8
|
// None of them do console.log — callers decide how to present output.
|
|
9
9
|
|
|
10
10
|
import { existsSync, readFileSync } from 'node:fs';
|
|
11
|
-
import { ENGAGEMENT_EVENTS_CREATE, ENGAGEMENT_EVENTS_INDEXES } from '../.claude/engagement/sql-constants.mjs';
|
|
12
11
|
import { join } from 'node:path';
|
|
13
12
|
import { randomUUID } from 'node:crypto';
|
|
14
13
|
|
|
14
|
+
// sql-constants lives in the installed engagement runtime (.claude/engagement/),
|
|
15
|
+
// which is gitignored in the CC source repo — absent on every fresh checkout/CI,
|
|
16
|
+
// where npm test must still pass (act:d64feaac). Fall back to the committed
|
|
17
|
+
// template source of truth. Same code in template and installed copies — no fork.
|
|
18
|
+
const { ENGAGEMENT_EVENTS_CREATE, ENGAGEMENT_EVENTS_INDEXES } =
|
|
19
|
+
await import('../.claude/engagement/sql-constants.mjs').catch((err) => {
|
|
20
|
+
if (err.code !== 'ERR_MODULE_NOT_FOUND') throw err;
|
|
21
|
+
return import('../templates/engagement/sql-constants.mjs');
|
|
22
|
+
});
|
|
23
|
+
|
|
15
24
|
// ---------------------------------------------------------------------------
|
|
16
25
|
// Helpers
|
|
17
26
|
// ---------------------------------------------------------------------------
|
|
@@ -5,59 +5,20 @@
|
|
|
5
5
|
// for Claude Code to call directly instead of shelling out to the CLI.
|
|
6
6
|
//
|
|
7
7
|
// Environment:
|
|
8
|
-
// PIB_DB_PATH — path to SQLite file
|
|
8
|
+
// PIB_DB_PATH — path to SQLite file. Overrides everything. Otherwise the
|
|
9
|
+
// path resolver (pib-db-path.mjs) picks main's pib.db when in
|
|
10
|
+
// a linked worktree, else cwd/pib.db.
|
|
9
11
|
|
|
10
12
|
import { join, dirname } from 'node:path';
|
|
11
13
|
import { fileURLToPath } from 'node:url';
|
|
12
14
|
import { createRequire } from 'node:module';
|
|
13
15
|
import { createInterface } from 'node:readline';
|
|
14
|
-
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
15
16
|
import * as lib from './pib-db-lib.mjs';
|
|
17
|
+
import { resolvePibDbPath } from './pib-db-path.mjs';
|
|
16
18
|
|
|
17
19
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
// the canonical database. Worktrees have a .git *file* (not directory)
|
|
21
|
-
// pointing to the main repo's .git/worktrees/<name>/ directory.
|
|
22
|
-
function resolveDbPath() {
|
|
23
|
-
if (process.env.PIB_DB_PATH) return process.env.PIB_DB_PATH;
|
|
24
|
-
|
|
25
|
-
const cwd = process.cwd();
|
|
26
|
-
const localDb = join(cwd, 'pib.db');
|
|
27
|
-
|
|
28
|
-
// If local pib.db exists and is non-trivial (>8KB), use it
|
|
29
|
-
if (existsSync(localDb)) {
|
|
30
|
-
try {
|
|
31
|
-
const stats = statSync(localDb);
|
|
32
|
-
if (stats.size > 8192) return localDb;
|
|
33
|
-
} catch { /* fall through to worktree detection */ }
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Check if we're in a git worktree (.git is a file, not a directory)
|
|
37
|
-
const gitPath = join(cwd, '.git');
|
|
38
|
-
if (existsSync(gitPath)) {
|
|
39
|
-
try {
|
|
40
|
-
const stats = statSync(gitPath);
|
|
41
|
-
if (stats.isFile()) {
|
|
42
|
-
// .git file contains: "gitdir: /path/to/main/.git/worktrees/<name>"
|
|
43
|
-
const content = readFileSync(gitPath, 'utf-8').trim();
|
|
44
|
-
const match = content.match(/^gitdir:\s*(.+)/);
|
|
45
|
-
if (match) {
|
|
46
|
-
// Walk up from .git/worktrees/<name> to find the main repo
|
|
47
|
-
const gitdir = match[1];
|
|
48
|
-
const mainGitDir = join(gitdir, '..', '..');
|
|
49
|
-
const mainRepoDir = dirname(mainGitDir);
|
|
50
|
-
const mainDb = join(mainRepoDir, 'pib.db');
|
|
51
|
-
if (existsSync(mainDb)) return mainDb;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
} catch { /* fall through to default */ }
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return localDb;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const DB_PATH = resolveDbPath();
|
|
21
|
+
const DB_PATH = resolvePibDbPath();
|
|
61
22
|
|
|
62
23
|
// ---------------------------------------------------------------------------
|
|
63
24
|
// SQLite setup
|
|
@@ -24,15 +24,18 @@
|
|
|
24
24
|
// node scripts/pib-db.mjs triage-history # Suppression list JSON
|
|
25
25
|
//
|
|
26
26
|
// Environment:
|
|
27
|
-
// PIB_DB_PATH — path to SQLite file
|
|
27
|
+
// PIB_DB_PATH — path to SQLite file. Overrides everything. Otherwise the
|
|
28
|
+
// path resolver (pib-db-path.mjs) picks main's pib.db when in
|
|
29
|
+
// a linked worktree, else cwd/pib.db.
|
|
28
30
|
|
|
29
31
|
import { join, dirname } from 'node:path';
|
|
30
32
|
import { fileURLToPath } from 'node:url';
|
|
31
33
|
import { createRequire } from 'node:module';
|
|
32
34
|
import * as lib from './pib-db-lib.mjs';
|
|
35
|
+
import { resolvePibDbPath } from './pib-db-path.mjs';
|
|
33
36
|
|
|
34
37
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
35
|
-
const DB_PATH =
|
|
38
|
+
const DB_PATH = resolvePibDbPath();
|
|
36
39
|
|
|
37
40
|
// ---------------------------------------------------------------------------
|
|
38
41
|
// SQLite setup — try better-sqlite3
|
|
@@ -68,10 +68,16 @@ fi
|
|
|
68
68
|
#
|
|
69
69
|
# Tag lookup is best-effort: if pib.db can't be read, GRP_LABEL is empty and
|
|
70
70
|
# this gate is skipped — the base breadcrumb gate above still applies.
|
|
71
|
-
#
|
|
72
|
-
#
|
|
71
|
+
# Worktree resolution mirrors scripts/pib-db-path.mjs (the JS resolver) and the
|
|
72
|
+
# VERIFY_DIR logic above: an explicit PIB_DB_PATH wins; otherwise, in a linked
|
|
73
|
+
# worktree whose main checkout HAS a pib.db, ALWAYS use main's db — never the
|
|
74
|
+
# worktree's own copy (work-tracking is project-level; one main db is the
|
|
75
|
+
# source of truth). Earlier this only redirected when the local pib.db was
|
|
76
|
+
# absent, which silently read a stale/empty worktree-local db whenever one
|
|
77
|
+
# existed (the symlink mux creates usually hid it) — diverging from the JS
|
|
78
|
+
# resolver that dropped exactly that "local db wins" heuristic. Keep in step.
|
|
73
79
|
DB_PATH="${PIB_DB_PATH:-pib.db}"
|
|
74
|
-
if [
|
|
80
|
+
if [ -z "$PIB_DB_PATH" ] && [ -n "$MAIN_CHECKOUT" ] && [ -f "$MAIN_CHECKOUT/pib.db" ]; then
|
|
75
81
|
DB_PATH="$MAIN_CHECKOUT/pib.db"
|
|
76
82
|
fi
|
|
77
83
|
TAGS=$(python3 -c "
|
|
@@ -5,9 +5,14 @@
|
|
|
5
5
|
# Triggers: PostToolUse on Write/Edit when the target path is under
|
|
6
6
|
# ~/.claude/projects/*/memory/*.md (Claude Code's auto-memory dir).
|
|
7
7
|
#
|
|
8
|
-
# Behavior: runs scripts/validate-memory.mjs
|
|
9
|
-
#
|
|
10
|
-
#
|
|
8
|
+
# Behavior: runs scripts/validate-memory.mjs in --check-file mode against
|
|
9
|
+
# the file just written. If THAT file is not reachable from MEMORY.md
|
|
10
|
+
# (no index line and no region pointer covers it — i.e. it was written
|
|
11
|
+
# straight to disk and isn't in the working set), emits a system reminder
|
|
12
|
+
# pointing Claude at /cc-remember. Keys on the written file's reachability,
|
|
13
|
+
# NOT on the whole-dir validation — so an unrelated pre-existing violation
|
|
14
|
+
# (e.g. a stale region pointer) never makes the guard cry wolf on every
|
|
15
|
+
# write, and the relaxed enumeration rule doesn't silently retire it.
|
|
11
16
|
#
|
|
12
17
|
# This is the post-omega-winddown replacement for omega-memory-guard.sh.
|
|
13
18
|
# Where the omega guard BLOCKED markdown writes (redirecting them to
|
|
@@ -45,8 +50,9 @@ if [ ! -f "$VALIDATOR" ]; then
|
|
|
45
50
|
exit 0
|
|
46
51
|
fi
|
|
47
52
|
|
|
48
|
-
#
|
|
49
|
-
|
|
53
|
+
# Check only THIS file's reachability. Exits non-zero with a one-line
|
|
54
|
+
# message when the just-written file isn't reachable from MEMORY.md.
|
|
55
|
+
OUTPUT=$(node "$VALIDATOR" --memory-dir "$MEMORY_DIR" --check-file "$FILE_PATH" --quiet 2>&1)
|
|
50
56
|
STATUS=$?
|
|
51
57
|
|
|
52
58
|
if [ $STATUS -eq 0 ]; then
|
|
@@ -56,15 +62,15 @@ fi
|
|
|
56
62
|
# Print the failure to stdout so it surfaces back to Claude as
|
|
57
63
|
# additional context. Frame as a suggestion, not a block.
|
|
58
64
|
cat <<REMINDER
|
|
59
|
-
[memory-index-guard] $(basename "$FILE_PATH") was written to a memory directory
|
|
60
|
-
but
|
|
65
|
+
[memory-index-guard] $(basename "$FILE_PATH") was written to a memory directory
|
|
66
|
+
but is not reachable from MEMORY.md:
|
|
61
67
|
|
|
62
68
|
$OUTPUT
|
|
63
69
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
automatically
|
|
67
|
-
"## Curated entries (hand-authored)"
|
|
70
|
+
The file is on disk but nothing makes it findable next session — it has no
|
|
71
|
+
working-set index line and no region pointer covers it. Use /cc-remember
|
|
72
|
+
(it indexes into the working set automatically), or add a covering region
|
|
73
|
+
pointer in MEMORY.md, or add a "## Curated entries (hand-authored)" line.
|
|
68
74
|
REMINDER
|
|
69
75
|
|
|
70
76
|
exit 0
|