fdeops 3.29.0 → 3.30.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 CHANGED
@@ -56,6 +56,8 @@ Read-only HTML of the record - promised, measured, accepted, and evidence. Regen
56
56
  /plugin install fdeops@fdeops
57
57
  ```
58
58
 
59
+ Make it fit your day: `fde setup` asks three choices for your daily overview, context size, and report masking. [First-use setup](docs/USAGE.md#make-fdeops-fit-your-day).
60
+
59
61
  The plugin adds session hooks and the slash commands below. Skill-only installation does not add hooks. See the [installation guide](docs/install.md) for setup details.
60
62
 
61
63
  </details>
package/bin/fde.js CHANGED
@@ -46,8 +46,26 @@ const ENGAGEMENTS_ROOT = ((process.env.FDEOPS_ENGAGEMENTS_ROOT || '').trim().rep
46
46
  || path.join(HOME, 'fde-engagements')
47
47
  const REGISTRY = path.join(ENGAGEMENTS_ROOT, '.registry')
48
48
  const masking = require('./lib/masking').createMasking(ENGAGEMENTS_ROOT)
49
+ const setup = require('./lib/setup')
50
+ const setupStore = setup.createSetup(ENGAGEMENTS_ROOT)
51
+ let preferences = setup.DEFAULTS
52
+ function contextBudget(args) {
53
+ return context.budgetArgs(args, preferences.context === 'compact' ? 4096 : 16384)
54
+ }
55
+ function portfolioView(args) {
56
+ if (args.includes('--all') && args.includes('--current')) throw new Error('Choose --all or --current, not both.')
57
+ return args.includes('--all') || (!args.includes('--current') && preferences.view === 'portfolio')
58
+ }
49
59
  function maskDisplay(text) {
50
- return ['dashboard', 'vault'].includes(process.argv[2]) ? String(text) : masking.mask(text)
60
+ return ['dashboard', 'vault'].includes(process.argv[2]) && preferences.privacy !== 'reports' ? String(text) : masking.mask(text)
61
+ }
62
+ // Classify the original record first; aliases must never become evidence or a signer.
63
+ function maskReport(value) {
64
+ if (preferences.privacy !== 'reports') return value
65
+ if (typeof value === 'string') return masking.mask(value)
66
+ if (Array.isArray(value)) return value.map(maskReport)
67
+ if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, maskReport(item)]))
68
+ return value
51
69
  }
52
70
  function maskedSections(sections, maxBytes) {
53
71
  return context.boundedSections(sections.map(text => masking.mask(text)), maxBytes)
@@ -990,7 +1008,7 @@ const {
990
1008
  countOpenRisks,
991
1009
  } = createTrustApi({
992
1010
  fs, path, readClean, readEng, parseMdTable, sectionBody, SIGNAL_LEDGER, memoryDirtyManual,
993
- stripTemplateNoise, stripLegendLines, extractRisks,
1011
+ stripTemplateNoise, stripLegendLines, extractRisks, maskDisplay,
994
1012
  })
995
1013
 
996
1014
  // Stakeholders: columns are matched by header wording, not position - real
@@ -1023,6 +1041,7 @@ function parseSignalHistoryEntries(eng) {
1023
1041
  }
1024
1042
 
1025
1043
  function displayNameFromSignalText(text) {
1044
+ if (preferences.privacy === 'reports' && masking.mask(text) !== text) return 'Contact (identifier masked)'
1026
1045
  const person = personFromSignalText(text)
1027
1046
  if (person) return person
1028
1047
  const t = String(text).trim()
@@ -1162,7 +1181,7 @@ function extractStats(eng) {
1162
1181
  const key = from + '→' + to
1163
1182
  if (seen.has(key)) continue
1164
1183
  seen.add(key)
1165
- const pre = text.slice(Math.max(0, m.index - 40), m.index)
1184
+ const pre = maskDisplay(text.slice(0, m.index)).slice(-40)
1166
1185
  const label = pre.split(/\s+/).filter(Boolean).slice(-3).join(' ').replace(/^[,:;.\-]+|[,:;.\-]+$/g, '') || 'metric'
1167
1186
  stats.push({ label, from, to })
1168
1187
  }
@@ -1315,7 +1334,7 @@ function cmdScan() {
1315
1334
 
1316
1335
  function cmdResume(args) {
1317
1336
  let maxBytes
1318
- try { ({ args, maxBytes } = context.budgetArgs(args)) } catch (e) { console.error(e.message); process.exit(2) }
1337
+ try { ({ args, maxBytes } = contextBudget(args)) } catch (e) { console.error(e.message); process.exit(2) }
1319
1338
 
1320
1339
  const initIdx = args.indexOf('--init')
1321
1340
  if (initIdx !== -1) {
@@ -1400,6 +1419,10 @@ function cmdResume(args) {
1400
1419
  const head = memoryHead(fdeDir)
1401
1420
  console.log(`memory git: ${head || 'ready'}${owner ? ` owner: ${owner.email}` : ''}`)
1402
1421
  }
1422
+ if (!setupStore.read()) {
1423
+ if (process.stdin.isTTY && process.stdout.isTTY) return setup.command(setupStore, [])
1424
+ console.log('Make FDEOps fit your day: run fde setup once for three short choices. Agents: ask these after binding; do not guess the answers.')
1425
+ }
1403
1426
  return
1404
1427
  }
1405
1428
  if (args[0] === '--bind') {
@@ -2548,7 +2571,7 @@ function cmdReceipts(args) {
2548
2571
  // overwrites an engagement record, existing file, or symlink.
2549
2572
  function cmdHandoff(args, label = 'Handoff') {
2550
2573
  let parsed
2551
- try { parsed = context.budgetArgs(args) } catch (e) { console.error(e.message); process.exit(1) }
2574
+ try { parsed = contextBudget(args) } catch (e) { console.error(e.message); process.exit(1) }
2552
2575
  let out = ''
2553
2576
  if (parsed.args.length) {
2554
2577
  if (parsed.args.length !== 2 || parsed.args[0] !== '--out' || !parsed.args[1] || parsed.args[1].startsWith('--')) {
@@ -2594,7 +2617,7 @@ function cmdHandoff(args, label = 'Handoff') {
2594
2617
 
2595
2618
  function cmdRecall(args) {
2596
2619
  let maxBytes
2597
- try { ({ args, maxBytes } = context.budgetArgs(args)) } catch (e) { console.error(e.message); process.exit(2) }
2620
+ try { ({ args, maxBytes } = contextBudget(args)) } catch (e) { console.error(e.message); process.exit(2) }
2598
2621
  const query = args.join(' ').trim()
2599
2622
  if (!query || Buffer.byteLength(query) > 2048 || args.some(a => a.startsWith('--'))) {
2600
2623
  console.error('usage: fde recall <topic> [--max-bytes 4096..65536]'); process.exit(2)
@@ -3609,7 +3632,7 @@ function engagementSlugFromPath(eng) {
3609
3632
  }
3610
3633
 
3611
3634
  function cmdStatus(args) {
3612
- const all = args.includes('--all')
3635
+ const all = portfolioView(args)
3613
3636
  if (!fs.existsSync(ENGAGEMENTS_ROOT)) { console.log('no engagements yet - fde resume --init <name>'); return }
3614
3637
  const rows = []
3615
3638
  if (all) {
@@ -3678,7 +3701,7 @@ function gatherEngagements(opts = {}) {
3678
3701
  }
3679
3702
 
3680
3703
  function cmdDashboard(args) {
3681
- const all = args.includes('--all')
3704
+ const all = portfolioView(args)
3682
3705
  const outIdx = args.indexOf('--out')
3683
3706
  const outPath = outIdx !== -1 && args[outIdx + 1]
3684
3707
  ? path.resolve(args[outIdx + 1].replace(/^~/, HOME))
@@ -3734,7 +3757,7 @@ function cmdDashboard(args) {
3734
3757
  ['trust-profile.md', 'Trust profile'],
3735
3758
  ].map(([f, title]) => [title, readClean(e.dir, f)])
3736
3759
  .filter(([, md]) => render.hasRealContent(md))
3737
- .map(([title, md]) => ({ title, html: render.mdBlockHtml(md, parseMdTable) }))
3760
+ .map(([title, md]) => ({ title, html: render.mdBlockHtml(maskDisplay(md), parseMdTable) }))
3738
3761
  e.searchBlob = render.escapeHtml([
3739
3762
  e.name, e.next, e.lastSession, e.reality, e.brief,
3740
3763
  ...e.log.map(g => g.text), ...e.risks.map(r => r.text),
@@ -3744,7 +3767,7 @@ function cmdDashboard(args) {
3744
3767
  ].join(' ').toLowerCase())
3745
3768
  })
3746
3769
 
3747
- const html = render.buildFieldbookHtml({ engagements, today, generatedAt: new Date().toISOString() })
3770
+ const html = render.buildFieldbookHtml({ engagements: maskReport(engagements), today, generatedAt: new Date().toISOString() })
3748
3771
 
3749
3772
  try {
3750
3773
  const isRecordPath = p => p.split(path.sep).some(part => ['.fde', '.git'].includes(part.toLowerCase()))
@@ -3915,10 +3938,10 @@ function cmdVault(args) {
3915
3938
  })
3916
3939
 
3917
3940
  const files = vault.buildVaultFiles({
3918
- engagements,
3941
+ engagements: maskReport(engagements),
3919
3942
  today: render.formatToday(new Date()),
3920
3943
  redacted,
3921
- engagementsRoot: ENGAGEMENTS_ROOT,
3944
+ engagementsRoot: maskReport(ENGAGEMENTS_ROOT),
3922
3945
  version: cliVersion(),
3923
3946
  })
3924
3947
 
@@ -4102,6 +4125,7 @@ ${fs.existsSync(html) ? `\n Open the fieldbook: ${html}` : ''}
4102
4125
  function printUsage() {
4103
4126
  console.log(`fde - deterministic core of fdeops
