fdeops 3.7.1 → 3.7.3
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 +1 -1
- package/README.md +19 -3
- package/bin/fde.js +30 -11
- package/hooks/pre-compact +25 -4
- package/hooks/session-start +33 -1
- package/package.json +4 -3
- package/skills/fde/SKILL.md +1 -1
package/AGENTS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AGENTS.md - working in the fdeops repository
|
|
2
2
|
|
|
3
|
-
This repository **is** fdeops - the
|
|
3
|
+
This repository **is** fdeops - the second brain for Forward Deployed Engineers. One `@fde` skill routes an entire client engagement across six domains, the `fde` CLI does the deterministic work, and per-customer memory writes itself into `.fde/` files.
|
|
4
4
|
|
|
5
5
|
## If you are helping use fdeops in an engagement
|
|
6
6
|
|
package/README.md
CHANGED
|
@@ -27,6 +27,8 @@ Your AI agent's memory is scoped to a **repo**. Client work isn't: one engagemen
|
|
|
27
27
|
|
|
28
28
|
fdeops adds the missing layer: memory scoped to the **client** - plain markdown at `~/fde-engagements/<client>/.fde/`, written as a side effect of doing the work. Local only, zero dependencies, no network, no telemetry.
|
|
29
29
|
|
|
30
|
+
A notes app stores what you type. fdeops loads the right client into your AI agent's context automatically and turns meetings into dated receipts you can defend - the difference is what happens without you opening it.
|
|
31
|
+
|
|
30
32
|
## Without fdeops vs with fdeops
|
|
31
33
|
|
|
32
34
|
| Moment | Without fdeops | With fdeops |
|
|
@@ -51,9 +53,11 @@ fdeops adds the missing layer: memory scoped to the **client** - plain markdown
|
|
|
51
53
|
**2. Bind your client workspace** - run once, inside the workspace:
|
|
52
54
|
|
|
53
55
|
```bash
|
|
54
|
-
|
|
56
|
+
npx fdeops resume --init garvey
|
|
55
57
|
```
|
|
56
58
|
|
|
59
|
+
(`npx` needs nothing pre-installed. Want the bare `fde` command the rest of this README uses? `npm i -g fdeops` - the plugin install alone does not put `fde` on your PATH.)
|
|
60
|
+
|
|
57
61
|
fdeops' `--init` creates the engagement memory at `~/fde-engagements/garvey/.fde/` (plain markdown, private to your machine) and binds this workspace to it. The hooks read that binding - context auto-loads at session start, auto-captures at session end. That is the whole setup.
|
|
58
62
|
|
|
59
63
|
**3. Work**
|
|
@@ -64,7 +68,7 @@ fdeops' `--init` creates the engagement memory at `~/fde-engagements/garvey/.fde
|
|
|
64
68
|
|
|
65
69
|
`@fde` is the one skill fdeops installs. Describe what's happening; it routes to the right field method and the memory writes itself. Full workflow: [docs/USAGE.md](docs/USAGE.md).
|
|
66
70
|
|
|
67
|
-
Not ready to install? `npx fdeops scan` runs on any repo you can read - day-1 recon (pure `git` + file reads, no config, no account) that maps hotspots, test gaps, and reverted attempts, and ends with the ASK ON DAY 1 questions the brief never mentions.
|
|
71
|
+
Not ready to install? `npx fdeops scan` runs on any repo you can read - day-1 recon (pure `git` + file reads, no config, no account) that maps hotspots, test gaps, and reverted attempts, and ends with the ASK ON DAY 1 questions the brief never mentions. The scan is heuristic by design - treat its output as leads to verify on day one, not findings.
|
|
68
72
|
|
|
69
73
|
<details>
|
|
70
74
|
<summary><strong>Other install paths</strong> - Cursor, Codex, Copilot, Gemini CLI, local LLMs, air-gapped</summary>
|
|
@@ -80,6 +84,17 @@ Not ready to install? `npx fdeops scan` runs on any repo you can read - day-1 re
|
|
|
80
84
|
|
|
81
85
|
---
|
|
82
86
|
|
|
87
|
+
## The week
|
|
88
|
+
|
|
89
|
+
This is the actual habit, not the 35 skills:
|
|
90
|
+
|
|
91
|
+
- **Monday morning** - open your agent, context loads, you're not re-explaining anything
|
|
92
|
+
- **After a meeting** - `fde debrief` turns raw notes into dated decisions, risks, and signals
|
|
93
|
+
- **Mid-scope-fight** - `fde receipts <term>` answers "when did we agree to that?"
|
|
94
|
+
- **Friday** - `fde status` gives you the sponsor update from the week's actual record
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
83
98
|
## How it works
|
|
84
99
|
|
|
85
100
|
Two hooks and one router, on top of the fieldbook:
|
|
@@ -100,7 +115,7 @@ Two hooks and one router, on top of the fieldbook:
|
|
|
100
115
|
|
|
101
116
|
Overlays for regulated domains (AI, fintech, healthcare, government) activate on signal. fdeops complements your agent's native repo memory: CLAUDE.md holds how the *code* works; the fieldbook holds how the *engagement* works. Full matrix: [docs/skills.md](docs/skills.md).
|
|
102
117
|
|
|
103
|
-
Works with **Claude Code** - **Cursor** - **Copilot** - **
|
|
118
|
+
Works with **Claude Code** - **Cursor** - **Copilot** - **Gemini CLI** - **Ollama** - **LM Studio** - any model that reads a markdown file.
|
|
104
119
|
|
|
105
120
|
---
|
|
106
121
|
|
|
@@ -174,6 +189,7 @@ Details: [PRIVACY.md](PRIVACY.md) · [SECURITY.md](SECURITY.md)
|
|
|
174
189
|
## Principles
|
|
175
190
|
|
|
176
191
|
- **The artifact is the memory** - producing work and recording it are one action
|
|
192
|
+
- **Methods, not autonomy** - each skill tells you what to check; the judgment, the trust, and the consequences stay yours
|
|
177
193
|
- **Trust before production** - earn the right to touch their systems
|
|
178
194
|
- **Brief is a hypothesis** - discover before building the wrong thing
|
|
179
195
|
- **Evidence on every claim** - these files get defended in front of skeptical clients
|
package/bin/fde.js
CHANGED
|
@@ -111,9 +111,17 @@ function resolveEngagement() {
|
|
|
111
111
|
}
|
|
112
112
|
} catch (_) {}
|
|
113
113
|
}
|
|
114
|
-
// 4) workspace dir name matches an engagement slug
|
|
114
|
+
// 4) workspace dir name matches an engagement slug. This is a convenience,
|
|
115
|
+
// NOT a binding - an unbound directory that merely happens to be named like a
|
|
116
|
+
// client (a fork, a demo, a second client with the same codename) would
|
|
117
|
+
// otherwise attach to that client's memory silently and get written into.
|
|
118
|
+
// Never silent: warn on stderr so cross-client contamination can't happen
|
|
119
|
+
// unnoticed, and tell the user how to make the binding explicit.
|
|
115
120
|
const guess = path.join(ENGAGEMENTS_ROOT, slugify(path.basename(cwd)), '.fde')
|
|
116
|
-
if (fs.existsSync(guess))
|
|
121
|
+
if (fs.existsSync(guess)) {
|
|
122
|
+
process.stderr.write(`⚠ resolved engagement by directory name ("${slugify(path.basename(cwd))}"), not a saved binding. If this is the right client, run \`fde resume --init ${slugify(path.basename(cwd))}\` here to bind it; if not, you are about to read/write the WRONG client's memory.\n`)
|
|
123
|
+
return guess
|
|
124
|
+
}
|
|
117
125
|
// 5) in-repo .fde (engagement-approved only)
|
|
118
126
|
if (fs.existsSync(path.join(cwd, '.fde'))) return path.join(cwd, '.fde')
|
|
119
127
|
return null
|
|
@@ -157,7 +165,9 @@ function sectionBody(md, heading) {
|
|
|
157
165
|
// zero-effort floor when NO token exists anywhere - prose like "escalated to CTO,
|
|
158
166
|
// resolved amicably" must not flip a client amber forever.
|
|
159
167
|
function computeSignals(eng) {
|
|
160
|
-
|
|
168
|
+
// readClean, not readEng: status/dashboard echo topRisk and stakeholder lines
|
|
169
|
+
// to the terminal and the rendered HTML - a <private> risk must never surface.
|
|
170
|
+
const ctx = readClean(eng, 'context.md'); const stake = readClean(eng, 'stakeholders.md'); const risks = readClean(eng, 'risks.md')
|
|
161
171
|
const phase = (ctx.match(/phase[:* ]+\**([a-z-]+)/i) || [])[1] || '?'
|
|
162
172
|
let latest = null
|
|
163
173
|
for (const l of stake.split('\n')) {
|
|
@@ -463,7 +473,7 @@ function cmdScan() {
|
|
|
463
473
|
const sec = grepFiles(confFiles, /(api[_-]?key|secret|password|token)\s*[:=]\s*['"][^'"]{8,}/i, 10)
|
|
464
474
|
.filter(h => !/example|template|test|sample|placeholder/i.test(h.file + h.text))
|
|
465
475
|
sec.length
|
|
466
|
-
? sec.forEach(h => out.push(` ${h.file}:${h.line} ${h.text.replace(/(['"])
|
|
476
|
+
? sec.forEach(h => out.push(` ${h.file}:${h.line} ${h.text.replace(/(['"])[^'"]+(['"])/, '$1REDACTED$2')}`))
|
|
467
477
|
: out.push(' none found')
|
|
468
478
|
out.push(' (grep-grade check - run gitleaks or trufflehog for real secret coverage)')
|
|
469
479
|
|
|
@@ -547,10 +557,11 @@ function cmdResume(args) {
|
|
|
547
557
|
process.exit(2)
|
|
548
558
|
}
|
|
549
559
|
console.log(`ENGAGEMENT: ${eng}\n`)
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
560
|
+
// readClean, not fs.readFileSync: this output is what an agent loads as
|
|
561
|
+
// context, so it goes through the same <private> redaction as the dashboard.
|
|
562
|
+
const ctx = readClean(eng, 'context.md')
|
|
563
|
+
if (!ctx) { console.log('(context.md empty - new engagement)') }
|
|
564
|
+
else { console.log(args.includes('--full') ? ctx : resumeView(ctx)) }
|
|
554
565
|
}
|
|
555
566
|
|
|
556
567
|
// Token discipline: context.md grows every session (the session-stop hook
|
|
@@ -660,7 +671,10 @@ function cmdReceipts(args) {
|
|
|
660
671
|
for (const f of ['decisions.md', 'delivery.md', 'risks.md', 'stakeholders.md', 'context.md', 'success.md', 'brief.md', 'reality.md']) {
|
|
661
672
|
const p = path.join(eng, f)
|
|
662
673
|
if (!fs.existsSync(p)) continue
|
|
663
|
-
|
|
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) => {
|
|
664
678
|
if (rx.test(l)) { console.log(`${f}:${i + 1} ${l.trim().slice(0, 160)}`); found++ }
|
|
665
679
|
})
|
|
666
680
|
}
|
|
@@ -724,9 +738,14 @@ function escapeHtml(s) {
|
|
|
724
738
|
// Closed pairs are redacted; an unclosed <private> redacts to end-of-text so a
|
|
725
739
|
// forgotten closing tag can never leak the rest of the file.
|
|
726
740
|
function stripPrivate(md) {
|
|
741
|
+
// Redact <private> before <!-- --> so a private block that itself contains a
|
|
742
|
+
// comment can't survive. Closed pairs first; then any unclosed <private> to
|
|
743
|
+
// end-of-text so a forgotten close tag never leaks the rest of the file.
|
|
744
|
+
// Case-insensitive (<PRIVATE> too). This is the ONE redactor every read path
|
|
745
|
+
// that can reach the model or a shared artifact must go through - see readClean.
|
|
727
746
|
return md
|
|
728
|
-
.replace(/<private>[\s\S]*?<\/private>/gi, '(private - redacted
|
|
729
|
-
.replace(/<private>[\s\S]*$/i, '(private - redacted
|
|
747
|
+
.replace(/<private>[\s\S]*?<\/private>/gi, '(private - redacted)')
|
|
748
|
+
.replace(/<private>[\s\S]*$/i, '(private - redacted)')
|
|
730
749
|
.replace(/<!--[\s\S]*?-->/g, '')
|
|
731
750
|
}
|
|
732
751
|
|
package/hooks/pre-compact
CHANGED
|
@@ -56,19 +56,40 @@ MARKER="[fdeops context preserved"
|
|
|
56
56
|
[ -f "$CONTEXT_FILE" ] || exit 0
|
|
57
57
|
|
|
58
58
|
# Avoid unbounded growth: skip if we already preserved today.
|
|
59
|
-
if grep -
|
|
60
|
-
LAST=$(grep "$MARKER" "$CONTEXT_FILE" | tail -1)
|
|
59
|
+
if grep -Fq "$MARKER" "$CONTEXT_FILE" 2>/dev/null; then
|
|
60
|
+
LAST=$(grep -F "$MARKER" "$CONTEXT_FILE" | tail -1)
|
|
61
61
|
if echo "$LAST" | grep -q "$(date -u +%Y-%m-%d)"; then
|
|
62
62
|
exit 0
|
|
63
63
|
fi
|
|
64
64
|
fi
|
|
65
65
|
|
|
66
|
+
# Redact <private> before extracting lines - this content is appended to
|
|
67
|
+
# context.md, which session-start loads into the model. A closed private block
|
|
68
|
+
# collapses to one placeholder line, so its inner text can't be tail'd or
|
|
69
|
+
# grepped out tag-stripped. Case-insensitive, mirrors the JS /gi redactor.
|
|
70
|
+
strip_private() {
|
|
71
|
+
awk '
|
|
72
|
+
BEGIN { inblock = 0 }
|
|
73
|
+
{
|
|
74
|
+
line = $0
|
|
75
|
+
lc = tolower(line)
|
|
76
|
+
if (inblock) { if (index(lc, "</private>") > 0) { inblock = 0 } next }
|
|
77
|
+
if (index(lc, "<private>") > 0) {
|
|
78
|
+
print "(private - redacted)"
|
|
79
|
+
if (index(lc, "</private>") == 0) { inblock = 1 }
|
|
80
|
+
next
|
|
81
|
+
}
|
|
82
|
+
print line
|
|
83
|
+
}
|
|
84
|
+
' "$1"
|
|
85
|
+
}
|
|
86
|
+
|
|
66
87
|
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
67
88
|
LAST_DECISIONS=""
|
|
68
89
|
OPEN_RISKS=""
|
|
69
90
|
|
|
70
|
-
[ -f "$DECISIONS_FILE" ] && LAST_DECISIONS=$(
|
|
71
|
-
[ -f "$RISKS_FILE" ] && OPEN_RISKS=$(grep -iE "open|active|unresolved"
|
|
91
|
+
[ -f "$DECISIONS_FILE" ] && LAST_DECISIONS=$(strip_private "$DECISIONS_FILE" 2>/dev/null | tail -20)
|
|
92
|
+
[ -f "$RISKS_FILE" ] && OPEN_RISKS=$(strip_private "$RISKS_FILE" 2>/dev/null | grep -iE "open|active|unresolved" | head -8)
|
|
72
93
|
|
|
73
94
|
cat >> "$CONTEXT_FILE" 2>/dev/null << EOF
|
|
74
95
|
|
package/hooks/session-start
CHANGED
|
@@ -79,6 +79,35 @@ if [ "$IS_FDE_PROJECT" -eq 0 ]; then
|
|
|
79
79
|
exit 0
|
|
80
80
|
fi
|
|
81
81
|
|
|
82
|
+
# Mirrors stripPrivate() in bin/fde.js: this is what an agent loads as
|
|
83
|
+
# context, so <private>...</private> blocks (sacred data, never-AI content)
|
|
84
|
+
# must never reach here unredacted, same guarantee as the dashboard renderer.
|
|
85
|
+
# <private> blocks are written one per line in the templates (own line, own
|
|
86
|
+
# closing tag), so a line-based state machine matches the real usage; an
|
|
87
|
+
# unclosed <private> redacts to end-of-file, same as the JS regex fallback.
|
|
88
|
+
strip_private() {
|
|
89
|
+
# Detection is case-insensitive (matches the JS /gi redactor) - <PRIVATE>,
|
|
90
|
+
# <Private>, <private> all redact. Detect on a lowercased copy of the line;
|
|
91
|
+
# never print the original when a tag is present.
|
|
92
|
+
awk '
|
|
93
|
+
BEGIN { inblock = 0 }
|
|
94
|
+
{
|
|
95
|
+
line = $0
|
|
96
|
+
lc = tolower(line)
|
|
97
|
+
if (inblock) {
|
|
98
|
+
if (index(lc, "</private>") > 0) { inblock = 0 }
|
|
99
|
+
next
|
|
100
|
+
}
|
|
101
|
+
if (index(lc, "<private>") > 0) {
|
|
102
|
+
print "(private - redacted)"
|
|
103
|
+
if (index(lc, "</private>") == 0) { inblock = 1 }
|
|
104
|
+
next
|
|
105
|
+
}
|
|
106
|
+
print line
|
|
107
|
+
}
|
|
108
|
+
' "$1"
|
|
109
|
+
}
|
|
110
|
+
|
|
82
111
|
# Token discipline: context.md grows every session (session-stop appends a
|
|
83
112
|
# snapshot). Inject a bounded view - curated head + most recent activity -
|
|
84
113
|
# instead of the whole log. Mirrors resumeView() in bin/fde.js; keep in sync.
|
|
@@ -106,7 +135,10 @@ if [ -n "$BOOTSTRAP" ]; then
|
|
|
106
135
|
fi
|
|
107
136
|
|
|
108
137
|
if [ -n "$CONTEXT_FILE" ] && [ -f "$CONTEXT_FILE" ]; then
|
|
109
|
-
|
|
138
|
+
REDACTED_CONTEXT=$(mktemp)
|
|
139
|
+
strip_private "$CONTEXT_FILE" > "$REDACTED_CONTEXT"
|
|
140
|
+
CONTENT="$CONTENT---\nEngagement context ($CONTEXT_FILE):\n$(bounded_context "$REDACTED_CONTEXT")\n"
|
|
141
|
+
rm -f "$REDACTED_CONTEXT"
|
|
110
142
|
fi
|
|
111
143
|
|
|
112
144
|
printf '%b' "$CONTENT"
|
package/package.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fdeops",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.3",
|
|
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",
|
|
7
7
|
"fde": "bin/fde.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"check": "node bin/check.js",
|
|
11
|
-
"
|
|
10
|
+
"check": "node bin/check.js && npm test",
|
|
11
|
+
"test": "node --test test/*.test.js",
|
|
12
|
+
"prepublishOnly": "node bin/check.js && npm test"
|
|
12
13
|
},
|
|
13
14
|
"files": [
|
|
14
15
|
"bin/",
|
package/skills/fde/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: fde
|
|
3
|
-
description: The
|
|
3
|
+
description: The second brain for Forward Deployed Engineers. 35 skills across 6 domains - from first meeting to final handoff. Tell it your situation, it routes to the right skill, does the work, and the engagement memory writes itself.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# @fde
|