fdeops 3.27.1 → 3.28.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 +229 -72
- package/bin/fde.js +124 -28
- package/bin/lib/render.js +1 -1
- package/bin/lib/trust.js +4 -28
- package/bin/lib/value-ledger.js +46 -1
- package/mcp/fdeops-ingest/package.json +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/fde/references/audit.md +3 -3
- package/skills/fde/references/debrief.md +13 -1
package/README.md
CHANGED
|
@@ -2,138 +2,295 @@
|
|
|
2
2
|
|
|
3
3
|
**Forward deployed engineering skills for AI coding agents.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
<a name="why-use-it"></a>
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
You're on a customer site. The AI coding agent writes code in their repo. This kit is the work around that code: the brief, who can say yes, proof on their staging then live, whether they signed off, whether they can run it after you leave.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Notes stay on your laptop, in a separate record for each client. Review the agent's proposed changes before saving them.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Keep the coding pack you already use. FDEOps adds the client brief, decisions, and evidence around that work.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
[Quick start](#quick-start) · [Daily fieldbook](#your-daily-fieldbook) · [30 skills](#all-30-skills) · [Documentation](docs/README.md)
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
<img width="960" height="640" alt="FDEOps: client delivery from the first meeting to handover" src="https://github.com/user-attachments/assets/2bcb8739-55ee-445d-8a1a-8b38433b7b58" />
|
|
16
16
|
|
|
17
|
-
|
|
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
19
|
## Quick Start
|
|
22
20
|
|
|
23
|
-
|
|
21
|
+
**Try it in a local checkout.** Requires Node.js 18+ and Git:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx fdeops scan
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
It prints what to look at on day one and the questions to ask. The scan reads local files without changing them. `npx` may download the package.
|
|
28
|
+
|
|
29
|
+
**Then install the skill:**
|
|
24
30
|
|
|
25
31
|
```bash
|
|
26
32
|
npx skills add suboss87/fdeops --skill fde
|
|
27
33
|
```
|
|
28
34
|
|
|
29
|
-
|
|
35
|
+
One chat. Name the client:
|
|
30
36
|
|
|
31
37
|
```text
|
|
32
38
|
@fde this is client01
|
|
33
39
|
```
|
|
34
40
|
|
|
35
|
-
|
|
41
|
+
That creates `~/fde-engagements/client01/.fde/` on your laptop. Paste kickoff notes in the same thread. `@fde` picks what to check. You still decide. After a meeting you review what changed, new asks, open questions, and next actions. Correct the proposal, then confirm the update.
|
|
42
|
+
|
|
43
|
+
Open the engagement fieldbook:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx fdeops dashboard --open
|
|
47
|
+
```
|
|
36
48
|
|
|
37
|
-
|
|
49
|
+
Read-only HTML of the record - promised, measured, accepted, and evidence. Regenerate after you change memory. Day to day: [docs/USAGE.md](docs/USAGE.md).
|
|
38
50
|
|
|
39
|
-
|
|
51
|
+
<details>
|
|
52
|
+
<summary><b>Claude Code</b></summary>
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
/plugin marketplace add suboss87/fdeops
|
|
56
|
+
/plugin install fdeops@fdeops
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The plugin adds session hooks and the slash commands below. Skill-only installation does not add hooks. See the [installation guide](docs/install.md) for setup details.
|
|
60
|
+
|
|
61
|
+
</details>
|
|
62
|
+
|
|
63
|
+
<details>
|
|
64
|
+
<summary><b>Cursor</b></summary>
|
|
65
|
+
|
|
66
|
+
After installing the skill, add the FDEOps instructions to the **client workspace** you have open:
|
|
40
67
|
|
|
41
68
|
```bash
|
|
42
|
-
npx fdeops
|
|
69
|
+
npx fdeops adapters .
|
|
43
70
|
```
|
|
44
71
|
|
|
45
|
-
|
|
72
|
+
See [adapters/](adapters/README.md).
|
|
46
73
|
|
|
47
|
-
|
|
74
|
+
</details>
|
|
48
75
|
|
|
49
|
-
|
|
76
|
+
<details>
|
|
77
|
+
<summary><b>Codex, other agents, and offline setup</b></summary>
|
|
50
78
|
|
|
51
|
-
|
|
79
|
+
Use the skill installation above in a supported host. If the agent cannot create and bind the client folder, run this from the client workspace:
|
|
52
80
|
|
|
53
|
-
```
|
|
54
|
-
|
|
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.
|
|
81
|
+
```bash
|
|
82
|
+
npx fdeops resume --init client01
|
|
58
83
|
```
|
|
59
84
|
|
|
60
|
-
|
|
85
|
+
For offline use, transfer an existing checkout to a machine with Node.js and Git, then run `node bin/install.js` from that checkout. Host adapters, local models, and advanced options are covered in [docs/install.md](docs/install.md).
|
|
86
|
+
|
|
87
|
+
</details>
|
|
61
88
|
|
|
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.
|
|
63
89
|
|
|
64
|
-
|
|
90
|
+
<a name="what-a-working-day-looks-like"></a>
|
|
65
91
|
|
|
66
92
|
## Your daily fieldbook
|
|
67
93
|
|
|
68
|
-
|
|
94
|
+
See what needs your attention before you open another client thread: an open risk, missing evidence, a result waiting for acceptance, or the next action.
|
|
95
|
+
|
|
96
|
+

|
|
69
97
|
|
|
70
98
|
```bash
|
|
71
|
-
npx fdeops dashboard --open
|
|
72
|
-
npx fdeops dashboard --all --open # all clients
|
|
99
|
+
npx fdeops dashboard --all --open
|
|
73
100
|
```
|
|
74
101
|
|
|
75
|
-
Open a client, inspect its record, and copy an action into your agent to continue
|
|
102
|
+
Open a client, inspect its record, and copy an action into your agent to continue. The dashboard is a read-only snapshot; repeat the command after updating your records to refresh it.
|
|
76
103
|
|
|
77
|
-
|
|
104
|
+
**Want to see the whole loop first?** `npx fdeops demo` runs fictional notes through review and prints a sample fieldbook path to open. It writes and resets its own folder under `~/fde-engagements/.demo/`, needs no AI model, and can be removed with `npx fdeops demo --clean`.
|
|
78
105
|
|
|
79
|
-
|
|
106
|
+
⭐ If FDEOps makes your client work easier, star the repo.
|
|
80
107
|
|
|
81
|
-
|
|
108
|
+
---
|
|
82
109
|
|
|
83
|
-
|
|
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 |
|
|
110
|
+
<a name="from-the-first-meeting-to-handover"></a>
|
|
91
111
|
|
|
92
|
-
|
|
112
|
+
## Commands
|
|
93
113
|
|
|
94
|
-
|
|
114
|
+
Describe the situation to `@fde`. It selects the relevant skill. The Claude Code plugin also provides these stage commands; other hosts use the same method through `@fde`.
|
|
95
115
|
|
|
96
|
-
|
|
116
|
+
| What you're doing | Command | Stage |
|
|
117
|
+
|-------------------|---------|-------|
|
|
118
|
+
| First days. Get the brief. Name who signs. | `/brief` | Land |
|
|
119
|
+
| Check the brief is the real job. | `/discover` | Discover |
|
|
120
|
+
| Sequence from done, not from the ticket. | `/plan` | Plan |
|
|
121
|
+
| Prove it on their staging, then go live. | `/ship` | Ship |
|
|
122
|
+
| What you promised, measured, and who accepted. | `/outcome` | Outcome |
|
|
123
|
+
| Hand it over. They run it without you. | `/close` | Close |
|
|
97
124
|
|
|
98
|
-
|
|
125
|
+
Claude Code shortcuts for the moments between stages: `/debrief` (notes into the record), `/prep` (one page before you walk in), `/trust` (process gap, or they stopped trusting you), `/receipts` (find what was recorded and where it came from), `/readout` (Friday page for the sponsor; not a seventh stage).
|
|
99
126
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
127
|
+
You can also just say it: “Prep me for the sponsor meeting,” “What did we agree about scope?” or “Help me hand this over.”
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## All 30 Skills
|
|
132
|
+
|
|
133
|
+
Thirty situations, grouped by stage. Each skill gives the agent steps to follow, a record or report to produce, and a checkpoint with you. You describe the work; `@fde` finds the skill.
|
|
134
|
+
|
|
135
|
+
Full detail: [docs/skills-reference.md](docs/skills-reference.md).
|
|
136
|
+
|
|
137
|
+
### Land
|
|
138
|
+
|
|
139
|
+
| Skill | What it does | Use when |
|
|
140
|
+
|--------|--------------|----------|
|
|
141
|
+
| [land](skills/fde/references/land.md) | Interrogate the brief | New client, first meeting, just got the brief |
|
|
142
|
+
| [audit](skills/fde/references/audit.md) | Verify inherited claims | Taking over, previous consultant left |
|
|
143
|
+
| [who-decides](skills/fde/references/who-decides.md) | Map decision rights | Need to know who matters |
|
|
144
|
+
| [earn-trust](skills/fde/references/earn-trust.md) | Earn access | Need access or credibility |
|
|
145
|
+
| [hold-scope](skills/fde/references/hold-scope.md) | Hold scope | "Also can you…", timeline unchanged |
|
|
146
|
+
|
|
147
|
+
### Discover
|
|
148
|
+
|
|
149
|
+
| Skill | What it does | Use when |
|
|
150
|
+
|--------|--------------|----------|
|
|
151
|
+
| [discover](skills/fde/references/discover.md) | Frame the problem | Brief feels wrong, shadow processes |
|
|
152
|
+
| [test-assumptions](skills/fde/references/test-assumptions.md) | Test assumptions | Brief feels too neat |
|
|
153
|
+
| [score-use-cases](skills/fde/references/score-use-cases.md) | Score use cases | Everything is P0 |
|
|
154
|
+
| [poc](skills/fde/references/poc.md) | Validate the solution | POC, spike, need to de-risk |
|
|
155
|
+
|
|
156
|
+
### Plan
|
|
157
|
+
|
|
158
|
+
| Skill | What it does | Use when |
|
|
159
|
+
|--------|--------------|----------|
|
|
160
|
+
| [plan](skills/fde/references/plan.md) | Sequence the work | What order, what is done |
|
|
161
|
+
| [business-case](skills/fde/references/business-case.md) | Build the business case | Defend budget or timeline |
|
|
162
|
+
| [three-options](skills/fde/references/three-options.md) | Generate options | "What should we do?" |
|
|
163
|
+
| [pick-three](skills/fde/references/pick-three.md) | Prioritize three | Everything is urgent |
|
|
164
|
+
|
|
165
|
+
### Ship
|
|
166
|
+
|
|
167
|
+
| Skill | What it does | Use when |
|
|
168
|
+
|--------|--------------|----------|
|
|
169
|
+
| [ship](skills/fde/references/ship.md) | Deliver the increment | Building, updating, or going live |
|
|
170
|
+
| [what-breaks](skills/fde/references/what-breaks.md) | Assess impact | Touching shared infrastructure |
|
|
171
|
+
| [rescue](skills/fde/references/rescue.md) | Resolve the incident | Down, or they went quiet |
|
|
172
|
+
| [review](skills/fde/references/review.md) | Review the change | Before merge, scope creep |
|
|
173
|
+
| [rollback](skills/fde/references/rollback.md) | Rehearse rollback | "We can always revert" |
|
|
109
174
|
|
|
110
|
-
|
|
175
|
+
### Outcome
|
|
111
176
|
|
|
112
|
-
|
|
177
|
+
| Skill | What it does | Use when |
|
|
178
|
+
|--------|--------------|----------|
|
|
179
|
+
| [readout](skills/fde/references/readout.md) | Report the outcome | Friday, sponsor update |
|
|
180
|
+
| [demo-prep](skills/fde/references/demo-prep.md) | Prepare the demo | Demo or exec walkthrough |
|
|
181
|
+
| [debrief](skills/fde/references/debrief.md) | Capture the meeting | Just left a meeting |
|
|
182
|
+
| [board-memo](skills/fde/references/board-memo.md) | Brief the board | Justify continued investment |
|
|
183
|
+
| [dashboard](skills/fde/references/dashboard.md) | Open the fieldbook | This customer, or all of them |
|
|
184
|
+
| [ingest](skills/fde/references/ingest.md) | Ingest sources | Transcript, Notion, Slack |
|
|
185
|
+
| [connect](skills/fde/references/connect.md) | Connect a source | Connect Granola |
|
|
113
186
|
|
|
114
|
-
|
|
187
|
+
### Close
|
|
115
188
|
|
|
116
|
-
|
|
189
|
+
| Skill | What it does | Use when |
|
|
190
|
+
|--------|--------------|----------|
|
|
191
|
+
| [close](skills/fde/references/close.md) | Transfer operations | Wrapping up |
|
|
192
|
+
| [runbook](skills/fde/references/runbook.md) | Write the runbook | They must operate without you |
|
|
193
|
+
| [switch-clients](skills/fde/references/switch-clients.md) | Switch engagements | 2+ clients |
|
|
194
|
+
| [encode-pattern](skills/fde/references/encode-pattern.md) | Encode the pattern | It will apply again |
|
|
195
|
+
| [red-team](skills/fde/references/red-team.md) | Challenge the plan | "Poke holes in this" |
|
|
117
196
|
|
|
118
|
-
|
|
197
|
+
Overlays (on signal, not on request): [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). AI companion (not a sixth overlay): [eval-pack](skills/fde/references/eval-pack.md).
|
|
119
198
|
|
|
120
|
-
|
|
199
|
+
Optional pull: you add the source MCP; we **pull** on request. [mcp/recipes/](mcp/recipes/)
|
|
121
200
|
|
|
122
|
-
|
|
201
|
+
---
|
|
123
202
|
|
|
124
|
-
|
|
203
|
+
<a name="use-the-cli-directly"></a>
|
|
204
|
+
|
|
205
|
+
## How Skills Work
|
|
206
|
+
|
|
207
|
+
One `@fde`. One file per situation. One folder per client.
|
|
208
|
+
|
|
209
|
+
Tell the agent what is happening. It reads the client record, opens the relevant skill, and works through the situation with you. After a meeting, it proposes the decisions, open questions, and next action. You correct what it misunderstood and confirm the update.
|
|
210
|
+
|
|
211
|
+
A request stays a request until agreed. A staging result stays separate from production. Recording a result does not mean the customer accepted it.
|
|
212
|
+
|
|
213
|
+
At the next session, FDEOps supplies a short summary instead of the whole history. Older detail stays on disk; `recall` finds relevant records when needed. The default summary is capped at 16 KiB, which limits FDEOps output rather than everything your agent loads.
|
|
214
|
+
|
|
215
|
+
Before a sponsor meeting, `npx fdeops defend` separates recorded acceptance from claims and missing evidence. For a successor, `npx fdeops handoff --out successor.md` creates a portable summary with risks and sources.
|
|
216
|
+
|
|
217
|
+
For free-form notes, start with `@fde`. The CLI's `debrief --smart` recognizes common phrases; it can miss details that your agent needs to help interpret. [Follow the notes → review → apply walkthrough](docs/USAGE.md#new-here-5-minutes).
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Engagement memory (`.fde/`)
|
|
222
|
+
|
|
223
|
+
One folder per client. Plain markdown. Grep it, copy it, take it into a meeting.
|
|
224
|
+
|
|
225
|
+
| File | Holds |
|
|
226
|
+
|------|-------|
|
|
227
|
+
| `context.md` | Where you are |
|
|
228
|
+
| `brief.md` / `success.md` | What they asked; what “done” is and who signs |
|
|
229
|
+
| `reality.md` / `terrain.md` | The real problem; the map |
|
|
230
|
+
| `stakeholders.md` | `[signal:green\|amber\|red]` - worst active signal wins; empty is **new**, not green |
|
|
231
|
+
| `trust-profile.md` | Sacred data, AI policy, approval chain |
|
|
232
|
+
| `decisions.md` / `risks.md` / `delivery.md` | Dated choices; live risks; what shipped, evidence, rollback, acceptance |
|
|
233
|
+
|
|
234
|
+
Schema: [docs/schema.md](docs/schema.md). Fieldbook: `npx fdeops dashboard --open` (bound) or `--all --open` (portfolio).
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Who this is for
|
|
239
|
+
|
|
240
|
+
Forward deployed engineers, independent consultants, and small agencies working with customer teams. You need to carry the brief, decisions, delivery evidence, and handover across meetings, repositories, and sometimes several clients.
|
|
241
|
+
|
|
242
|
+
If your work has no client commitments or operating handover to track, a simpler project note may be enough.
|
|
243
|
+
|
|
244
|
+
<a name="your-records-your-control"></a>
|
|
245
|
+
|
|
246
|
+
## Your data stays yours
|
|
247
|
+
|
|
248
|
+
The CLI works with local files and Git, without network calls or telemetry. Client records remain readable Markdown if you stop using FDEOps.
|
|
249
|
+
|
|
250
|
+
Your AI host may send the material it reads to its configured model. FDEOps redacts `<private>` blocks from CLI, dashboard, and hook outputs; do not load those raw blocks through the agent's file tools. Review reports before sharing client information.
|
|
251
|
+
|
|
252
|
+
You review proposed decisions. Enabled session hooks can save where the session left off automatically; direct CLI write commands update records when you run them.
|
|
253
|
+
|
|
254
|
+
The CLI and dashboard need no model. AI-assisted local use needs an agent with file and command access. Our small local-model tests produced wrong or incomplete answers, so check the [verification results](docs/verification.md) before relying on one for client work.
|
|
255
|
+
|
|
256
|
+
[Privacy](PRIVACY.md) · [Security](SECURITY.md)
|
|
257
|
+
|
|
258
|
+
## Principles
|
|
259
|
+
|
|
260
|
+
- **Who signs** - name who can accept the work.
|
|
261
|
+
- **Brief vs real job** - check what happens on the floor, not only the slide.
|
|
262
|
+
- **Back from done** - agree how you will test success before planning the build.
|
|
263
|
+
- **Their staging, then live** - prove the change and agree the release and rollback.
|
|
264
|
+
- **Promised, measured, accepted** - keep each separate, with its evidence.
|
|
265
|
+
- **They run it** - hand over the knowledge and ownership, not just the code.
|
|
266
|
+
- **The kit says what to check. You still decide.**
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
<a name="find-your-way-around"></a>
|
|
271
|
+
|
|
272
|
+
## Project Structure
|
|
125
273
|
|
|
126
274
|
| You want to… | Start here |
|
|
127
275
|
|---|---|
|
|
128
|
-
| Install or use FDEOps | [
|
|
129
|
-
| Understand or change a workflow | [
|
|
130
|
-
| Work on the CLI or
|
|
131
|
-
|
|
|
132
|
-
| Check
|
|
133
|
-
|
|
276
|
+
| Install or use FDEOps | [docs/](docs/README.md) |
|
|
277
|
+
| Understand or change a workflow | [skills/fde/](skills/fde/SKILL.md) and its `references/` |
|
|
278
|
+
| Work on the CLI or fieldbook | [bin/](bin/) and [test/](test/) |
|
|
279
|
+
| Walk through a client engagement | [examples/](examples/) |
|
|
280
|
+
| Check what has been tested | [evals/](evals/) and [verification](docs/verification.md) |
|
|
281
|
+
|
|
282
|
+
[Full repository map](docs/REPO_LAYOUT.md).
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
<a name="contribute"></a>
|
|
287
|
+
|
|
288
|
+
## Contributing
|
|
289
|
+
|
|
290
|
+
**[Subash Natarajan](https://www.linkedin.com/in/subashn/)**. [Issues](https://github.com/suboss87/fdeops/issues) · [Discussions](https://github.com/suboss87/fdeops/discussions) · [CONTRIBUTING.md](CONTRIBUTING.md) · [Code of Conduct](CODE_OF_CONDUCT.md)
|
|
134
291
|
|
|
135
|
-
|
|
292
|
+
Skills should be **specific** (actionable steps), **verifiable** (an artifact in `.fde/`), and **minimal**. The `fde` CLI stays local-only.
|
|
136
293
|
|
|
137
|
-
|
|
294
|
+
## License
|
|
138
295
|
|
|
139
|
-
|
|
296
|
+
MIT - use these skills on client work.
|
package/bin/fde.js
CHANGED
|
@@ -840,7 +840,7 @@ function valueLedgerRowCount(body) {
|
|
|
840
840
|
return t.rows.filter(r => r.some(c => String(c || '').trim())).length
|
|
841
841
|
}
|
|
842
842
|
|
|
843
|
-
function appendValueLedgerRow(eng, cells) {
|
|
843
|
+
function appendValueLedgerRow(eng, cells, { skipCommit = false } = {}) {
|
|
844
844
|
ensureMemoryGit(eng)
|
|
845
845
|
const p = path.join(eng, 'delivery.md')
|
|
846
846
|
let row
|
|
@@ -887,7 +887,7 @@ function appendValueLedgerRow(eng, cells) {
|
|
|
887
887
|
atomicWriteFile(p, md.endsWith('\n') ? md : md + '\n')
|
|
888
888
|
})
|
|
889
889
|
recordLastWrite(eng, 'delivery.md', row)
|
|
890
|
-
commitMemory(eng, 'log delivery', { files: ['delivery.md'] })
|
|
890
|
+
if (!skipCommit) commitMemory(eng, 'log delivery', { files: ['delivery.md'] })
|
|
891
891
|
}
|
|
892
892
|
|
|
893
893
|
function retireOpenRisks(eng, needle) {
|
|
@@ -983,7 +983,7 @@ const {
|
|
|
983
983
|
countOpenRisks,
|
|
984
984
|
} = createTrustApi({
|
|
985
985
|
fs, path, readClean, readEng, parseMdTable, sectionBody, SIGNAL_LEDGER, memoryDirtyManual,
|
|
986
|
-
stripTemplateNoise, stripLegendLines,
|
|
986
|
+
stripTemplateNoise, stripLegendLines, extractRisks,
|
|
987
987
|
})
|
|
988
988
|
|
|
989
989
|
// Stakeholders: columns are matched by header wording, not position - real
|
|
@@ -1099,14 +1099,14 @@ function extractStakeholders(eng) {
|
|
|
1099
1099
|
|
|
1100
1100
|
// Risks: table rows AND dated CLI/debrief bullets. Empty template cells ignored.
|
|
1101
1101
|
function extractRisks(eng) {
|
|
1102
|
-
const md = readClean(eng, 'risks.md')
|
|
1102
|
+
const md = stripTemplateNoise(readClean(eng, 'risks.md'))
|
|
1103
1103
|
const body = md.split(/^#{1,6}\s+Retired\b/im)[0] || md
|
|
1104
1104
|
const HIGH = /critical|blocker|exposure|breach|urgent|at risk|at stake|\brace\b|rollback|no test/i
|
|
1105
1105
|
const out = []
|
|
1106
1106
|
const seen = new Set()
|
|
1107
1107
|
const push = (text) => {
|
|
1108
1108
|
const t = String(text || '').trim()
|
|
1109
|
-
if (!t || seen.has(t.toLowerCase())) return
|
|
1109
|
+
if (!t || /^(?:\[[xX]\]\s|(?:closed|resolved|retired)\s*:)/i.test(t) || seen.has(t.toLowerCase())) return
|
|
1110
1110
|
seen.add(t.toLowerCase())
|
|
1111
1111
|
out.push({ text: t, severity: HIGH.test(t) ? 'high' : 'med' })
|
|
1112
1112
|
}
|
|
@@ -1114,13 +1114,23 @@ function extractRisks(eng) {
|
|
|
1114
1114
|
if (table) {
|
|
1115
1115
|
const riskIdx = colIndex(table.headers, /^risk$/i)
|
|
1116
1116
|
if (riskIdx !== -1) {
|
|
1117
|
-
|
|
1117
|
+
const statusIdx = colIndex(table.headers, /^status$/i)
|
|
1118
|
+
for (const cs of table.rows) {
|
|
1119
|
+
if (statusIdx !== -1 && /^(closed|resolved|retired)$/i.test((cs[statusIdx] || '').trim())) continue
|
|
1120
|
+
push(cs[riskIdx])
|
|
1121
|
+
}
|
|
1118
1122
|
}
|
|
1119
1123
|
}
|
|
1120
1124
|
for (const raw of body.split('\n')) {
|
|
1121
1125
|
const t = raw.trim()
|
|
1122
1126
|
const m = t.match(/^-\s*\[\d{4}-\d{2}-\d{2}\]\s*(?:\[@[^\]]+\]\s*)?(.*)$/)
|
|
1123
1127
|
if (m) push(m[1])
|
|
1128
|
+
else {
|
|
1129
|
+
// Inherited Markdown may predate the dated CLI format. Keep its open
|
|
1130
|
+
// bullets visible; absence of a date does not mean absence of a risk.
|
|
1131
|
+
const bullet = t.match(/^[-*+]\s+(.*)$/)
|
|
1132
|
+
if (bullet && !/^\[[xX]\]\s/.test(bullet[1])) push(bullet[1].replace(/^\[ \]\s*/, ''))
|
|
1133
|
+
}
|
|
1124
1134
|
}
|
|
1125
1135
|
return out
|
|
1126
1136
|
}
|
|
@@ -1541,7 +1551,8 @@ function cmdLog(args) {
|
|
|
1541
1551
|
return
|
|
1542
1552
|
}
|
|
1543
1553
|
if (type === 'delivery' && text.includes('|')) {
|
|
1544
|
-
|
|
1554
|
+
let cells
|
|
1555
|
+
try { cells = deliveryCells(text) } catch (error) { console.error(error.message); process.exitCode = 1; return }
|
|
1545
1556
|
appendValueLedgerRow(eng, cells)
|
|
1546
1557
|
const hash = memoryHead(eng)
|
|
1547
1558
|
console.log(`logged → delivery.md (value ledger)${hash ? ` @${hash}` : ''}`)
|
|
@@ -1876,6 +1887,8 @@ function stripApprovedStamp(text) {
|
|
|
1876
1887
|
// One screen a human can confirm in two minutes. The file-by-file routing
|
|
1877
1888
|
// still prints after this - agents edit prefixes; people read this.
|
|
1878
1889
|
function printDebriefReview(text, eng) {
|
|
1890
|
+
const repeats = repeatedDebriefStatements(eng, text)
|
|
1891
|
+
if (repeats.length) console.log(`REPLAY WARNING: ${repeats.length} source-backed statement(s) already recorded. Review newer facts and next action; applying again requires --allow-replay.\n`)
|
|
1879
1892
|
const buckets = { decided: [], asked: [], scope: [], delivery: [], open: [], next: [], signer: [] }
|
|
1880
1893
|
for (const raw of String(text || '').split('\n')) {
|
|
1881
1894
|
const line = raw.trim().replace(/^[-*+]\s+/, '')
|
|
@@ -2048,11 +2061,40 @@ function boundedDebriefPreview(eng, render, { proposal = true, maxBytes = 12000
|
|
|
2048
2061
|
return result
|
|
2049
2062
|
}
|
|
2050
2063
|
|
|
2051
|
-
function
|
|
2064
|
+
function deliveryCells(text) {
|
|
2065
|
+
const cells = text.split('|').map(s => s.trim())
|
|
2066
|
+
if (cells.length !== 7) throw new Error('delivery needs exactly 7 fields: Slice | Bucket | Promised | Measured | Accepted by | Evidence | Rollback. Use pending for unknowns; omit pipes for a narrative note.')
|
|
2067
|
+
return cells
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
// Exact sourced statement replay only; a shared source may contain new facts.
|
|
2071
|
+
// Do not silently deduplicate: the engineer decides whether a repeated event is intended.
|
|
2072
|
+
function repeatedDebriefStatements(eng, input) {
|
|
2073
|
+
const repeats = []
|
|
2074
|
+
const records = new Map()
|
|
2075
|
+
const normalize = value => value.replace(/\s*\|\s*/g, '|').replace(/\s+/g, ' ').trim()
|
|
2076
|
+
for (const raw of splitPrivate(input, { sealDangling: true }).clean.split('\n')) {
|
|
2077
|
+
const match = raw.trim().replace(/^[-*+]\s+/, '').match(/^(decision|risk|delivery|contact):\s*(.+)$/i)
|
|
2078
|
+
if (!match || !/\[source:[^\]]+\]/i.test(match[2])) continue
|
|
2079
|
+
const body = match[2].trim()
|
|
2080
|
+
const file = LOG_FILES[match[1].toLowerCase()]
|
|
2081
|
+
if (!records.has(file)) {
|
|
2082
|
+
const statements = String(readClean(eng, file) || '').split('\n').map(line => {
|
|
2083
|
+
if (/^\s*\|/.test(line)) return normalize(line.trim().replace(/^\|\s*\d{4}-\d{2}-\d{2}\s*\|/, '').replace(/\|\s*$/, ''))
|
|
2084
|
+
return normalize(line.replace(/^\s*[-*+]\s+/, '').replace(/^(?:\[(?:\d{4}-\d{2}-\d{2}|@[^\]]+|signal:[^\]]+)\]\s*)+/, ''))
|
|
2085
|
+
})
|
|
2086
|
+
records.set(file, new Set(statements))
|
|
2087
|
+
}
|
|
2088
|
+
if (records.get(file).has(normalize(body))) repeats.push(body)
|
|
2089
|
+
}
|
|
2090
|
+
return repeats
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
function routeDebriefInput(eng, input, { dry, force, sealed = [], allowReplay = false }) {
|
|
2052
2094
|
if (!dry && !debriefTransactionActive) {
|
|
2053
2095
|
ensureMemoryGit(eng)
|
|
2054
2096
|
return withDebriefRecords(eng, () => {
|
|
2055
|
-
const result = routeDebriefInput(eng, input, { dry, force, sealed })
|
|
2097
|
+
const result = routeDebriefInput(eng, input, { dry, force, sealed, allowReplay })
|
|
2056
2098
|
// Consuming the review is part of the write. If cleanup fails, restoring
|
|
2057
2099
|
// both the records and proposal makes the next explicit apply safe.
|
|
2058
2100
|
for (const file of [DEBRIEF_PROPOSE, DEBRIEF_PRIVATE, DEBRIEF_SEAL]) {
|
|
@@ -2063,12 +2105,14 @@ function routeDebriefInput(eng, input, { dry, force, sealed = [] }) {
|
|
|
2063
2105
|
return result
|
|
2064
2106
|
})
|
|
2065
2107
|
}
|
|
2108
|
+
const repeats = repeatedDebriefStatements(eng, input)
|
|
2109
|
+
if (repeats.length && !dry && !allowReplay) throw new Error('source-backed statement already recorded; review the existing record and newer next action. Explicitly confirm a repeat with --allow-replay, or remove the repeated statement from the proposal.')
|
|
2066
2110
|
const d = new Date()
|
|
2067
2111
|
const date = d.toISOString().slice(0, 10)
|
|
2068
2112
|
const counts = { decision: 0, risk: 0, delivery: 0, contact: 0, next: 0, signer: 0 }
|
|
2069
2113
|
const ctxLines = []
|
|
2070
2114
|
let nextAction = ''
|
|
2071
|
-
ensureMemoryGit(eng)
|
|
2115
|
+
if (!dry) ensureMemoryGit(eng)
|
|
2072
2116
|
// Sealed blocks are pulled out before routing, so a <private> block's interior
|
|
2073
2117
|
// lines are never previewed and never routed into decisions/risks/stakeholders
|
|
2074
2118
|
// unsealed. They land verbatim in context.md instead: the preview a human
|
|
@@ -2092,7 +2136,7 @@ function routeDebriefInput(eng, input, { dry, force, sealed = [] }) {
|
|
|
2092
2136
|
const who = body.replace(/\s+signs?(?:\s+off)?\b.*$/i, '').trim() || body.trim()
|
|
2093
2137
|
if (dry) {
|
|
2094
2138
|
console.log(`→ success.md **Stakeholder who signs off:** ${previewLine(who)}`)
|
|
2095
|
-
console.log(`→ stakeholders.md ${previewLine(
|
|
2139
|
+
console.log(`→ stakeholders.md ${previewLine(`- [${date}] ${who} signs off`)}`)
|
|
2096
2140
|
} else {
|
|
2097
2141
|
setSigner(eng, who)
|
|
2098
2142
|
appendLogEntry(eng, 'contact', datedEntry(eng, date, `${who} signs off`), { skipCommit: true })
|
|
@@ -2106,9 +2150,16 @@ function routeDebriefInput(eng, input, { dry, force, sealed = [] }) {
|
|
|
2106
2150
|
counts.next++
|
|
2107
2151
|
continue
|
|
2108
2152
|
}
|
|
2153
|
+
if (type === 'delivery' && body.includes('|')) {
|
|
2154
|
+
const cells = deliveryCells(body)
|
|
2155
|
+
if (dry) console.log(`→ delivery.md ## Value ledger ${previewLine(body)}`)
|
|
2156
|
+
else appendValueLedgerRow(eng, cells, { skipCommit: true })
|
|
2157
|
+
counts.delivery++
|
|
2158
|
+
continue
|
|
2159
|
+
}
|
|
2109
2160
|
const sigInline = (body.match(/\[signal:(red|amber|green)\]/i) || [])[1]
|
|
2110
2161
|
if (sigInline) body = body.replace(/\[signal:(red|amber|green)\]/i, '').trim()
|
|
2111
|
-
const entry = datedEntry(eng, date, body, type === 'contact' && sigInline ? sigInline.toLowerCase() : '')
|
|
2162
|
+
const entry = dry ? `- [${date}]${type === 'contact' && sigInline ? ` [signal:${sigInline.toLowerCase()}]` : ''} ${body}` : datedEntry(eng, date, body, type === 'contact' && sigInline ? sigInline.toLowerCase() : '')
|
|
2112
2163
|
if (dry) console.log(`→ ${LOG_FILES[type]} ${previewLine(entry)}`)
|
|
2113
2164
|
else appendLogEntry(eng, type, entry, { skipCommit: true })
|
|
2114
2165
|
counts[type]++
|
|
@@ -2147,6 +2198,21 @@ function cmdDebrief(args) {
|
|
|
2147
2198
|
|
|
2148
2199
|
function runDebrief(args, eng) {
|
|
2149
2200
|
args = args.slice()
|
|
2201
|
+
const replayIdx = args.indexOf('--allow-replay')
|
|
2202
|
+
const allowReplay = replayIdx !== -1
|
|
2203
|
+
if (allowReplay) args.splice(replayIdx, 1)
|
|
2204
|
+
if (args.includes('--review')) {
|
|
2205
|
+
if (args.length !== 1 || allowReplay) throw new Error('use debrief --review alone to inspect the pending proposal')
|
|
2206
|
+
const proposal = path.join(eng, DEBRIEF_PROPOSE)
|
|
2207
|
+
const refused = refuseSymlinkWrite(proposal, { soft: true })
|
|
2208
|
+
if (refused) throw new Error(refused)
|
|
2209
|
+
if (!fs.existsSync(proposal)) throw new Error('nothing to review - run debrief --smart <notes> first')
|
|
2210
|
+
const { clean: input } = splitPrivate(fs.readFileSync(proposal, 'utf8'), { sealDangling: true })
|
|
2211
|
+
return boundedDebriefPreview(eng, () => {
|
|
2212
|
+
printDebriefReview(input, eng)
|
|
2213
|
+
routeDebriefInput(eng, input, { dry: true, force: false })
|
|
2214
|
+
})
|
|
2215
|
+
}
|
|
2150
2216
|
const dryIdx = args.indexOf('--dry-run')
|
|
2151
2217
|
const dry = dryIdx !== -1
|
|
2152
2218
|
if (dry) args.splice(dryIdx, 1)
|
|
@@ -2202,14 +2268,14 @@ function runDebrief(args, eng) {
|
|
|
2202
2268
|
if (!apply) {
|
|
2203
2269
|
console.log(`\nproposal saved → ${proposePath}`)
|
|
2204
2270
|
console.log('confirm: fde debrief --apply')
|
|
2205
|
-
console.log('(edit the propose file
|
|
2271
|
+
console.log('(edit the propose file if mis-routed; debrief --review shows the pending REVIEW)')
|
|
2206
2272
|
return
|
|
2207
2273
|
}
|
|
2208
2274
|
input = clean
|
|
2209
2275
|
sealed = blocks
|
|
2210
2276
|
}
|
|
2211
2277
|
|
|
2212
|
-
const route = () => routeDebriefInput(eng, input, { dry, force, sealed })
|
|
2278
|
+
const route = () => routeDebriefInput(eng, input, { dry, force, sealed, allowReplay })
|
|
2213
2279
|
const { counts, ctxLines, privateBlocks } = boundedDebriefPreview(eng, route, { proposal: false, maxBytes: smart ? 4000 : 12000 })
|
|
2214
2280
|
if (!dry) {
|
|
2215
2281
|
const hash = commitMemory(eng, 'debrief', {
|
|
@@ -2433,12 +2499,30 @@ function cmdReceipts(args) {
|
|
|
2433
2499
|
if (!line.toLowerCase().includes(term.toLowerCase())) return
|
|
2434
2500
|
const source = decisionSources.get(i + 1) || sourceReference(line)
|
|
2435
2501
|
const hit = ` ${file}:${i + 1} ${line.trim().slice(0, 160)}${source ? ` [source: ${source.slice(0, 160)}]` : ' [source missing]'}${dirty.has(file) ? ' dirty file - review manual edits' : ''}`
|
|
2436
|
-
;(recordFiles.includes(file) && source ? records : claims).push(hit)
|
|
2502
|
+
;(recordFiles.includes(file) && source ? records : claims).push({ file, hit })
|
|
2437
2503
|
})
|
|
2438
2504
|
}
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2505
|
+
// Alternate the latest and earliest matching lines per file. Otherwise a
|
|
2506
|
+
// long history can spend the entire packet on approvals before a withdrawal.
|
|
2507
|
+
const select = hits => {
|
|
2508
|
+
const groups = new Map()
|
|
2509
|
+
for (const { file, hit } of hits) {
|
|
2510
|
+
if (!groups.has(file)) groups.set(file, [])
|
|
2511
|
+
groups.get(file).push(hit)
|
|
2512
|
+
}
|
|
2513
|
+
const selected = []
|
|
2514
|
+
let latest = true
|
|
2515
|
+
while (selected.length < 24 && [...groups.values()].some(group => group.length)) {
|
|
2516
|
+
for (const group of groups.values()) {
|
|
2517
|
+
if (group.length && selected.length < 24) selected.push(latest ? group.pop() : group.shift())
|
|
2518
|
+
}
|
|
2519
|
+
latest = !latest
|
|
2520
|
+
}
|
|
2521
|
+
return `Selected ${selected.length} of ${hits.length} matching lines; omitted matches require a narrower search.\n` + selected.join('\n')
|
|
2522
|
+
}
|
|
2523
|
+
const sections = ['RECEIPTS: a cited record is not proof of customer approval. File line numbers refer to the redacted view. Latest and earliest matching lines are sampled; file order is not authority. Check conflicting records.']
|
|
2524
|
+
if (records.length) sections.push('ON RECORD (dated, source-backed):\n' + select(records))
|
|
2525
|
+
if (claims.length) sections.push('CLAIMS & working notes (verify source and approval before citing):\n' + select(claims))
|
|
2442
2526
|
if (!records.length && !claims.length) sections.push(`no record of "${term}" - a gap in the record, not proof of absence`)
|
|
2443
2527
|
process.stdout.write(context.boundedSections(sections))
|
|
2444
2528
|
}
|
|
@@ -2473,7 +2557,8 @@ function cmdHandoff(args, label = 'Handoff') {
|
|
|
2473
2557
|
`## Constraints - trust-profile.md\n${stripTemplateNoise(readClean(eng, 'trust-profile.md')) || '(missing)'}`,
|
|
2474
2558
|
`## Signer and success - success.md\nSigner: ${signer || '(missing; do not infer)'}\n${success || '(missing)'}`,
|
|
2475
2559
|
`## 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)'}`,
|
|
2476
|
-
|
|
2560
|
+
label === 'Handoff' ? `## Operational handoff - handoff.md\n${stripTemplateNoise(readClean(eng, 'handoff.md')) || '(missing; record recovery steps and the operating owner before rotation)'}` : '',
|
|
2561
|
+
`## 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 + ']' + (r.acceptanceIssue ? '; ' + r.acceptanceIssue : '')).join('\n') || '(none)'}`,
|
|
2477
2562
|
`## 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.`,
|
|
2478
2563
|
`## Gaps before relying on this packet\n${gaps.map(g => '- ' + g).join('\n') || '(no deterministic lint gaps; human review still required)'}`,
|
|
2479
2564
|
], parsed.maxBytes)
|
|
@@ -2499,7 +2584,7 @@ function cmdRecall(args) {
|
|
|
2499
2584
|
}
|
|
2500
2585
|
const eng = resolveEngagement()
|
|
2501
2586
|
if (!eng) { console.error('no engagement - bind a client before recall'); process.exit(2) }
|
|
2502
|
-
const files = ['context.md', 'trust-profile.md', 'success.md', 'decisions.md', 'risks.md', 'delivery.md', 'stakeholders.md', 'brief.md', 'reality.md', 'assumptions.md', 'terrain.md']
|
|
2587
|
+
const files = ['context.md', 'trust-profile.md', 'success.md', 'decisions.md', 'risks.md', 'delivery.md', 'stakeholders.md', 'brief.md', 'reality.md', 'assumptions.md', 'terrain.md', 'handoff.md']
|
|
2503
2588
|
const result = context.recallSections(files.map(file => ({ file, text: readClean(eng, file) })), query)
|
|
2504
2589
|
process.stdout.write(context.boundedSections([
|
|
2505
2590
|
`RECALL - ${eng}\n${result.total ? `${result.sections.length} of ${result.total} matching lines; refine the query if evidence is omitted.` : 'No matching record. This is not proof that the event never happened.'}\nSources are local record assertions; verify dates, supersession and approval scope.`,
|
|
@@ -2729,12 +2814,20 @@ function successContractIssues(success) {
|
|
|
2729
2814
|
if (active !== -1 && line.trim()) checks[active] += ` ${line.trim()}`
|
|
2730
2815
|
}
|
|
2731
2816
|
const observable = checks.some(check => {
|
|
2817
|
+
// This is a lint check, not a semantic proof. Explicit fields let any
|
|
2818
|
+
// domain describe its test without depending on a vocabulary of verbs.
|
|
2819
|
+
const target = /(?:\b(?:within|under|at most|at least|exactly|zero|no missing|no duplicate|all|every|none|true|false|pass|fail|http)\b|[<>=])/i
|
|
2820
|
+
const vague = /\b(?:tbd|unknown|to be defined|improve|better|satisfactory|as expected|works well)\b/i
|
|
2821
|
+
if (vague.test(check)) return false
|
|
2822
|
+
const explicit = check.match(/(?:^|\s)(?:-\s*)?Input:\s*(.+?)\s+(?:-\s*)?Pass when:\s*(.+)$/i)
|
|
2823
|
+
if (explicit) return explicit[1].trim().length > 3 && target.test(explicit[2])
|
|
2732
2824
|
const stimulus = /\b(?:test|drill|replay|runs?|request|sample|given|when|simulate|inject|compare|restore|verified|observed|measured)\b/i.test(check)
|
|
2825
|
+
|| /^\d+\s+[a-z]/i.test(check)
|
|
2733
2826
|
const result = /\b(?:returns?|rejects?|matches?|equals?|arrives?|alerts?|restores?|passes?|fails?|contains?|produces?|shows?|remains?|receives?)\b/i.test(check)
|
|
2734
|
-
|
|
2735
|
-
return stimulus && result && target
|
|
2827
|
+
|| /\b(?:zero|no duplicate|no missing)\s+[a-z]/i.test(check)
|
|
2828
|
+
return stimulus && result && target.test(check)
|
|
2736
2829
|
})
|
|
2737
|
-
if (!observable) issues.push('success.md needs a binary acceptance check:
|
|
2830
|
+
if (!observable) issues.push('success.md needs a binary acceptance check: the wording was not recognized as a test/input and observable pass/fail result under **Done when:** or **Acceptance check:**. Use Input: and Pass when: for a domain-specific check; this lint does not prove readiness')
|
|
2738
2831
|
const signerLine = ((text.match(/^\*\*Stakeholder who signs off:\*\*[^\S\n]*(.*)$/m) || [])[1] || '').replace(/\[source:[^\]]+\]/gi, '').trim()
|
|
2739
2832
|
// A named primary signer may be followed by responsibilities or another
|
|
2740
2833
|
// signer's role. Preserve the full record; validate only the leading name.
|
|
@@ -2997,7 +3090,7 @@ function hasValueBucket(eng) {
|
|
|
2997
3090
|
}
|
|
2998
3091
|
|
|
2999
3092
|
// Shared classification keeps CLI, dashboard, and vault acceptance consistent.
|
|
3000
|
-
const { PENDING_CELL_RE, valueState, evidenceSource } = require('./lib/value-ledger')
|
|
3093
|
+
const { PENDING_CELL_RE, valueState, evidenceSource, reconcileValueRows } = require('./lib/value-ledger')
|
|
3001
3094
|
|
|
3002
3095
|
function parseValueLedger(eng) {
|
|
3003
3096
|
// Last section with actual rows, not merely the last non-empty one: a template
|
|
@@ -3027,9 +3120,9 @@ function parseValueLedger(eng) {
|
|
|
3027
3120
|
const evidence = cell(row, idx.evidence)
|
|
3028
3121
|
const acceptanceStatus = idx.acceptanceStatus === -1 ? undefined : cell(row, idx.acceptanceStatus)
|
|
3029
3122
|
const state = valueState({ measured, accepted, acceptanceStatus, evidence })
|
|
3030
|
-
rows.push({ slice, promised, measured, accepted, evidence, evidenceMissing: !evidenceSource(evidence), state })
|
|
3123
|
+
rows.push({ slice, promised, measured, accepted, acceptanceStatus, evidence, evidenceMissing: !evidenceSource(evidence), state })
|
|
3031
3124
|
}
|
|
3032
|
-
return { rows, columnMissing: idx.accepted === -1 }
|
|
3125
|
+
return { rows: reconcileValueRows(rows, readClean(eng, 'success.md')), columnMissing: idx.accepted === -1 }
|
|
3033
3126
|
}
|
|
3034
3127
|
|
|
3035
3128
|
function claimedValueRows(eng) {
|
|
@@ -3046,8 +3139,9 @@ function formatValueLedgerLine(r) {
|
|
|
3046
3139
|
}
|
|
3047
3140
|
const head = body ? `${name}: ${body}` : name
|
|
3048
3141
|
if (r.state === 'accepted') return `${head} · accepted by ${r.accepted}`
|
|
3049
|
-
|
|
3050
|
-
return `${head} · not yet
|
|
3142
|
+
const issue = r.acceptanceIssue ? `; ${r.acceptanceIssue}` : ''
|
|
3143
|
+
if (r.state === 'claimed') return `${head} · claimed, not yet accepted${issue}`
|
|
3144
|
+
return `${head} · not yet measured${issue}`
|
|
3051
3145
|
}
|
|
3052
3146
|
|
|
3053
3147
|
function valueLedgerStatusLines(eng, opts = {}) {
|
|
@@ -4003,6 +4097,8 @@ function printUsage() {
|
|
|
4003
4097
|
fde log --undo remove the last CLI log/debrief entry from memory
|
|
4004
4098
|
fde debrief [file] meeting notes → memory (prefixed lines; --dry-run; --force)
|
|
4005
4099
|
fde debrief --smart heuristic propose; REVIEW first (decided/asked/open/next/signer); --apply after one confirm
|
|
4100
|
+
--review inspect the pending REVIEW after editing, without replacing it
|
|
4101
|
+
--allow-replay explicitly apply already recorded sourced statements after review
|
|
4006
4102
|
--replace-proposal explicitly discard a pending review when proposing different notes
|
|
4007
4103
|
fde ingest stage … stage raw pull into <engagement>/.inbox/ (not .fde/)
|
|
4008
4104
|
fde ingest list list staged inbox items
|
package/bin/lib/render.js
CHANGED
|
@@ -374,7 +374,7 @@ function deliveryHtml(e) {
|
|
|
374
374
|
return `<section class="fb-block fb-delivery" aria-label="Delivery evidence">
|
|
375
375
|
<div class="fb-sec-row"><h2 class="fb-sec">Delivery evidence</h2><span class="fb-count">${rows.length} outcome${rows.length === 1 ? '' : 's'}</span></div>
|
|
376
376
|
<p class="fb-evidence-note">Recorded in delivery.md. Acceptance reflects the saved record, not independent verification.</p>
|
|
377
|
-
${rows.length ? `<div class="fb-table-scroll" role="region" aria-label="Promised, measured and accepted outcomes" tabindex="0"><table class="fb-table fb-value-table"><thead><tr><th scope="col">Outcome</th><th scope="col">Promised</th><th scope="col">Measured</th><th scope="col">Acceptance</th><th scope="col">Evidence</th></tr></thead><tbody>${rows.map(r => `<tr><th scope="row">${inlineMd(r.slice || 'Outcome')}<span class="fb-value-state t-${r.state === 'accepted' ? 'green' : 'amber'}">${labels[r.state]}</span
|
|
377
|
+
${rows.length ? `<div class="fb-table-scroll" role="region" aria-label="Promised, measured and accepted outcomes" tabindex="0"><table class="fb-table fb-value-table"><thead><tr><th scope="col">Outcome</th><th scope="col">Promised</th><th scope="col">Measured</th><th scope="col">Acceptance</th><th scope="col">Evidence</th></tr></thead><tbody>${rows.map(r => `<tr><th scope="row">${inlineMd(r.slice || 'Outcome')}<span class="fb-value-state t-${r.state === 'accepted' ? 'green' : 'amber'}">${labels[r.state]}</span>${r.acceptanceIssue ? `<span class="fb-muted">${inlineMd(r.acceptanceIssue)}</span>` : ''}</th><td>${cell(r.promised, 'Not recorded')}</td><td>${cell(r.measured, 'Not yet measured')}</td><td>${cell(r.accepted, 'Not recorded')}</td><td>${cell(r.evidence, 'Not recorded')}</td></tr>`).join('')}</tbody></table></div>` : `<div class="fb-empty-evidence"><strong>No delivery evidence yet</strong><p>Ask your agent to define one useful increment: the expected result, how to measure it, and who will accept it.</p></div>`}
|
|
378
378
|
</section>`
|
|
379
379
|
}
|
|
380
380
|
|
package/bin/lib/trust.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
function createTrustApi(deps) {
|
|
4
4
|
const {
|
|
5
5
|
fs, path, readClean, readEng, parseMdTable, sectionBody, SIGNAL_LEDGER, memoryDirtyManual,
|
|
6
|
-
stripTemplateNoise, stripLegendLines,
|
|
6
|
+
stripTemplateNoise, stripLegendLines, extractRisks,
|
|
7
7
|
} = deps
|
|
8
8
|
|
|
9
9
|
// phase / trust / top risk / freshness - identical heuristic for status + dashboard.
|
|
@@ -129,27 +129,7 @@ function createTrustApi(deps) {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
function countOpenRisks(eng) {
|
|
132
|
-
|
|
133
|
-
const body = md.split(/^#{1,6}\s+Retired\b/im)[0] || md
|
|
134
|
-
let n = 0
|
|
135
|
-
for (const raw of body.split('\n')) {
|
|
136
|
-
const t = raw.trim()
|
|
137
|
-
if (!t || t.startsWith('<!--') || /^#{1,6}\s/.test(t)) continue
|
|
138
|
-
if (/risk\s*\|\s*status|mitigation/i.test(t) || /^\|?[\s|:-]+$/.test(t)) continue
|
|
139
|
-
// Bullet risk with substance (skip empty "- " stubs). The bullet marker
|
|
140
|
-
// must be followed by space: "**Status:** open · closed" is a legend, and
|
|
141
|
-
// counting it as a risk reported one open risk on an empty register.
|
|
142
|
-
if (/^[-*]\s/.test(t)) {
|
|
143
|
-
if (t.replace(/^[-*]\s+/, '').trim()) n++
|
|
144
|
-
continue
|
|
145
|
-
}
|
|
146
|
-
// Table row: first cell must have risk text (day-1 "| | open | |" placeholders don't count).
|
|
147
|
-
if (/^\|/.test(t) && t.length > 12) {
|
|
148
|
-
const riskCell = t.split('|').map(c => c.trim())[1] || ''
|
|
149
|
-
if (riskCell) n++
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
return n
|
|
132
|
+
return extractRisks(eng).length
|
|
153
133
|
}
|
|
154
134
|
|
|
155
135
|
function nextActionLine(ctx) {
|
|
@@ -166,7 +146,7 @@ function createTrustApi(deps) {
|
|
|
166
146
|
function computeSignals(eng) {
|
|
167
147
|
// readClean, not readEng: status/dashboard echo topRisk and stakeholder lines
|
|
168
148
|
// to the terminal and the rendered HTML - a <private> risk must never surface.
|
|
169
|
-
const ctx = readClean(eng, 'context.md'); const stake = readClean(eng, 'stakeholders.md')
|
|
149
|
+
const ctx = readClean(eng, 'context.md'); const stake = readClean(eng, 'stakeholders.md')
|
|
170
150
|
// Prefer structured tokens from stakeholders + CLI ledger (ledger survives wipes)
|
|
171
151
|
const signalText = stake + '\n' + readClean(eng, SIGNAL_LEDGER)
|
|
172
152
|
const phase = parsePhase(ctx)
|
|
@@ -215,11 +195,7 @@ function createTrustApi(deps) {
|
|
|
215
195
|
trust = sLines.some(l => /\bred\b/i.test(l)) ? 'RED'
|
|
216
196
|
: sLines.some(l => /amber|gone quiet|routing around|escalat/i.test(l)) ? 'amber' : 'new'
|
|
217
197
|
}
|
|
218
|
-
const topRisk = (
|
|
219
|
-
const t = l.trim()
|
|
220
|
-
return /^[-|]/.test(t) && t.length > 20 && !/^\|?[-\s|]+$/.test(t) &&
|
|
221
|
-
!/risk\s*\|\s*status|mitigation/i.test(t) && !t.startsWith('<!--')
|
|
222
|
-
}) || '').replace(/\|/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 80)
|
|
198
|
+
const topRisk = (extractRisks(eng)[0]?.text || '').replace(/\s+/g, ' ').trim().slice(0, 80)
|
|
223
199
|
// Prefer trust trigger / memory warn over a random risk line; always keep mem.warn available
|
|
224
200
|
const reason = (trustReason || mem.warn) ? (trustReason || mem.warn) : topRisk
|
|
225
201
|
// What the triage line is actually quoting. A risk bullet printed under
|
package/bin/lib/value-ledger.js
CHANGED
|
@@ -26,6 +26,7 @@ function evidenceSource(evidence) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function valueState({ measured, accepted, acceptanceStatus, evidence }) {
|
|
29
|
+
if (withdrawalMention({ measured, accepted, acceptanceStatus, evidence })) return 'claimed'
|
|
29
30
|
if (!measured || PENDING_CELL_RE.test(measured)) return 'unmeasured'
|
|
30
31
|
if (!acceptanceName(accepted) || !evidenceSource(evidence)) return 'claimed'
|
|
31
32
|
// Explicit status is authoritative when the column exists. Unknown values
|
|
@@ -37,4 +38,48 @@ function valueState({ measured, accepted, acceptanceStatus, evidence }) {
|
|
|
37
38
|
return 'accepted'
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
// These checks flag explicit conflicts for human review. They do not authenticate
|
|
42
|
+
// approval, infer delegation, or revoke history when the current signer changes.
|
|
43
|
+
function withdrawalMention(row) {
|
|
44
|
+
return [row.measured, row.accepted, row.acceptanceStatus, row.evidence].filter(Boolean).some(value => {
|
|
45
|
+
const text = String(value)
|
|
46
|
+
.replace(/\[source:[^\]]*\]/gi, '')
|
|
47
|
+
.replace(/\S*[/\\]\S*/g, '') // artifact names are sources, not withdrawal events
|
|
48
|
+
.replace(/\b(?:not|never)\s+(?:been\s+)?(?:withdrawn|retracted|revoked|superseded)\b/gi, '')
|
|
49
|
+
// A withdrawn result is different from a result counting revoked tokens.
|
|
50
|
+
return /^(?:withdrawn|retracted|revoked|superseded)(?:\s*:|\s*$)/i.test(text.trim()) ||
|
|
51
|
+
/\b(?:approval|acceptance|evidence|measurement|result|assertion)\b[^.;\n]{0,40}\b(?:withdrawn|retracted|revoked|superseded)\b/i.test(text) ||
|
|
52
|
+
/\b(?:withdrawn|retracted|revoked|superseded|retracts?|withdraws?)\b[^.;\n]{0,25}\b(?:approval|acceptance|evidence|measurement|result|assertion)\b/i.test(text)
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function scopeIssue(row, goal) {
|
|
57
|
+
const rowPromise = String(row.promised || '')
|
|
58
|
+
const goalLine = String(goal).match(/^(?:\*\*)?(?:Done when|Acceptance check):(?:\*\*)?\s*(.*)$/im)
|
|
59
|
+
const scopeSpecified = /\b(?:production|staging|synthetic|slides?|demo|prototype|poc)\b/i.test(rowPromise)
|
|
60
|
+
const promise = scopeSpecified ? rowPromise : `${rowPromise} ${goalLine ? goalLine[1] : goal}`
|
|
61
|
+
const acceptance = String(row.accepted || '')
|
|
62
|
+
const limited = acceptance.match(/\b(staging|slides?(?: design)?|demo|prototype|poc)\s+only\b/i)
|
|
63
|
+
if (limited) {
|
|
64
|
+
const scope = limited[1].toLowerCase()
|
|
65
|
+
const matchingGoal = scope.startsWith('slide') ? /\bslides?\b/i.test(promise) : new RegExp('\\b' + scope + '\\b', 'i').test(promise)
|
|
66
|
+
if (!matchingGoal || /\b(?:production|clinical use|go.live)\b/i.test(promise)) return 'approval scope is limited; review against the promised outcome'
|
|
67
|
+
}
|
|
68
|
+
const measured = String(row.measured || '')
|
|
69
|
+
const productionUntested = measured.split(/[;.\n]/).some(clause => /\bproduction\b/i.test(clause) && /\b(?:not|never|untested|unmeasured|pending)\b/i.test(clause))
|
|
70
|
+
if (/\bproduction\b/i.test(promise) && /\b(?:staging|synthetic|prototype|poc)\b/i.test(measured) && (!/\bproduction\b/i.test(measured) || productionUntested)) return 'measurement scope differs from production promise; review required'
|
|
71
|
+
return ''
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function reconcileValueRows(rows, goal = '') {
|
|
75
|
+
const key = row => String(row.slice || '').trim().toLowerCase()
|
|
76
|
+
const withdrawn = new Set(rows.filter(withdrawalMention).map(key).filter(Boolean))
|
|
77
|
+
return rows.map(row => {
|
|
78
|
+
let acceptanceIssue = scopeIssue(row, goal)
|
|
79
|
+
if (withdrawalMention(row)) acceptanceIssue = 'withdrawal recorded; review current acceptance'
|
|
80
|
+
else if (withdrawn.has(key(row))) acceptanceIssue = 'conflicting withdrawal for this slice; review history before relying on acceptance'
|
|
81
|
+
return acceptanceIssue ? { ...row, acceptanceIssue, state: row.state === 'unmeasured' ? 'unmeasured' : 'claimed' } : row
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { PENDING_CELL_RE, acceptanceName, evidenceSource, valueState, reconcileValueRows }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fdeops",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.28.0",
|
|
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.
|
|
4
|
+
"version": "3.28.0",
|
|
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",
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
**Read first:** `context.md` if it exists - otherwise start cold. The point of this phase is to establish ground truth, not assume it.
|
|
6
6
|
|
|
7
|
-
## Method - part 1:
|
|
7
|
+
## Method - part 1: inspect the inherited record (you do this work)
|
|
8
8
|
|
|
9
9
|
Before forming any opinion:
|
|
10
10
|
|
|
11
|
-
1. **Inherit the paper.**
|
|
11
|
+
1. **Inherit the paper.** Start with `fde resume` and inventory the available docs, ADRs, ticket exports and operational handoff. Do not recursively load `.fde/` or raw transcripts. List the claims and unknowns, then use `fde recall <specific topic>` to retrieve bounded evidence for each consequential claim. Review the relevant source when an excerpt is insufficient; keep unrelated history on disk. Previous decisions are evidence, not verdicts.
|
|
12
12
|
2. **Run the discover scans** (see `discover.md` part 1: churn, test gaps, "temporary" grep, AI components). On a takeover, add:
|
|
13
13
|
```bash
|
|
14
14
|
git log --format="%an" | sort | uniq -c | sort -rn | head # who actually built this
|
|
@@ -55,7 +55,7 @@ Build without a plan in an inherited system is the fastest path to the second in
|
|
|
55
55
|
|
|
56
56
|
## Principles
|
|
57
57
|
|
|
58
|
-
-
|
|
58
|
+
- Inventory the record; verify consequential claims through targeted, bounded retrieval before forming an opinion.
|
|
59
59
|
- "It should work" is not "it works." Verify.
|
|
60
60
|
- The most dangerous systems are the ones everyone assumes someone else understands.
|
|
61
61
|
- Don't build until `audit.md`, `terrain.md`, `reality.md` are written.
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
- `signer: Priya` (she can say yes; lands in `success.md`)
|
|
31
31
|
- `next: send one-pager before Thursday 9am`
|
|
32
32
|
- unprefixed lines stay context color only
|
|
33
|
-
4. Show the **REVIEW** block first (decided / asked / open / next / signer). That is the one screen to confirm. File routing stays underneath.
|
|
33
|
+
4. After editing, run `fde debrief --review` to show the pending proposal without replacing it. Show the **REVIEW** block first (decided / asked / open / next / signer). That is the one screen to confirm. File routing stays underneath.
|
|
34
34
|
5. In **chat**, after that REVIEW, present a four-row card and omit empty rows:
|
|
35
35
|
- Decided
|
|
36
36
|
- Asked / open
|
|
@@ -74,3 +74,15 @@ Read back the 2-3 most consequential captures in one breath - so the FDE can cor
|
|
|
74
74
|
- Verbatim quote outranks paraphrase; hesitation outranks quote.
|
|
75
75
|
- Signals move on evidence, never on vibe alone.
|
|
76
76
|
- A meeting with no decisions and no actions - say so; that is a finding.
|
|
77
|
+
|
|
78
|
+
## Delivery rows and repeated updates
|
|
79
|
+
|
|
80
|
+
For a measured or promised slice, use a reviewed structured line:
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
delivery: Replay|risk-mitigation|zero duplicates|zero duplicates on staging|pending|[source: transcript:42]|pending
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The seven fields are Slice, Bucket, Promised, Measured, Accepted by, Evidence, Rollback. Keep unknowns `pending`; never infer approval. This lands in the value ledger during the same confirmed apply. A `delivery:` line without pipes stays a narrative note. Incorrect field counts refuse the write rather than shifting the meaning of cells.
|
|
87
|
+
|
|
88
|
+
A sourced statement already in the record triggers a replay warning. Before applying, compare newer facts and the current next action. Remove repeated statements from the proposal if this is an accidental re-import. Only after the engineer explicitly confirms an intentional repeat, apply with `fde debrief --apply --allow-replay`. This does not silently deduplicate history and does not authenticate sources.
|