4104
4127
  fde demo the whole loop on a fake client (fde demo --clean removes it)
4128
+ fde setup three first-use choices (or --show for saved preferences)
4105
4129
  fde privacy show masking capability and its boundaries
4106
4130
  fde scan day-1 recon of this repo (facts, no AI)
4107
4131
  fde resume load this workspace's engagement memory (bounded)
@@ -4131,8 +4155,8 @@ function printUsage() {
4131
4155
  fde receipts <term> source-backed records versus claims
4132
4156
  fde defend sponsor readout: accepted assertions, claims, sources, gaps
4133
4157
  fde handoff [--out file] portable redacted packet; stdout by default, new file only
4134
- fde status [--all] value ledger, then trust (pass --all for full portfolio)
4135
- fde dashboard [--all] [--open] [--out <path>] bound fieldbook (pass --all for every client)
4158
+ fde status [--all|--current] value ledger, then trust (scope follows setup)
4159
+ fde dashboard [--all|--current] [--open] [--out <path>] fieldbook (scope follows setup)
4136
4160
  fde vault derived Obsidian vault of every engagement (--current for one, --redacted for a shared screen, --out <dir>)
4137
4161
  hooks call these; you do not: capture (session-end snapshot), preserve (pre-compaction snapshot)
4138
4162
  env FDEOPS_ENGAGEMENTS_ROOT override ~/fde-engagements (init/status/dashboard/registry)
@@ -4144,7 +4168,7 @@ function printUsage() {
4144
4168
  const [cmd, ...rawArgs] = process.argv.slice(2)
4145
4169
  let outputBudget
4146
4170
  if (['resume', 'recall', 'handoff', 'defend'].includes(cmd) && !rawArgs.some(a => ['--full', '--init', '--bind', '--out'].includes(a))) {
4147
- try { outputBudget = context.budgetArgs(rawArgs).maxBytes } catch (_) {}
4171
+ try { outputBudget = context.budgetArgs(rawArgs, (setupStore.read() || setup.DEFAULTS).context === 'compact' ? 4096 : 16384).maxBytes } catch (_) {}
4148
4172
  }
4149
4173
  require('./lib/masking').protectOutput(masking, { maxBytes: outputBudget })
4150
4174
  let args
@@ -4162,14 +4186,17 @@ if (args.includes('--help') || args.includes('-h') || cmd === 'help' || cmd ===
4162
4186
  process.exit(0)
4163
4187
  }
4164
4188
  try {
4189
+ if (cmd !== 'setup' && cmd !== 'privacy') preferences = setupStore.read() || setup.DEFAULTS
4190
+ const finishAsync = result => { if (result && typeof result.catch === 'function') result.catch(error => { console.error(error.message); process.exitCode = 1 }) }
4165
4191
  switch (cmd) {
4192
+ case 'setup': finishAsync(setup.command(setupStore, args)); break
4166
4193
  case 'privacy':
4167
4194
  if (args.length) { console.error('usage: fde privacy'); process.exitCode = 2; break }
4168
- console.log(`FDEOps ${require('../package.json').version} - identifier masking enabled by default.\nCLI responses, smart proposals, handoff packets and ingest MCP results use local aliases.\nPatterns: common emails, international/US phones, SSN-shaped identifiers and supported credentials.\nNames and arbitrary sensitive prose are not detected; mark them <private>.\nRaw files, pasted chat, upstream MCP content and local dashboard/vault files bypass this protection.`)
4195
+ console.log(`FDEOps ${require('../package.json').version} - identifier masking enabled by default.\nCLI responses, smart proposals, handoff packets and ingest MCP results use local aliases.\nPatterns: common emails, international/US phones, SSN-shaped identifiers and supported credentials.\nNames and arbitrary sensitive prose are not detected; mark them <private>.\nRaw files, pasted chat and upstream MCP content bypass this protection. Local reports retain identifiers by default; fde setup can also mask newly generated report content.`)
4169
4196
  break
4170
4197
  case 'demo': cmdDemo(args); break
4171
4198
  case 'scan': cmdScan(); break
4172
- case 'resume': cmdResume(args); break
4199
+ case 'resume': finishAsync(cmdResume(args)); break
4173
4200
  case 'recall': cmdRecall(args); break
4174
4201
  case 'triage': cmdTriage(); break
4175
4202
  case 'log': cmdLog(args); break
package/bin/install.js CHANGED
@@ -376,7 +376,7 @@ function cmdInstall(opts = {}) {
376
376
  // `npx fdeops scan` must recon, not install - any fde subcommand passes straight
377
377
  // through to the CLI (fde.js reads process.argv itself, so require() is enough).
378
378
  const FDE_SUBCOMMANDS = [
379
- 'demo', 'scan', 'resume', 'triage', 'log', 'debrief', 'ingest', 'prep', 'doctor', 'redact',
379
+ 'setup', 'privacy', 'demo', 'scan', 'resume', 'triage', 'log', 'debrief', 'ingest', 'prep', 'doctor', 'redact',
380
380
  'tidy', 'garden', 'owner', 'receipts', 'recall', 'handoff', 'defend', 'capture', 'preserve', 'status', 'dashboard', 'vault', 'help',
381
381
  ]
382
382
 
@@ -14,9 +14,9 @@ function clipUtf8(text, bytes) {
14
14
  while (end > 0 && (buf[end] & 0xc0) === 0x80) end--
15
15
  return buf.subarray(0, end).toString('utf8')
16
16
  }
17
- function budgetArgs(args) {
17
+ function budgetArgs(args, defaultBytes = DEFAULT_BYTES) {
18
18
  const rest = [...args]
19
- let maxBytes = DEFAULT_BYTES
19
+ let maxBytes = defaultBytes
20
20
  const at = rest.indexOf('--max-bytes')
21
21
  if (at !== -1) {
22
22
  const raw = rest[at + 1]
@@ -0,0 +1,109 @@
1
+ 'use strict'
2
+ const fs = require('node:fs')
3
+ const path = require('node:path')
4
+ const crypto = require('node:crypto')
5
+ const paths = require('./install-paths')
6
+
7
+ const DEFAULTS = { view: 'current', context: 'standard', privacy: 'agent' }
8
+ const QUESTIONS = [
9
+ { key: 'view', title: 'What should your daily overview show?', options: [
10
+ ['current', 'The client I am working on'], ['portfolio', 'All my clients'],
11
+ ] },
12
+ { key: 'context', title: 'How much context should your agent start with?', options: [
13
+ ['standard', 'Standard (up to 16 KiB)'], ['compact', 'Compact (up to 4 KiB; retrieve details as needed)'],
14
+ ] },
15
+ { key: 'privacy', title: 'Where should common identifiers be masked?', options: [
16
+ ['agent', 'Agent context; keep originals in my local reports'],
17
+ ['reports', 'Agent context and newly generated Fieldbook/vault reports'],
18
+ ] },
19
+ ]
20
+ const LIMITS = 'Both options hide <private> content. Masking covers common identifier formats, not all personal data. Names and sensitive prose need private marking. Your AI host controls provider transmission; setup does not change it. Existing exports are not rewritten.'
21
+ function validate(value) {
22
+ if (!value || typeof value !== 'object' || Array.isArray(value) ||
23
+ Object.keys(value).length !== 3 || QUESTIONS.some(q => !q.options.some(([v]) => value[q.key] === v))) {
24
+ throw new Error('Invalid setup choices; run fde setup to see the supported options.')
25
+ }
26
+ return value
27
+ }
28
+ function createSetup(root) {
29
+ const file = path.join(root, '.preferences.json')
30
+ function read() {
31
+ let fd
32
+ try {
33
+ paths.checkPath(file)
34
+ fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK)
35
+ const st = fs.fstatSync(fd)
36
+ if (!st.isFile() || st.nlink !== 1 || st.size > 4096) throw new Error('unsafe')
37
+ return validate(JSON.parse(fs.readFileSync(fd, 'utf8')))
38
+ } catch (e) {
39
+ if (e.code === 'ENOENT') return null
40
+ throw new Error('Cannot read setup safely. Check .preferences.json in your engagements folder; no settings were changed.')
41
+ } finally { if (fd !== undefined) fs.closeSync(fd) }
42
+ }
43
+ function save(value) {
44
+ validate(value)
45
+ paths.mkdir(root)
46
+ paths.checkPath(file)
47
+ const temporary = path.join(root, `.preferences-${crypto.randomBytes(12).toString('hex')}.tmp`)
48
+ try {
49
+ fs.writeFileSync(temporary, JSON.stringify(value, null, 2) + '\n', { flag: 'wx', mode: 0o600 })
50
+ paths.checkPath(file)
51
+ fs.renameSync(temporary, file)
52
+ } finally { try { fs.unlinkSync(temporary) } catch (_) {} }
53
+ }
54
+ return { read, save }
55
+ }
56
+ function describe(value) {
57
+ return QUESTIONS.map(q => `${q.title} ${q.options.find(([v]) => v === value[q.key])[1]}`).join('\n')
58
+ }
59
+ function command(store, args) {
60
+ if (args.length === 1 && args[0] === '--show') {
61
+ const saved = store.read()
62
+ console.log(JSON.stringify({ configured: !!saved, ...(saved || DEFAULTS) }))
63
+ return
64
+ }
65
+ if (args.length) {
66
+ const choices = {}
67
+ if (!args.includes('--save') || args.filter(a => a === '--save').length !== 1) throw new Error('Use fde setup, or supply all three choices with --save.')
68
+ const rest = args.filter(a => a !== '--save')
69
+ for (let i = 0; i < rest.length; i += 2) {
70
+ const key = rest[i].replace(/^--/, '')
71
+ if (!rest[i].startsWith('--') || !Object.hasOwn(DEFAULTS, key) || Object.hasOwn(choices, key)) throw new Error('Unknown or repeated setup option.')
72
+ choices[key] = rest[i + 1]
73
+ }
74
+ validate(choices)
75
+ store.save(choices)
76
+ console.log('Setup saved for this engagements folder. Change it anytime with fde setup.\n' + describe(choices) + '\n' + LIMITS)
77
+ return
78
+ }
79
+ if (process.stdin.isTTY && process.stdout.isTTY) return interactive(store)
80
+ console.log('FIRST-USE SETUP: ask these three questions together, then save the answers. No settings have been changed.')
81
+ for (const [i, q] of QUESTIONS.entries()) console.log(`${i + 1}. ${q.title}\n` + q.options.map(([value, label]) => ` ${value}: ${label}`).join('\n'))
82
+ console.log(LIMITS + '\nAfter the user answers: fde setup --view current|portfolio --context standard|compact --privacy agent|reports --save\nUse fde setup --show to inspect existing choices. Never infer client policy from these preferences.')
83
+ }
84
+ async function interactive(store) {
85
+ // Only static prompts and validated enum labels bypass buffered CLI output.
86
+ const emit = text => fs.writeSync(1, text)
87
+ const rl = require('node:readline').createInterface({ input: process.stdin, terminal: false })
88
+ const lines = rl[Symbol.asyncIterator]()
89
+ const current = store.read() || DEFAULTS, choices = {}
90
+ try {
91
+ emit('Make FDEOps fit your day. Three choices; Enter keeps the current choice. Ctrl-C cancels.\n' + LIMITS + '\n')
92
+ for (const q of QUESTIONS) {
93
+ const selected = q.options.findIndex(([v]) => v === current[q.key])
94
+ while (true) {
95
+ emit(q.title + '\n' + q.options.map(([, label], i) => ` ${i + 1}. ${label}${i === selected ? ' (current)' : ''}`).join('\n') + '\n> ')
96
+ const next = await lines.next()
97
+ if (next.done) { emit('\nSetup cancelled; nothing saved.\n'); return }
98
+ const value = next.value.trim(), index = value === '' ? selected : /^[12]$/.test(value) ? Number(value) - 1 : -1
99
+ if (index >= 0) { choices[q.key] = q.options[index][0]; break }
100
+ emit('Choose 1 or 2, or press Enter.\n')
101
+ }
102
+ }
103
+ emit('\n' + describe(choices) + '\nSave these settings? [y/N] ')
104
+ const next = await lines.next()
105
+ if (!next.done && /^y(es)?$/i.test(next.value.trim())) { store.save(choices); emit('\nSetup saved. Change it anytime with fde setup.\n') }
106
+ else emit('\nSetup cancelled; nothing saved.\n')
107
+ } finally { rl.close() }
108
+ }
109
+ module.exports = { createSetup, command, DEFAULTS }
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, extractRisks,
6
+ stripTemplateNoise, stripLegendLines, extractRisks, maskDisplay = text => text,
7
7
  } = deps
8
8
 
9
9
  // phase / trust / top risk / freshness - identical heuristic for status + dashboard.
@@ -138,7 +138,7 @@ function createTrustApi(deps) {
138
138
  const body = sectionBody(ctx, 'Next action', { lastNonEmpty: true })
139
139
  for (const raw of body.split('\n')) {
140
140
  const t = raw.trim().replace(/^[-*]\s+/, '')
141
- if (t) return t.slice(0, 120)
141
+ if (t) return maskDisplay(t).slice(0, 120)
142
142
  }
143
143
  return ''
144
144
  }
@@ -180,7 +180,7 @@ function createTrustApi(deps) {
180
180
  trustReason = mem.warn
181
181
  } else if (worst) {
182
182
  trust = worst.sig === 'red' ? 'RED' : worst.sig
183
- trustReason = (worst.text || '').slice(0, 80)
183
+ trustReason = maskDisplay(worst.text || '').slice(0, 80)
184
184
  if (worst.date) {
185
185
  signalAge = Math.max(0, Math.floor((Date.now() - Date.parse(worst.date)) / 86400000))
186
186
  stale = signalAge > 21
@@ -195,7 +195,7 @@ function createTrustApi(deps) {
195
195
  trust = sLines.some(l => /\bred\b/i.test(l)) ? 'RED'
196
196
  : sLines.some(l => /amber|gone quiet|routing around|escalat/i.test(l)) ? 'amber' : 'new'
197
197
  }
198
- const topRisk = (extractRisks(eng)[0]?.text || '').replace(/\s+/g, ' ').trim().slice(0, 80)
198
+ const topRisk = maskDisplay((extractRisks(eng)[0]?.text || '').replace(/\s+/g, ' ').trim()).slice(0, 80)
199
199
  // Prefer trust trigger / memory warn over a random risk line; always keep mem.warn available
200
200
  const reason = (trustReason || mem.warn) ? (trustReason || mem.warn) : topRisk
201
201
  // What the triage line is actually quoting. A risk bullet printed under
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fdeops-ingest-mcp",
3
- "version": "3.29.0",
3
+ "version": "3.30.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.29.0",
3
+ "version": "3.30.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.29.0",
4
+ "version": "3.30.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",
@@ -56,9 +56,13 @@ Use the customer's existing coding, testing, review, and repository instructions
56
56
 
57
57
  Fallbacks: `node ~/.claude/fdeops/fde.js …`, then `npx --yes fdeops …`. Skill-only install is not "unavailable."
58
58
 
59
+ ## First-use preferences
60
+
61
+ Run `fde setup --show` before client reads. If unavailable, update the CLI before offering setup; never pretend preferences were saved. If `configured` is false, finish binding the named client, then run `fde setup` and present its **three questions together**, with the two choices each. Use answers already given; do not invent preferences or ask again per client. Save their selections using the displayed `--save` command; their answers authorize this settings write only. If they skip, continue with existing defaults and leave setup unsaved. On later sessions use saved choices; change them only when requested. Setup configures report scope, initial context size and masking of newly generated reports. It does not install a model, approve client data for AI, or change provider settings. Client policy always takes precedence. Reports can still contain names or other unrecognized sensitive prose; never treat them as anonymized.
62
+
59
63
  ## Entry (every session)
60
64
 
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.
65
+ 1. `fde resume` (16 KiB by default, 4 KiB with compact setup; a byte 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
66
  2. **NO ENGAGEMENT:** ask "What should we call this client?" then **you** init. Pasted notes → debrief after bind.
63
67
  3. Playback 2-3 lines. `hygiene:` → offer `fde doctor`; **never auto-rewrite**.
64
68
  4. Route. Read **one** `references/*.md`. Confirm, then write.