fdeops 3.22.2 → 3.24.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/bin/lib/render.js CHANGED
@@ -243,6 +243,49 @@ strong{font-weight:600}
243
243
  .fb-palette-hint{margin-left:auto;font-family:'Geist Mono',monospace;font-size:11px;color:var(--ink-faint)}
244
244
  @media(max-width:760px){.fb-rail{flex:0 0 220px}.fb-main-inner{padding:20px}}
245
245
  @media(max-width:640px){.fb-body{flex-direction:column}.fb-rail{flex:0 0 auto;max-height:40vh;border-right:none;border-bottom:1px solid var(--line)}.fb-main-inner{padding:16px}}
246
+ /* Daily work: evidence is the focal point; chrome stays quiet. */
247
+ :root{--ink-faint:#68665f;--amber:#86530d}
248
+ html[data-fde-theme="dark"]{--ink-faint:#a39f94}
249
+ button:focus-visible,input:focus-visible,textarea:focus-visible,summary:focus-visible,a:focus-visible,[tabindex="0"]:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
250
+ .fb-h1{font-family:'Geist',system-ui,sans-serif;font-size:32px;letter-spacing:-.035em}
251
+ .fb-h1:focus{outline:none}
252
+ .fb-sec{font-family:inherit;font-size:14px;font-weight:600;letter-spacing:0;text-transform:none;color:var(--ink)}
253
+ .fb-main{min-width:0}.fb-main-inner{max-width:1280px}.fb-top-grid>*{min-width:0}
254
+ .fb-meta-line{line-height:1.6}.fb-eyebrow{font-family:inherit;font-size:12px}
255
+ .fb-snapshot{font-size:11px;color:var(--ink-faint);line-height:1.6;margin-bottom:24px}
256
+ .fb-actions{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-top:20px}
257
+ .fb-action{padding:9px 13px;border:1px solid var(--line);border-radius:6px;background:var(--panel);color:var(--ink);font-size:12px;font-weight:500;cursor:pointer}
258
+ .fb-action:first-child{background:var(--accent);color:#fff;border-color:var(--accent)}
259
+ html[data-fde-theme="dark"] .fb-action:first-child{color:#102219}
260
+ .fb-action:hover{border-color:var(--accent)}.fb-actions>span{font-size:11px;color:var(--ink-faint);margin-left:4px}
261
+ .fb-queue-row,.fb-flag-row{border:0;background:transparent;width:100%;text-align:left;color:var(--ink)}
262
+ .fb-queue-row{padding:14px 10px;align-items:center}.fb-queue-slug{font-family:inherit;font-weight:600;color:var(--ink)}
263
+ .fb-flag-row{padding:10px}.fb-flag-text{font-family:inherit;font-size:13px}
264
+ .fb-value-table{min-width:700px;table-layout:fixed;margin:0}.fb-value-table th,.fb-value-table td{padding:14px 12px;overflow-wrap:anywhere;line-height:1.5}
265
+ .fb-value-table thead th{font-family:inherit;text-transform:none;letter-spacing:0;font-size:12px;font-weight:500}
266
+ .fb-value-table tbody th{font-family:inherit;text-transform:none;letter-spacing:0;font-size:13px;color:var(--ink)}
267
+ .fb-value-state{display:block;margin-top:5px;font-size:11px;font-weight:400}
268
+ .fb-table-scroll{overflow-x:auto;border:1px solid var(--line);border-radius:8px;margin-top:12px}
269
+ .fb-evidence-note{font-size:12px;line-height:1.5;color:var(--ink-faint);margin:8px 0}
270
+ .fb-empty-evidence{padding:18px;border:1px dashed var(--line);border-radius:8px;margin-top:12px;font-size:14px}
271
+ .fb-empty-evidence p{color:var(--ink-soft);max-width:65ch;line-height:1.6;margin:6px 0 0}
272
+ .fb-copy-status{font-size:13px;color:var(--accent);line-height:1.5}.fb-copy-status:empty{display:none}
273
+ .fb-dialog{padding:0;width:520px;max-width:calc(100vw - 32px);max-height:80vh;border:1px solid var(--line);border-radius:12px;background:var(--bg);color:var(--ink);box-shadow:var(--shadow)}
274
+ .fb-dialog::backdrop{background:rgba(0,0,0,.45)}
275
+ .fb-dialog-head{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;font-size:14px;font-weight:600;gap:12px;border-bottom:1px solid var(--line)}
276
+ .fb-dialog-help{padding:0 16px;font-size:13px;line-height:1.6;color:var(--ink-soft)}
277
+ #fb-prompt-text{display:block;width:calc(100% - 32px);margin:16px;padding:12px;border:1px solid var(--line);border-radius:6px;background:var(--panel);color:var(--ink);font:13px/1.6 inherit;resize:vertical}
278
+ .fb-palette-item{width:100%;border:0;background:transparent;text-align:left;color:var(--ink)}
279
+ .fb-empty-search{font-size:13px;line-height:1.6;color:var(--ink-soft);padding:12px}.fb-empty-search[hidden]{display:none}
280
+ .sr-only,.fb-skip:not(:focus){position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}
281
+ .fb-skip:focus{position:fixed;top:8px;left:8px;z-index:100;background:var(--bg);color:var(--ink);padding:10px}
282
+ .fb-more-body{overflow-x:auto}.fb-title-row{flex-wrap:wrap}.fb-person{flex-wrap:wrap}.fb-person-note{min-width:140px}
283
+ @media(max-width:1100px){.fb-main-inner{padding:24px}.fb-rail{flex-basis:240px}.fb-grid-wrap{grid-template-columns:1fr}.fb-need{display:none}}
284
+ @media(max-width:640px){.fb-header{padding:12px;gap:8px;flex-wrap:wrap}.fb-today{display:none}.fb-brand{margin-right:auto}.fb-tagline{font-family:inherit}.fb-btn{min-height:34px}.fb-rail{max-height:190px;flex-basis:auto}.fb-main-inner{padding:20px 16px}.fb-h1{font-size:28px}.fb-queue-row{flex-wrap:wrap;gap:8px}.fb-queue-slug{flex-basis:calc(100% - 24px)}.fb-queue-action{flex-basis:100%;padding-left:16px}.fb-queue-touched{padding-left:16px}.fb-flag-row{flex-direction:column;gap:4px}.fb-flag-slug{flex-basis:auto}.fb-grid-wrap{grid-template-columns:minmax(0,1fr)}.fb-log{flex-wrap:wrap}.fb-log-text{flex-basis:100%}.fb-hints-spacer{margin-left:0}.fb-snapshot{margin-bottom:18px}.fb-body{min-height:0}.fb-actions>span{width:100%;margin-top:4px}.fb-app{height:100dvh}}
285
+ @media(max-width:390px){.fb-value-table{min-width:0}.fb-person-name,.fb-person-role{flex:1 1 100%}.fb-rail{max-height:150px}.fb-main-inner{padding:16px 12px}}
286
+ @media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;scroll-behavior:auto!important}}
287
+ @media print{@page{margin:12mm}html,body,.fb-app{height:auto;overflow:visible;background:#fff;color:#000}.fb-header,.fb-rail,.fb-actions,.fb-hints,.fb-copy-status,.fb-dialog,.fb-skip{display:none!important}.fb-body,.fb-main{display:block;overflow:visible}.fb-main-inner{padding:0;max-width:none}.fb-top-grid{grid-template-columns:1fr}.fb-table-scroll{overflow:visible}.fb-value-table{min-width:0;table-layout:fixed;font-size:10px}.fb-value-table th,.fb-value-table td{padding:6px}.fb-view[hidden]{display:none!important}.fb-block{break-inside:avoid}.fb-vitals{background:#fff}*{color:#000!important;box-shadow:none!important}}
288
+
246
289
  `
247
290
  }
248
291
 
@@ -253,143 +296,7 @@ strong{font-weight:600}
253
296
  // command palette are wired the same way the theme toggle already was here -
254
297
  // addEventListener, guarded null checks, no inline handlers.
255
298
  function dashScript() {
256
- return [
257
- "(function(){",
258
- "'use strict';",
259
- "var SEL_KEY='fde-fieldbook-sel';",
260
- "function esc(s){return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}",
261
- "function all(sel){return Array.prototype.slice.call(document.querySelectorAll(sel));}",
262
- "",
263
- "var views=all('.fb-view');",
264
- "var navItems=all('.fb-nav');",
265
- "var searchInput=document.getElementById('fb-search');",
266
- "",
267
- "function selectView(id){",
268
- " if(!document.getElementById('view-'+id))return;",
269
- " views.forEach(function(v){v.hidden=(v.id!=='view-'+id);});",
270
- " navItems.forEach(function(n){n.classList.toggle('active',n.getAttribute('data-target')===id);});",
271
- " try{localStorage.setItem(SEL_KEY,id);}catch(e){}",
272
- "}",
273
- "navItems.forEach(function(n){n.addEventListener('click',function(){selectView(n.getAttribute('data-target'));});});",
274
- "all('[data-nav]').forEach(function(n){n.addEventListener('click',function(){selectView(n.getAttribute('data-nav'));});});",
275
- "",
276
- "var startSel='today';",
277
- "try{var s=localStorage.getItem(SEL_KEY);if(s&&document.getElementById('view-'+s))startSel=s;}catch(e){}",
278
- "selectView(startSel);",
279
- "",
280
- "// search: filters the rail, shows a match snippet in place of the default reason line",
281
- "// NOTE: snippetFor() below returns a string built ONLY from esc()-escaped",
282
- "// fragments plus hardcoded '<mark>' literals - never raw untrusted markup -",
283
- "// so assigning it via innerHTML cannot inject a tag from engagement content.",
284
- "function snippetFor(raw,term){",
285
- " var i=raw.indexOf(term);",
286
- " if(i<0)return '';",
287
- " var start=Math.max(0,i-24),end=Math.min(raw.length,i+term.length+40);",
288
- " var pre=esc(raw.slice(start,i)),hit=esc(raw.slice(i,i+term.length)),post=esc(raw.slice(i+term.length,end));",
289
- " return (start>0?'\\u2026':'')+pre+'<mark>'+hit+'</mark>'+post+(end<raw.length?'\\u2026':'');",
290
- "}",
291
- "if(searchInput){searchInput.addEventListener('input',function(){",
292
- " var term=searchInput.value.trim().toLowerCase();",
293
- " navItems.forEach(function(n){",
294
- " if(n.getAttribute('data-target')==='today')return;",
295
- " var raw=(n.getAttribute('data-search')||'').toLowerCase();",
296
- " var hit=!term||raw.indexOf(term)>-1;",
297
- " n.classList.toggle('hide',!hit);",
298
- " var reason=n.querySelector('.fb-nav-reason');",
299
- " if(!reason)return;",
300
- " if(term&&hit){var snip=snippetFor(raw,term);reason.innerHTML=snip;reason.classList.toggle('hide',!snip);}",
301
- " else{reason.innerHTML=n.getAttribute('data-default-reason')||'';reason.classList.toggle('hide',!reason.innerHTML);}",
302
- " });",
303
- "});}",
304
- "",
305
- "// keyboard: j/k/arrows move rail selection, / focuses search, Esc blurs/closes",
306
- "function visibleIds(){return navItems.filter(function(n){return !n.classList.contains('hide');}).map(function(n){return n.getAttribute('data-target');});}",
307
- "function currentId(){var v=document.querySelector('.fb-view:not([hidden])');return v?v.id.replace('view-',''):'today';}",
308
- "document.addEventListener('keydown',function(ev){",
309
- " if((ev.metaKey||ev.ctrlKey)&&(ev.key==='k'||ev.key==='K')){ev.preventDefault();openPalette();return;}",
310
- " if(paletteIsOpen()){if(ev.key==='Escape'){ev.preventDefault();closePalette();}return;}",
311
- " var t=ev.target,typing=t&&(t.tagName==='INPUT'||t.tagName==='TEXTAREA'||t.isContentEditable);",
312
- " if(typing){if(ev.key==='Escape')t.blur();return;}",
313
- " if(ev.key==='/'){ev.preventDefault();if(searchInput)searchInput.focus();return;}",
314
- " var down=ev.key==='ArrowDown'||ev.key==='j',up=ev.key==='ArrowUp'||ev.key==='k';",
315
- " if(down||up){",
316
- " ev.preventDefault();",
317
- " var ids=visibleIds();",
318
- " var idx=ids.indexOf(currentId());",
319
- " var next=ids[Math.min(ids.length-1,Math.max(0,idx+(down?1:-1)))];",
320
- " if(next)selectView(next);",
321
- " }",
322
- "});",
323
- "",
324
- "// command palette: search-filterable nav (today + each client + theme toggle)",
325
- "var paletteBackdrop=document.getElementById('fb-palette-backdrop');",
326
- "var paletteInput=document.getElementById('fb-palette-input');",
327
- "var paletteInner=document.querySelector('.fb-palette');",
328
- "var paletteItems=all('.fb-palette-item');",
329
- "var pActive=0;",
330
- "function paletteIsOpen(){return !!(paletteBackdrop&&paletteBackdrop.classList.contains('open'));}",
331
- "function visiblePaletteItems(){return paletteItems.filter(function(it){return !it.classList.contains('hide');});}",
332
- "function highlightPalette(){",
333
- " var visible=visiblePaletteItems();",
334
- " paletteItems.forEach(function(it){it.classList.remove('active');});",
335
- " if(visible[pActive]){visible[pActive].classList.add('active');visible[pActive].scrollIntoView({block:'nearest'});}",
336
- "}",
337
- "function filterPalette(q){",
338
- " q=q.toLowerCase();",
339
- " paletteItems.forEach(function(it){",
340
- " var label=(it.getAttribute('data-label')||'').toLowerCase();",
341
- " var kind=(it.getAttribute('data-kind')||'').toLowerCase();",
342
- " it.classList.toggle('hide',!(!q||label.indexOf(q)>-1||kind.indexOf(q)>-1));",
343
- " });",
344
- " pActive=0;highlightPalette();",
345
- "}",
346
- "function openPalette(){",
347
- " if(!paletteBackdrop)return;",
348
- " paletteBackdrop.classList.add('open');",
349
- " if(paletteInput){paletteInput.value='';paletteInput.focus();}",
350
- " filterPalette('');",
351
- "}",
352
- "function closePalette(){if(paletteBackdrop)paletteBackdrop.classList.remove('open');}",
353
- "function runActive(){",
354
- " var it=visiblePaletteItems()[pActive];",
355
- " if(!it)return;",
356
- " var target=it.getAttribute('data-target');",
357
- " if(target==='__theme__')toggleTheme();",
358
- " else if(target)selectView(target);",
359
- " closePalette();",
360
- "}",
361
- "if(paletteBackdrop)paletteBackdrop.addEventListener('click',function(ev){if(ev.target===paletteBackdrop)closePalette();});",
362
- "if(paletteInner)paletteInner.addEventListener('click',function(ev){ev.stopPropagation();});",
363
- "var paletteBtn=document.getElementById('fb-palette-btn');",
364
- "if(paletteBtn)paletteBtn.addEventListener('click',openPalette);",
365
- "paletteItems.forEach(function(it){",
366
- " it.addEventListener('click',function(){pActive=visiblePaletteItems().indexOf(it);runActive();});",
367
- " it.addEventListener('mouseenter',function(){var v=visiblePaletteItems();var i=v.indexOf(it);if(i>-1){pActive=i;highlightPalette();}});",
368
- "});",
369
- "if(paletteInput){",
370
- " paletteInput.addEventListener('input',function(){filterPalette(paletteInput.value);});",
371
- " paletteInput.addEventListener('keydown',function(ev){",
372
- " if(ev.key==='Escape'){closePalette();return;}",
373
- " if(ev.key==='ArrowDown'){ev.preventDefault();pActive=Math.min(visiblePaletteItems().length-1,pActive+1);highlightPalette();return;}",
374
- " if(ev.key==='ArrowUp'){ev.preventDefault();pActive=Math.max(0,pActive-1);highlightPalette();return;}",
375
- " if(ev.key==='Enter'){ev.preventDefault();runActive();}",
376
- " });",
377
- "}",
378
- "",
379
- "// theme toggle - local only, no network; pre-paint script in <head> avoids a flash",
380
- "var themeBtn=document.getElementById('fde-theme-btn');",
381
- "function currentTheme(){return document.documentElement.getAttribute('data-fde-theme')==='dark'?'dark':'light';}",
382
- "function setThemeLabel(){if(themeBtn)themeBtn.textContent=(currentTheme()==='dark'?'light':'dark');}",
383
- "function toggleTheme(){",
384
- " var next=currentTheme()==='dark'?'light':'dark';",
385
- " try{localStorage.setItem('fde-fieldbook-theme',next);}catch(e){}",
386
- " document.documentElement.setAttribute('data-fde-theme',next);",
387
- " setThemeLabel();",
388
- "}",
389
- "setThemeLabel();",
390
- "if(themeBtn)themeBtn.addEventListener('click',toggleTheme);",
391
- "})();",
392
- ].join('\n')
299
+ return '(' + require('./fieldbook-client').toString() + ')()'
393
300
  }
394
301
 
395
302
  // ---- HTML builders (one per fieldbook widget) - kept small and named so the
@@ -399,7 +306,7 @@ function railItemHtml(e) {
399
306
  const dotClass = dotClassFor(e.signals.trust)
400
307
  const touchedClass = e.quiet ? 't-amber' : 't-faint'
401
308
  const preview = e.next.length > 58 ? e.next.slice(0, 56) + '…' : e.next
402
- const defaultReason = e.signals.trust !== 'green' && preview ? escapeHtml(preview) : ''
309
+ const defaultReason = escapeHtml((e.attention || [])[0]?.text || preview)
403
310
  return `<button class="fb-nav" data-target="eng-${e.slug}" data-search="${e.searchBlob}" data-default-reason="${defaultReason}">
404
311
  <div class="fb-nav-row"><span class="dot dot-sm ${dotClass}"></span><span class="fb-nav-name">${inlineMd(e.name)}</span></div>
405
312
  <div class="fb-nav-meta">${escapeHtml(e.phaseLabel)} &middot; <span class="${touchedClass}">${escapeHtml(e.signals.updated)}</span></div>
@@ -410,46 +317,88 @@ function railItemHtml(e) {
410
317
  function queueRowHtml(e) {
411
318
  const dotClass = dotClassFor(e.signals.trust)
412
319
  const noAction = !e.hasNext
413
- const actionText = noAction ? 'no next action set - open and add one' : e.next
320
+ const actionText = noAction ? 'Set the next action with your agent' : e.next
414
321
  const touchedClass = e.quiet ? 't-amber' : 't-faint'
415
- return `<div class="fb-row fb-queue-row" data-nav="eng-${e.slug}">
322
+ return `<button type="button" class="fb-row fb-queue-row" data-nav="eng-${e.slug}">
416
323
  <span class="dot dot-md ${dotClass}"></span>
417
324
  <span class="fb-queue-slug">${inlineMd(e.name)}</span>
418
325
  <span class="fb-queue-action${noAction ? ' t-amber' : ''}">${inlineMd(actionText)}</span>
419
326
  <span class="fb-queue-touched ${touchedClass}">${escapeHtml(e.signals.updated)}</span>
420
- </div>`
327
+ </button>`
421
328
  }
422
329
 
423
330
  function flagRowHtml(e, text, colorClass) {
424
- return `<div class="fb-row fb-flag-row" data-nav="eng-${e.slug}">
331
+ return `<button type="button" class="fb-row fb-flag-row" data-nav="eng-${e.slug}">
425
332
  <span class="fb-flag-slug">${inlineMd(e.name)}</span>
426
333
  <span class="fb-flag-text t-${colorClass}">${escapeHtml(text)}</span>
427
- </div>`
334
+ </button>`
428
335
  }
429
336
 
430
337
  function todayViewHtml({ today, total, attentionCount, highRiskTotal, todayQueue, flagsHtml, hasEngagements }) {
431
338
  if (!hasEngagements) {
432
339
  return `<div id="view-today" class="fb-view">
433
- <div class="fb-eyebrow">portfolio</div>
434
- <h1 class="fb-h1">Today</h1>
340
+ <div class="fb-eyebrow">Your engagements</div>
341
+ <h1 tabindex="-1" class="fb-h1">Today</h1>
435
342
  <p class="empty">No engagements yet. Start one with <code>fde resume --init &lt;client-name&gt;</code>, then re-run <code>fde dashboard</code>.</p>
436
343
  </div>`
437
344
  }
438
345
  return `<div id="view-today" class="fb-view">
439
- <div class="fb-eyebrow">portfolio</div>
440
- <h1 class="fb-h1">Today</h1>
346
+ <div class="fb-eyebrow">Your engagements</div>
347
+ <h1 tabindex="-1" class="fb-h1">Today</h1>
441
348
  <div class="fb-meta-line">${escapeHtml(today)} &middot; ${total} engagement${total === 1 ? '' : 's'} &middot; ${attentionCount} need you &middot; ${highRiskTotal} high risk${highRiskTotal === 1 ? '' : 's'} open</div>
442
349
  <div class="fb-block">
443
- <div class="fb-sec">Queue - next action per client, worst first</div>
350
+ <div class="fb-sec">Next actions</div>
444
351
  ${todayQueue}
445
352
  </div>
446
353
  ${flagsHtml ? `<div class="fb-block">
447
- <div class="fb-sec">Flags</div>
354
+ <div class="fb-sec">Needs attention</div>
448
355
  ${flagsHtml}
449
356
  </div>` : ''}
450
357
  </div>`
451
358
  }
452
359
 
360
+ function attentionFor(e) {
361
+ const items = []
362
+ const add = (text, tone = 'amber') => items.push({ text, tone })
363
+ if (e.signals.memoryWarn) add('Record needs repair: ' + e.signals.memoryWarn, 'red')
364
+ if (e.highRisks) add(`${e.highRisks} high risk${e.highRisks === 1 ? '' : 's'} open`, 'red')
365
+ if (e.signals.trust === 'RED' || e.signals.trust === 'amber') add('Check in with the customer: trust is ' + trustWord(e.signals.trust), e.signals.trust === 'RED' ? 'red' : 'amber')
366
+ if (e.signals.trust === 'new') add('No dated trust signal yet - ask someone')
367
+ if (!e.hasNext) add('Set the next action')
368
+ const rows = e.valueRows || []
369
+ const missingEvidence = rows.filter(r => r.evidenceMissing).length
370
+ if (missingEvidence) add(`${missingEvidence} outcome${missingEvidence === 1 ? '' : 's'} missing evidence`)
371
+ const claimed = rows.filter(r => r.state === 'claimed').length
372
+ const unmeasured = rows.filter(r => r.state === 'unmeasured').length
373
+ if (claimed) add(`${claimed} measured outcome${claimed === 1 ? '' : 's'} awaiting acceptance`)
374
+ if (unmeasured) add(`${unmeasured} outcome${unmeasured === 1 ? '' : 's'} not yet measured`)
375
+ if (e.signals.stale) add('Reconfirm the dated trust signal')
376
+ if (e.quiet) add('Record last updated ' + e.signals.updated)
377
+ return items
378
+ }
379
+
380
+ function agentActionsHtml(e) {
381
+ const prefix = `@fde Confirm the workspace is bound to ${JSON.stringify(e.name)} before using its records. `
382
+ const actions = [
383
+ ['Prepare meeting', 'Prepare me for the next customer meeting. Use the recorded decisions, open risks, delivery evidence and next action. Cite sources and flag unknowns.'],
384
+ ['Debrief notes', 'Help me debrief a customer meeting. Ask me for the notes, then show proposed changes and conflicts for review before saving.'],
385
+ ['Review outcome', 'Review what we promised, measured and recorded as accepted. Check the evidence and customer approver; do not infer acceptance. Draft a concise customer readout.'],
386
+ ]
387
+ return `<div class="fb-actions" aria-label="Continue in your coding agent">${actions.map(([label, prompt]) => `<button type="button" class="fb-action" data-prompt="${escapeHtml(prefix + prompt)}">${label}</button>`).join('')}<span>Copy a prompt into your agent</span></div>`
388
+ }
389
+
390
+ function deliveryHtml(e) {
391
+ const rows = e.valueRows || []
392
+ const labels = { unmeasured: 'Not yet measured', claimed: 'Awaiting acceptance', accepted: 'Acceptance recorded' }
393
+ const pending = value => !value || /^(?:pending|tbd|unknown|n\/a|none|awaiting)(?:\b|$)/i.test(value)
394
+ const cell = (value, fallback) => pending(value) ? `<span class="t-faint">${escapeHtml(value || fallback)}</span>` : inlineMd(value)
395
+ return `<section class="fb-block fb-delivery" aria-label="Delivery evidence">
396
+ <div class="fb-sec-row"><h2 class="fb-sec">Delivery evidence</h2><span class="fb-count">${rows.length} outcome${rows.length === 1 ? '' : 's'}</span></div>
397
+ <p class="fb-evidence-note">Recorded in delivery.md. Acceptance reflects the saved record, not independent verification.</p>
398
+ ${rows.length ? `<div class="fb-table-scroll" role="region" aria-label="Promised, measured and accepted outcomes" tabindex="0"><table class="fb-table fb-value-table"><thead><tr><th scope="col">Outcome</th><th scope="col">Promised</th><th scope="col">Measured</th><th scope="col">Acceptance</th><th scope="col">Evidence</th></tr></thead><tbody>${rows.map(r => `<tr><th scope="row">${inlineMd(r.slice || 'Outcome')}<span class="fb-value-state t-${r.state === 'accepted' ? 'green' : 'amber'}">${labels[r.state]}</span></th><td>${cell(r.promised, 'Not recorded')}</td><td>${cell(r.measured, 'Not yet measured')}</td><td>${cell(r.accepted, 'Not recorded')}</td><td>${cell(r.evidence, 'Not recorded')}</td></tr>`).join('')}</tbody></table></div>` : `<div class="fb-empty-evidence"><strong>No delivery evidence yet</strong><p>Ask your agent to define one useful increment: the expected result, how to measure it, and who will accept it.</p></div>`}
399
+ </section>`
400
+ }
401
+
453
402
  function clientViewHtml(e) {
454
403
  const dotClass = dotClassFor(e.signals.trust)
455
404
  const sectorLine = [
@@ -460,8 +409,8 @@ function clientViewHtml(e) {
460
409
  ].filter(Boolean).join(' &middot; ')
461
410
 
462
411
  const nowBlock = `<div class="fb-block">
463
- <div class="fb-sec">Now</div>
464
- <div class="fb-now-action${e.hasNext ? '' : ' t-amber'}">${e.hasNext ? inlineMd(e.next) : 'next action not set'}</div>
412
+ <div class="fb-sec">Next action</div>
413
+ <div class="fb-now-action${e.hasNext ? '' : ' t-amber'}">${e.hasNext ? inlineMd(e.next) : 'Set the next action'}</div>
465
414
  ${e.lastSession ? `<div class="fb-now-session">${inlineMd(e.lastSession)}</div>` : ''}
466
415
  </div>`
467
416
 
@@ -470,7 +419,7 @@ ${e.lastSession ? `<div class="fb-now-session">${inlineMd(e.lastSession)}</div>`
470
419
  // contrast disappears. Each gets its own line, its own room to finish a
471
420
  // thought, not a race to fit before an ellipsis.
472
421
  const whyBlock = (e.brief || e.reality || e.realityMissing) ? `<div class="fb-block">
473
- <div class="fb-sec">Why</div>
422
+ <div class="fb-sec">The problem to solve</div>
474
423
  ${e.brief ? `<p class="fb-why"><span class="t-faint">What they asked for:</span> ${inlineMd(e.brief)}</p>` : ''}
475
424
  ${e.realityMissing ? `<p class="fb-why fb-why-missing"><span class="fb-accent-label">What's actually true:</span> ${escapeHtml(e.realityMissing)}</p>` : e.reality ? `<p class="fb-why fb-why-reality"><span class="fb-accent-label">What's actually true:</span> ${inlineMd(e.reality)}</p>` : ''}
476
425
  </div>` : ''
@@ -524,7 +473,7 @@ ${e.stakeholders.map(p => `<div class="fb-row fb-person">
524
473
  </div>` : ''
525
474
 
526
475
  const riskBlock = e.risks.length ? `<div class="fb-block">
527
- <div class="fb-sec-row"><div class="fb-sec">Risk</div><span class="fb-count">${e.risks.length} open</span></div>
476
+ <div class="fb-sec-row"><div class="fb-sec">Open risks</div><span class="fb-count">${e.risks.length} open</span></div>
528
477
  <div class="fb-list">
529
478
  ${e.risks.map(r => `<div class="fb-row fb-risk">
530
479
  <span class="fb-risk-sev t-${r.severity === 'high' ? 'red' : 'amber'}">${escapeHtml(r.severity)}</span>
@@ -534,7 +483,7 @@ ${e.risks.map(r => `<div class="fb-row fb-risk">
534
483
  </div>` : ''
535
484
 
536
485
  const logBlock = e.log.length ? `<div class="fb-block">
537
- <div class="fb-sec">Log</div>
486
+ <div class="fb-sec">Decisions &amp; activity</div>
538
487
  <div class="fb-list">
539
488
  ${e.log.map(g => `<div class="fb-row fb-log">
540
489
  <span class="fb-log-date">${escapeHtml(formatLogDate(g.date))}</span>
@@ -561,15 +510,16 @@ ${e.moreSections.map(s => `<details class="fb-more"><summary class="fb-sec fb-mo
561
510
  return `<div id="view-eng-${e.slug}" class="fb-view" hidden>
562
511
  <div class="fb-eyebrow">.fde/${escapeHtml(e.slug)}</div>
563
512
  <div class="fb-title-row">
564
- <h1 class="fb-h1">${inlineMd(e.name)}</h1>
513
+ <h1 tabindex="-1" class="fb-h1">${inlineMd(e.name)}</h1>
565
514
  <span class="fb-trust"><span class="dot dot-lg ${dotClass}"></span><span class="t-${dotClass}">${trustWord(e.signals.trust)}</span></span>
566
515
  </div>
567
516
  <div class="fb-meta-line">${sectorLine}</div>
517
+ ${agentActionsHtml(e)}
568
518
  <div class="fb-top-grid">
569
519
  <div>${nowBlock}${whyBlock}</div>
570
520
  ${vitalsBlock}
571
521
  </div>
572
- ${pairedBlock}${logBlock}${moreBlock}
522
+ ${deliveryHtml(e)}${pairedBlock}${logBlock}${moreBlock}
573
523
  </div>`
574
524
  }
575
525
 
@@ -579,26 +529,26 @@ function paletteItemsHtml(ordered) {
579
529
  ...ordered.map(e => ({ kind: 'open', label: e.slug, hint: trustWord(e.signals.trust), target: 'eng-' + e.slug })),
580
530
  { kind: 'action', label: 'toggle theme', hint: 'light / dark', target: '__theme__' },
581
531
  ]
582
- return items.map(it => `<div class="fb-palette-item" data-target="${escapeHtml(it.target)}" data-label="${escapeHtml(it.label)}" data-kind="${escapeHtml(it.kind)}">
532
+ return items.map(it => `<button type="button" class="fb-palette-item" data-target="${escapeHtml(it.target)}" data-label="${escapeHtml(it.label)}" data-kind="${escapeHtml(it.kind)}">
583
533
  <span class="fb-palette-kind">${escapeHtml(it.kind)}</span>
584
534
  <span class="fb-palette-label">${escapeHtml(it.label)}</span>
585
535
  <span class="fb-palette-hint">${escapeHtml(it.hint)}</span>
586
- </div>`).join('\n')
536
+ </button>`).join('\n')
587
537
  }
588
538
 
589
- function buildFieldbookHtml({ engagements, today }) {
539
+ function buildFieldbookHtml({ engagements, today, generatedAt = '' }) {
540
+ engagements = engagements.map(e => ({ ...e, attention: attentionFor(e) }))
590
541
  // rail + Today queue share one order: trust-first (red, amber, green)
591
542
  const tierRank = { RED: 0, amber: 1, green: 2, new: 3 }
592
- const ordered = engagements.slice().sort((a, b) => tierRank[a.signals.trust] - tierRank[b.signals.trust])
593
- const attentionCount = engagements.filter(e => e.signals.trust !== 'green' && e.signals.trust !== 'new').length
543
+ const ordered = engagements.slice().sort((a, b) => Number(b.attention.length > 0) - Number(a.attention.length > 0) || tierRank[a.signals.trust] - tierRank[b.signals.trust])
544
+ const attentionCount = engagements.filter(e => e.attention.length > 0).length
594
545
  const highRiskTotal = engagements.reduce((n, e) => n + e.highRisks, 0)
595
546
 
596
547
  const railItems = ordered.map(railItemHtml).join('\n')
597
548
  const todayQueue = ordered.map(queueRowHtml).join('\n') || '<p class="empty">No engagements yet.</p>'
598
549
  const flagRows = []
599
550
  ordered.forEach(e => {
600
- if (e.quiet) flagRows.push(flagRowHtml(e, `quiet - last touched ${e.signals.updated}`, 'amber'))
601
- if (e.highRisks) flagRows.push(flagRowHtml(e, `${e.highRisks} high risk${e.highRisks > 1 ? 's' : ''} open`, 'red'))
551
+ e.attention.forEach(item => flagRows.push(flagRowHtml(e, item.text, item.tone)))
602
552
  })
603
553
 
604
554
  const todayView = todayViewHtml({
@@ -612,47 +562,58 @@ function buildFieldbookHtml({ engagements, today }) {
612
562
  <meta name="viewport" content="width=device-width,initial-scale=1">
613
563
  <script>try{var t=localStorage.getItem("fde-fieldbook-theme");if(t==="dark"||(!t&&window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches))document.documentElement.setAttribute("data-fde-theme","dark");}catch(e){}</script>
614
564
  <title>FDE Fieldbook</title><style>${dashStyles()}</style></head><body>
565
+ <a class="fb-skip" href="#fb-main">Skip to content</a>
615
566
  <div class="fb-app">
616
567
  <header class="fb-header">
617
568
  <div class="fb-brand">
618
569
  <span class="fb-wordmark">FDEOPS</span>
619
- <span class="fb-tagline">fieldbook</span>
570
+ <span class="fb-tagline">Fieldbook</span>
620
571
  ${engagements.length ? `<span class="fb-need">${attentionCount}/${engagements.length} need you</span>` : ''}
621
572
  </div>
622
573
  <div class="fb-spacer"></div>
623
574
  <span class="fb-today">${escapeHtml(today)}</span>
624
- <button id="fb-palette-btn" class="fb-btn" type="button" title="Command palette (Ctrl/Cmd+K)">&#8984;K</button>
575
+ <button id="fb-print-btn" class="fb-btn" type="button">Print view</button>
576
+ <button id="fb-palette-btn" aria-label="Open command palette" class="fb-btn" type="button" title="Command palette (Ctrl/Cmd+K)">&#8984;K</button>
625
577
  <button id="fde-theme-btn" class="fb-btn" type="button">dark</button>
626
578
  </header>
627
579
  <div class="fb-body">
628
- <aside class="fb-rail">
629
- <div class="fb-rail-search"><input id="fb-search" class="fb-search" placeholder="search clients, notes, risks…" spellcheck="false"></div>
580
+ <aside class="fb-rail" aria-label="Engagement navigation">
581
+ <div class="fb-rail-search"><input id="fb-search" aria-label="Search engagements and records" class="fb-search" placeholder="search clients, notes, risks…" spellcheck="false"></div>
630
582
  <div class="fb-rail-list fb-scroll">
631
583
  <button class="fb-nav" data-target="today">
632
584
  <div class="fb-nav-row"><span class="fb-nav-today">today</span>${engagements.length ? `<span class="fb-nav-count">${attentionCount} need you</span>` : ''}</div>
633
585
  </button>
634
586
  ${railItems}
587
+ <p id="fb-search-empty" class="fb-empty-search" hidden>No matching engagements. Try a person, decision, or delivery.</p>
588
+ <span id="fb-search-count" class="sr-only" role="status"></span>
635
589
  </div>
636
590
  </aside>
637
- <main class="fb-main fb-scroll">
591
+ <main id="fb-main" tabindex="-1" class="fb-main fb-scroll">
638
592
  <div class="fb-main-inner">
593
+ <div class="fb-snapshot">Snapshot generated <time datetime="${escapeHtml(generatedAt)}">${escapeHtml(generatedAt ? generatedAt.replace('T', ' ').replace(/\.\d+Z$/, ' UTC') : today)}</time>. Re-run <code>fde dashboard</code> after updating your records.</div>
639
594
  ${todayView}
640
595
  ${clientViews}
596
+ <p id="fb-status" class="fb-copy-status" role="status" aria-live="polite"></p>
641
597
  <div class="fb-hints">
642
598
  <span><span class="t-soft">&uarr;&darr; / j k</span> client</span>
643
599
  <span><span class="t-soft">/</span> search</span>
644
600
  <span><span class="t-soft">&#8984;K</span> palette</span>
645
- <span class="fb-hints-spacer">deterministic render of .fde/ - edit the markdown, re-run fde dashboard</span>
601
+ <span class="fb-hints-spacer">Local records. No network. Changes are reviewed in your coding agent.</span>
646
602
  </div>
647
603
  </div>
648
604
  </main>
649
605
  </div>
650
- <div id="fb-palette-backdrop" class="fb-palette-backdrop">
651
- <div class="fb-palette">
652
- <input id="fb-palette-input" class="fb-palette-input" placeholder="jump to client or action…" spellcheck="false">
606
+ <dialog id="fb-palette-dialog" class="fb-dialog" role="dialog" aria-modal="true" aria-label="Command palette">
607
+ <div class="fb-dialog-head"><span>Jump to an engagement</span><button type="button" class="fb-btn" data-close-dialog>Close</button></div>
608
+ <input id="fb-palette-input" class="fb-palette-input" aria-label="Find an engagement or action" placeholder="Find an engagement or action" spellcheck="false">
653
609
  <div class="fb-palette-list fb-scroll">${paletteItems}</div>
654
- </div>
655
- </div>
610
+ <p id="fb-palette-empty" class="fb-empty-search" hidden>No matching commands.</p>
611
+ </dialog>
612
+ <dialog id="fb-prompt-dialog" class="fb-dialog" role="dialog" aria-modal="true" aria-labelledby="fb-prompt-title">
613
+ <div class="fb-dialog-head"><span id="fb-prompt-title">Copy into your coding agent</span><button type="button" class="fb-btn" data-close-dialog>Close</button></div>
614
+ <p class="fb-dialog-help">Automatic copy is unavailable. Copy the selected prompt, then paste it into your agent. Review any proposed changes there.</p>
615
+ <textarea id="fb-prompt-text" aria-label="Agent prompt" readonly rows="7"></textarea>
616
+ </dialog>
656
617
  </div>
657
618
  <script>${dashScript()}</script>
658
619
  </body></html>`
package/bin/lib/trust.js CHANGED
@@ -56,6 +56,22 @@ function createTrustApi(deps) {
56
56
  'blocker', 'outage', 'fire', 'issue', 'sev', 'sev1', 'sev2', 'p1', 'p2', 'p3',
57
57
  'resolved', 'risk', 'decision', 'delivery', 'contact',
58
58
  ])
59
+ // Articles and weekdays are not people. "the finance controller…" and
60
+ // "Friday's readout" must key on the role/name, not the filler word.
61
+ const SIGNAL_NAME_NOISE = new Set([
62
+ 'the', 'a', 'an', 'and', 'or', 'for', 'from', 'with', 'without', 'this', 'that',
63
+ 'these', 'those', 'their', 'our',
64
+ 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday',
65
+ 'today', 'tomorrow', 'yesterday', 'tonight',
66
+ ])
67
+
68
+ function isSignalNameNoise(word) {
69
+ const n = String(word || '').replace(/[^a-z0-9]/gi, '').toLowerCase()
70
+ if (SIGNAL_NAME_NOISE.has(n)) return true
71
+ // "Friday's" → fridays after stripping punctuation
72
+ if (n.endsWith('s') && SIGNAL_NAME_NOISE.has(n.slice(0, -1))) return true
73
+ return false
74
+ }
59
75
 
60
76
  function personFromSignalText(text) {
61
77
  let cleaned = String(text || '')
@@ -63,14 +79,19 @@ function createTrustApi(deps) {
63
79
  .replace(/\([^)]*\)/g, '')
64
80
  .replace(/\[signal:[^\]]+\]/gi, '')
65
81
  .replace(/\[\d{4}-\d{2}-\d{2}\]/g, '')
66
- .replace(/^([A-Z]{2,}[A-Z0-9_-]*):?\s+/, '')
82
+ .replace(/^([A-Z]{2,}[A-Z0-9_-]*):\s+/, '')
67
83
  .trim()
68
84
  const names = cleaned.match(/\b[A-Z][a-z]{1,20}(?:\s+[A-Z][a-z]{1,20})?\b/g) || []
69
85
  for (const name of names) {
70
86
  const first = name.split(/\s+/)[0].toLowerCase()
71
- if (SIGNAL_EVENT_KEYS.has(first)) continue
87
+ if (SIGNAL_EVENT_KEYS.has(first) || isSignalNameNoise(first) || isSignalNameNoise(name)) continue
72
88
  return name
73
89
  }
90
+ const acronyms = cleaned.match(/\b[A-Z]{3,6}\b/g) || []
91
+ for (const acronym of acronyms) {
92
+ if (SIGNAL_EVENT_KEYS.has(acronym.toLowerCase()) || isSignalNameNoise(acronym)) continue
93
+ return acronym
94
+ }
74
95
  return ''
75
96
  }
76
97
 
@@ -84,10 +105,11 @@ function createTrustApi(deps) {
84
105
  if (frag.length >= 3) return frag
85
106
  }
86
107
  const cleaned = String(text).replace(/\[@[^\]]+\]/g, '').replace(/\([^)]*\)/g, '')
87
- .replace(/^([A-Z]{2,}[A-Z0-9_-]*):?\s+/, '')
108
+ .replace(/^([A-Z]{2,}[A-Z0-9_-]*):\s+/, '')
88
109
  const words = cleaned.split(/\s+/).filter(w => {
89
110
  const n = w.replace(/[^a-z0-9]/gi, '').toLowerCase()
90
- return n.length >= 3 && !SIGNAL_EVENT_KEYS.has(n) && !/^(dr|mr|mrs|ms)$/i.test(w)
111
+ return n.length >= 3 && !SIGNAL_EVENT_KEYS.has(n) && !isSignalNameNoise(n)
112
+ && !/^(dr|mr|mrs|ms)$/i.test(w)
91
113
  })
92
114
  const frag = (words[0] || '').replace(/[^a-z0-9]/gi, '').toLowerCase()
93
115
  return frag.length >= 3 ? frag : ('anon:' + cleaned.slice(0, 48).toLowerCase())
@@ -243,6 +265,7 @@ function createTrustApi(deps) {
243
265
  stakeholdersMemoryHealth,
244
266
  personFromSignalText,
245
267
  signalSubjectKey,
268
+ isSignalNameNoise,
246
269
  parsePhase,
247
270
  countOpenRisks,
248
271
  nextActionLine,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fdeops-ingest-mcp",
3
- "version": "3.22.2",
3
+ "version": "3.24.0",
4
4
  "private": true,
5
5
  "description": "Thin stdio MCP sink for FDEOps ingest (stage → propose → apply). Zero runtime dependencies.",
6
6
  "bin": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fdeops",
3
- "version": "3.22.2",
3
+ "version": "3.24.0",
4
4
  "description": "Forward deployed engineering skills for AI coding agents. One @fde skill for the client work around the code: who can say yes, what went live, whether they signed off. Dated markdown on your laptop. You confirm each write.",
5
5
  "bin": {
6
6
  "fdeops": "bin/install.js",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "fdeops",
4
- "version": "3.22.2",
4
+ "version": "3.24.0",
5
5
  "description": "Forward deployed engineering skills for AI coding agents. One @fde skill for the client work around the code. You confirm; then it lands in .fde/ on your laptop.",
6
6
  "author": {
7
7
  "name": "Subash Natarajan",
@@ -28,7 +28,7 @@ A one-line typo or compile error in a file that will not ship. On a bound client
28
28
  | **When did we agree?** | Don't argue from memory. Search the record. | `fde receipts <term>` | - |
29
29
  | **What's the outcome?** | A number nobody signed is claimed, not delivered. | `fde status` | `references/readout.md` |
30
30
 
31
- After a meeting: `fde debrief --smart` → confirm → `--apply`. Walk-in: `fde prep`. Friday: `fde status`.
31
+ After a meeting: `fde debrief --smart` → one REVIEW screen (decided / asked / open / next / signer) → in chat, a four-row card (omit empty; Previously / Not yet agreed) → **Save this update?** (engineer accepted the record, not customer approval of every ask) → `--apply`. Walk-in: `fde prep`. Friday: `fde status`.
32
32
 
33
33
  ## Ground loop
34
34
 
@@ -44,6 +44,10 @@ A throwaway file can skip the loop. Bound client work cannot.
44
44
 
45
45
  **Skip is loud.** Bound + a change that will ship + no this-turn line in `delivery.md` = not done. Say that. Do not call it shipped. A coding pack may write the function; `@fde` still owns done.
46
46
 
47
+ ## Working with an engineering pack
48
+
49
+ Use the customer's existing coding, testing, review, and repository instructions for implementation. Carry the confirmed outcome, scope boundary, acceptance criteria, and evidence requirements into that workflow. Reference its existing plan from `decisions.md`; do not create a competing backlog or repeat questions already answered. FDEOps owns the engagement record and acceptance status. A coding pack's green tests do not establish customer acceptance. Never claim compatibility was tested with a host or pack you have not run.
50
+
47
51
  ## Human surface vs agent plumbing
48
52
 
49
53
  **FDE (human):** `@fde` + English, or `/brief` `/discover` `/plan` `/ship` `/outcome` `/close` `/debrief` `/prep` `/trust` `/receipts` `/readout`. Never a skill catalog.
@@ -65,12 +69,12 @@ Writes need a bind (`FDEOPS_ENGAGEMENT` or registry). Never install fdeops on in
65
69
  |----------|---------|
66
70
  | where are we | `fde resume` |
67
71
  | day-1 look at the repo | `fde scan` |
68
- | debrief / pasted notes | `fde debrief --smart` → you rewrite prefixes → confirm → `--apply`. `--smart` is a gate, not a brain. `references/debrief.md` |
72
+ | debrief / pasted notes | `fde debrief --smart` → REVIEW → four-row chat card → Save this update? → `--apply`. `--smart` is a gate, not a brain. `references/debrief.md` |
69
73
  | prep me for … | `fde prep "<label>"` |
70
74
  | when did we agree | `fde receipts <term>` |
71
75
  | sponsor update / the outcome | `fde status` |
72
76
  | they went quiet | `fde log contact "…" --signal amber\|green\|red` |
73
- | fieldbook page | `fde dashboard` |
77
+ | fieldbook page | `fde dashboard` (`--all` portfolio, `--open` to open the file) |
74
78
  | clean up the fieldbook | `fde doctor` - never auto-rewrite |
75
79
  | scrub a secret | `fde redact <term>` then `--apply` after confirm |
76
80
  | pull Granola/Slack/transcript | capability check → `fde ingest stage` → confirm → apply. Never auto-apply. `references/ingest.md` |
@@ -140,7 +144,7 @@ Work names (engage, diagnose, align, deliver, realize, transfer) are the same ma
140
144
 
141
145
  | You hear | Skill | Reference |
142
146
  |----------|-------|-----------|
143
- | Align, break this down, what order, sequence the build, plan the roadmap, create user stories, write the tasks | plan | `references/plan.md` |
147
+ | Align, break this down, what order, sequence the delivery, align the plan | plan | `references/plan.md` |
144
148
  | Sponsor needs justification, need to defend budget or timeline, build the business case | business-case | `references/business-case.md` |
145
149
  | Significant decision, multiple approaches, "what should we do?", generate solutions, generate options, not the playbook, from the surviving facts | three-options | `references/three-options.md` |
146
150
  | 20 things are "urgent," need to pick the 3 that matter, prioritize three | pick-three | `references/pick-three.md` |
@@ -154,7 +158,7 @@ Work names (engage, diagnose, align, deliver, realize, transfer) are the same ma
154
158
  | Deliver, start building, update their checkout, first module, visible progress, their tests, POC follow-through, ready to deploy, going live, pre-flight, deliver the increment, build the increment, create the launch plan, design their UI | ship | `references/ship.md` |
155
159
  | Review this change, review the pull request, is it safe, does it match what we agreed | review | `references/review.md` |
156
160
  | Diff grew / scope creep in the PR / "did we only build what we said" / KEEP JUSTIFY SPLIT DROP | review (+ ship if going live) | `references/review.md` Stage 1 · `references/ship.md` Intent vs diff |
157
- | Wrap the session / share the thinking / catch teammates up / before I open the PR | (memory contract - session digest) | SKILL.md **On exit** - write TL;DR + decisions/why into `.fde/`; no transcript sync |
161
+ | Wrap the session / share the thinking / catch teammates up / before I open the PR | (memory contract - session digest) | SKILL.md **Session digest** - write TL;DR + decisions/why into `.fde/`; no transcript sync |
158
162
  | "We can always revert" - need to actually test the escape route, rehearse rollback | rollback | `references/rollback.md` |
159
163
 
160
164
  ### Outcome