Skip to content

feat: 3.11.0 - fde vault, a derived and disposable Obsidian view of the fieldbook - #49

Merged
suboss87 merged 2 commits into
Mainfrom
devin/1787814793-fde-vault
Aug 27, 2026
Merged

feat: 3.11.0 - fde vault, a derived and disposable Obsidian view of the fieldbook#49
suboss87 merged 2 commits into
Mainfrom
devin/1787814793-fde-vault

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Obsidian skips any path starting with ., so opening ~/fde-engagements as a vault shows nothing - every client's record lives inside .fde/. fde vault answers that by generating a vault instead of pointing at one:

authoritative .fde/  ──► fde vault ──►  ~/fde-vault  (derived, gitignored, never read back)

New verb, no change to the memory model:

fde vault                       # ~/fde-vault - Portfolio + a page per engagement + Questions
fde vault --redacted            # ~/fde-vault-redacted - safe on a shared screen
fde vault --current --out <dir> # this workspace's engagement only, anywhere

The design rule that keeps your "nothing new to maintain" constraint intact: frontmatter and [[wikilinks]] exist only in the generated copy. Authoritative .fde/ files stay plain markdown a client can read, every existing parser is untouched, and the vault is deleted and rebuilt on every run - so a note typed into it is lost, and nothing in it can ever become a second source of truth. A test asserts both halves (the source file starts with #, not ---; a hand-typed scratch.md in the vault does not survive a rebuild).

What the vault gives an FDE that no existing surface does: one window over all clients, cross-client search, graph view from sponsor → decision → what shipped, and a Questions page that answers "which promise has nobody accepted?" deterministically - computed in JS, not shipped as Dataview queries, so it works in a stock Obsidian with no plugins.

--redacted is a second redaction on top of the <private> stripping every output already does: it drops stakeholders.md, trust-profile.md, people pages, the trust column and signal_* frontmatter, contact notes in the timeline, and [signal:x] / [@owner] tokens. The generated README states plainly what redaction does not do - decisions.md, risks.md and delivery.md are shown as written, so judgement still applies before you screen-share.

Because the command deletes its target before rebuilding, resolveVaultOut() refuses anything it would be wrong to delete - the engagements root (either containment direction), $HOME, the filesystem root, any path with a .fde component, a symlink, and any existing non-empty directory without the .fdeops-vault stamp this command writes. One test drives all five refusals and then asserts the decoy my-notes/notes.md still exists.

Files: new bin/lib/vault.js (pure builders - data in, {rel, content} out, no fs/network), cmdVault + resolveVaultOut + valueLedgerRows in bin/fde.js, vault added to the installer pass-through and cmdVault to the CLI-wiring gate, one router row in SKILL.md, a USAGE section, 3.11.0 across the four manifests.

npm run check: gates green, 116/116 (110 before, 6 new: portfolio/multi-engagement + private-leak sweep, --redacted, determinism + dropped engagement, the five refusals + a path with spaces, empty portfolio, unaccepted value in Questions).

Link to Devin session: https://app.devin.ai/sessions/f135381c4682413bae73dff38eb6d1a3
Open in Devin Desktop: https://app.devin.ai/desktop/session/f135381c4682413bae73dff38eb6d1a3?variant=devin
Requested by: @suboss87


Devin Review

Obsidian ignores dot-paths, so ~/fde-engagements as a vault shows nothing.
fde vault generates a portfolio vault from .fde/ instead: frontmatter and
[[wikilinks]] in the generated copy only, rebuilt from scratch each run,
gitignored, never parsed back. --redacted drops the political layer for a
shared screen. Refuses $HOME, the engagements root, .fde/, symlinks, and any
directory it did not write.

Co-Authored-By: Subash Natarajan <suboss87@gmail.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

Comment thread bin/lib/vault.js
…k, and cannot be aimed through a symlink

Three defects in the sponsor-facing build:

- stripInternalTokens() collapsed all whitespace runs, and --redacted feeds it
  whole section bodies, so every page an FDE would screen-share became run-on
  text with broken tables. It now closes up only the space the removed token
  occupied.
- next action, brief and reality were rendered raw on the hub and portfolio
  pages, so a [@owner] or [signal:red] typed there reached the sponsor view.
- resolveVaultOut() judged containment on the textual path, so a symlinked
  parent (ln -s ~/fde-engagements /tmp/l; --out /tmp/l/v) aimed the output
  inside the engagements root. Containment is now judged on the real path,
  while the symlink refusal still reads the path as given.
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Fixed in 9e728ac — real bug, and it was the worst kind: it only showed up on the pages an FDE would put on a sponsor's screen.

I went slightly narrower than the suggested [^\S\n]{2,}: the only reason to collapse whitespace here is the gap a removed token leaves behind, so the token itself now eats its own leading horizontal space and no global collapse runs at all. That also preserves markdown hard line breaks (two trailing spaces), which [^\S\n]{2,} → ' ' would have flattened.

.replace(/[^\S\n]*\[signal:(red|amber|green)\]/gi, '')
.replace(/[^\S\n]*\[@[^\]]+\]/g, '')
.trim()

Regression test asserts every heading in the source delivery.md is still alone on its line, the table separator row survives, and blank lines remain.

Two more from the same reading, in the same commit: next / brief / reality were rendered raw on the hub and portfolio pages, so a [@owner] typed there reached the redacted build; and resolveVaultOut() judged containment textually, so ln -s ~/fde-engagements /tmp/l; fde vault --out /tmp/l/v aimed the output inside the engagements root — containment is now judged on the resolved path while the symlink refusal still reads the path as given. 119/119.

