DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

AlexKaiqi /

AlexKaiqi/dsh-block-to-file

Topic repository only

simple runtime ability to map a block to file, such that bash can access

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@7323c74d

dsh-block-to-file

Model-facing block-to-file (b2f) runtime pipeline plugin.

Fenced code blocks whose info string contains file= are committed through the selected transaction backend before any tool call of the same assistant message executes. The default backend uses a plugin-owned Git object store; a host can mount its own context repository backend to own version reads and publication. The workspace at $DSH_B2F_ROOT does not need a .git directory; only the Git executable is required for the default backend. The plugin is not a tool: it observes assistant/message, validates every block, compares the target blobs with the agent's snapshot, and submits all files as one transaction. The default Git backend publishes with git update-ref CAS.

Protocol

```python file=src/app.py
def main():
    print("hello")
```

Attributes: file (required), mode=write|create|update|append|delete (default write), plus one edit mode (below); diff=full|limited|stats|none (default limited), encoding=utf-8, newline=preserve|lf|crlf.

An absolute file= path is accepted when it resolves inside the transaction root and is rewritten to its relative form; a path that escapes the root is rejected with PATH_ABSOLUTE.

append is computed from the observed blob and is idempotent: when that blob already ends with the block content, b2f reports [b2f] append skipped.

All blocks in one assistant message are one transaction. If any target blob is stale, nothing commits and feedback includes each stale file's latest complete content, blob OID, repository revision, and intervening b2f commits. A stale response becomes the agent's new observation for an immediate retry.

The bare object store lives at <root>.b2f-git. Git index construction and workspace-projection temp files live in <root>.b2f-tmp (or $DSH_B2F_TMP). Both locations are outside the workspace.

Partial edits

A partial edit is a front-end only: b2f resolves (observed blob, patch) → full content and the result travels the same path as any full-content proposal (stale comparison, precondition checks, ref CAS, projection, diff feedback). Resolution is pure and runs against the exact bytes of the blob the model observed — never the worktree and never through git apply, so no clean/smudge filter, core.autocrlf, or apply.whitespace setting can alter content on the way in.

editFormat selects one dialect. Only that dialect is described in the system prompt and only its mode is accepted, so the model never has to choose between patch formats; the other is rejected with EDIT_MODE_DISABLED.

editFormat: replace — anchors are content:

```python file=src/client.py mode=edit
<<<<<<< SEARCH
    timeout = 1
=======
    timeout = 3
    retries = 5
>>>>>>> REPLACE
```

Each SEARCH is matched against the observed content and must match exactly once; spans must not overlap. An empty REPLACE deletes. Insert by keeping the anchor lines in both sections. Matching is order-independent, so a failure is always attributable to one edit.

editFormat: git_diff — anchors are line numbers plus context:

```python file=src/client.py mode=diff
@@ -40,3 +40,4 @@
 def connect():
-    timeout = 1
+    timeout = 3
+    retries = 5
     return client
```

Emit hunks only — no diff --git, index, ---, or +++ lines. Stated line counts are ignored and recomputed from the body (as git apply --recount does), and the @@ start line is a hint: the hunk is located by searching outward for its context, nearest match winning, up to maxEditDrift lines. This tolerates the line drift that plain git apply cannot recover from.

Both dialects allow several edits per block and preserve the file's existing line endings, so newline= is rejected on an edit block. When anchors do not resolve, nothing commits and feedback returns the current content to re-anchor on.

Content echoed in [b2f] feedback is labelled path=, not file=, so a model that copies an echo back verbatim writes nothing. Under git_diff those echoes are line-numbered to match the read tool's format.

Configuration

b2f:
  root: "$WS"                # expands $WS / $DSH_B2F_ROOT; DSH_B2F_ROOT env wins
  editFormat: git_diff       # git_diff | replace | none
  maxEditDrift: 200          # lines a mode=diff hunk may drift from its @@ line
  maxFileSize: 1048576
  maxTotalSize: 2097152
  maxFilesPerMessage: 16
  diffLineLimit: 200
  canonicalRef: refs/heads/agent-canonical
  maxCasRetries: 8
  tempFileKeep: 16

Set editFormat: none to disable partial edits entirely.

Each settled transaction is emitted as b2f/transaction with the full report. Per-block editFormat, editsProposed, editsApplied, and fuzz are carried on every result, so first-apply success rate, retry counts, and drift tolerance can be compared across dialects without this plugin aggregating anything.

root must be an absolute workspace path, but it does not need to be a Git worktree. On first use b2f snapshots the workspace into its private bare store and creates canonicalRef from that baseline; after that the canonical ref is the only publication source of truth. Existing or nested Git worktrees contribute their tracked files without exposing their .git object stores. Unrelated concurrent b2f commits are retained when a candidate is rebuilt on the latest canonical head.

Transaction backends

The reusable editing core is available separately from the DSH adapter:

