fdeops 3.5.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.
Files changed (70) hide show
  1. package/AGENTS.md +18 -0
  2. package/CLAUDE.md.template +25 -0
  3. package/LICENSE +21 -0
  4. package/README.md +328 -0
  5. package/adapters/AGENTS.md +24 -0
  6. package/adapters/GEMINI.md +24 -0
  7. package/adapters/README.md +31 -0
  8. package/adapters/copilot-instructions.md +24 -0
  9. package/adapters/cursor.fde.mdc +29 -0
  10. package/bin/check.js +260 -0
  11. package/bin/fde.js +690 -0
  12. package/bin/install.js +220 -0
  13. package/hooks/hooks.json +40 -0
  14. package/hooks/pre-compact +66 -0
  15. package/hooks/run-hook.cmd +3 -0
  16. package/hooks/session-start +91 -0
  17. package/hooks/session-stop +86 -0
  18. package/package.json +52 -0
  19. package/skills/fde/SKILL.md +219 -0
  20. package/skills/fde/references/ai.md +91 -0
  21. package/skills/fde/references/artifacts.md +247 -0
  22. package/skills/fde/references/assumption-audit.md +77 -0
  23. package/skills/fde/references/audit.md +61 -0
  24. package/skills/fde/references/blast-radius.md +91 -0
  25. package/skills/fde/references/build.md +98 -0
  26. package/skills/fde/references/business-case.md +78 -0
  27. package/skills/fde/references/close.md +43 -0
  28. package/skills/fde/references/dashboard.md +40 -0
  29. package/skills/fde/references/debrief.md +36 -0
  30. package/skills/fde/references/debug.md +55 -0
  31. package/skills/fde/references/demo-prep.md +31 -0
  32. package/skills/fde/references/discover.md +163 -0
  33. package/skills/fde/references/exec-narrative.md +108 -0
  34. package/skills/fde/references/fintech.md +48 -0
  35. package/skills/fde/references/gov.md +47 -0
  36. package/skills/fde/references/handoff-engineering.md +139 -0
  37. package/skills/fde/references/healthcare.md +45 -0
  38. package/skills/fde/references/incremental-build.md +91 -0
  39. package/skills/fde/references/initiative-triage.md +78 -0
  40. package/skills/fde/references/land.md +75 -0
  41. package/skills/fde/references/multi-customer-ops.md +114 -0
  42. package/skills/fde/references/observability.md +103 -0
  43. package/skills/fde/references/options-analysis.md +81 -0
  44. package/skills/fde/references/pattern-extract.md +93 -0
  45. package/skills/fde/references/plan.md +108 -0
  46. package/skills/fde/references/qa-live.md +113 -0
  47. package/skills/fde/references/rescue.md +81 -0
  48. package/skills/fde/references/review.md +53 -0
  49. package/skills/fde/references/rollback-drill.md +102 -0
  50. package/skills/fde/references/scope-defense.md +71 -0
  51. package/skills/fde/references/security-audit.md +105 -0
  52. package/skills/fde/references/ship.md +121 -0
  53. package/skills/fde/references/sketch.md +40 -0
  54. package/skills/fde/references/stakeholder-radar.md +68 -0
  55. package/skills/fde/references/status.md +30 -0
  56. package/skills/fde/references/test-on-legacy.md +108 -0
  57. package/skills/fde/references/trust-engineering.md +100 -0
  58. package/skills/fde/references/use-case-scoring.md +70 -0
  59. package/templates/.fde/README.md +13 -0
  60. package/templates/.fde/brief.md +8 -0
  61. package/templates/.fde/context.md +14 -0
  62. package/templates/.fde/decisions.md +18 -0
  63. package/templates/.fde/delivery.md +7 -0
  64. package/templates/.fde/reality.md +7 -0
  65. package/templates/.fde/retrospectives/.gitkeep +0 -0
  66. package/templates/.fde/risks.md +5 -0
  67. package/templates/.fde/stakeholders.md +10 -0
  68. package/templates/.fde/success.md +7 -0
  69. package/templates/.fde/terrain.md +7 -0
  70. package/templates/.fde/trust-profile.md +11 -0
