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.
- package/AGENTS.md +18 -0
- package/CLAUDE.md.template +25 -0
- package/LICENSE +21 -0
- package/README.md +328 -0
- package/adapters/AGENTS.md +24 -0
- package/adapters/GEMINI.md +24 -0
- package/adapters/README.md +31 -0
- package/adapters/copilot-instructions.md +24 -0
- package/adapters/cursor.fde.mdc +29 -0
- package/bin/check.js +260 -0
- package/bin/fde.js +690 -0
- package/bin/install.js +220 -0
- package/hooks/hooks.json +40 -0
- package/hooks/pre-compact +66 -0
- package/hooks/run-hook.cmd +3 -0
- package/hooks/session-start +91 -0
- package/hooks/session-stop +86 -0
- package/package.json +52 -0
- package/skills/fde/SKILL.md +219 -0
- package/skills/fde/references/ai.md +91 -0
- package/skills/fde/references/artifacts.md +247 -0
- package/skills/fde/references/assumption-audit.md +77 -0
- package/skills/fde/references/audit.md +61 -0
- package/skills/fde/references/blast-radius.md +91 -0
- package/skills/fde/references/build.md +98 -0
- package/skills/fde/references/business-case.md +78 -0
- package/skills/fde/references/close.md +43 -0
- package/skills/fde/references/dashboard.md +40 -0
- package/skills/fde/references/debrief.md +36 -0
- package/skills/fde/references/debug.md +55 -0
- package/skills/fde/references/demo-prep.md +31 -0
- package/skills/fde/references/discover.md +163 -0
- package/skills/fde/references/exec-narrative.md +108 -0
- package/skills/fde/references/fintech.md +48 -0
- package/skills/fde/references/gov.md +47 -0
- package/skills/fde/references/handoff-engineering.md +139 -0
- package/skills/fde/references/healthcare.md +45 -0
- package/skills/fde/references/incremental-build.md +91 -0
- package/skills/fde/references/initiative-triage.md +78 -0
- package/skills/fde/references/land.md +75 -0
- package/skills/fde/references/multi-customer-ops.md +114 -0
- package/skills/fde/references/observability.md +103 -0
- package/skills/fde/references/options-analysis.md +81 -0
- package/skills/fde/references/pattern-extract.md +93 -0
- package/skills/fde/references/plan.md +108 -0
- package/skills/fde/references/qa-live.md +113 -0
- package/skills/fde/references/rescue.md +81 -0
- package/skills/fde/references/review.md +53 -0
- package/skills/fde/references/rollback-drill.md +102 -0
- package/skills/fde/references/scope-defense.md +71 -0
- package/skills/fde/references/security-audit.md +105 -0
- package/skills/fde/references/ship.md +121 -0
- package/skills/fde/references/sketch.md +40 -0
- package/skills/fde/references/stakeholder-radar.md +68 -0
- package/skills/fde/references/status.md +30 -0
- package/skills/fde/references/test-on-legacy.md +108 -0
- package/skills/fde/references/trust-engineering.md +100 -0
- package/skills/fde/references/use-case-scoring.md +70 -0
- package/templates/.fde/README.md +13 -0
- package/templates/.fde/brief.md +8 -0
- package/templates/.fde/context.md +14 -0
- package/templates/.fde/decisions.md +18 -0
- package/templates/.fde/delivery.md +7 -0
- package/templates/.fde/reality.md +7 -0
- package/templates/.fde/retrospectives/.gitkeep +0 -0
- package/templates/.fde/risks.md +5 -0
- package/templates/.fde/stakeholders.md +10 -0
- package/templates/.fde/success.md +7 -0
- package/templates/.fde/terrain.md +7 -0
- 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
|
+
}
|
package/hooks/hooks.json
ADDED
|
@@ -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,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
|
+
}
|