import { parseFileBlocks, validateFileBlocks, resolveFileProposal,
  validateResolvedFileProposals, GitBackend } from 'dsh-block-to-file/core'
import type { B2FBackend } from 'dsh-block-to-file/core'

This entry point does not load DSH or Cordis. A host supplies immutable observed content to the pure proposal resolver and validates the resolved batch before calling its backend. The package's main entry remains the compatible DSH plugin; its service binds Agent/Session lifecycle and scopes to this core. Writing files does not itself publish a domain event or advance a host's workflow.

maxFileSize and maxTotalSize limit the final UTF-8 replacement contents, in addition to the incoming block bodies. Append, partial edits and newline conversion cannot bypass them. The optional fields on B2FCommitConfig default to 1 MiB and 2 MiB; existing backend implementations remain compatible. The DSH adapter checks these limits before calling any managed backend, and the default Git backend also enforces them for direct callers. Configuring larger limits is supported: blob reads use their actual size and projection streams to disk.

The model-facing file-block syntax and edit dialects are the same for both modes. With no backend on a resolved scope, b2f uses GitBackend and its private bare store. With a backend, b2f resolves edits from that backend's immutable bytes and submits the full replacements to that authority. It does not create a private Git store, update a second ref, or run legacy publishers for that scope. A backend error never falls back to standalone Git.

A host registers a stable backend object through the existing root resolver:

Integrations must first require ctx.b2f.backendProtocolVersion === 1 (also exported as B2F_BACKEND_PROTOCOL_VERSION). Older services may support root resolvers while ignoring scope.backend; resolver availability alone does not establish backend support. Reject managed writes explicitly when this capability is missing instead of falling back to a private Git authority.

import type { B2FBackend } from 'dsh-block-to-file'

const backend: B2FBackend = contextRepositoryBackend
const scope = {
  root: contextCheckout,
  scope: 'context-repo',
  authorization: 'mounted-workspace' as const,
  backend,
}
const dispose = ctx.b2f.registerRootResolver((_agent, _session, paths) =>
  paths === undefined || paths.every(isContextPath) ? scope : undefined,
)
ctx.effect(() => dispose)

The host supplies contextRepositoryBackend, contextCheckout, and its path ownership predicate. The backend may use the checkout's existing Git repository or a different versioned store. Use one backend object per authority; don't construct a fresh object on each resolver call. Changing root, scope, or backend invalidates the agent's old snapshot and observations. A message spanning multiple backends is rejected before any backend prepares or commits.

Backend contract

B2FBackend has four methods, each synchronous or asynchronous:

Method Responsibility
captureSnapshot(context) Prepare a readable workspace and return its immutable starting revision.
head(context) Return the current revision without mutating the workspace.
readFile(context, revision, path) Read exact bytes and an opaque file version at that revision; return { content: null, fileVersion: 'absent' } for non-existence.
commit(request) Atomically check all expected file versions and submit the whole batch; materialize the resulting view before reporting success.

b2f validates block syntax, paths, configured limits and edit dialect, reads the observed content, and resolves append/SEARCH-REPLACE/diff into full replacement bytes. It reports failed existence conditions and unresolved edits without calling commit; if their observations became stale, it returns fresh versions instead. The backend receives changes with path, mode, content (null means delete), expectedVersion, observedRevision, and b2f's diff/edit result. It must recheck every expected version at publication, preserve unrelated concurrent changes, and apply its domain validation and authorization. The earlier reads are not an atomic concurrency barrier.

Requests include trusted root, scope, agentId, viewRevision, and a stable Session/message transactionId. Managed backends can use that ID for durable retry deduplication. The default Git backend retains its existing content-based retry behavior. config.canonicalRef and config.tempFileKeep configure the standalone Git backend; they do not define a managed backend's authority.

The backend returns the existing B2FReport union. Successful reports carry its own revision/commit identifiers and the prepared changes' result values; versions need not be Git OIDs. On a stale batch, return current content and versions in staleFiles, with no partial write. On domain rejection, return a failed report. If publication succeeded but materialization failed, return projection-failed with the committed revision and own the forward recovery; do not throw and imply that publication never happened. Same-message tools stay blocked until an ok report, and subsequent model sampling awaits outstanding b2f settlements even when the message contained no tools.

A read integration can record an exact managed observation with ctx.b2f.recordObservation(agent.id, observation, scope) after preparing the scope. The backend identity and scope must match the active snapshot. Provider filesystem versions are not automatically interchangeable with backend versions. Use await ctx.b2f.prepareSnapshot(agent, session, scope, config) for asynchronous backends; captureSnapshot remains the synchronous compatibility entry point. Managed snapshot preparation must succeed before model sampling. Direct callers use commitToScope(..., scope, transactionId) for managed transactions; the existing synchronous commit(...) API remains available for standalone Git.

