Repository navigation
Expand file tree
/
Copy pathsession-start
More file actions
executable file
·139 lines (126 loc) · 5.57 KB
/
Copy pathsession-start
File metadata and controls
executable file
·139 lines (126 loc) · 5.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
#!/bin/bash
# fdeops SessionStart - inject TRIAGE + bounded engagement context (not the full skill).
# Token discipline (L1 progressive disclosure): Claude/Cursor already discover @fde via
# skill metadata. Dumping skills/fde/SKILL.md here paid ~6-8k tokens every session even
# when the FDE was not doing field work. Load the skill body only when @fde triggers.
CONTEXT_FILE=""
resolve_engagement_dir() {
local raw="$1"
[ -z "$raw" ] && return 1
# strip surrounding whitespace/quotes only - paths may contain spaces
raw=$(printf '%s' "$raw" | sed -e 's/^[[:space:]"'"'"']*//' -e 's/[[:space:]"'"'"']*$//' -e "s|^~|$HOME|")
# Same forms bin/fde.js accepts: an absolute .fde, an absolute engagement
# folder, or a bare slug under the engagements root - keep the two in lockstep.
# Anything relative is refused: it would resolve against whatever directory
# the agent happened to start in.
case $raw in
/*)
[ -d "$raw/.fde" ] && printf '%s\n' "$raw/.fde" && return 0
[ -d "$raw" ] && printf '%s\n' "$raw" && return 0
return 1
;;
*/*|.*) return 1 ;;
esac
local root="${FDEOPS_ENGAGEMENTS_ROOT:-$HOME/fde-engagements}"
root="${root/#\~/$HOME}"
local slug
slug=$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]' | sed -e 's/[^a-z0-9]\+/-/g' -e 's/^-//' -e 's/-$//')
[ -n "$slug" ] && [ -d "$root/$slug/.fde" ] && printf '%s\n' "$root/$slug/.fde" && return 0
return 1
}
# Workspace registry written by `fde resume --init` - mirrors resolveEngagement()
# step 2 in bin/fde.js; keep the resolution order identical in bash and JS.
# Lines are "<workspace-path> <slug>": the path may contain spaces, the slug
# never does, so split on the LAST space (same as the JS lastIndexOf parse).
registry_engagement_dir() {
# FDEOPS_ENGAGEMENTS_ROOT mirrors ENGAGEMENTS_ROOT in bin/fde.js - the CLI and
# hooks MUST agree on the root or an isolated setup gets split memory.
local root="${FDEOPS_ENGAGEMENTS_ROOT:-$HOME/fde-engagements}"
root="${root/#\~/$HOME}"
local reg="$root/.registry" slug
[ -f "$reg" ] || return 1
slug=$(awk 'BEGIN{ws=ENVIRON["PWD"]; best=-1}
{ i = match($0, / [^ ]*$/)
if (i > 0) {
wpath = substr($0, 1, i - 1)
if (ws == wpath || index(ws, wpath "/") == 1) {
if (length(wpath) > best) { best = length(wpath); slug = substr($0, i + 1) }
}
} }
END { if (best >= 0) print slug }' "$reg" 2>/dev/null)
[ -z "$slug" ] && return 1
[ -d "$root/$slug/.fde" ] && printf '%s\n' "$root/$slug/.fde" && return 0
return 1
}
# 1) Environment variable (any agent)
if [ -z "$CONTEXT_FILE" ]; then
ENG_DIR=$(resolve_engagement_dir "${FDEOPS_ENGAGEMENT:-${FDEOS_ENGAGEMENT:-}}")
[ -n "$ENG_DIR" ] && CONTEXT_FILE="$ENG_DIR/context.md"
fi
# An override that cannot be honored is never ignored: falling through to the
# registry would load whichever engagement the workspace is bound to, and the
# operator named a different one. bin/fde.js refuses; so do we.
if [ -z "$CONTEXT_FILE" ] && [ -n "${FDEOPS_ENGAGEMENT:-${FDEOS_ENGAGEMENT:-}}" ]; then
exit 0
fi
# 2) Workspace registry binding (written by fde resume --init)
if [ -z "$CONTEXT_FILE" ]; then
ENG_DIR=$(registry_engagement_dir)
[ -n "$ENG_DIR" ] && CONTEXT_FILE="$ENG_DIR/context.md"
fi
# 3) Project CLAUDE.md
if [ -z "$CONTEXT_FILE" ] && [ -f "CLAUDE.md" ]; then
ENG=$(grep -m1 '^FDEOPS_ENGAGEMENT=\|^FDEOS_ENGAGEMENT=' CLAUDE.md 2>/dev/null | cut -d= -f2-)
ENG_DIR=$(resolve_engagement_dir "$ENG")
[ -n "$ENG_DIR" ] && CONTEXT_FILE="$ENG_DIR/context.md"
fi
# 4) Global fdeops pointer file
if [ -z "$CONTEXT_FILE" ] && [ -f "$HOME/.claude/FDEOPS-CLAUDE.md" ]; then
ENG=$(grep -m1 '^FDEOPS_ENGAGEMENT=\|^FDEOS_ENGAGEMENT=' "$HOME/.claude/FDEOPS-CLAUDE.md" 2>/dev/null | cut -d= -f2-)
ENG_DIR=$(resolve_engagement_dir "$ENG")
[ -n "$ENG_DIR" ] && CONTEXT_FILE="$ENG_DIR/context.md"
fi
# 5) Optional in-repo .fde (customer-approved only)
if [ -z "$CONTEXT_FILE" ] && [ -f ".fde/context.md" ]; then
ENG_DIR="$(pwd -P)/.fde"
CONTEXT_FILE=".fde/context.md"
fi
IS_FDE_PROJECT=0
[ -n "$CONTEXT_FILE" ] && [ -f "$CONTEXT_FILE" ] && IS_FDE_PROJECT=1
[ -f "CLAUDE.md" ] && grep -qi "fde\|fdeops" "CLAUDE.md" 2>/dev/null && IS_FDE_PROJECT=1
[ -n "${FDEOPS_ENGAGEMENT:-${FDEOS_ENGAGEMENT:-}}" ] && IS_FDE_PROJECT=1
[ -d ".fde" ] && IS_FDE_PROJECT=1
if [ "$IS_FDE_PROJECT" -eq 0 ]; then
exit 0
fi
# Prefer this installation so its hook and CLI share the same safety contract.
resolve_fde() {
for candidate in \
"${CLAUDE_PLUGIN_ROOT:+$CLAUDE_PLUGIN_ROOT/bin/fde.js}" \
"$(dirname "$0")/../bin/fde.js" \
"$HOME/.claude/fdeops/fde.js" \
"$HOME/.claude/plugins/fdeops/bin/fde.js"; do
if [ -n "$candidate" ] && [ -f "$candidate" ]; then
printf '%s\n' "$candidate"
return 0
fi
done
if command -v fde >/dev/null 2>&1; then
printf '%s\n' "fde"
return 0
fi
return 1
}
# The matching CLI owns triage, private redaction, and context bounds.
# Never fall back to reading raw notes when Node or the CLI is unavailable.
FDE_CMD=$(resolve_fde || true)
[ -n "$FDE_CMD" ] || exit 0
[ -n "$CONTEXT_FILE" ] && [ -f "$CONTEXT_FILE" ] || exit 0
if [ "$FDE_CMD" = "fde" ]; then
CONTEXT=$(FDEOPS_ENGAGEMENT="$ENG_DIR" fde resume 2>/dev/null) || exit 0
else
CONTEXT=$(FDEOPS_ENGAGEMENT="$ENG_DIR" node "$FDE_CMD" resume 2>/dev/null) || exit 0
fi
[ -n "$CONTEXT" ] || exit 0
printf '%s\n\n' 'fdeops: engagement fieldbook active. Human speaks plain language with @fde - you (the agent) run the local fde CLI for memory plumbing; never ask the human to type fde commands. Load skills/fde/SKILL.md when @fde triggers.'
printf '%s\n' 'Engagement context:' "$CONTEXT"