fdeops 3.13.0 → 3.15.1
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 +182 -116
- package/adapters/LOCAL-LLM.md +1 -1
- package/adapters/README.md +2 -2
- package/bin/check.js +77 -48
- package/bin/fde.js +20 -11
- package/bin/lib/trust.js +3 -2
- package/mcp/fdeops-ingest/package.json +1 -1
- package/package.json +2 -2
- package/plugin.json +3 -3
- package/skills/fde/SKILL.md +78 -158
- package/skills/fde/references/pattern-extract.md +9 -9
- package/skills/fde/archive/sdlc/README.md +0 -7
- package/skills/fde/archive/sdlc/build.md +0 -154
- package/skills/fde/archive/sdlc/debug.md +0 -55
- package/skills/fde/archive/sdlc/observability.md +0 -103
- package/skills/fde/archive/sdlc/qa-live.md +0 -113
- package/skills/fde/archive/sdlc/security-audit.md +0 -105
- package/skills/fde/archive/sdlc/test-on-legacy.md +0 -108
package/bin/check.js
CHANGED
|
@@ -41,12 +41,11 @@ for (const f of requiredTemplates) {
|
|
|
41
41
|
else ok(`template ${f}`)
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
} else {
|
|
48
|
-
ok('media/demo.gif')
|
|
44
|
+
const deadMedia = ['demo.gif', 'demo.sh', 'demo.tape', 'terminal-demo.svg', 'fieldbook-dashboard.png', 'fieldbook-detail.png', 'fieldbook-walkthrough.gif']
|
|
45
|
+
for (const name of deadMedia) {
|
|
46
|
+
if (fs.existsSync(path.join(root, 'media', name))) fail(`dead media/${name} must not ship — the recorded session is session.gif`)
|
|
49
47
|
}
|
|
48
|
+
ok('no staged mock media')
|
|
50
49
|
|
|
51
50
|
for (const dir of fs.readdirSync(path.join(root, 'skills'))) {
|
|
52
51
|
const skill = path.join(root, 'skills', dir, 'SKILL.md')
|
|
@@ -57,6 +56,10 @@ for (const dir of fs.readdirSync(path.join(root, 'skills'))) {
|
|
|
57
56
|
}
|
|
58
57
|
ok('skills structure')
|
|
59
58
|
|
|
59
|
+
if (fs.existsSync(path.join(root, 'skills', 'fde', 'archive'))) {
|
|
60
|
+
fail('skills/fde/archive must not exist — unrouted methods are dead code')
|
|
61
|
+
} else ok('no archived skill dump')
|
|
62
|
+
|
|
60
63
|
// v3: one skill + phase references (progressive disclosure)
|
|
61
64
|
const requiredReferences = [
|
|
62
65
|
'land.md', 'discover.md', 'audit.md', 'plan.md', 'review.md',
|
|
@@ -118,7 +121,7 @@ ok(`router dispatch (${mentioned.length} reference targets verified) + memory co
|
|
|
118
121
|
// Every routing row must parse. A row this misses is a method that could go
|
|
119
122
|
// undocumented for free, so an unparsed row is a hard failure, not a silent skip.
|
|
120
123
|
const routed = new Set()
|
|
121
|
-
const routing = (router.split(
|
|
124
|
+
const routing = (router.split(/^## Routing[^\n]*$/m)[1] || '').split('**Overlays')[0]
|
|
122
125
|
const methodCell = line => (line.split('|')[2] || '').trim().replace(/\s*\([^)]*\)\s*$/, '')
|
|
123
126
|
for (const line of routing.split('\n')) {
|
|
124
127
|
if (!/^\|/.test(line)) continue
|
|
@@ -179,24 +182,27 @@ ok(`router dispatch (${mentioned.length} reference targets verified) + memory co
|
|
|
179
182
|
// the agent can never reach, advertised anyway.
|
|
180
183
|
const unrouted = [...documented].filter(name => !routed.has(name))
|
|
181
184
|
if (unrouted.length) {
|
|
182
|
-
fail(`docs/skills-reference.md documents
|
|
185
|
+
fail(`docs/skills-reference.md documents skill(s) SKILL.md never routes to: ${unrouted.join(', ')}`)
|
|
183
186
|
}
|
|
184
187
|
for (const rel of ['docs/skills.md', 'docs/skills-reference.md']) {
|
|
185
188
|
const body = read(rel)
|
|
186
189
|
// `-` is a word boundary, so \bingest\b matches inside `ingest-connect`:
|
|
187
190
|
// a method could disappear from the docs behind a hyphenated sibling.
|
|
188
191
|
const absent = [...documented].filter(name => !new RegExp(`(?<![\\w-])${name}(?![\\w-])`).test(body))
|
|
189
|
-
if (absent.length) fail(`${rel} does not list
|
|
190
|
-
|
|
191
|
-
|
|
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`)
|
|
192
|
+
if (absent.length) fail(`${rel} does not list skill(s): ${absent.join(', ')}`)
|
|
193
|
+
const claims = [...body.matchAll(/(\d+)\s+skills/g)].map(m => Number(m[1]))
|
|
194
|
+
if (!claims.length) fail(`${rel} must state how many skills it documents`)
|
|
194
195
|
const wrong = [...new Set(claims.filter(n => n !== documented.size))]
|
|
195
196
|
if (wrong.length) {
|
|
196
|
-
fail(`${rel} claims ${wrong.join('/')}
|
|
197
|
+
fail(`${rel} claims ${wrong.join('/')} skills; ${documented.size} are documented`)
|
|
197
198
|
}
|
|
198
199
|
}
|
|
199
|
-
ok(`public
|
|
200
|
+
ok(`public skill count is verifiable (${documented.size} documented, ${routed.size} routed)`)
|
|
201
|
+
|
|
202
|
+
const refDir = path.join(root, 'skills', 'fde', 'references')
|
|
203
|
+
const extra = fs.readdirSync(refDir).filter(f => f.endsWith('.md') && !mentioned.includes(f))
|
|
204
|
+
if (extra.length) fail(`unrouted reference file(s) — dead method: ${extra.join(', ')}`)
|
|
205
|
+
else ok('no unrouted reference files')
|
|
200
206
|
}
|
|
201
207
|
|
|
202
208
|
const install = read('bin/install.js')
|
|
@@ -215,26 +221,20 @@ if (read('package.json').includes('postinstall')) {
|
|
|
215
221
|
}
|
|
216
222
|
|
|
217
223
|
const readme = read('README.md')
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
// it must stay reachable and reproducible - an orphaned gif rots silently.
|
|
226
|
-
const recordingHosts = ['README.md', 'docs/USAGE.md'].filter(f => read(f).includes('media/session.gif'))
|
|
227
|
-
if (!recordingHosts.length) {
|
|
228
|
-
fail('media/session.gif must be embedded in README.md or docs/USAGE.md (the recorded session is the proof)')
|
|
229
|
-
} else if (!recordingHosts.some(f => read(f).includes('media/record-session.sh'))) {
|
|
230
|
-
fail('link media/record-session.sh next to the recording, so it can be re-recorded')
|
|
224
|
+
if (/session\.gif|demo\.gif|<img /i.test(readme)) {
|
|
225
|
+
fail('README must not embed images — front door is text; the recording lives in docs/USAGE.md')
|
|
226
|
+
} else ok('README is text (no gif)')
|
|
227
|
+
|
|
228
|
+
const usage = read('docs/USAGE.md')
|
|
229
|
+
if (!usage.includes('media/session.gif') || !usage.includes('media/record-session.sh')) {
|
|
230
|
+
fail('docs/USAGE.md must embed media/session.gif and link media/record-session.sh')
|
|
231
231
|
} else {
|
|
232
232
|
const gifPath = path.join(root, 'media', 'session.gif')
|
|
233
233
|
const rec = path.join(root, 'media', 'record-session.sh')
|
|
234
234
|
if (!fs.existsSync(gifPath) || fs.statSync(gifPath).size < 50000) fail('media/session.gif missing or too small')
|
|
235
235
|
else if (!fs.existsSync(rec)) fail('media/record-session.sh missing - the recording must be reproducible')
|
|
236
236
|
else if (!fs.existsSync(path.join(root, 'media', 'session.cast'))) fail('media/session.cast missing - keep the source recording next to the gif')
|
|
237
|
-
else ok(
|
|
237
|
+
else ok('recorded session in docs/USAGE.md (gif + reproducible recorder + cast)')
|
|
238
238
|
}
|
|
239
239
|
|
|
240
240
|
// Every repo-relative README link and image must resolve, or the front door 404s.
|
|
@@ -248,11 +248,11 @@ if (brokenLinks.length) fail(`README links to missing paths: ${brokenLinks.join(
|
|
|
248
248
|
else ok('README links all resolve')
|
|
249
249
|
|
|
250
250
|
for (const section of [
|
|
251
|
-
'How
|
|
252
|
-
'
|
|
251
|
+
'How Skills Work',
|
|
252
|
+
'Quick Start',
|
|
253
253
|
'Engagement memory',
|
|
254
254
|
'Who this is for',
|
|
255
|
-
'
|
|
255
|
+
'Commands',
|
|
256
256
|
'Principles',
|
|
257
257
|
]) {
|
|
258
258
|
if (!readme.includes(section)) fail(`README missing section: ${section}`)
|
|
@@ -262,6 +262,16 @@ if (!readme.includes('AI coding agent')) {
|
|
|
262
262
|
}
|
|
263
263
|
ok('README clarity sections')
|
|
264
264
|
|
|
265
|
+
for (const cmd of ['/brief', '/discover', '/plan', '/ship', '/got', '/close', '/debrief', '/prep', '/quiet', '/agreed', '/status']) {
|
|
266
|
+
if (!readme.includes(cmd)) fail(`README must document slash command ${cmd}`)
|
|
267
|
+
}
|
|
268
|
+
ok('README slash commands documented')
|
|
269
|
+
|
|
270
|
+
// Front-door map is the embed left-to-right (LAND → CLOSE), not a pile of situations.
|
|
271
|
+
if (!readme.slice(0, 4000).includes('LAND') || !readme.slice(0, 4000).includes('/discover')) {
|
|
272
|
+
fail('README must include the LAND→CLOSE command-map diagram near the top')
|
|
273
|
+
} else ok('README command-map diagram')
|
|
274
|
+
|
|
265
275
|
if (readme.includes('your-client-repo')) {
|
|
266
276
|
fail('README must not instruct install in customer repo (your-client-repo)')
|
|
267
277
|
} else ok('README no customer-repo install')
|
|
@@ -270,9 +280,8 @@ if (!readme.includes('fde-engagements') || !/fdeops.*init.*engagement/i.test(rea
|
|
|
270
280
|
fail('README must document fde-engagements + init flow')
|
|
271
281
|
} else ok('README engagement path')
|
|
272
282
|
|
|
273
|
-
// The
|
|
274
|
-
//
|
|
275
|
-
// a field user wants.
|
|
283
|
+
// The advertised command must name the one skill a field user wants.
|
|
284
|
+
// Contributor CLI attack notes live in evals/testing-fieldbook.md, not as a skill.
|
|
276
285
|
for (const m of readme.match(/^.*npx skills add .*$/gm) || []) {
|
|
277
286
|
if (!m.includes('--skill fde')) {
|
|
278
287
|
fail(`README skills-add command must pin --skill fde: ${m.trim()}`)
|
|
@@ -298,19 +307,7 @@ if (!read('skills/fde/SKILL.md').includes('npx --yes fdeops')) {
|
|
|
298
307
|
fail(`SKILL.md description needs several "Use when …" triggers so it fires on intent (found ${triggers.length})`)
|
|
299
308
|
} else if (/Use when the human says @fde or/.test(desc)) {
|
|
300
309
|
fail('SKILL.md description must not gate on @fde - name the client-work intents first')
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Six invented words (fieldbook, terrain, reality, trust signal, receipts,
|
|
305
|
-
// vault) carry the method. They were never defined in one place, so a stranger
|
|
306
|
-
// met them scattered through the docs and guessed.
|
|
307
|
-
{
|
|
308
|
-
const gloss = /Words used here[\s\S]{0,1200}/.exec(readme)
|
|
309
|
-
const missing = ['fieldbook', 'reality', 'terrain', 'trust signal', 'receipts', 'vault']
|
|
310
|
-
.filter(w => !gloss || !new RegExp(w, 'i').test(gloss[0]))
|
|
311
|
-
if (!gloss) fail('README must define its invented words once (a "Words used here" line)')
|
|
312
|
-
else if (missing.length) fail(`README glossary is missing: ${missing.join(', ')}`)
|
|
313
|
-
else ok('README defines its own vocabulary')
|
|
310
|
+
} else ok('SKILL.md description triggers on intent')
|
|
314
311
|
}
|
|
315
312
|
|
|
316
313
|
if (!fs.existsSync(path.join(root, 'docs', 'USAGE.md'))) {
|
|
@@ -535,6 +532,16 @@ const plugin = JSON.parse(read('.claude-plugin/plugin.json'))
|
|
|
535
532
|
if (pkg.version !== plugin.version) {
|
|
536
533
|
fail(`version mismatch package.json ${pkg.version} vs plugin ${plugin.version}`)
|
|
537
534
|
} else ok('plugin version aligned')
|
|
535
|
+
if (plugin.commands !== './.claude/commands' || plugin.skills !== './skills') {
|
|
536
|
+
fail('.claude-plugin/plugin.json must declare skills and commands')
|
|
537
|
+
} else {
|
|
538
|
+
for (const cmd of ['brief', 'discover', 'plan', 'ship', 'got', 'close', 'debrief', 'prep', 'quiet', 'agreed', 'status']) {
|
|
539
|
+
const rel = `.claude/commands/${cmd}.md`
|
|
540
|
+
if (!fs.existsSync(path.join(root, rel))) fail(`${rel} missing`)
|
|
541
|
+
else if (!read(rel).includes('@fde')) fail(`${rel} must load @fde`)
|
|
542
|
+
}
|
|
543
|
+
ok('slash commands load @fde')
|
|
544
|
+
}
|
|
538
545
|
|
|
539
546
|
if (!fs.existsSync(path.join(root, 'mcp', 'fdeops-ingest', 'server.js'))) {
|
|
540
547
|
fail('mcp/fdeops-ingest/server.js missing (ingest MCP sink)')
|
|
@@ -607,12 +614,34 @@ for (const manifest of ['plugin.json', 'mcp.json']) {
|
|
|
607
614
|
else ok(`${manifest} published to npm`)
|
|
608
615
|
}
|
|
609
616
|
|
|
617
|
+
const skillDirs = []
|
|
610
618
|
for (const entry of fs.readdirSync(path.join(root, 'skills'))) {
|
|
611
619
|
const dir = path.join(root, 'skills', entry)
|
|
612
620
|
if (!fs.statSync(dir).isDirectory()) continue
|
|
613
621
|
if (!fs.existsSync(path.join(dir, 'SKILL.md'))) fail(`skills/${entry}/ has no SKILL.md - clients skip it`)
|
|
614
|
-
else
|
|
622
|
+
else {
|
|
623
|
+
skillDirs.push(entry)
|
|
624
|
+
ok(`skill ${entry} discoverable`)
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if (skillDirs.length !== 1 || skillDirs[0] !== 'fde') {
|
|
628
|
+
fail(`public tree ships one skill (skills/fde); found: ${skillDirs.join(', ') || '(none)'}`)
|
|
629
|
+
} else ok('one public skill')
|
|
630
|
+
|
|
631
|
+
function findSkillMd(dir, acc = []) {
|
|
632
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
633
|
+
if (e.name === '.git' || e.name === 'node_modules' || e.name === '.agents') continue
|
|
634
|
+
const p = path.join(dir, e.name)
|
|
635
|
+
if (e.isDirectory()) findSkillMd(p, acc)
|
|
636
|
+
else if (e.name === 'SKILL.md') acc.push(path.relative(root, p))
|
|
637
|
+
}
|
|
638
|
+
return acc
|
|
615
639
|
}
|
|
640
|
+
const skillFiles = findSkillMd(root)
|
|
641
|
+
const allowedSkill = path.join('skills', 'fde', 'SKILL.md')
|
|
642
|
+
if (skillFiles.length !== 1 || skillFiles[0] !== allowedSkill) {
|
|
643
|
+
fail(`only ${allowedSkill} may exist; found: ${skillFiles.join(', ') || '(none)'}`)
|
|
644
|
+
} else ok('one SKILL.md')
|
|
616
645
|
|
|
617
646
|
if (!fs.existsSync(path.join(root, '.github', 'ISSUE_TEMPLATE', 'bug_report.yml'))) {
|
|
618
647
|
fail('GitHub issue template missing')
|
package/bin/fde.js
CHANGED
|
@@ -738,11 +738,20 @@ function appendLogEntry(eng, type, entry, opts = {}) {
|
|
|
738
738
|
// degrading to "nothing found" rather than guessing when the shape does not
|
|
739
739
|
// match. Never fabricate a number, a name, or a signal that is not in the text.
|
|
740
740
|
|
|
741
|
+
const PHASES = ['land', 'discover', 'plan', 'ship', 'prove', 'close']
|
|
742
|
+
const PHASE_ALIASES = { build: 'ship' } // legacy SDLC name; public map is ship
|
|
741
743
|
const PHASE_LABELS = {
|
|
742
|
-
land: '
|
|
743
|
-
|
|
744
|
+
land: 'Land', discover: 'Discover', plan: 'Plan',
|
|
745
|
+
ship: 'Ship', prove: 'Prove', close: 'Close',
|
|
746
|
+
}
|
|
747
|
+
function canonicalPhase(phase) {
|
|
748
|
+
const p = String(phase).toLowerCase()
|
|
749
|
+
return PHASE_ALIASES[p] || p
|
|
750
|
+
}
|
|
751
|
+
function phaseLabel(phase) {
|
|
752
|
+
const p = canonicalPhase(phase)
|
|
753
|
+
return PHASE_LABELS[p] || phase
|
|
744
754
|
}
|
|
745
|
-
function phaseLabel(phase) { return PHASE_LABELS[String(phase).toLowerCase()] || phase }
|
|
746
755
|
|
|
747
756
|
// First real (non-blank, non-heading) line of a prose file, bold-label prefix
|
|
748
757
|
// stripped ("**Confirmed:** text..." -> "text...") - same spirit as the
|
|
@@ -1312,11 +1321,11 @@ function cmdLog(args) {
|
|
|
1312
1321
|
const eng = resolveEngagement({ forWrite: true })
|
|
1313
1322
|
if (!eng) { console.error('no engagement - run: fde resume --init <name>'); process.exit(2) }
|
|
1314
1323
|
|
|
1315
|
-
// fde log phase <land|discover|plan|
|
|
1324
|
+
// fde log phase <land|discover|plan|ship|prove|close> - advances portfolio phase
|
|
1316
1325
|
if (type === 'phase') {
|
|
1317
|
-
const phase = (text || '').toLowerCase().trim()
|
|
1318
|
-
if (!
|
|
1319
|
-
console.error(
|
|
1326
|
+
const phase = canonicalPhase((text || '').toLowerCase().trim())
|
|
1327
|
+
if (!PHASES.includes(phase)) {
|
|
1328
|
+
console.error(`usage: fde log phase <${PHASES.join('|')}>`)
|
|
1320
1329
|
process.exit(1)
|
|
1321
1330
|
}
|
|
1322
1331
|
const hash = setContextPhase(eng, phase)
|
|
@@ -1324,7 +1333,7 @@ function cmdLog(args) {
|
|
|
1324
1333
|
return
|
|
1325
1334
|
}
|
|
1326
1335
|
|
|
1327
|
-
if (!LOG_FILES[type] || !text) { console.error(
|
|
1336
|
+
if (!LOG_FILES[type] || !text) { console.error(`usage: fde log <decision|risk|delivery|contact> <text> [--signal red|amber|green] [--force]\n fde log phase <${PHASES.join('|')}>\n fde log --undo`); process.exit(1) }
|
|
1328
1337
|
if (signal && type !== 'contact') { console.error('--signal only applies to: fde log contact'); process.exit(1) }
|
|
1329
1338
|
const hit = findSecretHit(text)
|
|
1330
1339
|
if (hit && !force) { refuseSecret('log text', hit); process.exit(1) }
|
|
@@ -2091,7 +2100,7 @@ function collectDoctorIssues(eng) {
|
|
|
2091
2100
|
|
|
2092
2101
|
if (s.phase === '?' || s.phase === 'unset') {
|
|
2093
2102
|
if (hasDatedWork) {
|
|
2094
|
-
issues.push(
|
|
2103
|
+
issues.push(`phase is unset but dated work exists - run: fde log phase <${PHASES.join('|')}>`)
|
|
2095
2104
|
}
|
|
2096
2105
|
}
|
|
2097
2106
|
if (s.stale) issues.push(`trust signal is STALE (${s.signalAge}d) - reconfirm with fde log contact ... --signal`)
|
|
@@ -2161,7 +2170,7 @@ function collectDoctorIssues(eng) {
|
|
|
2161
2170
|
}
|
|
2162
2171
|
// Failure-path (exception-led operating map): required once past discover.
|
|
2163
2172
|
// Land seeds; discover fills; plan+ without a real break→owner row is wallpaper.
|
|
2164
|
-
if (/^(plan|
|
|
2173
|
+
if (/^(plan|ship|prove|close)$/.test(s.phase) && !hasOperatingMapContent(eng)) {
|
|
2165
2174
|
issues.push(
|
|
2166
2175
|
`phase is ${s.phase} with empty operating map - fill terrain.md ## Operating map (exception-led): break → who notices → workaround → evidence`
|
|
2167
2176
|
)
|
|
@@ -3178,7 +3187,7 @@ function printUsage() {
|
|
|
3178
3187
|
fde resume --bind show what this workspace is bound to, and what resolves
|
|
3179
3188
|
fde triage TRIAGE block only (hooks / Cursor session entry)
|
|
3180
3189
|
fde log <type> <text> append decision|risk|delivery|contact (contact takes --signal red|amber|green; --force to allow secret-like text)
|
|
3181
|
-
fde log phase <phase> set engagement phase (land|discover|plan|
|
|
3190
|
+
fde log phase <phase> set engagement phase (land|discover|plan|ship|prove|close)
|
|
3182
3191
|
fde log --undo remove the last CLI log/debrief entry from memory
|
|
3183
3192
|
fde debrief [file] meeting notes → memory (prefixed lines; --dry-run; --force)
|
|
3184
3193
|
fde debrief --smart heuristic propose (prefix + light keywords); agent routes, CLI gates → --apply
|
package/bin/lib/trust.js
CHANGED
|
@@ -64,8 +64,9 @@ function createTrustApi(deps) {
|
|
|
64
64
|
if (!m) return '?'
|
|
65
65
|
const raw = m[1].replace(/\*/g, '').trim()
|
|
66
66
|
if (!raw || /\|/.test(raw) || /^unset$/i.test(raw) || /^[\[(]/.test(raw)) return '?'
|
|
67
|
-
const one = raw.toLowerCase().match(/^(land|discover|plan|build|ship|close)\b/)
|
|
68
|
-
|
|
67
|
+
const one = raw.toLowerCase().match(/^(land|discover|plan|build|ship|prove|close)\b/)
|
|
68
|
+
if (!one) return '?'
|
|
69
|
+
return one[1] === 'build' ? 'ship' : one[1]
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
function countOpenRisks(eng) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fdeops",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "3.15.1",
|
|
4
|
+
"description": "Forward deployed engineering skills for AI coding agents. Your agent forgets the client every morning - the sponsor, the promise, who signed off. FDEOps keeps that as dated markdown on your laptop: one @fde skill, a deterministic local CLI, 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"
|
package/plugin.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "fdeops",
|
|
4
|
-
"version": "3.
|
|
5
|
-
"description": "
|
|
4
|
+
"version": "3.15.1",
|
|
5
|
+
"description": "Forward deployed engineering skills for AI coding agents: per-client memory in local .fde/ files, one @fde skill. Local-only, no network.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Subash Natarajan",
|
|
8
8
|
"url": "https://github.com/suboss87"
|
|
9
9
|
},
|
|
10
|
-
"homepage": "https://
|
|
10
|
+
"homepage": "https://github.com/suboss87/fdeops",
|
|
11
11
|
"repository": "https://github.com/suboss87/FDEOps",
|
|
12
12
|
"license": "MIT",
|
|
13
13
|
"keywords": [
|