registerPublisher is a separate, legacy post-commit bridge for standalone Git transactions. Its failure can leave a local commit behind. Use a backend when the external system must decide whether the file transaction commits.

Standalone workspace recovery

The Git backend serializes snapshot preparation, commit comparison, ref updates and filesystem projection across local b2f processes sharing one root. Its internal lock ref records a process owner; a dead owner's lock is reclaimed by CAS. Contention is bounded to 30 seconds and fails explicitly when still busy. This lock is local to one machine and does not coordinate arbitrary editors or shell writers. Projection rechecks local contents after staging, but ordinary tools should not write the same paths concurrently with a b2f transaction.

refs/b2f/projected records the last complete filesystem projection separately from canonical. A restart can finish a transaction interrupted after publication, including a partially projected multi-file batch. A new Agent does not replay the entire bootstrap-to-head delta over user work: local edits, including a deliberate restore to older bytes, are preserved. A proposal touching local drift returns worktree-dirty; unrelated local edits are retained. Existing stores without a projection cursor preserve their current workspace on upgrade rather than guessing whether old-looking bytes are unfinished projection or user work.

Canonical publication is atomic. Filesystem replacement is atomic per file; same-message tools wait for the complete batch. A projection failure after publication reports its committed revision and keeps those tools blocked. Temporary-file cleanup preserves unrelated files and other live processes' staging files, including when roots share DSH_B2F_TMP.

Environment

Variable Purpose
DSH_B2F_ROOT workspace root; file= paths are relative to it
DSH_B2F_PLUGINS_DIR public plugin-artifact root (default $DSH_B2F_ROOT/plugins)
DSH_B2F_PRIVATE_DIR private plugin state (default $DSH_B2F_ROOT/.b2f/plugins)
DSH_B2F_TMP atomic-write temp dir (default <root>.b2f-tmp, outside the working tree)

Testing

pnpm install
pnpm check

Usage

Install from npm and add the package to the DSH profile:

npm install dsh-block-to-file
dsh plugin --profile web add dsh-block-to-file

During local development, link this checkout instead (pnpm install && pnpm build, then dsh plugin --profile web add "$PWD").

Mount the plugin in the Host composition, where its b2f service can be shared by every Agent session:

- id: block-to-file
  name: 'dsh-block-to-file'
  config:
    root: $WS

Do not mount this service provider as a loose row in an Agent preset. A preset that owns b2f must isolate the b2f service and place every consumer in that same isolate realm.

b2f replaces the model-facing str_replace_editor write path. Remove or disable the official editor in YOUR composition (preset / overlay) — this package intentionally does not patch or remove any official plugin.

For generic per-agent checkouts or sandboxes, install a path-aware root resolver at activation time and retain its Fiber-scoped disposer. Return undefined for paths the resolver does not own so older registrations or the default Session workspace can handle them:

const dispose = ctx.b2f.registerRootResolver(
  (agent, session, paths) => paths?.every(isCheckoutPath)
    ? {
        root: checkoutRootFor(agent, session),
        scope: 'checkout',
        authorization: 'mounted-workspace',
      }
    : undefined,
)
ctx.effect(() => dispose)

For standalone Git, a consumer may also register an async post-commit publisher. Same-message tools await the newest publisher that claims the transaction; a rejection becomes publication-failed and blocks those tools. A successful receipt is rendered separately from the local workspace commit:

const disposePublisher = ctx.b2f.registerPublisher(async request => {
  if (request.scope !== 'checkout') return undefined
  const result = await publishCanonical(request)
  return { scope: 'example', revision: result.revision, noOp: result.noOp }
})
ctx.effect(() => disposePublisher)

Every path is resolved independently. If one message spans more than one root or named scope, the whole transaction fails with MIXED_ROOT_SCOPE. Resolvers may prepare a scope asynchronously. The newest resolver returning a claim wins. Roots that need asynchronous preparation are skipped by the pre-step snapshot and captured on demand at commit time, so an async resolver never fails an agent step. When ctx.sandboxPolicy is mounted, b2f consumes that same per-Session policy: read-only rejects every mutation, workspace-write accepts the Session root and trusted mounted-workspace claims, and danger-full-access retains the configured b2f boundary. The default resolver uses session.header.cwd, falling back to the static config.root / $WS / $DSH_B2F_ROOT value. b2f pins an agent's canonical snapshot when its repository view is first prepared and advances it only after commit or stale feedback. When ctx.fs is mounted, a successful b2f settlement resolves and stats each result through that provider and emits fs/observed before same-message tools run. Provider-native FsVersion values are deliberately not reused as Git blob observations; a read-capable plugin with exact b2f version information may instead call ctx.b2f.recordObservation(agentId, {...}).

# in your preset or overlay cordis.yml
- id: str-replace-editor
  disabled: true

tool-bash remains the single model-facing tool for operating on files.

—/ 5

No ratings yet

Manifest verification required

Commit 7323c74d9e20

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout