fdeops 3.27.0 → 3.27.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/README.md +77 -96
- package/bin/check.js +15 -32
- package/bin/fde.js +147 -26
- package/bin/lib/render.js +1 -1
- package/mcp/fdeops-ingest/package.json +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
package/README.md
CHANGED
|
@@ -1,158 +1,139 @@
|
|
|
1
1
|
# FDEOps
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Forward deployed engineering skills for AI coding agents.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Your agent writes code. FDEOps helps you deliver the client work around it: understand the problem, agree what done means, prove the result, and hand it over.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
| You switch clients, agents, or engineers | Decisions, constraints, evidence, and one next action in each client's record |
|
|
7
|
+
<a name="who-this-is-for"></a>
|
|
8
|
+
|
|
9
|
+
An open-source kit for FDEs, independent consultants, and small agencies working across client projects. Use it with your coding agent. Keep a separate record for each client on your own laptop.
|
|
10
|
+
|
|
11
|
+
[Install](#quick-start) · [See the dashboard](#your-daily-fieldbook) · [Documentation](docs/README.md)
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
<img width="960" height="640" alt="FDEOps: forward deployed engineering from the first client meeting to handover" src="https://github.com/user-attachments/assets/2bcb8739-55ee-445d-8a1a-8b38433b7b58" />
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
## Why use it?
|
|
16
|
+
|
|
17
|
+
- **Pick up where you left off.** The brief, decisions, risks, and next action travel with the client record, across sessions and agents.
|
|
18
|
+
- **Know what you can stand behind.** Keep what was promised, what was measured, and what the customer accepted separate, with evidence for each result.
|
|
19
|
+
- **See what needs you today.** An offline dashboard shows the next step and missing evidence, measurement, or approval across your clients.
|
|
17
20
|
|
|
18
21
|
## Quick Start
|
|
19
22
|
|
|
20
|
-
Requires Node.js 18+, Git, and an AI coding agent
|
|
23
|
+
Requires Node.js 18+, Git, and an AI coding agent that supports skills. Install the FDEOps skill:
|
|
21
24
|
|
|
22
25
|
```bash
|
|
23
26
|
npx skills add suboss87/fdeops --skill fde
|
|
24
27
|
```
|
|
25
28
|
|
|
26
|
-
Open
|
|
29
|
+
Open a client workspace and tell your agent:
|
|
27
30
|
|
|
28
31
|
```text
|
|
29
32
|
@fde this is client01
|
|
30
33
|
```
|
|
31
34
|
|
|
32
|
-
The agent creates `~/fde-engagements/client01/.fde/` and
|
|
33
|
-
|
|
34
|
-
**Try the record before using client data:** `npx fdeops demo` runs a fictional notes-to-fieldbook workflow without a model. It writes under `~/fde-engagements/.demo/` and resets that sandbox each run. Remove it with `npx fdeops demo --clean`. For repository reconnaissance without writing engagement records, use `npx fdeops scan`.
|
|
35
|
-
|
|
36
|
-
<details>
|
|
37
|
-
<summary>Host installation and offline use</summary>
|
|
35
|
+
The agent creates `~/fde-engagements/client01/.fde/` on your laptop and links the workspace to that client. Paste the brief or meeting notes into the same conversation. `@fde` works through the situation with you; you review the proposed record before it is applied.
|
|
38
36
|
|
|
39
|
-
Claude Code plugin
|
|
37
|
+
**Claude Code plugin or another host?** Follow the [installation guide](docs/install.md). It explains hooks, adapters, offline setup, and the CLI fallback if your agent cannot run setup. Skill-only installation does not add automatic session hooks.
|
|
40
38
|
|
|
41
|
-
|
|
42
|
-
/plugin marketplace add suboss87/fdeops
|
|
43
|
-
/plugin install fdeops@fdeops
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
The plugin registers session hooks and slash commands. Skill-only installations do not register those hooks. For Cursor, Codex, Gemini, or Copilot, install the skill and run `npx fdeops adapters .` in the client workspace; this writes instruction pointers.
|
|
47
|
-
|
|
48
|
-
Clone installation:
|
|
39
|
+
**Try it without an AI model:**
|
|
49
40
|
|
|
50
41
|
```bash
|
|
51
|
-
|
|
52
|
-
cd fdeops
|
|
53
|
-
node bin/install.js
|
|
42
|
+
npx fdeops demo
|
|
54
43
|
```
|
|
55
44
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
</details>
|
|
59
|
-
|
|
60
|
-
## Commands
|
|
45
|
+
The demo runs fictional notes through review, applies them in a sandbox, and prints a fieldbook path to open. It writes and resets only its demo folder under `~/fde-engagements/.demo/`. Remove the demo with `npx fdeops demo --clean`. `npx` may download packages; the FDEOps CLI itself works locally.
|
|
61
46
|
|
|
62
|
-
|
|
47
|
+
⭐ **If FDEOps makes your client work easier, star the repo.**
|
|
63
48
|
|
|
64
|
-
|
|
65
|
-
|---|---|---|
|
|
66
|
-
| Land | `/brief` | The brief, unknowns, and who can accept the work |
|
|
67
|
-
| Discover | `/discover` | The actual workflow and evidence behind the problem |
|
|
68
|
-
| Plan | `/plan` | A small deliverable, constraints, and acceptance criteria |
|
|
69
|
-
| Ship | `/ship` | Proof on their staging, release approval, and rollback |
|
|
70
|
-
| Outcome | `/outcome` | Promised, measured, accepted, and supporting evidence |
|
|
71
|
-
| Close | `/close` | Runbook, operating owner, and handover gaps |
|
|
49
|
+
## What a working day looks like
|
|
72
50
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
## One loop you can defend
|
|
76
|
-
|
|
77
|
-
Paste messy notes after a meeting:
|
|
51
|
+
You come out of a meeting with this:
|
|
78
52
|
|
|
79
53
|
```text
|
|
80
|
-
@fde
|
|
81
|
-
but Mara has not approved
|
|
82
|
-
|
|
54
|
+
@fde Mara wants CSV upload first. Devon asked for ERP sync,
|
|
55
|
+
but Mara has not approved that scope. The staging replay took
|
|
56
|
+
five minutes; production has not been measured. Ask Mara for
|
|
57
|
+
staging access before Friday. Source: kickoff meeting, 10 September.
|
|
83
58
|
```
|
|
84
59
|
|
|
85
|
-
|
|
86
|
-
2. **Apply:** confirm the proposed interpretation. An ERP request stays a request; a staging result stays a staging result. Saving the record does not mean the customer approved either.
|
|
87
|
-
3. **Defend:** ask `@fde What did we agree about ERP, and what evidence supports the result?` Review the supplied source, superseded decisions, and missing evidence before using the answer in a sponsor update.
|
|
60
|
+
The agent shows you what changed, what was only requested, what is still unknown, and what happens next. You correct it and confirm the update. A request does not become agreed scope. A staging result does not become a production result. Naming a signer does not mean they accepted the work.
|
|
88
61
|
|
|
89
|
-
|
|
62
|
+
Tomorrow, `@fde` resumes from that record. Before a sponsor meeting, ask what was promised, what was measured, and where the proof is. When someone takes over, give them the handoff packet.
|
|
90
63
|
|
|
91
|
-
|
|
64
|
+
[Walk through notes → REVIEW → apply → evidence](docs/USAGE.md#new-here-5-minutes).
|
|
92
65
|
|
|
93
|
-
|
|
66
|
+
## Your daily fieldbook
|
|
94
67
|
|
|
95
|
-
|
|
68
|
+

|
|
96
69
|
|
|
97
70
|
```bash
|
|
98
|
-
npx fdeops dashboard --open #
|
|
71
|
+
npx fdeops dashboard --open # current client
|
|
99
72
|
npx fdeops dashboard --all --open # all clients
|
|
100
73
|
```
|
|
101
74
|
|
|
102
|
-
|
|
75
|
+
Open a client, inspect its record, and copy an action into your agent to continue the work. The dashboard is a read-only snapshot. After updating the record, run the command again to refresh it. The screenshot uses fictional clients.
|
|
103
76
|
|
|
104
|
-
|
|
77
|
+
<a name="how-skills-work"></a>
|
|
105
78
|
|
|
106
|
-
|
|
79
|
+
## From the first meeting to handover
|
|
107
80
|
|
|
108
|
-
|
|
109
|
-
@fde + your situation → one relevant skill → reviewed work → .fde/ → fieldbook
|
|
110
|
-
```
|
|
81
|
+
Describe the situation to `@fde`; it selects the relevant workflow. You do not need to learn a catalog of skills.
|
|
111
82
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
83
|
+
| Stage | What you work through |
|
|
84
|
+
|---|---|
|
|
85
|
+
| Land | Understand the brief and name who can accept the work |
|
|
86
|
+
| Discover | Check the actual workflow, constraints, and source of the problem |
|
|
87
|
+
| Plan | Agree a small deliverable and an observable test for “done” |
|
|
88
|
+
| Ship | Prove it on their staging, review release approval, and test recovery |
|
|
89
|
+
| Outcome | Compare promised and measured results; record customer acceptance |
|
|
90
|
+
| Close | Transfer the runbook, evidence, open risks, and operating ownership |
|
|
115
91
|
|
|
116
|
-
|
|
92
|
+
The Claude Code plugin also provides `/brief`, `/discover`, `/plan`, `/ship`, `/outcome`, and `/close`. Daily plugin shortcuts are `/debrief`, `/prep`, `/trust`, `/receipts`, and `/readout`. Other hosts use the same `@fde` entry and methodology.
|
|
117
93
|
|
|
118
|
-
|
|
119
|
-
|---|---|
|
|
120
|
-
| `context.md` | Current state and next action |
|
|
121
|
-
| `brief.md`, `reality.md`, `terrain.md` | Request, observed problem, and system constraints |
|
|
122
|
-
| `success.md`, `stakeholders.md`, `trust-profile.md` | Acceptance criteria, people, data policy, and authority |
|
|
123
|
-
| `decisions.md`, `risks.md`, `delivery.md` | Choices, unresolved risks, measurements, evidence, and recorded acceptance |
|
|
94
|
+
Each skill gives the agent steps to follow, a record to produce, and a checkpoint with you. There are 30 routed skills, plus industry overlays. Start with the [three delivery checklists](docs/skills.md#three-delivery-checklists); use the [full reference](docs/skills-reference.md) when you need the detail.
|
|
124
95
|
|
|
125
|
-
|
|
96
|
+
## Use the CLI directly
|
|
126
97
|
|
|
127
|
-
|
|
98
|
+
The CLI does the file work without calling a model. Commands that change records write when you run them; `debrief --smart` stages a proposal for review before `--apply`.
|
|
128
99
|
|
|
129
|
-
|
|
100
|
+
| You need to… | Run |
|
|
101
|
+
|---|---|
|
|
102
|
+
| Resume a client | `npx fdeops resume` |
|
|
103
|
+
| Review messy notes | `npx fdeops debrief --smart notes.md` |
|
|
104
|
+
| Apply the reviewed proposal | `npx fdeops debrief --apply` |
|
|
105
|
+
| Find a source | `npx fdeops recall "retry decision"` |
|
|
106
|
+
| Prepare a sponsor readout | `npx fdeops defend` |
|
|
107
|
+
| Check readiness to plan or build | `npx fdeops doctor --ready` |
|
|
108
|
+
| Export a successor packet | `npx fdeops handoff --out successor.md` |
|
|
130
109
|
|
|
131
|
-
|
|
110
|
+
[All commands and examples](docs/USAGE.md).
|
|
132
111
|
|
|
133
|
-
-
|
|
134
|
-
- Name who can accept which outcome.
|
|
135
|
-
- Keep promised, measured, and accepted results separate.
|
|
136
|
-
- Prove a small change where the client will operate it, with a tested recovery path.
|
|
137
|
-
- Leave a record another engineer can understand and challenge.
|
|
138
|
-
- Keep each client separate and confirm the active binding before writing.
|
|
112
|
+
For free-form notes, start with `@fde` so your agent can help interpret them. The CLI's `--smart` option recognizes common phrases; review its proposal because it can miss requests or next actions in ordinary prose.
|
|
139
113
|
|
|
140
|
-
## Your
|
|
114
|
+
## Your records, your control
|
|
141
115
|
|
|
142
|
-
|
|
116
|
+
Each client has its own `.fde/` folder of Markdown files. The brief, success criteria, decisions, risks, and delivery ledger stay readable outside FDEOps. The CLI uses local files and Git, with no network calls or telemetry. Optional [MCP sources](mcp/recipes/) are connected through your agent host, then pulled and reviewed on request.
|
|
143
117
|
|
|
144
|
-
|
|
118
|
+
Default session context is capped at **16 KiB**, including constraints and selected records. Older detail stays on disk and can be retrieved with `recall`. This limits FDEOps output, not everything your agent puts into its context window.
|
|
145
119
|
|
|
146
|
-
|
|
120
|
+
Your AI host may send what it reads to its configured model. FDEOps redacts `<private>` blocks from its CLI, hook, and dashboard outputs; do not paste or load raw private blocks through file tools. Enabled session hooks can record mechanical session state automatically. Review reports before sharing client information.
|
|
147
121
|
|
|
148
|
-
|
|
122
|
+
The CLI and dashboard need no model. The tested small local models produced wrong or incomplete answers; read the results before relying on one for client work. [Verification and limits](docs/verification.md) · [Privacy](PRIVACY.md) · [Security](SECURITY.md).
|
|
149
123
|
|
|
150
|
-
##
|
|
124
|
+
## Find your way around
|
|
151
125
|
|
|
152
|
-
|
|
126
|
+
| You want to… | Start here |
|
|
127
|
+
|---|---|
|
|
128
|
+
| Install or use FDEOps | [Documentation](docs/README.md) |
|
|
129
|
+
| Understand or change a workflow | [The `@fde` skill](skills/fde/SKILL.md) and its `references/` |
|
|
130
|
+
| Work on the CLI or dashboard | [`bin/`](bin/), shared helpers in `bin/lib/`, regressions in [`test/`](test/) |
|
|
131
|
+
| Explore sample client records | [`examples/`](examples/) |
|
|
132
|
+
| Check measured behavior and known gaps | [`evals/`](evals/) and [verification](docs/verification.md) |
|
|
133
|
+
| Understand every folder | [Repository map](docs/REPO_LAYOUT.md) |
|
|
153
134
|
|
|
154
|
-
|
|
135
|
+
## Contribute
|
|
155
136
|
|
|
156
|
-
|
|
137
|
+
Share an anonymized failure case, a reproducible bug, or a focused improvement through [Issues](https://github.com/suboss87/FDEOps/issues) or [Discussions](https://github.com/suboss87/FDEOps/discussions). Read the [contribution guide](CONTRIBUTING.md) before changing a workflow or command.
|
|
157
138
|
|
|
158
|
-
|
|
139
|
+
Built and maintained by **[Subash Natarajan](https://github.com/suboss87)**. MIT licensed; preserve applicable notices when redistributing.
|
package/bin/check.js
CHANGED
|
@@ -264,39 +264,28 @@ for (const m of readme.matchAll(/(?:\]\(|src=")([^)"#\s]+)(?:\)|")/g)) {
|
|
|
264
264
|
if (brokenLinks.length) fail(`README links to missing paths: ${brokenLinks.join(', ')}`)
|
|
265
265
|
else ok('README links all resolve')
|
|
266
266
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
'Who this is for',
|
|
272
|
-
'Commands',
|
|
273
|
-
'Principles',
|
|
274
|
-
]) {
|
|
275
|
-
if (!readme.includes(section)) fail(`README missing section: ${section}`)
|
|
267
|
+
// Validate usable entry points, not a fixed heading order or marketing copy.
|
|
268
|
+
// Host-specific commands and advanced setup belong in the linked guides.
|
|
269
|
+
for (const target of ['docs/install.md', 'docs/USAGE.md', 'docs/REPO_LAYOUT.md', 'docs/verification.md', 'PRIVACY.md']) {
|
|
270
|
+
if (!readme.includes(target)) fail(`README must link its public guide: ${target}`)
|
|
276
271
|
}
|
|
277
|
-
if (!readme.includes('AI coding agent')) {
|
|
278
|
-
fail('README must
|
|
272
|
+
if (!readme.includes('AI coding agent') || !/FDE|Forward Deployed Engineer/i.test(readme)) {
|
|
273
|
+
fail('README must identify the tool and its intended users')
|
|
274
|
+
}
|
|
275
|
+
if (!/npx skills add suboss87\/fdeops --skill fde/.test(readme) || !/@fde\s+this is/.test(readme)) {
|
|
276
|
+
fail('README must show skill installation and how to start a client')
|
|
279
277
|
}
|
|
280
|
-
ok('README clarity sections')
|
|
281
|
-
|
|
282
278
|
for (const cmd of ['/brief', '/discover', '/plan', '/ship', '/outcome', '/close', '/debrief', '/prep', '/trust', '/receipts', '/readout']) {
|
|
283
|
-
if (!readme.includes(cmd)) fail(`
|
|
279
|
+
if (!(readme + usage).includes(cmd)) fail(`Public usage documentation missing slash command ${cmd}`)
|
|
284
280
|
}
|
|
285
281
|
if (/(^|[^\w/])\/got\b/.test(readme)) fail('README must use /outcome, not /got')
|
|
286
|
-
ok('README
|
|
287
|
-
|
|
288
|
-
// Front-door map is the embed left-to-right (Land → Close). After the GitHub
|
|
289
|
-
// poster (#68) the table is the map: /brief /discover /plan /ship /outcome /close.
|
|
290
|
-
const front = readme.slice(0, 4000)
|
|
291
|
-
if (!['/brief', '/discover', '/plan', '/ship', '/outcome', '/close'].every(c => front.includes(c))) {
|
|
292
|
-
fail('README must include the Land→Close command map near the top')
|
|
293
|
-
} else ok('README command-map diagram')
|
|
282
|
+
ok('README entry points and documented commands')
|
|
294
283
|
|
|
295
284
|
if (readme.includes('your-client-repo')) {
|
|
296
285
|
fail('README must not instruct install in customer repo (your-client-repo)')
|
|
297
286
|
} else ok('README no customer-repo install')
|
|
298
287
|
|
|
299
|
-
if (!readme.includes('fde-engagements') || !/fdeops.*init
|
|
288
|
+
if (!readme.includes('fde-engagements') || !/fdeops.*resume --init/i.test(readme + read('docs/install.md'))) {
|
|
300
289
|
fail('README must document fde-engagements + init flow')
|
|
301
290
|
} else ok('README engagement path')
|
|
302
291
|
|
|
@@ -334,9 +323,9 @@ if (!fs.existsSync(path.join(root, 'docs', 'USAGE.md'))) {
|
|
|
334
323
|
fail('docs/USAGE.md missing')
|
|
335
324
|
} else ok('docs/USAGE.md')
|
|
336
325
|
|
|
337
|
-
if (!
|
|
338
|
-
fail('
|
|
339
|
-
} else ok('
|
|
326
|
+
if (!read('docs/install.md').includes('FDEOPS_ENGAGEMENT')) {
|
|
327
|
+
fail('Installation guide must document FDEOPS_ENGAGEMENT')
|
|
328
|
+
} else ok('Installation override documented')
|
|
340
329
|
|
|
341
330
|
const badPhrases = ['team of ten', 'solo 100x', '100x engineer']
|
|
342
331
|
for (const phrase of badPhrases) {
|
|
@@ -356,12 +345,6 @@ for (const rx of derivativeFraming) {
|
|
|
356
345
|
if (/docs\/internal|PMF_360/i.test(readme)) {
|
|
357
346
|
fail('README must not link docs/internal or PMF_360')
|
|
358
347
|
}
|
|
359
|
-
if (!/One command per stage/.test(readme) || !/Skills load automatically/.test(readme)) {
|
|
360
|
-
fail('README must formulate Commands as: one command per stage, skills load automatically')
|
|
361
|
-
}
|
|
362
|
-
if (!/Not prompts/.test(readme)) {
|
|
363
|
-
fail('README catalog must say skills are not prompts')
|
|
364
|
-
}
|
|
365
348
|
if (/\b(30|31|37)\s+methods\b|\broutes methods\b|\bphase methods\b|\bfield methods\b|\bengagement methods\b/.test(readme)) {
|
|
366
349
|
fail('README must call the catalog skills, not methods')
|
|
367
350
|
}
|
package/bin/fde.js
CHANGED
|
@@ -487,7 +487,11 @@ function formatFsError(err, action, target) {
|
|
|
487
487
|
return `cannot ${action} ${where}${code ? ` (${code})` : ''}${err && err.message && !code ? ': ' + err.message : ''}`
|
|
488
488
|
}
|
|
489
489
|
|
|
490
|
+
let debriefTransactionActive = false
|
|
491
|
+
const ownedDebriefLocks = new Set()
|
|
492
|
+
|
|
490
493
|
function failFs(err, action, target) {
|
|
494
|
+
if (debriefTransactionActive || ownedDebriefLocks.size) throw new Error(formatFsError(err, action, target))
|
|
491
495
|
console.error(formatFsError(err, action, target))
|
|
492
496
|
process.exit(1)
|
|
493
497
|
}
|
|
@@ -503,6 +507,7 @@ function refuseSymlinkWrite(p, opts = {}) {
|
|
|
503
507
|
const msg = st.isSymbolicLink()
|
|
504
508
|
? `refused: ${path.basename(p)} is a symlink - write would leave the engagement tree. Replace it with a real file.`
|
|
505
509
|
: `refused: ${path.basename(p)} is not a regular file - remove it and re-run; every write is refused while it is there.`
|
|
510
|
+
if ((debriefTransactionActive || ownedDebriefLocks.size) && !opts.soft) throw new Error(msg)
|
|
506
511
|
if (opts.soft) return msg
|
|
507
512
|
console.error(msg)
|
|
508
513
|
process.exit(1)
|
|
@@ -518,6 +523,8 @@ function refuseSymlinkWrite(p, opts = {}) {
|
|
|
518
523
|
// Exclusive create lock + retry. Two parallel agent sessions (or hook + CLI)
|
|
519
524
|
// appending the same .fde file otherwise interleave/corrupt under load.
|
|
520
525
|
function withFileLock(targetPath, fn, opts = {}) {
|
|
526
|
+
if (ownedDebriefLocks.has(targetPath)) return fn()
|
|
527
|
+
if (debriefTransactionActive || ownedDebriefLocks.size) opts = { ...opts, soft: true }
|
|
521
528
|
const lockPath = targetPath + '.lock'
|
|
522
529
|
const deadline = Date.now() + 5000
|
|
523
530
|
while (true) {
|
|
@@ -1789,28 +1796,24 @@ function readDebriefInput(args) {
|
|
|
1789
1796
|
if (args[0]) {
|
|
1790
1797
|
const notesPath = args[0].replace(/^~/, HOME)
|
|
1791
1798
|
let st
|
|
1792
|
-
try { st = fs.statSync(notesPath) } catch (_) {
|
|
1799
|
+
try { st = fs.statSync(notesPath) } catch (_) { throw new Error(`cannot read ${args[0]}`) }
|
|
1793
1800
|
if (st.size > DEBRIEF_MAX_BYTES) {
|
|
1794
|
-
|
|
1795
|
-
process.exit(1)
|
|
1801
|
+
throw new Error(`debrief refused: ${args[0]} is ${st.size} bytes (max ${DEBRIEF_MAX_BYTES}). Split the notes or paste the relevant section.`)
|
|
1796
1802
|
}
|
|
1797
1803
|
let buf
|
|
1798
|
-
try { buf = fs.readFileSync(notesPath) } catch (_) {
|
|
1804
|
+
try { buf = fs.readFileSync(notesPath) } catch (_) { throw new Error(`cannot read ${args[0]}`) }
|
|
1799
1805
|
if (buf.includes(0) || looksLikeBinaryNoise(buf.toString('utf8'))) {
|
|
1800
|
-
|
|
1801
|
-
process.exit(1)
|
|
1806
|
+
throw new Error(`debrief refused: ${args[0]} looks binary or mostly non-printable. Paste text notes only.`)
|
|
1802
1807
|
}
|
|
1803
1808
|
input = buf.toString('utf8')
|
|
1804
1809
|
} else {
|
|
1805
1810
|
let buf
|
|
1806
1811
|
try { buf = fs.readFileSync(0) } catch (_) { buf = Buffer.alloc(0) }
|
|
1807
1812
|
if (Buffer.byteLength(buf) > DEBRIEF_MAX_BYTES) {
|
|
1808
|
-
|
|
1809
|
-
process.exit(1)
|
|
1813
|
+
throw new Error(`debrief refused: stdin is over ${DEBRIEF_MAX_BYTES} bytes. Split the notes.`)
|
|
1810
1814
|
}
|
|
1811
1815
|
if (buf.includes(0) || looksLikeBinaryNoise(buf.toString('utf8'))) {
|
|
1812
|
-
|
|
1813
|
-
process.exit(1)
|
|
1816
|
+
throw new Error('debrief refused: stdin looks binary or mostly non-printable. Paste text notes only.')
|
|
1814
1817
|
}
|
|
1815
1818
|
input = buf.toString('utf8')
|
|
1816
1819
|
}
|
|
@@ -1823,15 +1826,29 @@ function previewLine(text, max = 240) {
|
|
|
1823
1826
|
return `${t.slice(0, max)}… (${t.length} chars)`
|
|
1824
1827
|
}
|
|
1825
1828
|
|
|
1826
|
-
function writeProposal(eng, text) {
|
|
1829
|
+
function writeProposal(eng, text, { replace = false, locked = false } = {}) {
|
|
1830
|
+
if (!locked && ownedDebriefLocks.has(path.join(eng, DEBRIEF_PROPOSE))) return writeProposal(eng, text, { replace, locked: true })
|
|
1831
|
+
if (!locked) return withFileLock(path.join(eng, DEBRIEF_PROPOSE), () => {
|
|
1832
|
+
ownedDebriefLocks.add(path.join(eng, DEBRIEF_PROPOSE))
|
|
1833
|
+
try { return writeProposal(eng, text, { replace, locked: true }) }
|
|
1834
|
+
finally { ownedDebriefLocks.delete(path.join(eng, DEBRIEF_PROPOSE)) }
|
|
1835
|
+
}, { soft: true })
|
|
1836
|
+
if (!debriefTransactionActive) return withDebriefRecords(eng, () => writeProposal(eng, text, { replace, locked: true }), [DEBRIEF_PROPOSE, DEBRIEF_PRIVATE, DEBRIEF_SEAL])
|
|
1827
1837
|
const { clean, blocks } = splitPrivate(text, { sealDangling: true })
|
|
1828
1838
|
const proposePath = path.join(eng, DEBRIEF_PROPOSE)
|
|
1829
1839
|
const privatePath = path.join(eng, DEBRIEF_PRIVATE)
|
|
1840
|
+
if (fs.existsSync(proposePath) && !replace) {
|
|
1841
|
+
const existing = fs.readFileSync(proposePath, 'utf8')
|
|
1842
|
+
const existingPrivate = readSealedProposal(eng)
|
|
1843
|
+
if (existing !== clean || JSON.stringify(existingPrivate) !== JSON.stringify(blocks)) {
|
|
1844
|
+
throw new Error('pending proposal already exists. Review and apply it first, or explicitly replace it with fde debrief --smart <notes> --replace-proposal.')
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1830
1847
|
// Seal first. A refused or failed sidecar write must not leave behind a
|
|
1831
1848
|
// proposal whose (private - redacted) marker has nothing left behind it.
|
|
1832
1849
|
if (blocks.length) {
|
|
1833
1850
|
const blocked = refuseSymlinkWrite(privatePath, { soft: true })
|
|
1834
|
-
if (blocked)
|
|
1851
|
+
if (blocked) throw new Error(blocked)
|
|
1835
1852
|
withFileLock(privatePath, () => { atomicWriteFile(privatePath, sealedText(blocks), { mode: 0o600 }) })
|
|
1836
1853
|
try { fs.chmodSync(privatePath, 0o600) } catch (_) {}
|
|
1837
1854
|
} else {
|
|
@@ -1898,12 +1915,13 @@ function printDebriefReview(text, eng) {
|
|
|
1898
1915
|
let any = false
|
|
1899
1916
|
for (const [label, items] of order) {
|
|
1900
1917
|
if (!items.length) {
|
|
1901
|
-
if (['stated asks', 'proposed scope', 'next action', 'named signer (authority, not approval)'].includes(label)) console.log(` ${label}: not
|
|
1918
|
+
if (['stated asks', 'proposed scope', 'next action', 'named signer (authority, not approval)'].includes(label)) console.log(` ${label}: not detected - review the notes`)
|
|
1902
1919
|
continue
|
|
1903
1920
|
}
|
|
1904
1921
|
any = true
|
|
1905
1922
|
console.log(` ${label}:`)
|
|
1906
|
-
for (const item of items) console.log(` - ${item}`)
|
|
1923
|
+
for (const item of items.slice(0, 5)) console.log(` - ${item}`)
|
|
1924
|
+
if (items.length > 5) console.log(` - ${items.length - 5} more omitted here; review the full proposal before applying.`)
|
|
1907
1925
|
}
|
|
1908
1926
|
if (!any) console.log(' (nothing prefixed yet - edit .debrief-propose, then apply)')
|
|
1909
1927
|
const recorded = eng ? parseValueLedger(eng).rows : []
|
|
@@ -1972,7 +1990,79 @@ function readSealedProposal(eng) {
|
|
|
1972
1990
|
} catch (_) { return [] }
|
|
1973
1991
|
}
|
|
1974
1992
|
|
|
1993
|
+
// A debrief touches several records. Acquire every cooperating writer's lock
|
|
1994
|
+
// before the first append, and restore snapshots if an ordinary write fails.
|
|
1995
|
+
// This is not a power-loss transaction; no history is deleted or reset.
|
|
1996
|
+
function withDebriefRecords(eng, apply, files = ['decisions.md', 'risks.md', 'delivery.md', 'stakeholders.md',
|
|
1997
|
+
'success.md', 'context.md', SIGNAL_LEDGER, LAST_WRITE, DEBRIEF_PROPOSE, DEBRIEF_PRIVATE, DEBRIEF_SEAL]) {
|
|
1998
|
+
files = files.slice().sort()
|
|
1999
|
+
const snapshots = new Map()
|
|
2000
|
+
function lockAt(index) {
|
|
2001
|
+
if (index < files.length) {
|
|
2002
|
+
const target = path.join(eng, files[index])
|
|
2003
|
+
if (ownedDebriefLocks.has(target)) return lockAt(index + 1)
|
|
2004
|
+
return withFileLock(target, () => {
|
|
2005
|
+
ownedDebriefLocks.add(target)
|
|
2006
|
+
try { return lockAt(index + 1) } finally { ownedDebriefLocks.delete(target) }
|
|
2007
|
+
}, { soft: true })
|
|
2008
|
+
}
|
|
2009
|
+
for (const file of files) {
|
|
2010
|
+
const target = path.join(eng, file)
|
|
2011
|
+
const blocked = refuseSymlinkWrite(target, { soft: true })
|
|
2012
|
+
if (blocked) throw new Error(blocked)
|
|
2013
|
+
snapshots.set(target, fs.existsSync(target) ? { bytes: fs.readFileSync(target), mode: fs.statSync(target).mode & 0o777 } : null)
|
|
2014
|
+
}
|
|
2015
|
+
debriefTransactionActive = true
|
|
2016
|
+
try { return apply() } catch (error) {
|
|
2017
|
+
const failed = []
|
|
2018
|
+
for (const [target, previous] of snapshots) {
|
|
2019
|
+
try {
|
|
2020
|
+
if (previous) atomicWriteFile(target, previous.bytes, { mode: previous.mode, soft: true })
|
|
2021
|
+
else if (fs.existsSync(target)) fs.unlinkSync(target)
|
|
2022
|
+
} catch (_) { failed.push(path.basename(target)) }
|
|
2023
|
+
}
|
|
2024
|
+
if (failed.length) throw new Error(`${error.message}; recovery could not restore ${failed.join(', ')}. Inspect these records and the pending proposal before retrying.`)
|
|
2025
|
+
throw new Error(`${error.message}; no record changes kept. The proposal is retained; retry after resolving the cause.`)
|
|
2026
|
+
} finally { debriefTransactionActive = false }
|
|
2027
|
+
}
|
|
2028
|
+
return lockAt(0)
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
// Limit model-facing review output while preserving the full editable proposal.
|
|
2032
|
+
function boundedDebriefPreview(eng, render, { proposal = true, maxBytes = 12000 } = {}) {
|
|
2033
|
+
const original = console.log, originalError = console.error
|
|
2034
|
+
let bytes = 0, omitted = 0
|
|
2035
|
+
const bounded = output => (...args) => {
|
|
2036
|
+
const line = args.join(' ') + '\n'
|
|
2037
|
+
const size = Buffer.byteLength(line)
|
|
2038
|
+
if (bytes + size > maxBytes) { omitted++; return }
|
|
2039
|
+
bytes += size; output(...args)
|
|
2040
|
+
}
|
|
2041
|
+
console.log = bounded(original)
|
|
2042
|
+
console.error = bounded(originalError)
|
|
2043
|
+
let result
|
|
2044
|
+
try { result = render() } finally { console.log = original; console.error = originalError }
|
|
2045
|
+
if (omitted) console.log(proposal
|
|
2046
|
+
? `\n${omitted} preview lines omitted. Review the complete proposal at ${path.join(eng, DEBRIEF_PROPOSE)} before applying.`
|
|
2047
|
+
: `\n${omitted} preview lines omitted. Review the full input notes before applying.`)
|
|
2048
|
+
return result
|
|
2049
|
+
}
|
|
2050
|
+
|
|
1975
2051
|
function routeDebriefInput(eng, input, { dry, force, sealed = [] }) {
|
|
2052
|
+
if (!dry && !debriefTransactionActive) {
|
|
2053
|
+
ensureMemoryGit(eng)
|
|
2054
|
+
return withDebriefRecords(eng, () => {
|
|
2055
|
+
const result = routeDebriefInput(eng, input, { dry, force, sealed })
|
|
2056
|
+
// Consuming the review is part of the write. If cleanup fails, restoring
|
|
2057
|
+
// both the records and proposal makes the next explicit apply safe.
|
|
2058
|
+
for (const file of [DEBRIEF_PROPOSE, DEBRIEF_PRIVATE, DEBRIEF_SEAL]) {
|
|
2059
|
+
try { fs.unlinkSync(path.join(eng, file)) } catch (error) {
|
|
2060
|
+
if (error.code !== 'ENOENT') throw new Error(formatFsError(error, 'remove', file))
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
return result
|
|
2064
|
+
})
|
|
2065
|
+
}
|
|
1976
2066
|
const d = new Date()
|
|
1977
2067
|
const date = d.toISOString().slice(0, 10)
|
|
1978
2068
|
const counts = { decision: 0, risk: 0, delivery: 0, contact: 0, next: 0, signer: 0 }
|
|
@@ -2044,6 +2134,18 @@ function routeDebriefInput(eng, input, { dry, force, sealed = [] }) {
|
|
|
2044
2134
|
}
|
|
2045
2135
|
|
|
2046
2136
|
function cmdDebrief(args) {
|
|
2137
|
+
const eng = resolveEngagement({ forWrite: true })
|
|
2138
|
+
if (!eng) { console.error('no engagement - run: fde resume --init <name>'); process.exitCode = 2; return }
|
|
2139
|
+
const target = path.join(eng, DEBRIEF_PROPOSE)
|
|
2140
|
+
try {
|
|
2141
|
+
withFileLock(target, () => {
|
|
2142
|
+
ownedDebriefLocks.add(target)
|
|
2143
|
+
try { return runDebrief(args, eng) } finally { ownedDebriefLocks.delete(target) }
|
|
2144
|
+
}, { soft: true })
|
|
2145
|
+
} catch (error) { console.error(error.message); process.exitCode = 1 }
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
function runDebrief(args, eng) {
|
|
2047
2149
|
args = args.slice()
|
|
2048
2150
|
const dryIdx = args.indexOf('--dry-run')
|
|
2049
2151
|
const dry = dryIdx !== -1
|
|
@@ -2058,35 +2160,45 @@ function cmdDebrief(args) {
|
|
|
2058
2160
|
const forceIdx = args.indexOf('--force')
|
|
2059
2161
|
if (forceIdx !== -1) { force = true; args.splice(forceIdx, 1) }
|
|
2060
2162
|
|
|
2061
|
-
const
|
|
2062
|
-
|
|
2163
|
+
const replaceIdx = args.indexOf('--replace-proposal')
|
|
2164
|
+
const replace = replaceIdx !== -1
|
|
2165
|
+
if (replace) args.splice(replaceIdx, 1)
|
|
2166
|
+
if (replace && !smart) throw new Error('--replace-proposal requires --smart <notes>')
|
|
2063
2167
|
|
|
2168
|
+
if (!smart && !apply && !dry && fs.existsSync(path.join(eng, DEBRIEF_PROPOSE))) {
|
|
2169
|
+
throw new Error('pending proposal already exists. Review and apply it before writing another debrief.')
|
|
2170
|
+
}
|
|
2171
|
+
if (apply && !smart && args[0] && fs.existsSync(path.join(eng, DEBRIEF_PROPOSE))) {
|
|
2172
|
+
throw new Error('pending proposal already exists. Use debrief --apply without a notes file to apply that review.')
|
|
2173
|
+
}
|
|
2064
2174
|
let input = ''
|
|
2065
2175
|
let sealed = []
|
|
2066
2176
|
if (apply && !smart && !args[0]) {
|
|
2067
2177
|
try { input = stripControlChars(fs.readFileSync(path.join(eng, DEBRIEF_PROPOSE), 'utf8')) } catch (_) {
|
|
2068
2178
|
console.error('nothing to apply - run: fde debrief --smart <notes.md> then fde debrief --apply')
|
|
2069
|
-
process.
|
|
2179
|
+
return void (process.exitCode = 1)
|
|
2070
2180
|
}
|
|
2071
2181
|
sealed = readSealedProposal(eng)
|
|
2072
2182
|
const expected = readSealCount(eng)
|
|
2073
2183
|
if (expected === null ? (!sealed.length && input.includes(PRIVATE_MARKER)) : sealed.length < expected) {
|
|
2074
2184
|
console.error(`refused: the proposal seals a private note but ${DEBRIEF_PRIVATE} is missing or unreadable - applying now would drop it silently.`)
|
|
2075
2185
|
console.error('re-run the propose step (fde debrief --smart <notes> | fde ingest propose <id>).')
|
|
2076
|
-
process.
|
|
2186
|
+
return void (process.exitCode = 1)
|
|
2077
2187
|
}
|
|
2078
2188
|
} else {
|
|
2079
2189
|
input = readDebriefInput(args)
|
|
2080
2190
|
}
|
|
2081
2191
|
|
|
2082
2192
|
if (smart) {
|
|
2083
|
-
const { proposePath, clean, blocks } = writeProposal(eng, smartProposeText(input))
|
|
2193
|
+
const { proposePath, clean, blocks } = writeProposal(eng, smartProposeText(input), { replace })
|
|
2194
|
+
boundedDebriefPreview(eng, () => {
|
|
2084
2195
|
console.log('SMART PROPOSE (heuristic - review before apply; no new facts invented beyond line rewrites)\n')
|
|
2085
2196
|
printDebriefReview(clean, eng)
|
|
2086
2197
|
console.log('Prefix vocabulary (lines that route): decision: risk: delivery: contact: next: signer:')
|
|
2087
2198
|
console.log('Optional on a decision: [approved: Name YYYY-MM-DD]. Missing means unconfirmed.')
|
|
2088
2199
|
console.log('Everything else → context.md. Keep the prefixes; the preview gate stays.\n')
|
|
2089
2200
|
routeDebriefInput(eng, clean, { dry: true, force, sealed: blocks })
|
|
2201
|
+
}, { maxBytes: 8000 })
|
|
2090
2202
|
if (!apply) {
|
|
2091
2203
|
console.log(`\nproposal saved → ${proposePath}`)
|
|
2092
2204
|
console.log('confirm: fde debrief --apply')
|
|
@@ -2097,14 +2209,12 @@ function cmdDebrief(args) {
|
|
|
2097
2209
|
sealed = blocks
|
|
2098
2210
|
}
|
|
2099
2211
|
|
|
2100
|
-
const
|
|
2212
|
+
const route = () => routeDebriefInput(eng, input, { dry, force, sealed })
|
|
2213
|
+
const { counts, ctxLines, privateBlocks } = boundedDebriefPreview(eng, route, { proposal: false, maxBytes: smart ? 4000 : 12000 })
|
|
2101
2214
|
if (!dry) {
|
|
2102
2215
|
const hash = commitMemory(eng, 'debrief', {
|
|
2103
2216
|
files: ['decisions.md', 'risks.md', 'delivery.md', 'stakeholders.md', 'success.md', 'context.md', SIGNAL_LEDGER],
|
|
2104
2217
|
})
|
|
2105
|
-
try { fs.unlinkSync(path.join(eng, DEBRIEF_PROPOSE)) } catch (_) {}
|
|
2106
|
-
try { fs.unlinkSync(path.join(eng, DEBRIEF_PRIVATE)) } catch (_) {}
|
|
2107
|
-
try { fs.unlinkSync(path.join(eng, DEBRIEF_SEAL)) } catch (_) {}
|
|
2108
2218
|
if (hash) console.log(`memory @${hash}`)
|
|
2109
2219
|
}
|
|
2110
2220
|
const plural = {
|
|
@@ -2224,10 +2334,14 @@ function cmdIngest(args) {
|
|
|
2224
2334
|
console.error(`ingest propose refused: staged item is over ${DEBRIEF_MAX_BYTES} bytes after provenance. Split it.`)
|
|
2225
2335
|
process.exit(1)
|
|
2226
2336
|
}
|
|
2227
|
-
|
|
2337
|
+
let proposal
|
|
2338
|
+
try { proposal = writeProposal(eng, smartProposeText(input)) } catch (error) { console.error(error.message); process.exitCode = 1; return }
|
|
2339
|
+
const { proposePath, clean, blocks } = proposal
|
|
2340
|
+
boundedDebriefPreview(eng, () => {
|
|
2228
2341
|
console.log(`INGEST PROPOSE from ${path.basename(item)} (via:${source})\n`)
|
|
2229
2342
|
printDebriefReview(clean, eng)
|
|
2230
2343
|
routeDebriefInput(eng, clean, { dry: true, force: false, sealed: blocks })
|
|
2344
|
+
})
|
|
2231
2345
|
console.log(`\nproposal saved → ${proposePath}`)
|
|
2232
2346
|
console.log('confirm: fde ingest apply')
|
|
2233
2347
|
console.log('(agent: rewrite lines with decision:/risk:/contact:/next: prefixes before apply)')
|
|
@@ -2353,13 +2467,13 @@ function cmdHandoff(args, label = 'Handoff') {
|
|
|
2353
2467
|
const claims = selected.filter(d => !hasSource(d.text))
|
|
2354
2468
|
const decisionText = d => `${d.text} (decisions.md:${d.line}, redacted view)`
|
|
2355
2469
|
const next = stripTemplateNoise(sectionBody(readClean(eng, 'context.md'), 'Next action', { lastNonEmpty: true }))
|
|
2356
|
-
const gaps = collectDoctorIssues(eng)
|
|
2470
|
+
const gaps = collectDoctorIssues(eng, { readiness: true })
|
|
2357
2471
|
const report = context.boundedSections([
|
|
2358
2472
|
`# ${label}: ${engagementSlugFromPath(eng)}\nSnapshot: ${new Date().toISOString()} · memory ${memoryHead(eng) || 'unversioned'}\nRead-only record, not proof of approval. Confirm sources with the named customer before relying on a claim. Private blocks are excluded; review remaining client information before sharing.`,
|
|
2359
2473
|
`## Constraints - trust-profile.md\n${stripTemplateNoise(readClean(eng, 'trust-profile.md')) || '(missing)'}`,
|
|
2360
2474
|
`## Signer and success - success.md\nSigner: ${signer || '(missing; do not infer)'}\n${success || '(missing)'}`,
|
|
2361
2475
|
`## Next action - context.md\n${next || '(missing)'}\n\n## Open risks - risks.md\n${extractRisks(eng).map(r => '- ' + r.text).join('\n') || '(none recorded; not proof of no risk)'}`,
|
|
2362
|
-
`## Accepted value - recorded assertion with source\n${ledger.filter(r => r.state === 'accepted').map(rowText).join('\n') || '(none)'}\n\n## CLAIMS and unmeasured promises\n${ledger.filter(r => r.state !== 'accepted').map(r => rowText(r) + ' [' + r.state + ']').join('\n') || '(none)'}`,
|
|
2476
|
+
`## Accepted value - recorded assertion with source\nOnly structured value-ledger rows are summarized here; review other notes in delivery.md before presenting or handing over this record.\n${ledger.filter(r => r.state === 'accepted').map(rowText).join('\n') || '(none)'}\n\n## CLAIMS and unmeasured promises\n${ledger.filter(r => r.state !== 'accepted').map(r => rowText(r) + ' [' + r.state + ']').join('\n') || '(none)'}`,
|
|
2363
2477
|
`## ON RECORD decisions - source supplied, not automatic approval\n${records.map(decisionText).join('\n') || '(none)'}\n\n## CLAIM decisions - source missing\n${claims.map(decisionText).join('\n') || '(none)'}\nSelected ${selected.length} of ${decisions.length} dated decisions. Retrieve older or conflicting decisions with fde recall.`,
|
|
2364
2478
|
`## Gaps before relying on this packet\n${gaps.map(g => '- ' + g).join('\n') || '(no deterministic lint gaps; human review still required)'}`,
|
|
2365
2479
|
], parsed.maxBytes)
|
|
@@ -3522,7 +3636,13 @@ function cmdDashboard(args) {
|
|
|
3522
3636
|
const html = render.buildFieldbookHtml({ engagements, today, generatedAt: new Date().toISOString() })
|
|
3523
3637
|
|
|
3524
3638
|
try {
|
|
3639
|
+
const isRecordPath = p => p.split(path.sep).some(part => ['.fde', '.git'].includes(part.toLowerCase()))
|
|
3640
|
+
if (isRecordPath(outPath)) throw new Error('save the dashboard outside .fde/ and .git/; these folders hold records, not reports')
|
|
3641
|
+
let existingParent = path.dirname(outPath)
|
|
3642
|
+
while (!fs.existsSync(existingParent)) existingParent = path.dirname(existingParent)
|
|
3643
|
+
if (isRecordPath(fs.realpathSync(existingParent))) throw new Error('save the dashboard outside .fde/ and .git/; this path points into a record folder')
|
|
3525
3644
|
fs.mkdirSync(path.dirname(outPath), { recursive: true })
|
|
3645
|
+
if (isRecordPath(fs.realpathSync(path.dirname(outPath)))) throw new Error('save the dashboard outside .fde/ and .git/; this path points into a record folder')
|
|
3526
3646
|
atomicWriteFile(outPath, html)
|
|
3527
3647
|
} catch (e) {
|
|
3528
3648
|
failFs(e, 'write fieldbook', outPath)
|
|
@@ -3883,6 +4003,7 @@ function printUsage() {
|
|
|
3883
4003
|
fde log --undo remove the last CLI log/debrief entry from memory
|
|
3884
4004
|
fde debrief [file] meeting notes → memory (prefixed lines; --dry-run; --force)
|
|
3885
4005
|
fde debrief --smart heuristic propose; REVIEW first (decided/asked/open/next/signer); --apply after one confirm
|
|
4006
|
+
--replace-proposal explicitly discard a pending review when proposing different notes
|
|
3886
4007
|
fde ingest stage … stage raw pull into <engagement>/.inbox/ (not .fde/)
|
|
3887
4008
|
fde ingest list list staged inbox items
|
|
3888
4009
|
fde ingest propose <id> smart-propose a staged item → .debrief-propose (confirm before apply)
|
package/bin/lib/render.js
CHANGED
|
@@ -573,7 +573,7 @@ ${railItems}
|
|
|
573
573
|
</aside>
|
|
574
574
|
<main id="fb-main" tabindex="-1" class="fb-main fb-scroll">
|
|
575
575
|
<div class="fb-main-inner">
|
|
576
|
-
<div class="fb-snapshot"><strong>Read-only snapshot</strong> · Snapshot generated <time datetime="${escapeHtml(generatedAt)}">${escapeHtml(generatedAt ? generatedAt.replace('T', ' ').replace(/\.\d+Z$/, ' UTC') : today)}</time>.
|
|
576
|
+
<div class="fb-snapshot"><strong>Read-only snapshot</strong> · Snapshot generated <time datetime="${escapeHtml(generatedAt)}">${escapeHtml(generatedAt ? generatedAt.replace('T', ' ').replace(/\.\d+Z$/, ' UTC') : today)}</time>. After updating your records, run the dashboard command that created this file again, keeping the same <code>--all</code> and <code>--out</code> options if used. Then reload. Reloading this page alone does not refresh the record.</div>
|
|
577
577
|
${todayView}
|
|
578
578
|
${clientViews}
|
|
579
579
|
<p id="fb-status" class="fb-copy-status" role="status" aria-live="polite"></p>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fdeops",
|
|
3
|
-
"version": "3.27.
|
|
3
|
+
"version": "3.27.1",
|
|
4
4
|
"description": "Client delivery tools for Forward Deployed Engineers. One @fde skill, local Markdown engagement records, and an offline dashboard for decisions, evidence, approvals, and next actions.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"fdeops": "bin/install.js",
|
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "fdeops",
|
|
4
|
-
"version": "3.27.
|
|
4
|
+
"version": "3.27.1",
|
|
5
5
|
"description": "Forward deployed engineering skills for AI coding agents. One @fde skill for the client work around the code. You confirm; then it lands in .fde/ on your laptop.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Subash Natarajan",
|