package/bin/install.js ADDED
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+ const os = require('os')
6
+
7
+ const SKILLS_SRC = path.join(__dirname, '..', 'skills')
8
+ const HOOKS_SRC = path.join(__dirname, '..', 'hooks')
9
+ const CLAUDE_MD_SRC = path.join(__dirname, '..', 'CLAUDE.md.template')
10
+ const FDE_TEMPLATES_SRC = path.join(__dirname, '..', 'templates', '.fde')
11
+ const ADAPTERS_SRC = path.join(__dirname, '..', 'adapters')
12
+
13
+ const GLOBAL_SKILLS_DIR = path.join(os.homedir(), '.claude', 'skills')
14
+ const GLOBAL_HOOKS_DIR = path.join(os.homedir(), '.claude', 'hooks')
15
+ const HOOK_SCRIPTS = ['session-start', 'session-stop', 'pre-compact']
16
+ const ENGAGEMENTS_ROOT = path.join(os.homedir(), 'fde-engagements')
17
+
18
+ function copyDir(src, dest) {
19
+ fs.mkdirSync(dest, { recursive: true })
20
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
21
+ const srcPath = path.join(src, entry.name)
22
+ const destPath = path.join(dest, entry.name)
23
+ if (entry.isDirectory()) {
24
+ copyDir(srcPath, destPath)
25
+ } else {
26
+ fs.copyFileSync(srcPath, destPath)
27
+ }
28
+ }
29
+ }
30
+
31
+ function copyTemplateTree(src, dest, onlyMissing) {
32
+ let merged = 0
33
+ fs.mkdirSync(dest, { recursive: true })
34
+ for (const name of fs.readdirSync(src)) {
35
+ const srcPath = path.join(src, name)
36
+ const destPath = path.join(dest, name)
37
+ if (fs.statSync(srcPath).isDirectory()) {
38
+ merged += copyTemplateTree(srcPath, destPath, onlyMissing)
39
+ } else if (!onlyMissing || !fs.existsSync(destPath)) {
40
+ fs.copyFileSync(srcPath, destPath)
41
+ if (onlyMissing) merged++
42
+ }
43
+ }
44
+ return merged
45
+ }
46
+
47
+ function slugify(name) {
48
+ return name
49
+ .toLowerCase()
50
+ .replace(/[^a-z0-9]+/g, '-')
51
+ .replace(/^-|-$/g, '')
52
+ || 'engagement'
53
+ }
54
+
55
+ // v2 shipped 16 standalone skills; v3 is one `fde` skill + references.
56
+ // Leaving the old ones in place would route users to stale content.
57
+ const LEGACY_SKILL_DIRS = [
58
+ 'fde-land', 'fde-discover', 'fde-audit', 'fde-rescue', 'fde-sketch',
59
+ 'fde-close', 'fde-engineering', 'fde-plan', 'fde-build', 'fde-review',
60
+ 'fde-debug', 'fde-ship', 'fde-dashboard', 'healthcare-fde', 'fintech-fde',
61
+ 'gov-fde',
62
+ ]
63
+
64
+ function removeLegacySkills() {
65
+ let removed = 0
66
+ for (const dir of LEGACY_SKILL_DIRS) {
67
+ const p = path.join(GLOBAL_SKILLS_DIR, dir)
68
+ if (fs.existsSync(path.join(p, 'SKILL.md'))) {
69
+ fs.rmSync(p, { recursive: true, force: true })
70
+ removed++
71
+ }
72
+ }
73
+ return removed
74
+ }
75
+
76
+ function installSkills() {
77
+ const removed = removeLegacySkills()
78
+ if (removed > 0) console.log(` Removed ${removed} v2 skill dir(s) (now covered by @fde)`)
79
+ copyDir(SKILLS_SRC, GLOBAL_SKILLS_DIR)
80
+ fs.mkdirSync(GLOBAL_HOOKS_DIR, { recursive: true })
81
+ for (const name of HOOK_SCRIPTS) {
82
+ const src = path.join(HOOKS_SRC, name)
83
+ if (!fs.existsSync(src)) continue
84
+ const dest = path.join(GLOBAL_HOOKS_DIR, `fdeops-${name}`)
85
+ fs.copyFileSync(src, dest)
86
+ try {
87
+ fs.chmodSync(dest, '755')
88
+ } catch (_) {}
89
+ }
90
+ const globalPointer = path.join(os.homedir(), '.claude', 'FDEOPS-CLAUDE.md')
91
+ if (!fs.existsSync(globalPointer)) {
92
+ fs.copyFileSync(CLAUDE_MD_SRC, globalPointer)
93
+ }
94
+ fs.copyFileSync(CLAUDE_MD_SRC, path.join(os.homedir(), '.claude', 'FDEOPS-CLAUDE.md.template'))
95
+
96
+ // the fde CLI + templates, so the skill can call it from any workspace
97
+ const cliHome = path.join(os.homedir(), '.claude', 'fdeops')
98
+ fs.mkdirSync(cliHome, { recursive: true })
99
+ fs.copyFileSync(path.join(__dirname, 'fde.js'), path.join(cliHome, 'fde.js'))
100
+ try { fs.chmodSync(path.join(cliHome, 'fde.js'), '755') } catch (_) {}
101
+ copyDir(FDE_TEMPLATES_SRC, path.join(cliHome, 'templates', '.fde'))
102
+
103
+ // cross-platform pointer templates, so `fdeops adapters` works from anywhere
104
+ if (fs.existsSync(ADAPTERS_SRC)) copyDir(ADAPTERS_SRC, path.join(cliHome, 'adapters'))
105
+ }
106
+
107
+ // One brain (skills/fde/SKILL.md) reached through a thin pointer per tool.
108
+ // Each tool reads a different file in a different place; the content is the same.
109
+ const ADAPTER_TARGETS = [
110
+ { label: 'CLAUDE.md', dest: 'CLAUDE.md', src: CLAUDE_MD_SRC, appendable: true },
111
+ { label: 'AGENTS.md', dest: 'AGENTS.md', src: path.join(ADAPTERS_SRC, 'AGENTS.md'), appendable: true },
112
+ { label: 'GEMINI.md', dest: 'GEMINI.md', src: path.join(ADAPTERS_SRC, 'GEMINI.md'), appendable: true },
113
+ { label: '.github/copilot-instructions.md', dest: path.join('.github', 'copilot-instructions.md'), src: path.join(ADAPTERS_SRC, 'copilot-instructions.md'), appendable: true },
114
+ { label: '.cursor/rules/fde.mdc', dest: path.join('.cursor', 'rules', 'fde.mdc'), src: path.join(ADAPTERS_SRC, 'cursor.fde.mdc'), appendable: false },
115
+ ]
116
+
117
+ const FDE_MARKER = '<!-- fdeops adapter - points your AI tool at @fde; safe to keep -->'
118
+
119
+ function placePointer(destPath, content, label, appendable) {
120
+ fs.mkdirSync(path.dirname(destPath), { recursive: true })
121
+ if (fs.existsSync(destPath)) {
122
+ const existing = fs.readFileSync(destPath, 'utf8')
123
+ if (/FDEOS|fdeops/i.test(existing)) {
124
+ console.log(` skip ${label} (already wired)`)
125
+ return
126
+ }
127
+ if (!appendable) {
128
+ console.log(` skip ${label} (exists - left untouched)`)
129
+ return
130
+ }
131
+ fs.writeFileSync(destPath, `${existing.trimEnd()}\n\n${FDE_MARKER}\n\n${content}`)
132
+ console.log(` append ${label}`)
133
+ return
134
+ }
135
+ fs.writeFileSync(destPath, content)
136
+ console.log(` write ${label}`)
137
+ }
138
+
139
+ function cmdAdapters(targetDir) {
140
+ const dest = path.resolve(targetDir || process.cwd())
141
+ console.log('')
142
+ console.log(` fdeops cross-platform adapters → ${dest}`)
143
+ console.log(' One brain (skills/fde/SKILL.md). These are thin pointers per tool.')
144
+ console.log('')
145
+ for (const a of ADAPTER_TARGETS) {
146
+ if (!fs.existsSync(a.src)) { console.log(` skip ${a.label} (template missing)`); continue }
147
+ placePointer(path.join(dest, a.dest), fs.readFileSync(a.src, 'utf8'), a.label, a.appendable)
148
+ }
149
+ console.log('')
150
+ console.log(' Open this workspace in Claude Code, Cursor, Codex, Gemini CLI, or Copilot')
151
+ console.log(' and type @fde - each tool now routes to the same engagement brain.')
152
+ console.log('')
153
+ }
154
+
155
+ function cmdInit(engagementName) {
156
+ if (!engagementName) {
157
+ console.error(' Usage: node bin/install.js init <engagement-name>')
158
+ console.error(' Example: node bin/install.js init retailbank-payments')
159
+ process.exit(1)
160
+ }
161
+ const slug = slugify(engagementName)
162
+ const root = path.join(ENGAGEMENTS_ROOT, slug)
163
+ const fdeDir = path.join(root, '.fde')
164
+ const created = !fs.existsSync(fdeDir)
165
+ const merged = copyTemplateTree(FDE_TEMPLATES_SRC, fdeDir, !created)
166
+
167
+ const pointer = path.join(root, 'ENGAGEMENT.md')
168
+ if (!fs.existsSync(pointer)) {
169
+ fs.writeFileSync(
170
+ pointer,
171
+ `# ${engagementName}\n\nEngagement root: \`${fdeDir}\`\n\nPoint your **AI coding agent** at this folder (not a human colleague). Add to ~/.claude/FDEOPS-CLAUDE.md:\n\n\`\`\`\nFDEOPS_ENGAGEMENT=${fdeDir}\n\`\`\`\n\nOpen your workspace. In the AI chat, type \`@fde\`.\n`,
172
+ )
173
+ }
174
+
175
+ console.log('')
176
+ console.log(' fdeops engagement created (private notes on your machine)')
177
+ console.log('')
178
+ console.log(` ${fdeDir}`)
179
+ if (created) console.log(' (new)')
180
+ else if (merged > 0) console.log(` (${merged} missing template file(s) added)`)
181
+ console.log('')
182
+ console.log(' Next:')
183
+ console.log(' 1. Open your workspace for this engagement')
184
+ console.log(` 2. Point your AI coding agent at: FDEOPS_ENGAGEMENT=${fdeDir}`)
185
+ console.log(' 3. In the AI chat (not email), type: @fde and describe what is happening')
186
+ console.log('')
187
+ }
188
+
189
+ function cmdInstall() {
190
+ console.log('')
191
+ console.log(' fdeops - installs on YOUR machine only')
192
+ console.log('')
193
+ installSkills()
194
+ console.log(' Skills → ~/.claude/skills/')
195
+ console.log(' Hooks → ~/.claude/hooks/fdeops-*')
196
+ console.log(' CLI → ~/.claude/fdeops/fde.js (try: node ~/.claude/fdeops/fde.js scan)')
197
+ console.log('')
198
+ console.log(' Create an engagement (stays off customer infrastructure):')
199
+ console.log(' node bin/install.js init <engagement-name>')
200
+ console.log('')
201
+ console.log(' Example:')
202
+ console.log(' node bin/install.js init acme-payments')
203
+ console.log(' (npm 3.0.0+: npx fdeops@latest init <engagement-name>)')
204
+ console.log('')
205
+ console.log(' Use another AI tool (Cursor, Codex, Gemini CLI, Copilot)? Wire it up:')
206
+ console.log(' node bin/install.js adapters <engagement-workspace>')
207
+ console.log('')
208
+ console.log(' Then open your workspace and use @fde')
209
+ console.log(' Docs: docs/install.md')
210
+ console.log('')
211
+ }
212
+
213
+ const arg = process.argv[2]
214
+ if (arg === 'init') {
215
+ cmdInit(process.argv[3])
216
+ } else if (arg === 'adapters') {
217
+ cmdAdapters(process.argv[3])
218
+ } else {
219
+ cmdInstall()
220
+ }
@@ -0,0 +1,40 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "matcher": "",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "'${CLAUDE_PLUGIN_ROOT}/hooks/session-start'",
10
+ "async": false
11
+ }
12
+ ]
13
+ }
14
+ ],
15
+ "PreCompact": [
16
+ {
17
+ "matcher": "",
18
+ "hooks": [
19
+ {
20
+ "type": "command",
21
+ "command": "'${CLAUDE_PLUGIN_ROOT}/hooks/pre-compact'",
22
+ "async": false
23
+ }
24
+ ]
25
+ }
26
+ ],
27
+ "SessionEnd": [
28
+ {
29
+ "matcher": "",
30
+ "hooks": [
31
+ {
32
+ "type": "command",
33
+ "command": "'${CLAUDE_PLUGIN_ROOT}/hooks/session-stop'",
34
+ "async": false
35
+ }
36
+ ]
37
+ }
38
+ ]
39
+ }
40
+ }
@@ -0,0 +1,66 @@
1
+ #!/bin/bash
2
+ # fdeops PreCompact - preserve engagement state before context compaction.
3
+ # Resolves the engagement the same way as session-start/session-stop.
4
+
5
+ resolve_engagement_dir() {
6
+ local raw="$1"
7
+ [ -z "$raw" ] && return 1
8
+ # strip surrounding whitespace/quotes only - paths may contain spaces
9
+ raw=$(printf '%s' "$raw" | sed -e 's/^[[:space:]"'"'"']*//' -e 's/[[:space:]"'"'"']*$//' -e "s|^~|$HOME|")
10
+ [ -d "$raw" ] && printf '%s\n' "$raw" && return 0
11
+ return 1
12
+ }
13
+
14
+ ENG_DIR=$(resolve_engagement_dir "${FDEOPS_ENGAGEMENT:-${FDEOS_ENGAGEMENT:-}}")
15
+
16
+ if [ -z "$ENG_DIR" ] && [ -f "CLAUDE.md" ]; then
17
+ ENG=$(grep -m1 '^FDEOPS_ENGAGEMENT=\|^FDEOS_ENGAGEMENT=' CLAUDE.md 2>/dev/null | cut -d= -f2-)
18
+ ENG_DIR=$(resolve_engagement_dir "$ENG")
19
+ fi
20
+
21
+ if [ -z "$ENG_DIR" ] && [ -f "$HOME/.claude/FDEOPS-CLAUDE.md" ]; then
22
+ ENG=$(grep -m1 '^FDEOPS_ENGAGEMENT=\|^FDEOS_ENGAGEMENT=' "$HOME/.claude/FDEOPS-CLAUDE.md" 2>/dev/null | cut -d= -f2-)
23
+ ENG_DIR=$(resolve_engagement_dir "$ENG")
24
+ fi
25
+
26
+ if [ -z "$ENG_DIR" ] && [ -d ".fde" ]; then
27
+ ENG_DIR=".fde"
28
+ fi
29
+
30
+ [ -z "$ENG_DIR" ] && exit 0
31
+
32
+ CONTEXT_FILE="$ENG_DIR/context.md"
33
+ DECISIONS_FILE="$ENG_DIR/decisions.md"
34
+ RISKS_FILE="$ENG_DIR/risks.md"
35
+ MARKER="[fdeops context preserved"
36
+
37
+ [ -f "$CONTEXT_FILE" ] || exit 0
38
+
39
+ # Avoid unbounded growth: skip if we already preserved today.
40
+ if grep -q "$MARKER" "$CONTEXT_FILE" 2>/dev/null; then
41
+ LAST=$(grep "$MARKER" "$CONTEXT_FILE" | tail -1)
42
+ if echo "$LAST" | grep -q "$(date -u +%Y-%m-%d)"; then
43
+ exit 0
44
+ fi
45
+ fi
46
+
47
+ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
48
+ LAST_DECISIONS=""
49
+ OPEN_RISKS=""
50
+
51
+ [ -f "$DECISIONS_FILE" ] && LAST_DECISIONS=$(tail -20 "$DECISIONS_FILE" 2>/dev/null)
52
+ [ -f "$RISKS_FILE" ] && OPEN_RISKS=$(grep -iE "open|active|unresolved" "$RISKS_FILE" 2>/dev/null | head -8)
53
+
54
+ cat >> "$CONTEXT_FILE" 2>/dev/null << EOF
55
+
56
+ ---
57
+ $MARKER at $TIMESTAMP]
58
+ Recent decisions (tail):
59
+ $LAST_DECISIONS
60
+
61
+ Open risks:
62
+ $OPEN_RISKS
63
+ ---
64
+ EOF
65
+
66
+ exit 0
@@ -0,0 +1,3 @@
1
+ @echo off
2
+ :: Windows wrapper for session-start (uses PowerShell)
3
+ powershell -ExecutionPolicy Bypass -File "%~dp0session-start"
@@ -0,0 +1,91 @@
1
+ #!/bin/bash
2
+ # fdeops SessionStart - load @fde + engagement context from FDE laptop path.
3
+
4
+ CONTEXT_FILE=""
5
+ BOOTSTRAP=""
6
+
7
+ for candidate in \
8
+ "${CLAUDE_PLUGIN_ROOT:+$CLAUDE_PLUGIN_ROOT/skills/fde/SKILL.md}" \
9
+ "$(dirname "$0")/../skills/fde/SKILL.md" \
10
+ "$HOME/.claude/skills/fde/SKILL.md"; do
11
+ if [ -n "$candidate" ] && [ -f "$candidate" ]; then
12
+ BOOTSTRAP="$candidate"
13
+ break
14
+ fi
15
+ done
16
+
17
+ resolve_engagement_dir() {
18
+ local raw="$1"
19
+ [ -z "$raw" ] && return 1
20
+ # strip surrounding whitespace/quotes only - paths may contain spaces
21
+ raw=$(printf '%s' "$raw" | sed -e 's/^[[:space:]"'"'"']*//' -e 's/[[:space:]"'"'"']*$//' -e "s|^~|$HOME|")
22
+ [ -d "$raw" ] && printf '%s\n' "$raw" && return 0
23
+ return 1
24
+ }
25
+
26
+ # 1) Environment variable (any agent)
27
+ if [ -z "$CONTEXT_FILE" ]; then
28
+ ENG_DIR=$(resolve_engagement_dir "${FDEOPS_ENGAGEMENT:-${FDEOS_ENGAGEMENT:-}}")
29
+ [ -n "$ENG_DIR" ] && CONTEXT_FILE="$ENG_DIR/context.md"
30
+ fi
31
+
32
+ # 2) Project CLAUDE.md
33
+ if [ -z "$CONTEXT_FILE" ] && [ -f "CLAUDE.md" ]; then
34
+ ENG=$(grep -m1 '^FDEOPS_ENGAGEMENT=\|^FDEOS_ENGAGEMENT=' CLAUDE.md 2>/dev/null | cut -d= -f2-)
35
+ ENG_DIR=$(resolve_engagement_dir "$ENG")
36
+ [ -n "$ENG_DIR" ] && CONTEXT_FILE="$ENG_DIR/context.md"
37
+ fi
38
+
39
+ # 3) Global fdeops pointer file
40
+ if [ -z "$CONTEXT_FILE" ] && [ -f "$HOME/.claude/FDEOPS-CLAUDE.md" ]; then
41
+ ENG=$(grep -m1 '^FDEOPS_ENGAGEMENT=\|^FDEOS_ENGAGEMENT=' "$HOME/.claude/FDEOPS-CLAUDE.md" 2>/dev/null | cut -d= -f2-)
42
+ ENG_DIR=$(resolve_engagement_dir "$ENG")
43
+ [ -n "$ENG_DIR" ] && CONTEXT_FILE="$ENG_DIR/context.md"
44
+ fi
45
+
46
+ # 4) Optional in-repo .fde (customer-approved only)
47
+ if [ -z "$CONTEXT_FILE" ] && [ -f ".fde/context.md" ]; then
48
+ CONTEXT_FILE=".fde/context.md"
49
+ fi
50
+
51
+ IS_FDE_PROJECT=0
52
+ [ -n "$CONTEXT_FILE" ] && [ -f "$CONTEXT_FILE" ] && IS_FDE_PROJECT=1
53
+ [ -f "CLAUDE.md" ] && grep -qi "fde\|fdeops" "CLAUDE.md" 2>/dev/null && IS_FDE_PROJECT=1
54
+ [ -n "${FDEOPS_ENGAGEMENT:-${FDEOS_ENGAGEMENT:-}}" ] && IS_FDE_PROJECT=1
55
+ [ -d ".fde" ] && IS_FDE_PROJECT=1
56
+
57
+ if [ "$IS_FDE_PROJECT" -eq 0 ]; then
58
+ exit 0
59
+ fi
60
+
61
+ # Token discipline: context.md grows every session (session-stop appends a
62
+ # snapshot). Inject a bounded view - curated head + most recent activity -
63
+ # instead of the whole log. Mirrors resumeView() in bin/fde.js; keep in sync.
64
+ bounded_context() {
65
+ local f="$1" total head_end tail_start hidden
66
+ total=$(wc -l < "$f" 2>/dev/null | tr -d ' ')
67
+ [ -z "$total" ] && { cat "$f"; return; }
68
+ if [ "$total" -le 160 ]; then cat "$f"; return; fi
69
+ head_end=$(grep -n -m1 'fdeops auto-capture' "$f" 2>/dev/null | cut -d: -f1)
70
+ if [ -n "$head_end" ]; then head_end=$((head_end - 1)); else head_end=120; fi
71
+ [ "$head_end" -gt 120 ] && head_end=120
72
+ [ "$head_end" -lt 0 ] && head_end=0
73
+ tail_start=$((total - 40 + 1))
74
+ if [ "$tail_start" -le "$((head_end + 1))" ]; then cat "$f"; return; fi
75
+ hidden=$((tail_start - head_end - 1))
76
+ [ "$head_end" -ge 1 ] && sed -n "1,${head_end}p" "$f"
77
+ printf '\n_(… %s lines of earlier session log hidden - `fde resume --full` or open context.md for the full history)_\n\n' "$hidden"
78
+ sed -n "${tail_start},\$p" "$f"
79
+ }
80
+
81
+ CONTENT=""
82
+
83
+ if [ -n "$BOOTSTRAP" ]; then
84
+ CONTENT="$CONTENT$(cat "$BOOTSTRAP")\n\n"
85
+ fi
86
+
87
+ if [ -n "$CONTEXT_FILE" ] && [ -f "$CONTEXT_FILE" ]; then
88
+ CONTENT="$CONTENT---\nEngagement context ($CONTEXT_FILE):\n$(bounded_context "$CONTEXT_FILE")\n"
89
+ fi
90
+
91
+ printf '%b' "$CONTENT"
@@ -0,0 +1,86 @@
1
+ #!/bin/bash
2
+ # fdeops SessionEnd - write-side memory backstop.
3
+ #
4
+ # The skill's memory contract says the agent appends a meaningful
5
+ # "where we left off" to context.md before the session ends. This hook
6
+ # guarantees a deterministic floor when it doesn't: date, workspace state,
7
+ # and which engagement artifacts moved. Next session-start loads it back.
8
+ #
9
+ # Deliberately dumb: no model calls, no network, append-only, exits silently.
10
+
11
+ cat >/dev/null 2>&1 || true # drain hook stdin; payload unused
12
+
13
+ resolve_engagement_dir() {
14
+ local raw="$1"
15
+ [ -z "$raw" ] && return 1
16
+ # strip surrounding whitespace/quotes only - paths may contain spaces
17
+ raw=$(printf '%s' "$raw" | sed -e 's/^[[:space:]"'"'"']*//' -e 's/[[:space:]"'"'"']*$//' -e "s|^~|$HOME|")
18
+ [ -d "$raw" ] && printf '%s\n' "$raw" && return 0
19
+ return 1
20
+ }
21
+
22
+ ENG_DIR=""
23
+
24
+ # 1) Environment variable (any agent)
25
+ ENG_DIR=$(resolve_engagement_dir "${FDEOPS_ENGAGEMENT:-${FDEOS_ENGAGEMENT:-}}")
26
+
27
+ # 2) Project CLAUDE.md
28
+ if [ -z "$ENG_DIR" ] && [ -f "CLAUDE.md" ]; then
29
+ ENG=$(grep -m1 '^FDEOPS_ENGAGEMENT=\|^FDEOS_ENGAGEMENT=' CLAUDE.md 2>/dev/null | cut -d= -f2-)
30
+ ENG_DIR=$(resolve_engagement_dir "$ENG")
31
+ fi
32
+
33
+ # 3) Global fdeops pointer file
34
+ if [ -z "$ENG_DIR" ] && [ -f "$HOME/.claude/FDEOPS-CLAUDE.md" ]; then
35
+ ENG=$(grep -m1 '^FDEOPS_ENGAGEMENT=\|^FDEOS_ENGAGEMENT=' "$HOME/.claude/FDEOPS-CLAUDE.md" 2>/dev/null | cut -d= -f2-)
36
+ ENG_DIR=$(resolve_engagement_dir "$ENG")
37
+ fi
38
+
39
+ # 4) Optional in-repo .fde (customer-approved only)
40
+ if [ -z "$ENG_DIR" ] && [ -d ".fde" ]; then
41
+ ENG_DIR=".fde"
42
+ fi
43
+
44
+ [ -z "$ENG_DIR" ] && exit 0
45
+ CONTEXT_FILE="$ENG_DIR/context.md"
46
+
47
+ TODAY=$(date +%F)
48
+ NOW=$(date +%H:%M)
49
+
50
+ # Workspace state (only if the cwd is a git repo)
51
+ BRANCH=$(git -C "$PWD" branch --show-current 2>/dev/null)
52
+ LAST_COMMIT=$(git -C "$PWD" log -1 --format="%h %s" 2>/dev/null | head -c 100)
53
+ CHANGED=$(git -C "$PWD" status --porcelain 2>/dev/null | head -8 | sed 's/^...//' | tr '\n' ' ')
54
+
55
+ # Engagement artifacts updated in the last 12h (the deliverable=memory trail)
56
+ UPDATED=$(find "$ENG_DIR" -maxdepth 1 -name "*.md" ! -name "context.md" -mmin -720 2>/dev/null \
57
+ | while read -r f; do basename "$f"; done | tr '\n' ' ')
58
+
59
+ # Idle session (no edits, no artifact updates) → leave the memory clean.
60
+ # LAST_COMMIT alone is not "movement": it exists in any repo with history.
61
+ [ -z "$CHANGED" ] && [ -z "$UPDATED" ] && exit 0
62
+
63
+ {
64
+ printf '\n<!-- fdeops auto-capture -->\n'
65
+ printf '## Session end - %s %s\n' "$TODAY" "$NOW"
66
+ [ -n "$BRANCH" ] && printf -- '- workspace: `%s` @ %s\n' "$BRANCH" "${LAST_COMMIT:-no commits yet}"
67
+ [ -n "$CHANGED" ] && printf -- '- uncommitted: %s\n' "$CHANGED"
68
+ [ -n "$UPDATED" ] && printf -- '- engagement files updated: %s\n' "$UPDATED"
69
+ } 2>/dev/null >> "$CONTEXT_FILE" || true
70
+
71
+ # Refresh the local fieldbook.html so the portfolio view is current next time
72
+ # it's opened. Deterministic render of .fde/ - zero tokens, best-effort, never
73
+ # allowed to break the session.
74
+ if command -v node >/dev/null 2>&1; then
75
+ for FDE_CLI in \
76
+ "${CLAUDE_PLUGIN_ROOT:+$CLAUDE_PLUGIN_ROOT/bin/fde.js}" \
77
+ "$(dirname "$0")/../bin/fde.js" \
78
+ "$HOME/.claude/fdeops/fde.js"; do
79
+ if [ -n "$FDE_CLI" ] && [ -f "$FDE_CLI" ]; then
80
+ node "$FDE_CLI" dashboard >/dev/null 2>&1 || true
81
+ break
82
+ fi
83
+ done
84
+ fi
85
+
86
+ exit 0
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "fdeops",
3
+ "version": "3.5.0",
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
+ "bin": {
6
+ "fdeops": "bin/install.js",
7
+ "fde": "bin/fde.js"
8
+ },
9
+ "scripts": {
10
+ "check": "node bin/check.js",
11
+ "prepublishOnly": "node bin/check.js"
12
+ },
13
+ "files": [
14
+ "bin/",
15
+ "skills/",
16
+ "hooks/",
17
+ "templates/",
18
+ "adapters/",
19
+ "CLAUDE.md.template",
20
+ "AGENTS.md"
21
+ ],
22
+ "keywords": [
23
+ "claude-code",
24
+ "claude-code-plugin",
25
+ "ai-agent",
26
+ "skills",
27
+ "plugin",
28
+ "fde",
29
+ "forward-deployed",
30
+ "forward-deployed-engineer",
31
+ "engineering",
32
+ "client-site",
33
+ "legacy-code",
34
+ "brownfield",
35
+ "cursor",
36
+ "windsurf",
37
+ "cline",
38
+ "codex",
39
+ "gemini-cli",
40
+ "copilot",
41
+ "agents-md"
42
+ ],
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/suboss87/fdeops.git"
46
+ },
47
+ "author": "Subash Natarajan <suboss87@gmail.com> (https://github.com/suboss87)",
48
+ "license": "MIT",
49
+ "engines": {
50
+ "node": ">=18"
51
+ }
52
+ }