fdeops 3.27.0 → 3.28.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/trust.js CHANGED
@@ -3,7 +3,7 @@
3
3
  function createTrustApi(deps) {
4
4
  const {
5
5
  fs, path, readClean, readEng, parseMdTable, sectionBody, SIGNAL_LEDGER, memoryDirtyManual,
6
- stripTemplateNoise, stripLegendLines,
6
+ stripTemplateNoise, stripLegendLines, extractRisks,
7
7
  } = deps
8
8
 
9
9
  // phase / trust / top risk / freshness - identical heuristic for status + dashboard.
@@ -129,27 +129,7 @@ function createTrustApi(deps) {
129
129
  }
130
130
 
131
131
  function countOpenRisks(eng) {
132
- const md = readClean(eng, 'risks.md')
133
- const body = md.split(/^#{1,6}\s+Retired\b/im)[0] || md
134
- let n = 0
135
- for (const raw of body.split('\n')) {
136
- const t = raw.trim()
137
- if (!t || t.startsWith('<!--') || /^#{1,6}\s/.test(t)) continue
138
- if (/risk\s*\|\s*status|mitigation/i.test(t) || /^\|?[\s|:-]+$/.test(t)) continue
139
- // Bullet risk with substance (skip empty "- " stubs). The bullet marker
140
- // must be followed by space: "**Status:** open · closed" is a legend, and
141
- // counting it as a risk reported one open risk on an empty register.
142
- if (/^[-*]\s/.test(t)) {
143
- if (t.replace(/^[-*]\s+/, '').trim()) n++
144
- continue
145
- }
146
- // Table row: first cell must have risk text (day-1 "| | open | |" placeholders don't count).
147
- if (/^\|/.test(t) && t.length > 12) {
148
- const riskCell = t.split('|').map(c => c.trim())[1] || ''
149
- if (riskCell) n++
150
- }
151
- }
152
- return n
132
+ return extractRisks(eng).length
153
133
  }
154
134
 
155
135
  function nextActionLine(ctx) {
@@ -166,7 +146,7 @@ function createTrustApi(deps) {
166
146
  function computeSignals(eng) {
167
147
  // readClean, not readEng: status/dashboard echo topRisk and stakeholder lines
168
148
  // to the terminal and the rendered HTML - a <private> risk must never surface.
169
- const ctx = readClean(eng, 'context.md'); const stake = readClean(eng, 'stakeholders.md'); const risks = readClean(eng, 'risks.md')
149
+ const ctx = readClean(eng, 'context.md'); const stake = readClean(eng, 'stakeholders.md')
170
150
  // Prefer structured tokens from stakeholders + CLI ledger (ledger survives wipes)
171
151
  const signalText = stake + '\n' + readClean(eng, SIGNAL_LEDGER)
172
152
  const phase = parsePhase(ctx)
@@ -215,11 +195,7 @@ function createTrustApi(deps) {
215
195
  trust = sLines.some(l => /\bred\b/i.test(l)) ? 'RED'
216
196
  : sLines.some(l => /amber|gone quiet|routing around|escalat/i.test(l)) ? 'amber' : 'new'
217
197
  }
218
- const topRisk = (risks.split('\n').find(l => {
219
- const t = l.trim()
220
- return /^[-|]/.test(t) && t.length > 20 && !/^\|?[-\s|]+$/.test(t) &&
221
- !/risk\s*\|\s*status|mitigation/i.test(t) && !t.startsWith('<!--')
222
- }) || '').replace(/\|/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 80)
198
+ const topRisk = (extractRisks(eng)[0]?.text || '').replace(/\s+/g, ' ').trim().slice(0, 80)
223
199
  // Prefer trust trigger / memory warn over a random risk line; always keep mem.warn available
224
200
  const reason = (trustReason || mem.warn) ? (trustReason || mem.warn) : topRisk
225
201
  // What the triage line is actually quoting. A risk bullet printed under
@@ -26,6 +26,7 @@ function evidenceSource(evidence) {
26
26
  }
27
27
 
28
28
  function valueState({ measured, accepted, acceptanceStatus, evidence }) {
29
+ if (withdrawalMention({ measured, accepted, acceptanceStatus, evidence })) return 'claimed'
29
30
  if (!measured || PENDING_CELL_RE.test(measured)) return 'unmeasured'
30
31
  if (!acceptanceName(accepted) || !evidenceSource(evidence)) return 'claimed'
31
32
  // Explicit status is authoritative when the column exists. Unknown values
@@ -37,4 +38,48 @@ function valueState({ measured, accepted, acceptanceStatus, evidence }) {
37
38
  return 'accepted'
38
39
  }
39
40
 
40
- module.exports = { PENDING_CELL_RE, acceptanceName, evidenceSource, valueState }
41
+ // These checks flag explicit conflicts for human review. They do not authenticate
42
+ // approval, infer delegation, or revoke history when the current signer changes.
43
+ function withdrawalMention(row) {
44
+ return [row.measured, row.accepted, row.acceptanceStatus, row.evidence].filter(Boolean).some(value => {
45
+ const text = String(value)
46
+ .replace(/\[source:[^\]]*\]/gi, '')
47
+ .replace(/\S*[/\\]\S*/g, '') // artifact names are sources, not withdrawal events
48
+ .replace(/\b(?:not|never)\s+(?:been\s+)?(?:withdrawn|retracted|revoked|superseded)\b/gi, '')
49
+ // A withdrawn result is different from a result counting revoked tokens.
50
+ return /^(?:withdrawn|retracted|revoked|superseded)(?:\s*:|\s*$)/i.test(text.trim()) ||
51
+ /\b(?:approval|acceptance|evidence|measurement|result|assertion)\b[^.;\n]{0,40}\b(?:withdrawn|retracted|revoked|superseded)\b/i.test(text) ||
52
+ /\b(?:withdrawn|retracted|revoked|superseded|retracts?|withdraws?)\b[^.;\n]{0,25}\b(?:approval|acceptance|evidence|measurement|result|assertion)\b/i.test(text)
53
+ })
54
+ }
55
+
56
+ function scopeIssue(row, goal) {
57
+ const rowPromise = String(row.promised || '')
58
+ const goalLine = String(goal).match(/^(?:\*\*)?(?:Done when|Acceptance check):(?:\*\*)?\s*(.*)$/im)
59
+ const scopeSpecified = /\b(?:production|staging|synthetic|slides?|demo|prototype|poc)\b/i.test(rowPromise)
60
+ const promise = scopeSpecified ? rowPromise : `${rowPromise} ${goalLine ? goalLine[1] : goal}`
61
+ const acceptance = String(row.accepted || '')
62
+ const limited = acceptance.match(/\b(staging|slides?(?: design)?|demo|prototype|poc)\s+only\b/i)
63
+ if (limited) {
64
+ const scope = limited[1].toLowerCase()
65
+ const matchingGoal = scope.startsWith('slide') ? /\bslides?\b/i.test(promise) : new RegExp('\\b' + scope + '\\b', 'i').test(promise)
66
+ if (!matchingGoal || /\b(?:production|clinical use|go.live)\b/i.test(promise)) return 'approval scope is limited; review against the promised outcome'
67
+ }
68
+ const measured = String(row.measured || '')
69
+ const productionUntested = measured.split(/[;.\n]/).some(clause => /\bproduction\b/i.test(clause) && /\b(?:not|never|untested|unmeasured|pending)\b/i.test(clause))
70
+ if (/\bproduction\b/i.test(promise) && /\b(?:staging|synthetic|prototype|poc)\b/i.test(measured) && (!/\bproduction\b/i.test(measured) || productionUntested)) return 'measurement scope differs from production promise; review required'
71
+ return ''
72
+ }
73
+
74
+ function reconcileValueRows(rows, goal = '') {
75
+ const key = row => String(row.slice || '').trim().toLowerCase()
76
+ const withdrawn = new Set(rows.filter(withdrawalMention).map(key).filter(Boolean))
77
+ return rows.map(row => {
78
+ let acceptanceIssue = scopeIssue(row, goal)
79
+ if (withdrawalMention(row)) acceptanceIssue = 'withdrawal recorded; review current acceptance'
80
+ else if (withdrawn.has(key(row))) acceptanceIssue = 'conflicting withdrawal for this slice; review history before relying on acceptance'
81
+ return acceptanceIssue ? { ...row, acceptanceIssue, state: row.state === 'unmeasured' ? 'unmeasured' : 'claimed' } : row
82
+ })
83
+ }
84
+
85
+ module.exports = { PENDING_CELL_RE, acceptanceName, evidenceSource, valueState, reconcileValueRows }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fdeops-ingest-mcp",
3
- "version": "3.27.0",
3
+ "version": "3.28.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.27.0",
3
+ "version": "3.28.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.27.0",
4
+ "version": "3.28.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",
@@ -4,11 +4,11 @@
4
4
 
5
5
  **Read first:** `context.md` if it exists - otherwise start cold. The point of this phase is to establish ground truth, not assume it.
6
6
 
7
- ## Method - part 1: read everything that exists (you do this work)
7
+ ## Method - part 1: inspect the inherited record (you do this work)
8
8
 
9
9
  Before forming any opinion:
10
10
 
11
- 1. **Inherit the paper.** Any previous `.fde/`, docs, README claims, ADRs, ticket history the FDE can export. Read it all - the previous FDE's decisions are evidence, not verdicts.
11
+ 1. **Inherit the paper.** Start with `fde resume` and inventory the available docs, ADRs, ticket exports and operational handoff. Do not recursively load `.fde/` or raw transcripts. List the claims and unknowns, then use `fde recall <specific topic>` to retrieve bounded evidence for each consequential claim. Review the relevant source when an excerpt is insufficient; keep unrelated history on disk. Previous decisions are evidence, not verdicts.
12
12
  2. **Run the discover scans** (see `discover.md` part 1: churn, test gaps, "temporary" grep, AI components). On a takeover, add:
13
13
  ```bash
14
14
  git log --format="%an" | sort | uniq -c | sort -rn | head # who actually built this
@@ -55,7 +55,7 @@ Build without a plan in an inherited system is the fastest path to the second in
55
55
 
56
56
  ## Principles
57
57
 
58
- - Read everything that exists before forming any opinion.
58
+ - Inventory the record; verify consequential claims through targeted, bounded retrieval before forming an opinion.
59
59
  - "It should work" is not "it works." Verify.
60
60
  - The most dangerous systems are the ones everyone assumes someone else understands.
61
61
  - Don't build until `audit.md`, `terrain.md`, `reality.md` are written.
@@ -30,7 +30,7 @@
30
30
  - `signer: Priya` (she can say yes; lands in `success.md`)
31
31
  - `next: send one-pager before Thursday 9am`
32
32
  - unprefixed lines stay context color only
33
- 4. Show the **REVIEW** block first (decided / asked / open / next / signer). That is the one screen to confirm. File routing stays underneath.
33
+ 4. After editing, run `fde debrief --review` to show the pending proposal without replacing it. Show the **REVIEW** block first (decided / asked / open / next / signer). That is the one screen to confirm. File routing stays underneath.
34
34
  5. In **chat**, after that REVIEW, present a four-row card and omit empty rows:
35
35
  - Decided
36
36
  - Asked / open
@@ -74,3 +74,15 @@ Read back the 2-3 most consequential captures in one breath - so the FDE can cor
74
74
  - Verbatim quote outranks paraphrase; hesitation outranks quote.
75
75
  - Signals move on evidence, never on vibe alone.
76
76
  - A meeting with no decisions and no actions - say so; that is a finding.
77
+
78
+ ## Delivery rows and repeated updates
79
+
80
+ For a measured or promised slice, use a reviewed structured line:
81
+
82
+ ```text
83
+ delivery: Replay|risk-mitigation|zero duplicates|zero duplicates on staging|pending|[source: transcript:42]|pending
84
+ ```
85
+
86
+ The seven fields are Slice, Bucket, Promised, Measured, Accepted by, Evidence, Rollback. Keep unknowns `pending`; never infer approval. This lands in the value ledger during the same confirmed apply. A `delivery:` line without pipes stays a narrative note. Incorrect field counts refuse the write rather than shifting the meaning of cells.
87
+
88
+ A sourced statement already in the record triggers a replay warning. Before applying, compare newer facts and the current next action. Remove repeated statements from the proposal if this is an accidental re-import. Only after the engineer explicitly confirms an intentional repeat, apply with `fde debrief --apply --allow-replay`. This does not silently deduplicate history and does not authenticate sources.