fdeops 3.7.3 → 3.7.4

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/fde.js CHANGED
@@ -93,9 +93,17 @@ function resolveEngagement() {
93
93
  // 1) explicit env (back-compat: accept old FDEOS_ENGAGEMENT too)
94
94
  const env = (process.env.FDEOPS_ENGAGEMENT || process.env.FDEOS_ENGAGEMENT || '').replace(/^~/, HOME).trim()
95
95
  if (env && fs.existsSync(env)) return env
96
- // 2) workspace registry binding (written by resume --init)
96
+ // 2) workspace registry binding (written by resume --init). Match the cwd OR
97
+ // any ancestor of it - FDEs run commands from src/, packages/api/, etc., not
98
+ // just the repo root where they bound. Nearest (deepest) registered ancestor
99
+ // wins, exactly like git searching upward for .git. `startsWith(workspace +
100
+ // sep)` requires a true path-boundary ancestor, so /work/repo-2 never matches
101
+ // a binding on /work/repo. Kept in lockstep with registry_engagement_dir in
102
+ // hooks/session-start, session-stop, pre-compact.
97
103
  const cwd = process.cwd()
98
- const reg = readRegistry().find(r => r.workspace === cwd)
104
+ const reg = readRegistry()
105
+ .filter(r => cwd === r.workspace || cwd.startsWith(r.workspace + path.sep))
106
+ .sort((a, b) => b.workspace.length - a.workspace.length)[0]
99
107
  if (reg) {
100
108
  const p = path.join(ENGAGEMENTS_ROOT, reg.slug, '.fde')
101
109
  if (fs.existsSync(p)) return p
@@ -667,18 +675,42 @@ function cmdReceipts(args) {
667
675
  const eng = resolveEngagement()
668
676
  if (!eng) { console.error('no engagement - run: fde resume --init <name>'); process.exit(2) }
669
677
  const rx = new RegExp(term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i')
670
- let found = 0
671
- for (const f of ['decisions.md', 'delivery.md', 'risks.md', 'stakeholders.md', 'context.md', 'success.md', 'brief.md', 'reality.md']) {
672
- const p = path.join(eng, f)
673
- if (!fs.existsSync(p)) continue
674
- // readClean, not raw read: receipts must not grep sealed <private> notes back
675
- // out. Redaction can shift line numbers past a multi-line block; the file:line
676
- // is advisory, and not leaking a sealed secret is worth an approximate number.
677
- readClean(eng, f).split('\n').forEach((l, i) => {
678
- if (rx.test(l)) { console.log(`${f}:${i + 1} ${l.trim().slice(0, 160)}`); found++ }
679
- })
678
+ // "receipts" answers "what did we AGREE?" - so dated, agreed records are the
679
+ // receipt. brief.md is the client's hypothesis and reality.md/context.md are
680
+ // working notes; a hit there is a CLAIM, not an agreement. Keeping them in the
681
+ // same list let an FDE cite a sales promise as a receipt - so they get a
682
+ // separate, clearly-labelled section that is never mistaken for the record.
683
+ const AGREEMENTS = ['decisions.md', 'delivery.md', 'success.md', 'risks.md', 'stakeholders.md']
684
+ const CLAIMS = ['brief.md', 'reality.md', 'context.md']
685
+ const collect = files => {
686
+ const hits = []
687
+ for (const f of files) {
688
+ if (!fs.existsSync(path.join(eng, f))) continue
689
+ // readClean, not raw read: receipts must not grep sealed <private> notes
690
+ // back out. Redaction can shift line numbers past a multi-line block; the
691
+ // file:line is advisory - not leaking a sealed secret is worth that.
692
+ readClean(eng, f).split('\n').forEach((l, i) => {
693
+ if (rx.test(l)) hits.push(` ${f}:${i + 1} ${l.trim().slice(0, 160)}`)
694
+ })
695
+ }
696
+ return hits
697
+ }
698
+ const agreed = collect(AGREEMENTS)
699
+ const claimed = collect(CLAIMS)
700
+ if (agreed.length) {
701
+ console.log('AGREED (dated record - defensible):')
702
+ agreed.forEach(h => console.log(h))
703
+ }
704
+ if (claimed.length) {
705
+ if (agreed.length) console.log('')
706
+ console.log('CLAIMS & working notes (stated, NOT an agreement - verify before citing):')
707
+ claimed.forEach(h => console.log(h))
708
+ }
709
+ if (!agreed.length && !claimed.length) {
710
+ console.log(`no record of "${term}" - nothing was ever logged about it. A gap in the record, not proof of absence: if it WAS agreed, log it now, dated today.`)
711
+ } else if (!agreed.length) {
712
+ console.log('\n(no dated agreement matched - only unverified claims above. If this was agreed, log it: fde log decision "...")')
680
713
  }
681
- if (!found) console.log(`no record of "${term}" - nothing was ever logged about it. A gap in the record, not proof of absence: if it WAS agreed, log it now, dated today.`)
682
714
  }
683
715
 
684
716
  function cmdCapture() {
package/hooks/pre-compact CHANGED
@@ -18,9 +18,15 @@ resolve_engagement_dir() {
18
18
  registry_engagement_dir() {
19
19
  local reg="$HOME/fde-engagements/.registry" slug
20
20
  [ -f "$reg" ] || return 1
21
- slug=$(awk 'BEGIN{ws=ENVIRON["PWD"]}
21
+ slug=$(awk 'BEGIN{ws=ENVIRON["PWD"]; best=-1}
22
22
  { i = match($0, / [^ ]*$/)
23
- if (i > 0 && substr($0, 1, i - 1) == ws) { print substr($0, i + 1); exit } }' "$reg" 2>/dev/null)
23
+ if (i > 0) {
24
+ wpath = substr($0, 1, i - 1)
25
+ if (ws == wpath || index(ws, wpath "/") == 1) {
26
+ if (length(wpath) > best) { best = length(wpath); slug = substr($0, i + 1) }
27
+ }
28
+ } }
29
+ END { if (best >= 0) print slug }' "$reg" 2>/dev/null)
24
30
  [ -z "$slug" ] && return 1
25
31
  [ -d "$HOME/fde-engagements/$slug/.fde" ] && printf '%s\n' "$HOME/fde-engagements/$slug/.fde" && return 0
26
32
  return 1
@@ -30,9 +30,15 @@ resolve_engagement_dir() {
30
30
  registry_engagement_dir() {
31
31
  local reg="$HOME/fde-engagements/.registry" slug
32
32
  [ -f "$reg" ] || return 1
33
- slug=$(awk 'BEGIN{ws=ENVIRON["PWD"]}
33
+ slug=$(awk 'BEGIN{ws=ENVIRON["PWD"]; best=-1}
34
34
  { i = match($0, / [^ ]*$/)
35
- if (i > 0 && substr($0, 1, i - 1) == ws) { print substr($0, i + 1); exit } }' "$reg" 2>/dev/null)
35
+ if (i > 0) {
36
+ wpath = substr($0, 1, i - 1)
37
+ if (ws == wpath || index(ws, wpath "/") == 1) {
38
+ if (length(wpath) > best) { best = length(wpath); slug = substr($0, i + 1) }
39
+ }
40
+ } }
41
+ END { if (best >= 0) print slug }' "$reg" 2>/dev/null)
36
42
  [ -z "$slug" ] && return 1
37
43
  [ -d "$HOME/fde-engagements/$slug/.fde" ] && printf '%s\n' "$HOME/fde-engagements/$slug/.fde" && return 0
38
44
  return 1
@@ -26,9 +26,15 @@ resolve_engagement_dir() {
26
26
  registry_engagement_dir() {
27
27
  local reg="$HOME/fde-engagements/.registry" slug
28
28
  [ -f "$reg" ] || return 1
29
- slug=$(awk 'BEGIN{ws=ENVIRON["PWD"]}
29
+ slug=$(awk 'BEGIN{ws=ENVIRON["PWD"]; best=-1}
30
30
  { i = match($0, / [^ ]*$/)
31
- if (i > 0 && substr($0, 1, i - 1) == ws) { print substr($0, i + 1); exit } }' "$reg" 2>/dev/null)
31
+ if (i > 0) {
32
+ wpath = substr($0, 1, i - 1)
33
+ if (ws == wpath || index(ws, wpath "/") == 1) {
34
+ if (length(wpath) > best) { best = length(wpath); slug = substr($0, i + 1) }
35
+ }
36
+ } }
37
+ END { if (best >= 0) print slug }' "$reg" 2>/dev/null)
32
38
  [ -z "$slug" ] && return 1
33
39
  [ -d "$HOME/fde-engagements/$slug/.fde" ] && printf '%s\n' "$HOME/fde-engagements/$slug/.fde" && return 0
34
40
  return 1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fdeops",
3
- "version": "3.7.3",
3
+ "version": "3.7.4",
4
4
  "description": "Field kit for engineers embedded in client work - a real CLI (recon, memory, portfolio), one @fde skill with field judgment on top, and hooks that make it automatic. Claude Code plugin and any agent that loads skills.",
5
5
  "bin": {
6
6
  "fdeops": "bin/install.js",