@suboss87
suboss87 merged commit 03f3d0d into Main Aug 27, 2026
1 check passed
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Runtime verification — fde vault (9e728ac, shipped as 3.11.0)

Adversarial shell matrix in disposable HOME + FDEOPS_ENGAGEMENTS_ROOT sandboxes, every invocation under timeout, contrasted against both parents (8a980b5 fix parent, c960338 feature parent). Floor: npm run check exit 0, npm test 119/119.

No product defect found. All three fixes in the final commit are proven load-bearing. Note this verification landed after merge.

Privacy: 19 needles, 13+ source shapes, 0 leaks

Private blocks seeded into every .fde file plus a markdown table cell, a Value-ledger row, Signal history, .signal-ledger, .inbox, a sealed sidecar and a context debrief block — with the control proving all 19 are on disk (needles present on disk: 19 / 19).

sweep needles + fragments
full tree (21 files) 0
--redacted tree (15 files) 0
both trees after all 14 verbs ran 0
14 CLI verbs, 98 KB dashboard, 3 hooks 0

An unbalanced <private> opener seals to EOF. A private table cell and ledger row render (private - redacted) in place, keeping the row structure. The only raw <private>/[signal:x] literals anywhere are in the generated README.md, which documents what it strips.

The whitespace fix is load-bearing (same fixture, pre-fix vs fixed)
redacted page pre-fix lines fixed pre-fix table rows fixed pre-fix max line fixed
Risks.md 21 31 2 4 140 93
Delivery.md 23 54 6 8 363 109
Terrain.md 25 43 3 5 175 127

Pre-fix a sponsor saw # Risk register | Risk | Status | Owner | Mitigation | glued into one line. Fixed: all five section pages have identical line counts to full mode, equal table rows and headings, and a line-by-line diff whose only differences are the removed tokens. The narrower [^\S\n]* over [^\S\n]{2,} is vindicated — a markdown hard break (two trailing spaces) survives.

Token sinks: full mode has 8 [signal:] + 5 [@] live; redacted has 0 across all six sinks (section body, timeline, hub Next action/Brief/Reality, Portfolio column). At 50 engagements, 250 live tokens → 0.

Containment fix: a symlinked parent no longer smuggles output into the fieldbook
== PARENT 8a980b5 ==  exit=0  vault → /tmp/.../link-to-root/newvault
   *** OUTPUT LANDED INSIDE THE ENGAGEMENTS ROOT
   *** files written inside the root: 12
== FIXED 9e728ac ==   exit=1  refused: ... would contain or sit inside your engagements root
   new files appearing under the root: 0

21-case refusal matrix — root, parent of root (nested root so it is distinct from $HOME), child, $HOME, /, .fde component, non-empty unstamped dir, file, fifo, /dev/null, missing value, plus 3-hop symlink chain, link→$HOME, link→.fde, dangling link — all exit 1, target byte-identical, engagements root byte-identical after the whole matrix. Allowed-by-intent cases (relative, ~/ expanded, ~foo literal, empty dir, stamped dir, --out twice) all land exactly where expected.

Never authoritative · rebuild · hostile data · packaging · YAML

Never authoritative: manifest of 111 source files byte-identical after all four modes; .fde/ gained 0 wikilinks and 0 frontmatter; an fs tracer across 11 verbs shows 0 reads inside the vault (the writer only lstats its own output for the stamp gate); deleting the vault changes no exit code.

Rebuild: two runs byte-identical; hand-typed files vanish; a removed engagement and an emptied page disappear with 0 dangling links; 154-file redacted rebuild byte-identical at 50 engagements.

Hostile data: 15 hostile engagement names + stakeholder names ../../escape, .., a/b, /etc/passwd → traversal canary never created, 0 files outside the vault, 270 wikilinks 0 dangling. fifo/symlink/dir/unreadable/5 MB/binary/CRLF/malformed: no hang, no stack trace, symlink target body leaked 0x.

Scale: 50 engagements → 255 pages in 67 ms.

Packaging: package/bin/lib/vault.js ships; tarball npm i -g into a clean prefix + clean HOME runs vault from outside any checkout with stamp "version": "3.11.0"; install.js deploys lib/vault.js and the deployed CLI runs it; npx --package=<tgz> fde vault works. The registry still publishes 3.10.2, so that path is untested.

YAML: 255 frontmatter blocks parsed with PyYAML 5.4.1, 0 errors, 0 injection under hostile values (" \ : #, emoji, braces, leading -/>).

Three intended behaviours worth a second look
  1. A dot-prefixed engagement dir is silently invisible to every verb (pre-existing — identical at c960338). It is also the defence that makes an engagement named .. unreachable, since safeTitle strips / [ ] | # ^ but not dots. All three escape routes are closed; worth a comment in safeTitle so a future refactor cannot reopen it.
  2. FDEOPS_ENGAGEMENT=/etc fde vault --current exits 0 with an empty untitled engagement instead of refusing (pre-existing). No system content leaks — root:x:0:0, nameserver, PATH= all 0 hits.
  3. A redacted Value-ledger row still names its acceptor (| (private - redacted) ledger row | 4h/wk saved | Dana |) because section bodies are shown as written — by design, but a sponsor-facing page naming an internal acceptor may surprise users.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant