fdeops 3.9.19 → 3.10.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/README.md +45 -7
- package/adapters/LOCAL-LLM.md +1 -1
- package/adapters/README.md +1 -1
- package/bin/check.js +219 -4
- package/bin/fde.js +379 -26
- package/bin/install.js +223 -20
- package/bin/lib/memory.js +2 -2
- package/bin/lib/render.js +2 -0
- package/mcp/README.md +5 -3
- package/mcp/fdeops-ingest/README.md +3 -1
- package/mcp/fdeops-ingest/package.json +1 -1
- package/mcp/fdeops-ingest/server.js +27 -17
- package/mcp/recipes/README.md +15 -0
- package/mcp/recipes/file.md +25 -0
- package/mcp/recipes/granola.md +58 -0
- package/mcp/recipes/notion.md +56 -0
- package/mcp.json +10 -0
- package/package.json +4 -2
- package/plugin.json +21 -0
- package/skills/fde/SKILL.md +4 -2
- package/skills/fde/references/assumption-audit.md +10 -0
- package/skills/fde/references/build.md +13 -1
- package/skills/fde/references/business-case.md +10 -0
- package/skills/fde/references/close.md +11 -1
- package/skills/fde/references/discover.md +10 -0
- package/skills/fde/references/ingest-connect.md +36 -0
- package/skills/fde/references/ingest.md +13 -3
- package/skills/fde/references/land.md +20 -0
- package/skills/fde/references/options-analysis.md +10 -0
- package/skills/fde/references/plan.md +10 -0
- package/skills/fde/references/scope-defense.md +10 -0
- package/skills/fde/references/ship.md +10 -0
- package/skills/fde/references/stakeholder-radar.md +21 -0
- package/skills/fde/references/status.md +12 -2
- package/templates/.fde/delivery.md +3 -3
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[](LICENSE)
|
|
8
8
|
[](https://nodejs.org)
|
|
9
9
|
|
|
10
|
-
**Memory + methodology + skills, in one kit.** Skill packs teach your AI agent how to build
|
|
10
|
+
**Memory + methodology + skills, in one kit.** Skill packs - BMAD, Spec-Kit, [mattpocock/skills](https://github.com/mattpocock/skills) - teach your AI agent how to *build*. None of them remember who the client is, what you promised them, or who agreed it was delivered. FDEOps adds the missing layer: a private fieldbook per engagement (`.fde/`), a field methodology (land → close), and one `@fde` skill that routes it all.
|
|
11
11
|
|
|
12
12
|
Built for Forward Deployed Engineers, and anyone embedded in client work: consultants, agency developers, solutions architects, fractional CTOs. Feels like a second brain; behaves like a defensible record (dated, sourced, yours).
|
|
13
13
|
|
|
@@ -31,6 +31,7 @@ Day to day you only need `@fde` and normal English. No command cheat sheet.
|
|
|
31
31
|
|------|--------------|--------------|
|
|
32
32
|
| **Start of week** | Open your AI coding agent (nothing to paste) | It already knows where you left off - trust, phase, what's next |
|
|
33
33
|
| **After a meeting** | `@fde` debrief these notes *(paste or attach them)* | Proposed updates to the record - you review, then confirm |
|
|
34
|
+
| **Pull from tools** | `@fde` connect Granola *(once)* · then `@fde` pull today's Acme transcript | Wire any source MCP you choose; FDEOps stages → proposes → you confirm. Recipes: [mcp/recipes/](mcp/recipes/) |
|
|
34
35
|
| **Before a stakeholder meeting** | `@fde` prep me for tomorrow's meeting with the sponsor | A short brief from what you already logged - not a blank chat |
|
|
35
36
|
| **Someone disputes scope** | `@fde` when did we agree to drop that feature? | Dated answers from the record (or a clear gap if nothing was logged) |
|
|
36
37
|
| **End of week** | `@fde` draft the sponsor update from the record | Status grounded in what actually happened |
|
|
@@ -39,19 +40,35 @@ Same client folder every time (`~/fde-engagements/<client>/.fde/`). Your AI codi
|
|
|
39
40
|
|
|
40
41
|
---
|
|
41
42
|
|
|
42
|
-
##
|
|
43
|
-
|
|
44
|
-
**1. Install** (pick one)
|
|
43
|
+
## See it in 60 seconds (no client, no install)
|
|
45
44
|
|
|
46
45
|
```bash
|
|
47
|
-
npx
|
|
46
|
+
npx fdeops demo
|
|
48
47
|
```
|
|
49
48
|
|
|
49
|
+
Walks one fake engagement end to end with the **real** commands: messy kickoff notes → proposed updates → you confirm → cold session reload → meeting prep → dated receipts → the fieldbook page. Nothing of yours is read, nothing leaves the machine, and it lives in a throwaway `~/fde-engagements/.demo/` that never appears in your portfolio. Remove it with `npx fdeops demo --clean`.
|
|
50
|
+
|
|
51
|
+
Or watch it first. One real session below - day 1 kickoff notes, the next morning's cold start, and "when did we agree to that?" six weeks later. Every line is the CLI's own output; only the typing pace is staged, and you can re-record it yourself with [`media/record-session.sh`](media/record-session.sh):
|
|
52
|
+
|
|
53
|
+
<p align="center"><img alt="A real fdeops session: messy kickoff notes routed into dated memory after you confirm, then a cold session that already knows the client, a grounded sponsor-meeting brief, and dated receipts" src="media/session.gif" width="900" /></p>
|
|
54
|
+
|
|
55
|
+
Note the two things a chat window cannot do: **nothing is written until you confirm the routing**, and the `<private>` block in those notes lands sealed on disk as `(private - redacted)` - it never appears in `resume`, `prep`, `receipts`, or the dashboard.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Quickstart
|
|
60
|
+
|
|
61
|
+
**1. Install.** Claude Code gets session hooks (context loads before you type); everywhere else the same fieldbook loads when you ask.
|
|
62
|
+
|
|
50
63
|
```text
|
|
51
64
|
/plugin marketplace add suboss87/fdeops # Claude Code
|
|
52
65
|
/plugin install fdeops@fdeops
|
|
53
66
|
```
|
|
54
67
|
|
|
68
|
+
```bash
|
|
69
|
+
npx skills add suboss87/fdeops # Cursor, Codex, and skills-compatible agents
|
|
70
|
+
```
|
|
71
|
+
|
|
55
72
|
**2. Bind once** - inside the client workspace (setup only; not a daily habit):
|
|
56
73
|
|
|
57
74
|
```bash
|
|
@@ -72,13 +89,15 @@ npx fdeops resume # prints a short "where we are" for this clien
|
|
|
72
89
|
|
|
73
90
|
`@fde` routes and updates the fieldbook - you confirm judgment. Full workflow: [docs/USAGE.md](docs/USAGE.md).
|
|
74
91
|
|
|
92
|
+
**It's working if** `npx fdeops resume` prints this client's phase, trust signal, and next action - and tomorrow's session starts from that instead of a blank chat.
|
|
93
|
+
|
|
75
94
|
<details>
|
|
76
95
|
<summary><strong>Other install paths</strong> · scan · env</summary>
|
|
77
96
|
|
|
78
97
|
- **Cursor / Codex / Copilot / Gemini CLI:** `npx fdeops adapters .` - [adapters/](adapters/README.md)
|
|
79
98
|
- **Local LLMs (Ollama, LM Studio, llama.cpp):** load `skills/fde/SKILL.md` as the system prompt - [guide](adapters/LOCAL-LLM.md)
|
|
80
99
|
- **Manual / air-gapped:** `git clone https://github.com/suboss87/fdeops.git && cd fdeops && node bin/install.js`
|
|
81
|
-
- **Try without install:** `npx fdeops scan` - day-1 recon (heuristic leads, not findings)
|
|
100
|
+
- **Try without install:** `npx fdeops demo` - the whole loop on a fake client · `npx fdeops scan` - day-1 recon of this repo (heuristic leads, not findings)
|
|
82
101
|
- **Requires:** [Node.js](https://nodejs.org) >= 18 for the CLI and adapters
|
|
83
102
|
- **Advanced:** `FDEOPS_ENGAGEMENT` overrides the workspace registry. Full matrix: [docs/install.md](docs/install.md)
|
|
84
103
|
|
|
@@ -91,7 +110,7 @@ npx fdeops resume # prints a short "where we are" for this clien
|
|
|
91
110
|
- **You** describe the situation with `@fde` (or plain language once the skill is loaded)
|
|
92
111
|
- **Session start / end** - small hooks load where you left off and capture what changed (no re-paste)
|
|
93
112
|
- **Local CLI** - memory writes, search, and status with no model tokens; the agent runs it. You do not need to learn it for daily use ([docs/USAGE.md](docs/USAGE.md))
|
|
94
|
-
- **Pluggable pull (ingest)** -
|
|
113
|
+
- **Pluggable pull (ingest)** - FDEOps is the **sink**, not a connector pack. You add any source MCP (Granola, Notion, Drive, …) in Cursor/Claude; say `@fde connect …` for a guided config + recipe, then pull in plain language. Raw text → `.inbox/` → propose → you confirm → `.fde/`. No ambient sync; nothing unreviewed enters the fieldbook. See [mcp/recipes/](mcp/recipes/).
|
|
95
114
|
|
|
96
115
|
fdeops complements repo memory: CLAUDE.md holds how the *code* works; the fieldbook holds how the *client engagement* works.
|
|
97
116
|
|
|
@@ -125,6 +144,25 @@ Overlays for regulated domains (AI, fintech, healthcare, government) activate on
|
|
|
125
144
|
|
|
126
145
|
---
|
|
127
146
|
|
|
147
|
+
## The field methods
|
|
148
|
+
|
|
149
|
+
You never pick one - you describe the situation and `@fde` routes. They are listed here because the methodology is the product, and it should be readable before you install anything. **37 methods across 6 domains**, each one a method (the thinking, the artifact it drafts, the checkpoint with you), not advice:
|
|
150
|
+
|
|
151
|
+
| Domain | Methods |
|
|
152
|
+
|--------|---------|
|
|
153
|
+
| **1. Embed & Trust** - first days, access, credibility | [land](skills/fde/references/land.md) · [audit](skills/fde/references/audit.md) · [stakeholder-radar](skills/fde/references/stakeholder-radar.md) · [trust-engineering](skills/fde/references/trust-engineering.md) · [scope-defense](skills/fde/references/scope-defense.md) |
|
|
154
|
+
| **2. Discover & Diagnose** - find the real problem | [discover](skills/fde/references/discover.md) · [assumption-audit](skills/fde/references/assumption-audit.md) · [use-case-scoring](skills/fde/references/use-case-scoring.md) · [sketch](skills/fde/references/sketch.md) |
|
|
155
|
+
| **3. Plan & Align** - sequence it, sell it | [plan](skills/fde/references/plan.md) · [business-case](skills/fde/references/business-case.md) · [options-analysis](skills/fde/references/options-analysis.md) · [initiative-triage](skills/fde/references/initiative-triage.md) |
|
|
156
|
+
| **4. Build & Guard** - the work, and not breaking their business | [build](skills/fde/references/build.md) · [incremental-build](skills/fde/references/incremental-build.md) · [test-on-legacy](skills/fde/references/test-on-legacy.md) · [blast-radius](skills/fde/references/blast-radius.md) · [debug](skills/fde/references/debug.md) · [rescue](skills/fde/references/rescue.md) · [security-audit](skills/fde/references/security-audit.md) · [observability](skills/fde/references/observability.md) |
|
|
157
|
+
| **5. Ship & Verify** - production, with a way back | [ship](skills/fde/references/ship.md) · [review](skills/fde/references/review.md) · [rollback-drill](skills/fde/references/rollback-drill.md) · [qa-live](skills/fde/references/qa-live.md) |
|
|
158
|
+
| **6. Operate & Close** - the part that decides renewals | [status](skills/fde/references/status.md) · [demo-prep](skills/fde/references/demo-prep.md) · [debrief](skills/fde/references/debrief.md) · [exec-narrative](skills/fde/references/exec-narrative.md) · [dashboard](skills/fde/references/dashboard.md) · [multi-customer-ops](skills/fde/references/multi-customer-ops.md) · [close](skills/fde/references/close.md) · [handoff-engineering](skills/fde/references/handoff-engineering.md) · [pattern-extract](skills/fde/references/pattern-extract.md) · [red-team](skills/fde/references/red-team.md) · [ingest](skills/fde/references/ingest.md) · [ingest-connect](skills/fde/references/ingest-connect.md) |
|
|
159
|
+
|
|
160
|
+
Plus five overlays that activate on signal rather than being chosen - [ai](skills/fde/references/ai.md) · [artifacts](skills/fde/references/artifacts.md) · [fintech](skills/fde/references/fintech.md) · [healthcare](skills/fde/references/healthcare.md) · [gov](skills/fde/references/gov.md) - which add the checks that domain demands to whatever method is already running.
|
|
161
|
+
|
|
162
|
+
Every method, with the exact phrases that route to it: **[docs/skills-reference.md](docs/skills-reference.md)**.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
128
166
|
## Engagement memory (`.fde/`)
|
|
129
167
|
|
|
130
168
|
The **fieldbook** is the system of record for the embed - one folder per client, plain markdown you can read, grep, and take with you:
|
package/adapters/LOCAL-LLM.md
CHANGED
|
@@ -62,7 +62,7 @@ The model reads SKILL.md, routes to the right skill, and produces artifacts in y
|
|
|
62
62
|
|
|
63
63
|
## Model size recommendations
|
|
64
64
|
|
|
65
|
-
The methodology is detailed (
|
|
65
|
+
The methodology is detailed (37 methods, routing logic, evidence format, memory contract). Larger models handle it better:
|
|
66
66
|
|
|
67
67
|
| Model class | Experience |
|
|
68
68
|
|-------------|-----------|
|
package/adapters/README.md
CHANGED
|
@@ -31,4 +31,4 @@ Defaults to the current directory if no path is given. Existing files are never
|
|
|
31
31
|
|
|
32
32
|
## The principle
|
|
33
33
|
|
|
34
|
-
The adapter only tells the tool **where the brain is and how to behave**. All the method - the
|
|
34
|
+
The adapter only tells the tool **where the brain is and how to behave**. All the method - the 37 methods, the overlays, the memory contract - lives once in `skills/fde/SKILL.md`. Update the brain, every platform gets it. That's why fdeops feels native in whatever the FDE already uses, without five things to keep in sync.
|
package/bin/check.js
CHANGED
|
@@ -76,6 +76,29 @@ for (const f of requiredReferences) {
|
|
|
76
76
|
}
|
|
77
77
|
ok('phase references')
|
|
78
78
|
|
|
79
|
+
// A method reference teaches by showing one engagement, not by describing a shape:
|
|
80
|
+
// every judgment-heavy reference carries a worked example that names the memory
|
|
81
|
+
// file the work lands in. Prose-only guidance drifts into advice nobody can apply.
|
|
82
|
+
const exampleReferences = [
|
|
83
|
+
'land.md', 'discover.md', 'plan.md', 'build.md', 'ship.md', 'close.md',
|
|
84
|
+
'status.md', 'stakeholder-radar.md', 'options-analysis.md', 'business-case.md',
|
|
85
|
+
'assumption-audit.md', 'scope-defense.md',
|
|
86
|
+
]
|
|
87
|
+
for (const f of exampleReferences) {
|
|
88
|
+
const p = path.join(root, 'skills', 'fde', 'references', f)
|
|
89
|
+
if (!fs.existsSync(p)) { fail(`missing method reference skills/fde/references/${f}`); continue }
|
|
90
|
+
const body = fs.readFileSync(p, 'utf8')
|
|
91
|
+
const section = (body.match(/\n## Worked example\n([\s\S]*?)(?=\n## |$)/) || [])[1]
|
|
92
|
+
if (!section) {
|
|
93
|
+
fail(`references/${f} missing ## Worked example`)
|
|
94
|
+
} else if (section.trim().length < 400) {
|
|
95
|
+
fail(`references/${f} worked example is too thin to teach anything`)
|
|
96
|
+
} else if (!/`[a-z-]+\.md`/.test(section)) {
|
|
97
|
+
fail(`references/${f} worked example never names the memory file the work lands in`)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
ok(`worked examples (${exampleReferences.length} references)`)
|
|
101
|
+
|
|
79
102
|
const router = read('skills/fde/SKILL.md')
|
|
80
103
|
if (!router.includes('memory contract')) fail('SKILL.md must define the memory contract')
|
|
81
104
|
// every references/<name>.md the router mentions must exist
|
|
@@ -88,6 +111,94 @@ for (const refFile of mentioned) {
|
|
|
88
111
|
}
|
|
89
112
|
ok(`router dispatch (${mentioned.length} reference targets verified) + memory contract`)
|
|
90
113
|
|
|
114
|
+
// Public claims must match the router. The docs advertise a method count and a
|
|
115
|
+
// per-domain list; both drifted from SKILL.md once (ingest / ingest-connect
|
|
116
|
+
// routed but undocumented), and a number nobody can verify is worse than none.
|
|
117
|
+
{
|
|
118
|
+
// Every routing row must parse. A row this misses is a method that could go
|
|
119
|
+
// undocumented for free, so an unparsed row is a hard failure, not a silent skip.
|
|
120
|
+
const routed = new Set()
|
|
121
|
+
const routing = (router.split('## Routing - 6 domains')[1] || '').split('**Overlays')[0]
|
|
122
|
+
const methodCell = line => (line.split('|')[2] || '').trim().replace(/\s*\([^)]*\)\s*$/, '')
|
|
123
|
+
for (const line of routing.split('\n')) {
|
|
124
|
+
if (!/^\|/.test(line)) continue
|
|
125
|
+
// A row that names a method but no reference would route that method while
|
|
126
|
+
// nothing requires anyone to document it - shape checks can only police rows
|
|
127
|
+
// they recognise, so name a method here and you must name its reference.
|
|
128
|
+
if (!/references\/[a-z0-9-]+\.md/.test(line)) {
|
|
129
|
+
const orphan = methodCell(line)
|
|
130
|
+
// a method name, not a `-` placeholder (CLI-only rows) or a `---` separator
|
|
131
|
+
if (/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(orphan)) {
|
|
132
|
+
fail(`SKILL.md routes '${orphan}' without naming a reference: ${line.trim().slice(0, 80)}`)
|
|
133
|
+
}
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
// Candidate rows are selected on the reference name in ANY form, then the
|
|
137
|
+
// shape is enforced - a row this cannot read must fail, never be skipped,
|
|
138
|
+
// or a method could go undocumented by being written unusually.
|
|
139
|
+
if (!/`references\/[a-z0-9-]+\.md`/.test(line)) {
|
|
140
|
+
fail(`SKILL.md routing row must name its reference as \`references/<name>.md\`: ${line.trim().slice(0, 80)}`)
|
|
141
|
+
continue
|
|
142
|
+
}
|
|
143
|
+
// | You hear | <method> | <cell mentioning references/*.md> |
|
|
144
|
+
const method = methodCell(line)
|
|
145
|
+
if (!/^[a-z0-9-]+$/.test(method)) {
|
|
146
|
+
fail(`check.js cannot read the method name in a SKILL.md routing row: ${line.trim().slice(0, 80)}`)
|
|
147
|
+
continue
|
|
148
|
+
}
|
|
149
|
+
routed.add(method)
|
|
150
|
+
}
|
|
151
|
+
if (!routed.size) fail('check.js could not parse the SKILL.md routing table')
|
|
152
|
+
|
|
153
|
+
// docs/skills-reference.md is the canonical per-method list: one row per
|
|
154
|
+
// method inside the six domain tables, ending at the Overlays section.
|
|
155
|
+
const reference = read('docs/skills-reference.md')
|
|
156
|
+
const documented = new Set()
|
|
157
|
+
let documentedRows = 0
|
|
158
|
+
for (const line of reference.split('### Overlays')[0].split('\n')) {
|
|
159
|
+
const m = line.match(/^\|\s*\[([a-z0-9-]+)\]\(\.\.\/skills\/fde\/references\/([a-z0-9-]+\.md)\)/)
|
|
160
|
+
if (!m) continue
|
|
161
|
+
documentedRows++
|
|
162
|
+
documented.add(m[1])
|
|
163
|
+
// A link nobody followed is the same unverifiable claim this gate exists for:
|
|
164
|
+
// the target must exist, and it must be the method the text names.
|
|
165
|
+
if (m[2] !== `${m[1]}.md`) {
|
|
166
|
+
fail(`docs/skills-reference.md links [${m[1]}] at references/${m[2]}`)
|
|
167
|
+
} else if (!fs.existsSync(path.join(root, 'skills', 'fde', 'references', m[2]))) {
|
|
168
|
+
fail(`docs/skills-reference.md links references/${m[2]}, which does not exist`)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (documented.size !== documentedRows) {
|
|
172
|
+
fail(`docs/skills-reference.md lists ${documentedRows} method rows for ${documented.size} methods - a duplicate row inflates the count`)
|
|
173
|
+
}
|
|
174
|
+
const undocumented = [...routed].filter(name => !documented.has(name))
|
|
175
|
+
if (undocumented.length) {
|
|
176
|
+
fail(`SKILL.md routes skill(s) missing from docs/skills-reference.md: ${undocumented.join(', ')}`)
|
|
177
|
+
}
|
|
178
|
+
// and the other direction: a documented method nothing routes to is a method
|
|
179
|
+
// the agent can never reach, advertised anyway.
|
|
180
|
+
const unrouted = [...documented].filter(name => !routed.has(name))
|
|
181
|
+
if (unrouted.length) {
|
|
182
|
+
fail(`docs/skills-reference.md documents method(s) SKILL.md never routes to: ${unrouted.join(', ')}`)
|
|
183
|
+
}
|
|
184
|
+
for (const rel of ['docs/skills.md', 'docs/skills-reference.md']) {
|
|
185
|
+
const body = read(rel)
|
|
186
|
+
// `-` is a word boundary, so \bingest\b matches inside `ingest-connect`:
|
|
187
|
+
// a method could disappear from the docs behind a hyphenated sibling.
|
|
188
|
+
const absent = [...documented].filter(name => !new RegExp(`(?<![\\w-])${name}(?![\\w-])`).test(body))
|
|
189
|
+
if (absent.length) fail(`${rel} does not list method(s): ${absent.join(', ')}`)
|
|
190
|
+
// every count claim, not just the first: an earlier sentence must not shadow
|
|
191
|
+
// a stale headline (or the reverse).
|
|
192
|
+
const claims = [...body.matchAll(/(\d+)\s+methods/g)].map(m => Number(m[1]))
|
|
193
|
+
if (!claims.length) fail(`${rel} must state how many methods it documents`)
|
|
194
|
+
const wrong = [...new Set(claims.filter(n => n !== documented.size))]
|
|
195
|
+
if (wrong.length) {
|
|
196
|
+
fail(`${rel} claims ${wrong.join('/')} methods; ${documented.size} are documented`)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
ok(`public method count is verifiable (${documented.size} documented, ${routed.size} routed)`)
|
|
200
|
+
}
|
|
201
|
+
|
|
91
202
|
const install = read('bin/install.js')
|
|
92
203
|
if (install.includes('scaffoldFdeInProject(process.cwd())')) {
|
|
93
204
|
fail('install.js must not auto-scaffold .fde in customer cwd')
|
|
@@ -104,8 +215,34 @@ if (read('package.json').includes('postinstall')) {
|
|
|
104
215
|
}
|
|
105
216
|
|
|
106
217
|
const readme = read('README.md')
|
|
107
|
-
|
|
108
|
-
|
|
218
|
+
// The README shows one recording: media/session.gif, a real CLI session.
|
|
219
|
+
// media/demo.gif is a hand-typed mock kept for history - it must never be embedded,
|
|
220
|
+
// or the front door shows output no command actually produced.
|
|
221
|
+
if (readme.includes('demo.gif')) fail('README must not embed media/demo.gif (staged mock, not real CLI output)')
|
|
222
|
+
else ok('README no staged demo gif')
|
|
223
|
+
|
|
224
|
+
if (!readme.includes('media/session.gif')) {
|
|
225
|
+
fail('README must embed media/session.gif (the recorded session is the front door)')
|
|
226
|
+
} else if (!readme.includes('media/record-session.sh')) {
|
|
227
|
+
fail('README must link media/record-session.sh next to the recording, so it can be re-recorded')
|
|
228
|
+
} else {
|
|
229
|
+
const gifPath = path.join(root, 'media', 'session.gif')
|
|
230
|
+
const rec = path.join(root, 'media', 'record-session.sh')
|
|
231
|
+
if (!fs.existsSync(gifPath) || fs.statSync(gifPath).size < 50000) fail('media/session.gif missing or too small')
|
|
232
|
+
else if (!fs.existsSync(rec)) fail('media/record-session.sh missing - the recording must be reproducible')
|
|
233
|
+
else if (!fs.existsSync(path.join(root, 'media', 'session.cast'))) fail('media/session.cast missing - keep the source recording next to the gif')
|
|
234
|
+
else ok('README recorded session (gif + reproducible recorder + cast)')
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Every repo-relative README link and image must resolve, or the front door 404s.
|
|
238
|
+
const brokenLinks = []
|
|
239
|
+
for (const m of readme.matchAll(/(?:\]\(|src=")([^)"#\s]+)(?:\)|")/g)) {
|
|
240
|
+
const target = m[1]
|
|
241
|
+
if (/^(https?:|mailto:|#|\/)/.test(target)) continue
|
|
242
|
+
if (!fs.existsSync(path.join(root, target))) brokenLinks.push(target)
|
|
243
|
+
}
|
|
244
|
+
if (brokenLinks.length) fail(`README links to missing paths: ${brokenLinks.join(', ')}`)
|
|
245
|
+
else ok('README links all resolve')
|
|
109
246
|
|
|
110
247
|
for (const section of [
|
|
111
248
|
'How it works',
|
|
@@ -180,7 +317,15 @@ if (!fs.existsSync(path.join(root, 'docs', 'schema.md'))) fail('docs/schema.md m
|
|
|
180
317
|
else ok('docs/schema.md')
|
|
181
318
|
|
|
182
319
|
if (!fs.existsSync(path.join(root, 'SECURITY.md'))) fail('SECURITY.md missing')
|
|
183
|
-
else
|
|
320
|
+
else {
|
|
321
|
+
// A reporting section that only offers GitHub Security Advisories is a dead end
|
|
322
|
+
// whenever private reporting is off for the repo (that page 403s), which is how
|
|
323
|
+
// issue #9 sat unreported. Require a channel that does not depend on a repo setting.
|
|
324
|
+
const sec = read('SECURITY.md')
|
|
325
|
+
if (!/[\w.+-]+@[\w-]+\.[\w.]+/.test(sec)) {
|
|
326
|
+
fail('SECURITY.md must give a reporting channel that works when GitHub private reporting is off (an email address)')
|
|
327
|
+
} else ok('SECURITY.md (reachable reporting channel)')
|
|
328
|
+
}
|
|
184
329
|
|
|
185
330
|
const exampleFiles = ['reality.md', 'decisions.md', 'delivery.md', 'stakeholders.md', 'assumptions.md']
|
|
186
331
|
for (const f of exampleFiles) {
|
|
@@ -351,7 +496,77 @@ if (!fs.existsSync(path.join(root, 'mcp', 'fdeops-ingest', 'server.js'))) {
|
|
|
351
496
|
fail('ingest MCP must expose ingest_stage')
|
|
352
497
|
} else if (!read('skills/fde/references/ingest.md').includes('stage')) {
|
|
353
498
|
fail('skills/fde/references/ingest.md missing stage contract')
|
|
354
|
-
} else
|
|
499
|
+
} else if (!fs.existsSync(path.join(root, 'skills', 'fde', 'references', 'ingest-connect.md'))) {
|
|
500
|
+
fail('skills/fde/references/ingest-connect.md missing')
|
|
501
|
+
} else {
|
|
502
|
+
for (const recipe of ['file.md', 'granola.md', 'notion.md']) {
|
|
503
|
+
if (!fs.existsSync(path.join(root, 'mcp', 'recipes', recipe))) fail(`mcp/recipes/${recipe} missing`)
|
|
504
|
+
}
|
|
505
|
+
if (!read('README.md').includes('mcp/recipes')) fail('README must point at mcp/recipes for connect clarity')
|
|
506
|
+
ok('ingest MCP + connect recipes + skill reference')
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Agent Plugins 1.0.0 conformance (agent-plugins.org/specification).
|
|
510
|
+
// plugin.json has a closed schema: an illegal field is fatal to the whole plugin,
|
|
511
|
+
// and an mcp.json whose $schema version differs from plugin.json silently disables MCP.
|
|
512
|
+
const AP_VERSION = '1.0.0'
|
|
513
|
+
const AP_PLUGIN_SCHEMA = `https://agent-plugins.org/schemas/${AP_VERSION}/plugin.schema.json`
|
|
514
|
+
const AP_MCP_SCHEMA = `https://agent-plugins.org/schemas/${AP_VERSION}/mcp.schema.json`
|
|
515
|
+
const AP_MANIFEST_FIELDS = [
|
|
516
|
+
'$schema', 'name', 'version', 'description', 'author',
|
|
517
|
+
'homepage', 'repository', 'license', 'keywords', 'extensions',
|
|
518
|
+
]
|
|
519
|
+
|
|
520
|
+
const apManifest = JSON.parse(read('plugin.json'))
|
|
521
|
+
const apUnknown = Object.keys(apManifest).filter(k => !AP_MANIFEST_FIELDS.includes(k))
|
|
522
|
+
if (apManifest.$schema !== AP_PLUGIN_SCHEMA) {
|
|
523
|
+
fail(`plugin.json $schema must be ${AP_PLUGIN_SCHEMA}`)
|
|
524
|
+
} else if (apUnknown.length) {
|
|
525
|
+
fail(`plugin.json has non-portable field(s) ${apUnknown.join(', ')} - client-specific data belongs under extensions`)
|
|
526
|
+
} else if (!/^(?!.*(?:--|\.\.))[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/.test(apManifest.name)) {
|
|
527
|
+
fail(`plugin.json name "${apManifest.name}" violates Agent Plugins name constraints`)
|
|
528
|
+
} else if (apManifest.version !== pkg.version) {
|
|
529
|
+
fail(`version mismatch package.json ${pkg.version} vs plugin.json ${apManifest.version}`)
|
|
530
|
+
} else ok('agent plugins manifest')
|
|
531
|
+
|
|
532
|
+
const apMcp = JSON.parse(read('mcp.json'))
|
|
533
|
+
const apServers = apMcp.mcpServers || {}
|
|
534
|
+
if (apMcp.$schema !== AP_MCP_SCHEMA) {
|
|
535
|
+
fail(`mcp.json $schema must be ${AP_MCP_SCHEMA} (a version mismatch with plugin.json disables MCP)`)
|
|
536
|
+
} else if (Object.keys(apMcp).some(k => k !== '$schema' && k !== 'mcpServers')) {
|
|
537
|
+
fail('mcp.json permits only $schema and mcpServers')
|
|
538
|
+
} else {
|
|
539
|
+
for (const [name, srv] of Object.entries(apServers)) {
|
|
540
|
+
if (srv.type !== 'stdio') fail(`mcp.json ${name}: fdeops ships stdio servers only (local-only core)`)
|
|
541
|
+
else if (!srv.command || (/[\s/]/.test(srv.command) && !srv.command.startsWith('./'))) {
|
|
542
|
+
fail(`mcp.json ${name}: command must be one executable token or a ./-relative path`)
|
|
543
|
+
} else if ((srv.args || []).some(a => path.isAbsolute(a))) {
|
|
544
|
+
fail(`mcp.json ${name}: args must use \${PLUGIN_ROOT}, never an absolute path`)
|
|
545
|
+
} else if (Object.keys(srv.env || {}).some(k => k === 'PLUGIN_ROOT' || k === 'PLUGIN_DATA')) {
|
|
546
|
+
fail(`mcp.json ${name}: env must not set PLUGIN_ROOT/PLUGIN_DATA (client-supplied)`)
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
const ingest = apServers['fdeops-ingest']
|
|
550
|
+
const rootArg = ingest && (ingest.args || []).find(a => String(a).startsWith('${PLUGIN_ROOT}/'))
|
|
551
|
+
const target = rootArg ? String(rootArg).replace('${PLUGIN_ROOT}/', '') : ''
|
|
552
|
+
if (!ingest) fail('mcp.json must declare the fdeops-ingest stdio server')
|
|
553
|
+
else if (!rootArg) fail('mcp.json fdeops-ingest: args must include a ${PLUGIN_ROOT}-relative server path')
|
|
554
|
+
else if (!fs.existsSync(path.join(root, target))) fail(`mcp.json fdeops-ingest points at missing ${target}`)
|
|
555
|
+
else ok('agent plugins mcp config')
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// A manifest that ships only in git and not in the npm tarball is the worst kind of drift.
|
|
559
|
+
for (const manifest of ['plugin.json', 'mcp.json']) {
|
|
560
|
+
if (!(pkg.files || []).includes(manifest)) fail(`package.json files must include ${manifest}`)
|
|
561
|
+
else ok(`${manifest} published to npm`)
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
for (const entry of fs.readdirSync(path.join(root, 'skills'))) {
|
|
565
|
+
const dir = path.join(root, 'skills', entry)
|
|
566
|
+
if (!fs.statSync(dir).isDirectory()) continue
|
|
567
|
+
if (!fs.existsSync(path.join(dir, 'SKILL.md'))) fail(`skills/${entry}/ has no SKILL.md - clients skip it`)
|
|
568
|
+
else ok(`skill ${entry} discoverable`)
|
|
569
|
+
}
|
|
355
570
|
|
|
356
571
|
if (!fs.existsSync(path.join(root, '.github', 'ISSUE_TEMPLATE', 'bug_report.yml'))) {
|
|
357
572
|
fail('GitHub issue template missing')
|