fdeops 3.26.0 → 3.27.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.
@@ -0,0 +1,42 @@
1
+ 'use strict'
2
+
3
+ // Source syntax records an inspectable attribution, never authenticates it.
4
+ // The entry's automatic log date is deliberately not a source.
5
+ const PLACEHOLDER = /^(?:none|unknown|tbd|pending|n\/?a|source|evidence|not provided|missing|\?+|-+)$/i
6
+ function sourceReference(value) {
7
+ const text = String(value || '')
8
+ const explicit = text.match(/\[source:\s*([^\]\n]+)\]/i)
9
+ if (explicit) {
10
+ const source = explicit[1].trim()
11
+ return source && !PLACEHOLDER.test(source) ? source : ''
12
+ }
13
+ const url = text.match(/https?:\/\/[^\s<>\])|]+/i)
14
+ if (url) return url[0]
15
+ const id = text.match(/\b(?:PR\s*#\s*\d+|commit\s+[a-f0-9]{7,40}|(?:transcript|meeting|email|run)\s*(?:id\s*)?:\s*[\w][\w./#-]*)/i)
16
+ if (id && !PLACEHOLDER.test(id[0].split(':').pop().trim())) return id[0]
17
+ const dated = text.match(/\b(?:meeting|call|email|transcript)\b[^\n|]{0,60}\b\d{4}-\d{2}-\d{2}\b/i)
18
+ if (dated) return dated[0]
19
+ const file = text.match(/(?:^|[\s(])((?:[\w.-]+\/)*[\w.-]+\.(?:md|txt|csv|json|pdf|png|html|log))(?:$|[\s)#])/i)
20
+ return file ? file[1] : ''
21
+ }
22
+ module.exports = { sourceReference, hasSource: value => Boolean(sourceReference(value)) }
23
+
24
+ // Both CLI bullets and the documented long-form decision format are records.
25
+ // Keep the source with its decision body; a heading alone is not the evidence.
26
+ function datedDecisions(value) {
27
+ const lines = String(value || '').split('\n')
28
+ const entries = []
29
+ for (let i = 0; i < lines.length; i++) {
30
+ const bullet = lines[i].match(/^\s*[-*]\s*\[(\d{4}-\d{2}-\d{2})\]\s*(.+)/)
31
+ const heading = lines[i].match(/^(#{2,3})\s+\[?(\d{4}-\d{2}-\d{2})(?:\]|\s+-)?\s+(.+)/)
32
+ if (bullet) entries.push({ date: bullet[1], text: lines[i].trim(), line: i + 1 })
33
+ else if (heading) {
34
+ let end = i + 1
35
+ while (end < lines.length && !/^#{1,3}\s/.test(lines[end]) && !/^\s*[-*]\s*\[\d{4}-\d{2}-\d{2}\]/.test(lines[end])) end++
36
+ entries.push({ date: heading[2], text: lines.slice(i, end).join('\n').trim(), line: i + 1 })
37
+ i = end - 1
38
+ }
39
+ }
40
+ return entries.sort((a, b) => a.date.localeCompare(b.date) || a.line - b.line)
41
+ }
42
+ module.exports.datedDecisions = datedDecisions
package/bin/lib/render.js CHANGED
@@ -1,5 +1,7 @@
1
1
  "use strict"
2
2
 
3
+ const { deliverySummary } = require('./delivery-gaps')
4
+
3
5
  // Pure, local HTML renderer for the FDE fieldbook. Data extraction stays in fde.js.
4
6
 
5
7
  function escapeHtml(s) {
@@ -199,6 +201,7 @@ strong{font-weight:600}
199
201
  .fb-queue-row:nth-child(even),.fb-flag-row:nth-child(even){background:var(--panel)}
200
202
  .fb-queue-slug{font-family:'Geist Mono',monospace;font-size:12px;color:var(--ink-faint);flex:0 0 130px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
201
203
  .fb-queue-action{font-size:14px;line-height:1.45;color:var(--ink);flex:1 1 auto}
204
+ .fb-queue-reason{display:block;font-size:12px;color:var(--ink-faint);margin-top:5px;line-height:1.5}
202
205
  .fb-queue-touched{font-family:'Geist Mono',monospace;font-size:10.5px;color:var(--ink-faint);flex:0 0 auto}
203
206
  .fb-flag-slug{font-family:'Geist Mono',monospace;font-size:12px;color:var(--ink-faint);flex:0 0 130px}
204
207
  .fb-flag-text{font-family:'Geist Mono',monospace;font-size:12.5px}
@@ -308,13 +311,12 @@ function railItemHtml(e) {
308
311
 
309
312
  function queueRowHtml(e) {
310
313
  const dotClass = dotClassFor(e.signals.trust)
311
- const noAction = !e.hasNext
312
- const actionText = noAction ? 'Set the next action with your agent' : e.next
314
+ const action = e.firstAction
313
315
  const touchedClass = e.quiet ? 't-amber' : 't-faint'
314
316
  return `<button type="button" class="fb-row fb-queue-row" data-client="eng-${e.slug}" data-nav="eng-${e.slug}">
315
317
  <span class="dot dot-md ${dotClass}"></span>
316
318
  <span class="fb-queue-slug">${inlineMd(e.name)}</span>
317
- <span class="fb-queue-action${noAction ? ' t-amber' : ''}">${inlineMd(actionText)}</span>
319
+ <span class="fb-queue-action">${inlineMd(action.text)}<span class="fb-queue-reason${action.tone ? ' t-' + action.tone : ''}">${escapeHtml(action.reason)} &middot; ${escapeHtml(action.source)}</span></span>
318
320
  <span class="fb-queue-touched ${touchedClass}">${escapeHtml(e.signals.updated)}</span>
319
321
  </button>`
320
322
  }
@@ -340,7 +342,8 @@ function todayViewHtml({ today, total, attentionCount, highRiskTotal, todayQueue
340
342
  <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>
341
343
  <p id="fb-results-empty" class="fb-empty-search" hidden>No clients match these filters. <button type="button" class="fb-btn" data-reset-filters>Clear filters</button></p>
342
344
  <div class="fb-block" data-filter-section>
343
- <div class="fb-sec">Next actions</div>
345
+ <div class="fb-sec">Recommended first actions</div>
346
+ <p class="fb-evidence-note">One starting point per client, based on the saved record. Confirm the gap before acting; these suggestions do not change recorded next actions.</p>
344
347
  ${todayQueue}
345
348
  </div>
346
349
  ${flagsHtml ? `<div class="fb-block" data-filter-section>
@@ -350,26 +353,6 @@ ${flagsHtml}
350
353
  </div>`
351
354
  }
352
355
 
353
- function attentionFor(e) {
354
- const items = []
355
- const add = (text, tone = 'amber') => items.push({ text, tone })
356
- if (e.signals.memoryWarn) add('Record needs repair: ' + e.signals.memoryWarn, 'red')
357
- if (e.highRisks) add(`${e.highRisks} high risk${e.highRisks === 1 ? '' : 's'} open`, 'red')
358
- 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')
359
- if (e.signals.trust === 'new') add('No dated trust signal yet - ask someone')
360
- if (!e.hasNext) add('Set the next action')
361
- const rows = e.valueRows || []
362
- const missingEvidence = rows.filter(r => r.evidenceMissing).length
363
- if (missingEvidence) add(`${missingEvidence} outcome${missingEvidence === 1 ? '' : 's'} missing evidence`)
364
- const claimed = rows.filter(r => r.state === 'claimed').length
365
- const unmeasured = rows.filter(r => r.state === 'unmeasured').length
366
- if (claimed) add(`${claimed} measured outcome${claimed === 1 ? '' : 's'} awaiting acceptance`)
367
- if (unmeasured) add(`${unmeasured} outcome${unmeasured === 1 ? '' : 's'} not yet measured`)
368
- if (e.signals.stale) add('Reconfirm the dated trust signal')
369
- if (e.quiet) add('Record last updated ' + e.signals.updated)
370
- return items
371
- }
372
-
373
356
  function agentActionsHtml(e) {
374
357
  const prefix = `@fde Confirm the workspace is bound to ${JSON.stringify(e.name)} before using its records. `
375
358
  const actions = [
@@ -385,7 +368,7 @@ function agentActionsHtml(e) {
385
368
 
386
369
  function deliveryHtml(e) {
387
370
  const rows = e.valueRows || []
388
- const labels = { unmeasured: 'Not yet measured', claimed: 'Awaiting acceptance', accepted: 'Acceptance recorded' }
371
+ const labels = { unmeasured: 'Not yet measured', claimed: 'CLAIM · Awaiting acceptance', accepted: 'Acceptance recorded' }
389
372
  const pending = value => !value || /^(?:pending|tbd|unknown|n\/a|none|awaiting)(?:\b|$)/i.test(value)
390
373
  const cell = (value, fallback) => pending(value) ? `<span class="t-faint">${escapeHtml(value || fallback)}</span>` : inlineMd(value)
391
374
  return `<section class="fb-block fb-delivery" aria-label="Delivery evidence">
@@ -452,7 +435,7 @@ ${e.realityMissing ? `<p class="fb-why fb-why-missing"><span class="fb-accent-la
452
435
  const vitalsBlock = `<div class="fb-vitals">
453
436
  ${vitalsRows.map(([label, val, tone]) => `<div class="fb-vital-row"><span class="fb-vital-label">${escapeHtml(label)}</span><span class="fb-vital-val${tone ? ' t-' + tone : ''}">${escapeHtml(val)}</span></div>`).join('\n')}
454
437
  ${teamStatus ? `<div class="fb-vital-row"><span class="fb-vital-label">team status</span><span class="fb-vital-val">${teamStatus}</span></div>` : ''}
455
- ${e.stats.length ? `<div class="fb-vital-div"></div>${e.stats.map(s => `<div class="fb-vital-row"><span class="fb-vital-label">${inlineMd(s.label)}</span><span class="fb-vital-val">${inlineMd(s.from)} <span class="fb-arrow">&rarr;</span> <span class="fb-stat-to">${inlineMd(s.to)}</span></span></div>`).join('\n')}` : ''}
438
+ ${e.stats.length ? `<div class="fb-vital-div"></div><p class="fb-footnote">Metric excerpts are CLAIMS; verify source and acceptance.</p>${e.stats.map(s => `<div class="fb-vital-row"><span class="fb-vital-label">${inlineMd(s.label)}</span><span class="fb-vital-val">${inlineMd(s.from)} <span class="fb-arrow">&rarr;</span> <span class="fb-stat-to">${inlineMd(s.to)}</span></span></div>`).join('\n')}` : ''}
456
439
  </div>`
457
440
 
458
441
  const peopleBlock = e.stakeholders.length ? `<div class="fb-block">
@@ -534,10 +517,13 @@ function paletteItemsHtml(ordered) {
534
517
  }
535
518
 
536
519
  function buildFieldbookHtml({ engagements, today, generatedAt = '' }) {
537
- engagements = engagements.map(e => ({ ...e, attention: attentionFor(e) }))
538
- // rail + Today queue share one order: trust-first (red, amber, green)
520
+ engagements = engagements.map(e => {
521
+ const { gaps, firstAction } = deliverySummary(e)
522
+ return { ...e, attention: gaps, firstAction }
523
+ })
524
+ // Rail and Today queue share an order: urgent record gaps first, then trust.
539
525
  const tierRank = { RED: 0, amber: 1, green: 2, new: 3 }
540
- 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])
526
+ const ordered = engagements.slice().sort((a, b) => Number(b.firstAction.tone === 'red') - Number(a.firstAction.tone === 'red') || Number(b.attention.length > 0) - Number(a.attention.length > 0) || tierRank[a.signals.trust] - tierRank[b.signals.trust])
541
527
  const attentionCount = engagements.filter(e => e.attention.length > 0).length
542
528
  const highRiskTotal = engagements.reduce((n, e) => n + e.highRisks, 0)
543
529
 
@@ -557,7 +543,7 @@ function buildFieldbookHtml({ engagements, today, generatedAt = '' }) {
557
543
 
558
544
  return `<!doctype html><html lang="en"><head><meta charset="utf-8">
559
545
  <meta name="viewport" content="width=device-width,initial-scale=1">
560
- <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>
546
+ <script>document.documentElement.setAttribute("data-fde-theme","dark");try{if(localStorage.getItem("fde-fieldbook-theme")==="light")document.documentElement.setAttribute("data-fde-theme","light");}catch(e){}</script>
561
547
  <title>FDE Fieldbook</title><style>${dashStyles()}</style></head><body>
562
548
  <a class="fb-skip" href="#fb-main">Skip to content</a>
563
549
  <div class="fb-app">
@@ -1,4 +1,5 @@
1
1
  'use strict'
2
+ const { hasSource } = require('./provenance')
2
3
 
3
4
  // Empty measurements and unsigned outcomes are different facts. Keep their
4
5
  // interpretation shared by status, reports, and exported vaults.
@@ -19,9 +20,14 @@ function acceptanceName(value) {
19
20
  return name
20
21
  }
21
22
 
23
+ function evidenceSource(evidence) {
24
+ const text = String(evidence || '').trim()
25
+ return hasSource(text) && !PENDING_CELL_RE.test(text) && !/\b(?:no evidence|never (?:run|tested|measured)|not (?:run|tested|measured|verified)|unverified|invalid|retracted)\b/i.test(text)
26
+ }
27
+
22
28
  function valueState({ measured, accepted, acceptanceStatus, evidence }) {
23
29
  if (!measured || PENDING_CELL_RE.test(measured)) return 'unmeasured'
24
- if (!acceptanceName(accepted)) return 'claimed'
30
+ if (!acceptanceName(accepted) || !evidenceSource(evidence)) return 'claimed'
25
31
  // Explicit status is authoritative when the column exists. Unknown values
26
32
  // fail closed. Existing name-only ledgers remain readable during migration.
27
33
  if (acceptanceStatus !== undefined) {
@@ -31,4 +37,4 @@ function valueState({ measured, accepted, acceptanceStatus, evidence }) {
31
37
  return 'accepted'
32
38
  }
33
39
 
34
- module.exports = { PENDING_CELL_RE, acceptanceName, valueState }
40
+ module.exports = { PENDING_CELL_RE, acceptanceName, evidenceSource, valueState }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fdeops-ingest-mcp",
3
- "version": "3.26.0",
3
+ "version": "3.27.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.26.0",
3
+ "version": "3.27.0",
4
4
  "description": "Client delivery tools for Forward Deployed Engineers. One @fde skill, local Markdown engagement records, and an offline dashboard for decisions, evidence, approvals, and next actions.",
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.26.0",
4
+ "version": "3.27.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,13 +28,13 @@ 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` → 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`.
31
+ After a meeting: `fde debrief --smart` → one REVIEW screen (decisions / asks / scope / delivery gaps / 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
 
35
35
  On someone else's site the work is not "write code, remember later." Every change on a bound client stays on `@fde`:
36
36
 
37
- 1. **Name it** in `decisions.md` (plan) or kill it in a day (poc).
37
+ 1. **Name it** in `decisions.md` (plan), or timebox the riskiest assumption and record what the POC proves.
38
38
  2. **Characterise their code** before you change it. Brownfield: their tests, their runner. Greenfield: the empty tree, first path they can click.
39
39
  3. **Prove it on their staging.** Staging they operate, a screen the signer in `success.md` can reject.
40
40
  4. **If a model judges:** `evals.md` Verdict SHIP before that change is done (eval-pack).
@@ -58,7 +58,7 @@ Fallbacks: `node ~/.claude/fdeops/fde.js …`, then `npx --yes fdeops …`. Skil
58
58
 
59
59
  ## Entry (every session)
60
60
 
61
- 1. `fde resume` (16 KiB output ceiling, not a model token count). Read the goals, risks and current context. If truncated or a decision needs evidence, run `fde recall <specific topic>`; narrow the query rather than loading the whole history. `--max-bytes 4096` reduces the allowance for smaller models. `--full` only when the complete log is explicitly needed.
61
+ 1. `fde resume` (16 KiB output ceiling, not a model token count). Read client constraints first, then signer, goals, risks, delivery ledger and current context. This command is the inspectable packet the session hook loads; never substitute a recursive read of `.fde/` or raw transcripts. If truncated or a decision needs evidence, run `fde recall <specific topic>`; narrow the query rather than loading the whole history. `--max-bytes 4096` reduces the allowance for smaller models. `--full` only when the complete log is explicitly needed.
62
62
  2. **NO ENGAGEMENT:** ask "What should we call this client?" then **you** init. Pasted notes → debrief after bind.
63
63
  3. Playback 2-3 lines. `hygiene:` → offer `fde doctor`; **never auto-rewrite**.
64
64
  4. Route. Read **one** `references/*.md`. Confirm, then write.
@@ -72,7 +72,8 @@ Writes need a bind (`FDEOPS_ENGAGEMENT` or registry). Never install fdeops on in
72
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` |
73
73
  | prep me for … | `fde prep "<label>"` |
74
74
  | when did we agree | `fde receipts <term>` |
75
- | sponsor update / the outcome | `fde status` |
75
+ | sponsor update / defend the number | `fde defend` |
76
+ | successor / rotation / portable handoff | `fde handoff` (stdout; `--out new-file.md` only after export requested) |
76
77
  | they went quiet | `fde log contact "…" --signal amber\|green\|red` |
77
78
  | fieldbook page | `fde dashboard` (`--all` portfolio, `--open` to open the file) |
78
79
  | clean up the fieldbook | `fde doctor` - never auto-rewrite |
@@ -85,7 +86,7 @@ Writes need a bind (`FDEOPS_ENGAGEMENT` or registry). Never install fdeops on in
85
86
 
86
87
  1. **On entry:** `fde resume` only. Pull other `.fde/` files when the skill needs them.
87
88
  2. **Deliverable = memory.** The work *is* the `.fde/` file. The reference names which one.
88
- 3. **Evidence.** Every claim has a source. Traceable beats plausible.
89
+ 3. **Evidence.** Without a supplied source, a decision or measurement remains CLAIM. Use `[source: meeting YYYY-MM-DD]`, a PR/URL, transcript ID, or artifact path. The automatic log date is not attribution. ON RECORD means a source was supplied, not that it was authenticated or the customer approved. Never invent a source, signer, or acceptance.
89
90
  4. **No invented facts.** People, quotes, meetings, numbers: they said it or the repo shows it. Else `unknown - ask: <question>`.
90
91
  5. **Session digest** (end of session and before a PR) - thinking, not the chat. Confirm, then write. Never a transcript dump.
91
92
 
@@ -4,6 +4,8 @@
4
4
 
5
5
  **Read first:** `reality.md`, `success.md`, `terrain.md`, `stakeholders.md`. Load `business-case.md` if poc produced one. Not the full folder.
6
6
 
7
+ **Before committing a plan or building:** run `fde doctor --ready`. Missing binary success or a named customer-side signer blocks progression: review the proposed acceptance check and authority with the FDE first. Use a test/input and observable pass/fail under **Done when:** or **Acceptance check:**. A number, role, or successful demo alone is insufficient. Do not invent missing facts to pass lint.
8
+
7
9
  ## Validation gate (confirm understanding, clarify where it elevates)
8
10
 
9
11
  Before planning, state what you're working from in 2-3 lines:
@@ -14,10 +14,10 @@ A green check on synthetic data is not a validated solution. The person who can
14
14
 
15
15
  **1. Pick by score when several use cases compete.** Use the scoring model from `discover.md` - (Value × Data readiness) / Complexity. If discover or score-use-cases already produced a ranking, reuse it; never invent a third ranking.
16
16
 
17
- **2. Build the minimum that tests the assumption.** No error handling, no polish. Same-day demo if possible. Rough is honest. The POC is done when the person who can say no has seen it and reacted, not when the code looks finished.
17
+ **2. Build the minimum that tests the assumption.** Timebox the experiment with the FDE; aim for a same-day result when access and evidence permit it. Skip cosmetic polish, but keep the input validation, access controls, and failure handling needed to protect the test environment and data. Label shortcuts and simulated inputs. The POC is done when the person who can say no has seen the evidence and reacted, not when the code looks finished.
18
18
 
19
19
  **3. AI directions - test these before anything else:**
20
- - Data: available, clean, sufficient volume? Synthetic-data prototypes say nothing about production behaviour.
20
+ - Data: available, clean, sufficient volume? Synthetic data can test mechanics, but does not establish production quality or real-world coverage.
21
21
  - Environment: are external model calls even allowed here?
22
22
  - Latency: acceptable against real user expectations, not ideal conditions?
23
23
  - Is AI the right tool at all - or is this a data-quality or process problem wearing an AI costume?
@@ -26,6 +26,14 @@ A green check on synthetic data is not a validated solution. The person who can
26
26
 
27
27
  **5. Translate to business language** once validated: problem solved, cost of inaction, success in numbers, 2-3 trade-offs. Three sentences max for the stakeholder - can't say it in three, don't understand it yet.
28
28
 
29
+ ## If proceeding to production
30
+
31
+ Carry the hypothesis, test evidence, customer reaction, and remaining unknowns into the existing `plan` and `ship` workflow. A working demo does not establish production readiness or customer acceptance.
32
+
33
+ Inspect the prototype before deciding what to reuse. Keep components whose behavior and boundaries are suitable and tested. Replace or harden shortcuts that fail production requirements; rewrite only where the evidence justifies it. Record the decision and remaining work in `decisions.md`, rather than treating all prototype code as disposable or all working code as ready to deploy.
34
+
35
+ Production work includes the actual data path, permissions, failure recovery, realistic load, observability, ownership, and required AI evaluations. Use the existing ship gates for those checks.
36
+
29
37
  ## Artifact
30
38
 
31
39
  **`prototype-log.md`** - what was built, shown, the actual reaction, what was learned (including kills - a killed prototype that saved three weeks is a win worth recording).
@@ -38,7 +46,7 @@ Tell the FDE: did the riskiest assumption hold · what the customer's reaction a
38
46
 
39
47
  ## Principles
40
48
 
41
- - Speed of learning beats code quality. Never more than a day.
49
+ - Optimize for a bounded learning outcome. Agree a timebox and revisit scope if access or evidence blocks it; never skip necessary safeguards to meet an arbitrary duration.
42
50
  - Write pass/fail before you build. A demo with no kill line is a show.
43
51
  - Show it rough. Polish misleads.
44
52
  - Prototype the killer assumption, not the demo.
@@ -12,7 +12,9 @@ Do not ask them to pick a mode. Name where you are, then start at the matching s
12
12
 
13
13
  If going live, opening question: **has anyone actually *run* the rollback, or is it still a slide?** If only planned, that's today's work - say so plainly.
14
14
 
15
- A same-day throwaway that kills an assumption is `poc`. This skill is the real change on a repo they will own, then production.
15
+ A bounded experiment that tests an assumption is `poc`. This skill turns a validated direction into a maintainable change on a repo they will own, then production. Inspect existing prototype code and retain suitable tested parts; replace unsafe shortcuts based on evidence. A successful demo alone does not satisfy the readiness gates below.
16
+
17
+ **Before committing a plan or building:** run `fde doctor --ready`. Missing binary success or a named customer-side signer blocks progression: review the proposed acceptance check and authority with the FDE first. Use a test/input and observable pass/fail under **Done when:** or **Acceptance check:**. A number, role, or successful demo alone is insufficient. Do not invent missing facts to pass lint.
16
18
 
17
19
  ## Field (name it once, then the same loop)
18
20