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/README.md +133 -174
- package/bin/check.js +5 -0
- package/bin/fde.js +146 -14
- package/bin/lib/fieldbook-client.js +166 -0
- package/bin/lib/render.js +135 -174
- package/bin/lib/trust.js +27 -4
- package/mcp/fdeops-ingest/package.json +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/fde/SKILL.md +9 -5
- package/skills/fde/references/dashboard.md +5 -5
- package/skills/fde/references/debrief.md +12 -5
- package/skills/fde/references/discover.md +5 -1
- package/skills/fde/references/earn-trust.md +1 -1
- package/skills/fde/references/ingest.md +1 -1
- package/skills/fde/references/land.md +6 -6
- package/skills/fde/references/pick-three.md +1 -1
- package/skills/fde/references/plan.md +8 -0
- package/skills/fde/references/poc.md +1 -1
- package/skills/fde/references/readout.md +4 -2
- package/skills/fde/references/review.md +1 -1
- package/skills/fde/references/score-use-cases.md +1 -1
- package/skills/fde/references/ship.md +4 -2
- package/skills/fde/references/switch-clients.md +2 -2
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,'&').replace(/</g,'<').replace(/>/g,'>');}",
|
|
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.
|
|
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)} · <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 ? '
|
|
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 `<
|
|
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
|
-
</
|
|
327
|
+
</button>`
|
|
421
328
|
}
|
|
422
329
|
|
|
423
330
|
function flagRowHtml(e, text, colorClass) {
|
|
424
|
-
return `<
|
|
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
|
-
</
|
|
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">
|
|
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 <client-name></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">
|
|
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)} · ${total} engagement${total === 1 ? '' : 's'} · ${attentionCount} need you · ${highRiskTotal} high risk${highRiskTotal === 1 ? '' : 's'} open</div>
|
|
442
349
|
<div class="fb-block">
|
|
443
|
-
<div class="fb-sec">
|
|
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">
|
|
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(' · ')
|
|
461
410
|
|
|
462
411
|
const nowBlock = `<div class="fb-block">
|
|
463
|
-
<div class="fb-sec">
|
|
464
|
-
<div class="fb-now-action${e.hasNext ? '' : ' t-amber'}">${e.hasNext ? inlineMd(e.next) : 'next action
|
|
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">
|
|
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">
|
|
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">
|
|
486
|
+
<div class="fb-sec">Decisions & 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 => `<
|
|
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
|
-
</
|
|
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.
|
|
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
|
-
|
|
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">
|
|
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-
|
|
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)">⌘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">↑↓ / j k</span> client</span>
|
|
643
599
|
<span><span class="t-soft">/</span> search</span>
|
|
644
600
|
<span><span class="t-soft">⌘K</span> palette</span>
|
|
645
|
-
<span class="fb-hints-spacer">
|
|
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
|
-
<
|
|
651
|
-
<div class="fb-
|
|
652
|
-
<input id="fb-palette-input" class="fb-palette-input" placeholder="
|
|
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
|
-
|
|
655
|
-
</
|
|
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_-]*)
|
|
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_-]*)
|
|
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) &&
|
|
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,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fdeops",
|
|
3
|
-
"version": "3.
|
|
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.
|
|
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",
|
package/skills/fde/SKILL.md
CHANGED
|
@@ -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` →
|
|
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` →
|
|
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
|
|
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 **
|
|
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
|