dsh-hashline-edit 0.1.1 → 0.2.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.
Files changed (130) hide show
  1. package/README.md +70 -69
  2. package/cordis.patch.yml +3 -3
  3. package/lib/index.js +4151 -4516
  4. package/lib/types/dsh/contracts.d.ts +69 -6
  5. package/lib/types/dsh/fs.d.ts +38 -3
  6. package/lib/types/dsh/guidance.d.ts +5 -5
  7. package/lib/types/dsh/hide.d.ts +79 -0
  8. package/lib/types/dsh/host-compat.d.ts +7 -78
  9. package/lib/types/dsh/index.d.ts +21 -9
  10. package/lib/types/dsh/metrics/events.d.ts +8 -1
  11. package/lib/types/dsh/metrics/instrument.d.ts +20 -14
  12. package/lib/types/dsh/metrics/observer.d.ts +28 -0
  13. package/lib/types/dsh/sandbox.d.ts +2 -2
  14. package/lib/types/dsh/shell-hook.d.ts +17 -8
  15. package/lib/types/dsh/shell-view.d.ts +52 -0
  16. package/lib/types/dsh/tools/apply-patch.d.ts +38 -0
  17. package/lib/types/dsh/tools/context.d.ts +19 -1
  18. package/lib/types/dsh/tools/output.d.ts +32 -4
  19. package/lib/types/dsh/tools/read.d.ts +12 -15
  20. package/lib/types/dsh/tools/undo.d.ts +5 -0
  21. package/lib/types/dsh/write-hook.d.ts +8 -22
  22. package/lib/types/src/apply-patch/commit.d.ts +49 -0
  23. package/lib/types/src/apply-patch/grammar.d.ts +29 -0
  24. package/lib/types/src/apply-patch/lifecycle.d.ts +32 -0
  25. package/lib/types/src/apply-patch/plan.d.ts +50 -0
  26. package/lib/types/src/apply-patch/render.d.ts +51 -0
  27. package/lib/types/src/apply-patch/resolve.d.ts +129 -0
  28. package/lib/types/src/apply-patch/tool.d.ts +54 -0
  29. package/lib/types/src/apply-patch/types.d.ts +362 -0
  30. package/lib/types/src/domain-errors.d.ts +16 -2
  31. package/lib/types/src/edit-undo.d.ts +11 -2
  32. package/lib/types/src/file-content/detection.d.ts +17 -0
  33. package/lib/types/src/file-content/index.d.ts +2 -2
  34. package/lib/types/src/file-content/preview.d.ts +1 -1
  35. package/lib/types/src/fs-lifecycle.d.ts +44 -0
  36. package/lib/types/src/hashline/resolve.d.ts +6 -0
  37. package/lib/types/src/host-utils.d.ts +63 -0
  38. package/lib/types/src/mutation-engine/batch-span-gate.d.ts +6 -0
  39. package/lib/types/src/mutation-engine/edit-request.d.ts +34 -0
  40. package/lib/types/src/mutation-engine/pipeline.d.ts +18 -3
  41. package/lib/types/src/mutation-engine/types.d.ts +50 -58
  42. package/lib/types/src/read-expansion/grammar-manifest.d.ts +4 -2
  43. package/lib/types/src/read-expansion/java-decoration.d.ts +21 -0
  44. package/lib/types/src/read-expansion/span-expansion.d.ts +6 -2
  45. package/lib/types/src/read-expansion/span-kind.d.ts +3 -2
  46. package/lib/types/src/read-expansion/vue-decoration.d.ts +14 -0
  47. package/lib/types/src/served-session/serve-render.d.ts +9 -3
  48. package/lib/types/src/undo-store.d.ts +17 -1
  49. package/package.json +18 -22
  50. package/prompts/apply-patch-guidelines.md +10 -0
  51. package/prompts/apply-patch.md +1 -0
  52. package/prompts/read-guidelines.md +5 -5
  53. package/prompts/read.md +1 -1
  54. package/prompts/undo-last-edit-guidelines.md +2 -2
  55. package/prompts/undo-last-edit-snippet.md +1 -1
  56. package/prompts/undo-last-edit.md +1 -1
  57. package/src/apply-patch/commit.ts +190 -0
  58. package/src/apply-patch/grammar.ts +620 -0
  59. package/src/apply-patch/index.ts +17 -0
  60. package/src/apply-patch/lifecycle.ts +195 -0
  61. package/src/apply-patch/plan.ts +800 -0
  62. package/src/apply-patch/render.ts +137 -0
  63. package/src/apply-patch/resolve.ts +470 -0
  64. package/src/apply-patch/tool.ts +257 -0
  65. package/src/apply-patch/types.ts +387 -0
  66. package/src/cut-repair.ts +77 -11
  67. package/src/domain-errors.ts +58 -10
  68. package/src/edit-undo.ts +379 -25
  69. package/src/file-content/detection.ts +34 -0
  70. package/src/file-content/index.ts +2 -2
  71. package/src/file-content/preview.ts +1 -1
  72. package/src/file-content/served-header.ts +1 -1
  73. package/src/fs-lifecycle.ts +201 -0
  74. package/src/hash-store.ts +18 -1
  75. package/src/hashline/lease-resolve.ts +14 -8
  76. package/src/hashline/resolve.ts +30 -27
  77. package/src/host-utils.ts +224 -0
  78. package/src/mutation-engine/batch-span-gate.ts +17 -4
  79. package/src/mutation-engine/edit-request.ts +39 -0
  80. package/src/mutation-engine/pipeline.ts +535 -63
  81. package/src/mutation-engine/types.ts +49 -65
  82. package/src/mutation-queue.ts +1 -1
  83. package/src/prompts.ts +0 -28
  84. package/src/read-expansion/ast-provider.ts +4 -0
  85. package/src/read-expansion/grammar-manifest.ts +21 -2
  86. package/src/read-expansion/java-decoration.ts +34 -0
  87. package/src/read-expansion/span-expansion.ts +6 -2
  88. package/src/read-expansion/span-kind.ts +3 -1
  89. package/src/read-expansion/vue-decoration.ts +43 -0
  90. package/src/served-session/serve-render.ts +5 -5
  91. package/src/undo-store.ts +74 -5
  92. package/lib/types/dsh/encoding-bridge.d.ts +0 -25
  93. package/lib/types/dsh/encoding.d.ts +0 -144
  94. package/lib/types/dsh/tools/edit.d.ts +0 -10
  95. package/lib/types/src/edit-tool.d.ts +0 -41
  96. package/lib/types/src/mutation-engine/engine.d.ts +0 -31
  97. package/lib/types/src/payload-contract.d.ts +0 -124
  98. package/prompts/edit-guidelines.md +0 -10
  99. package/prompts/edit-snippet.md +0 -1
  100. package/prompts/edit.md +0 -1
  101. package/src/bash-classifier.ts +0 -1626
  102. package/src/bash-conversion/decide.ts +0 -812
  103. package/src/bash-display-transform.ts +0 -50
  104. package/src/bash-multifile-config.ts +0 -239
  105. package/src/bash-view-config.ts +0 -158
  106. package/src/edit-normalize.ts +0 -8
  107. package/src/edit-render.ts +0 -161
  108. package/src/edit-tool.ts +0 -116
  109. package/src/edit.ts +0 -136
  110. package/src/integrations/pi-lens/command.ts +0 -712
  111. package/src/integrations/pi-lens/config.ts +0 -148
  112. package/src/integrations/pi-lens/io-bridge-adapter.ts +0 -234
  113. package/src/integrations/pi-lens/io-bridge.ts +0 -112
  114. package/src/integrations/pi-lens/settings-overlay.ts +0 -272
  115. package/src/lifecycle-hooks/index.ts +0 -1992
  116. package/src/lifecycle-hooks/types.ts +0 -64
  117. package/src/mutated-files.ts +0 -75
  118. package/src/mutation-engine/engine.ts +0 -173
  119. package/src/mutation-engine/index.ts +0 -17
  120. package/src/payload-contract.ts +0 -637
  121. package/src/preview-controller.ts +0 -84
  122. package/src/read.ts +0 -865
  123. package/src/search-hook.ts +0 -41
  124. package/src/store-config.ts +0 -73
  125. package/src/store-janitor-warning.ts +0 -169
  126. package/src/store-janitor.ts +0 -539
  127. package/src/store-kernel/settings.ts +0 -233
  128. package/src/store.ts +0 -154
  129. package/src/tui-presenter.ts +0 -134
  130. package/src/write-hook.ts +0 -130
@@ -2,7 +2,7 @@
2
2
  * Shared host contracts for the dsh adapter.
3
3
  *
4
4
  * The `dsh/` tree is the DeepSeek Harness host layer over the host-agnostic
5
- * hashline core in `src/`. Every lane (host, read, edit, platform) compiles
5
+ * hashline core in `src/`. Every lane (host, read, apply-patch, platform) compiles
6
6
  * against this file; renaming or reshaping an export here is a cross-lane
7
7
  * breaking change.
8
8
  *
@@ -52,10 +52,11 @@ export interface FileStat {
52
52
  *
53
53
  * Paths crossing this interface are absolute and already validated by
54
54
  * `dsh/fs.ts`: implementations may assume no null byte and no relative path.
55
- * Reads return raw bytes so the platform lane owns BOM sniffing and encoding
56
- * governance without a second decode step; callers that want text decode the
57
- * bytes through `src/file-content/loader.ts`'s `decodeNormText` (which is
58
- * node:fs-bound today and therefore takes a path, not this interface).
55
+ * Reads return raw bytes so a byte-level refusal stays possible without a second
56
+ * decode step; callers that want text decode the bytes through
57
+ * `src/file-content/detection.ts`'s `decodeUtf8Strict` (the host's one admission:
58
+ * strict UTF-8, BOM stripped) or, for the anchor pipeline, through `decodeNormText`
59
+ * (which is node:fs-bound today and therefore takes a path, not this interface).
59
60
  */
60
61
  export interface FileIO {
61
62
  /** Read a whole regular file as raw bytes. */
@@ -66,6 +67,15 @@ export interface FileIO {
66
67
  stat(path: string, signal?: AbortSignal): Promise<FileStat | undefined>;
67
68
  /** Direct child names of a directory, sorted lexicographically. */
68
69
  readdir(path: string, signal?: AbortSignal): Promise<string[]>;
70
+ /**
71
+ * Delete one regular file. A lifecycle delete targets a file; a directory at the same path is
72
+ * a caller bug that must surface (never a recursive removal).
73
+ */
74
+ remove?(path: string, signal?: AbortSignal): Promise<void>;
75
+ /** Rename one regular file. The destination must not exist and its parent is created first. */
76
+ rename?(from: string, to: string, signal?: AbortSignal): Promise<void>;
77
+ /** Create one directory and missing parents. */
78
+ mkdir?(path: string, signal?: AbortSignal): Promise<void>;
69
79
  }
70
80
  /** The two sandbox-escalation arguments a mutating tool may carry. */
71
81
  export interface SandboxArgs {
@@ -118,6 +128,54 @@ export interface SandboxController {
118
128
  */
119
129
  mapError(error: unknown, decision: SandboxDecision): unknown;
120
130
  }
131
+ /**
132
+ * The host observation channel one agent install publishes through.
133
+ *
134
+ * The host's observation policy fills its per-session state exclusively from
135
+ * `fs/observed`, and that state is what lets a built-in `write` replace an
136
+ * existing file. Shadowed `read`/`apply_patch` calls never touch the provider's own
137
+ * write path, so without this channel a file this plugin served stays
138
+ * "unobserved" and the built-in overwrite is refused forever.
139
+ *
140
+ * A mounted `ctx.fs` supplies the channel. Without one (`dsh/fs.ts`'s local
141
+ * `node:fs` fallback, and every test double that drives the tools directly)
142
+ * there is no channel at all: `FsTarget` and `FsVersion` are provider-owned
143
+ * values a consumer must never manufacture, so a local-only composition
144
+ * publishes nothing instead of publishing a fabricated identity.
145
+ */
146
+ export interface FsObservationChannel {
147
+ /**
148
+ * Publish what a successful call observed or produced for one file.
149
+ *
150
+ * Reuses the version an in-flight `stat` already reported unless
151
+ * {@link FsObserveOptions.fresh} is set, which re-stats; a write must pass
152
+ * `fresh` because the pre-write version is stale by then. A target the
153
+ * provider confirms missing is published as `{ kind: "absent" }`.
154
+ *
155
+ * Never throws and never fails the tool call: a provider round-trip or
156
+ * listener failure is reported through the channel's diagnostic sink.
157
+ * At most one observation per call and file is published.
158
+ */
159
+ publish(absolutePath: string, exec?: object, options?: FsObserveOptions): Promise<void>;
160
+ /**
161
+ * Dispatch `fs/write-intent` for a file this plugin is about to write, so
162
+ * host listeners still see the change this plugin is making.
163
+ *
164
+ * The returned intent is deliberately not enforced: this plugin's mutations
165
+ * go straight to `node:fs`, so the provider's guarded-write semantics (which
166
+ * is where an intent is applied) do not run for them. A listener failure is
167
+ * reported and the write proceeds — this dispatch is bookkeeping, not a
168
+ * guard this plugin owns.
169
+ */
170
+ announceWrite(absolutePath: string, exec?: object, signal?: AbortSignal): Promise<void>;
171
+ }
172
+ /** Knobs for one {@link FsObservationChannel.publish} call. */
173
+ export interface FsObserveOptions {
174
+ /** Re-stat through the provider instead of reusing the version this call already saw. */
175
+ readonly fresh?: boolean | undefined;
176
+ /** Caller cancellation, forwarded into the provider round-trip. */
177
+ readonly signal?: AbortSignal | undefined;
178
+ }
121
179
  /** Host services one tool factory receives at agent-install time. */
122
180
  export interface ToolDependencies {
123
181
  /** Local filesystem seam; M1 is `dsh/fs.ts`'s node:fs implementation. */
@@ -126,6 +184,11 @@ export interface ToolDependencies {
126
184
  readonly sandbox: SandboxController;
127
185
  /** Read-only IO bridge; absent keeps the legacy node:fs read path. */
128
186
  readonly coreIo?: CoreReadIo;
187
+ /**
188
+ * Host observation channel; absent without a mounted `ctx.fs`, where a
189
+ * provider-owned target and version cannot be manufactured.
190
+ */
191
+ readonly observations?: FsObservationChannel;
129
192
  /** Optional telemetry recorder; undefined keeps the tool path emission-free. */
130
193
  readonly metrics?: ToolMetrics;
131
194
  }
@@ -136,7 +199,7 @@ export interface ToolDependencies {
136
199
  * factories at registration time. The fixed names are part of the lane
137
200
  * contract:
138
201
  * - `dsh/tools/read.ts` → `buildReadTool` (tool name `read`)
139
- * - `dsh/tools/edit.ts` → `buildEditTool` (tool name `edit`)
202
+ * - `dsh/tools/apply-patch.ts` → `buildApplyPatchTool` (tool name `apply_patch`)
140
203
  * - `dsh/tools/undo.ts` → `buildUndoTool` (tool name `undo_last_edit`)
141
204
  */
142
205
  export type ToolDefinitionFactory = (deps: ToolDependencies) => ToolDefinition;
@@ -1,5 +1,6 @@
1
- import type { FileIO, FileStat } from "./contracts.js";
2
- import { type FileSystem } from "@deepseek-ai/dsh-fs";
1
+ import type { FileIO, FileStat, FsObservationChannel } from "./contracts.js";
2
+ import type { Context } from "@deepseek-ai/cordis";
3
+ import { type FileSystem, type FsTarget, type FsVersion } from "@deepseek-ai/dsh-fs";
3
4
  import type { CoreReadIo } from "../src/io-port.js";
4
5
  /** Stable codes for request-shape failures raised before any OS call. */
5
6
  export type FileValidationCode = "E_PATH_EMPTY" | "E_PATH_NULL_BYTE" | "E_PATH_NOT_ABSOLUTE" | "E_ACCESS_MODE";
@@ -40,6 +41,9 @@ export declare class LocalFileIO implements FileIO {
40
41
  write(path: string, data: Uint8Array | string, signal?: AbortSignal): Promise<void>;
41
42
  stat(path: string, signal?: AbortSignal): Promise<FileStat | undefined>;
42
43
  readdir(path: string, signal?: AbortSignal): Promise<string[]>;
44
+ remove(path: string, signal?: AbortSignal): Promise<void>;
45
+ rename(from: string, to: string, signal?: AbortSignal): Promise<void>;
46
+ mkdir(path: string, signal?: AbortSignal): Promise<void>;
43
47
  }
44
48
  /** Create the M1 local filesystem seam. */
45
49
  export declare function createLocalFileIO(): FileIO;
@@ -48,6 +52,11 @@ export declare function createLocalFileIO(): FileIO;
48
52
  * fallback when no `ctx.fs` service is mounted; it reads through `node:fs`.
49
53
  */
50
54
  export declare function createCoreReadIo(io: FileIO): CoreReadIo;
55
+ /**
56
+ * One successful `stat` through the `ctx.fs` bridge: the target it settled on
57
+ * and the provider's opaque version token for it.
58
+ */
59
+ export type CtxFsStatObserver = (target: FsTarget, version: FsVersion) => void;
51
60
  /**
52
61
  * Adapt a mounted `ctx.fs` provider to the core read port. Targets are cached
53
62
  * by path and opaque `targetKey`; `resolve` returns the backend's absolute
@@ -56,5 +65,31 @@ export declare function createCoreReadIo(io: FileIO): CoreReadIo;
56
65
  * path round-trip. `FsInfo.version` stays opaque and is deliberately not turned
57
66
  * into a fake inode identity: ADR-0043 adoption is skipped for such targets and
58
67
  * the read path falls back to content hashing.
68
+ * @param fs - the mounted provider.
69
+ * @param onStat - receives the target and provider version of every successful
70
+ * `stat`. `CoreFileStat` has no field for the version, so without this hook the
71
+ * freshness token the observation channel needs would be dropped.
72
+ * @returns the read port the core consumes.
73
+ */
74
+ export declare function createCtxFsCoreReadIo(fs: FileSystem, onStat?: CtxFsStatObserver): CoreReadIo;
75
+ /** The read bridge plus the observation channel for one mounted `ctx.fs`. */
76
+ export interface CtxFsBridge {
77
+ /** The read port the core consumes. */
78
+ readonly io: CoreReadIo;
79
+ /** The channel that publishes `fs/observed` and dispatches `fs/write-intent`. */
80
+ readonly observations: FsObservationChannel;
81
+ }
82
+ /**
83
+ * Build the read bridge and the observation channel over one `ctx.fs` provider.
84
+ *
85
+ * Both share one resolved-target cache, and the bridge reports every `stat` it
86
+ * performs, so a successful read publishes the exact version it saw instead of
87
+ * paying for a second provider round-trip.
88
+ *
89
+ * @param ctx - the context that owns the observed tool calls. Publishing from
90
+ * the agent's own scope keeps a scope-filtered host listener on that agent.
91
+ * @param fs - the mounted provider.
92
+ * @param warn - host diagnostic sink for a publication that could not complete.
93
+ * @returns the bridge and its channel.
59
94
  */
60
- export declare function createCtxFsCoreReadIo(fs: FileSystem): CoreReadIo;
95
+ export declare function createCtxFsBridge(ctx: Context, fs: FileSystem, warn: (message: string) => void): CtxFsBridge;
@@ -26,20 +26,20 @@ export declare function parseSectionFile(content: string): ParsedSection;
26
26
  export declare function isBlankOverride(content: string): boolean;
27
27
  /** One overridable tool section. */
28
28
  export interface GuidanceSection {
29
- /** Registry section name, e.g. `tool:edit`. */
29
+ /** Registry section name, e.g. `tool:apply_patch`. */
30
30
  readonly name: string;
31
- /** Override file name inside a preset directory, e.g. `edit.md`. */
31
+ /** Override file name inside a preset directory, e.g. `apply-patch.md`. */
32
32
  readonly file: string;
33
33
  /** Order used when the harness registry names no slot for this section. */
34
34
  readonly defaultOrder: number;
35
35
  /** The `systemPrompt.getSectionOrder` slot this section shadows, when one exists. */
36
- readonly orderName?: "TOOL_READ" | "TOOL_EDIT";
36
+ readonly orderName?: "TOOL_READ" | "TOOL_EDIT" | "TOOL_APPLY_PATCH";
37
37
  /** The compiled default text. */
38
38
  renderDefault(): string;
39
39
  }
40
40
  /**
41
- * The three sections, in default-order sequence. Defaults are byte-identical to
42
- * the pi host's guidance because they come from the same prompt sources.
41
+ * The three sections, in default-order sequence. Read and undo reuse the pi
42
+ * host's prompt sources; apply_patch owns the v4 mutation guidance.
43
43
  */
44
44
  export declare const GUIDANCE_SECTIONS: readonly GuidanceSection[];
45
45
  /** Render the compiled default text for one section. */
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Hide the native file-mutation tools so every text change goes through
3
+ * `apply_patch` — the one mutation entry point of the v4 surface.
4
+ *
5
+ * DSH 0.2.2-rc.1 owns tool declarations in the tool registry, not in the
6
+ * `system-prompt/assemble` waterfall: the waterfall receives sections, contexts
7
+ * and variables only. The authoritative schema filter is therefore the
8
+ * per-agent registry restriction (`tools.restrict({ deny })`), which removes a
9
+ * global tool from both the native schema list and the PTC `tools:sdk` section
10
+ * because both are rendered from `tools.definitions(scope, toolSet)`.
11
+ *
12
+ * Three seams still back that up, because a restriction only governs the
13
+ * registry view and a stale session or a hand-rolled section could carry a
14
+ * residual binding:
15
+ *
16
+ * 1. `tools.restrict({ deny })` drops the hidden schemas for this agent;
17
+ * 2. `system-prompt/assemble` empties the `tool:edit` guidance section (its
18
+ * text is prose, never a tool mention) and deletes the hidden tools' member
19
+ * blocks from any rendered PTC `tools:sdk` section;
20
+ * 3. `tools.guard` is the monotonic execution backstop for a stale schema or
21
+ * any residual binding.
22
+ *
23
+ * `write` is hidden with `edit`/`str_replace_editor`. A whole-file replacement
24
+ * is one `*** Update File:` identity hunk spanning every served line with `+`
25
+ * lines for the new content, so no native mutation tool has to stay visible.
26
+ * The native `apply_patch` name is also denied in the inherited/preset layer
27
+ * when it exists; the plugin's own scoped registration shadows it otherwise.
28
+ *
29
+ * The layer is installed only when `apply_patch` actually registered: hiding
30
+ * the native mutation tools while owning no replacement would leave the
31
+ * profile with no way to change a file.
32
+ *
33
+ * @module dsh-hashline-edit/hide
34
+ */
35
+ /** A boolean config value, or a live getter (used when a host exposes volatile config). */
36
+ type BooleanSource = boolean | (() => boolean);
37
+ /** The hide-layer configuration after the plugin-level defaults are applied. */
38
+ export interface NativeHideConfig {
39
+ /** The registered replacement tool name, protected from `hiddenToolNames`. */
40
+ readonly toolName: string;
41
+ /** Hide `write`/`edit`/`str_replace_editor`; defaults to true. */
42
+ readonly hideNativeWriteTools?: BooleanSource;
43
+ /** Extra global tool names to hide alongside the native set. */
44
+ readonly hiddenToolNames?: readonly string[];
45
+ /** The Agent scope key used for the agent-view probe; absent keeps the global probe. */
46
+ readonly scope?: unknown;
47
+ }
48
+ /**
49
+ * Delete the hidden tools' member blocks from one rendered `tools:sdk` text.
50
+ *
51
+ * Structural, renderer-specific parsing — never a mention match:
52
+ * - TypeScript: a member starts at ` name: type` (2-space indent) inside
53
+ * `ToolArgsMap`/`ToolOutputMap`, may open braces on the same line, and ends
54
+ * at the first line that closes every opened brace and ends with `;`. Its
55
+ * optional single-line JSDoc comment directly above goes with it.
56
+ * - Python: a `Tools` protocol member starts at an indented `async def` line
57
+ * and spans until the next line at that member indent or shallower.
58
+ * @param text - the rendered SDK section text.
59
+ * @param hidden - the tool names to remove.
60
+ * @returns the text with every hidden member block removed.
61
+ */
62
+ export declare function stripSdkMembers(text: string, hidden: ReadonlySet<string>): string;
63
+ interface LoggerLike {
64
+ warn?(message: string): void;
65
+ }
66
+ declare function warn(rootCtx: {
67
+ logger?: LoggerLike;
68
+ }, message: string): void;
69
+ /**
70
+ * Install the hide layer for one agent. Only call this after `apply_patch`
71
+ * registered successfully.
72
+ *
73
+ * @param rootCtx - the plugin/root scope, used for the global-name probe and warnings.
74
+ * @param agentCtx - the agent scope whose tool view is restricted.
75
+ * @param config - the resolved hide configuration.
76
+ * @returns the exact disposer; disposing lifts the restriction and every listener.
77
+ */
78
+ export declare function registerNativeToolHiding(rootCtx: Parameters<typeof warn>[0], agentCtx: unknown, config: NativeHideConfig): () => void;
79
+ export {};
@@ -1,82 +1,11 @@
1
- /** Retained line budget, matching pi's `DEFAULT_MAX_LINES`. */
2
- export declare const DEFAULT_MAX_LINES = 2000;
3
- /** Retained byte budget, matching pi's `DEFAULT_MAX_BYTES` (50KB). */
4
- export declare const DEFAULT_MAX_BYTES: number;
5
- /** The truncation report the core's read page renders. */
6
- export interface TruncationResult {
7
- /** The truncated content. */
8
- content: string;
9
- /** Whether truncation occurred. */
10
- truncated: boolean;
11
- /** Which limit was hit: `"lines"`, `"bytes"`, or `null` when nothing was cut. */
12
- truncatedBy: "lines" | "bytes" | null;
13
- /** Total number of lines in the original content. */
14
- totalLines: number;
15
- /** Total number of bytes in the original content. */
16
- totalBytes: number;
17
- /** Number of complete lines in the truncated output. */
18
- outputLines: number;
19
- /** Number of bytes in the truncated output. */
20
- outputBytes: number;
21
- /** Whether the last line was partially truncated (tail truncation only). */
22
- lastLinePartial: boolean;
23
- /** Whether the first line exceeded the byte limit (head truncation only). */
24
- firstLineExceedsLimit: boolean;
25
- /** The line limit that was applied. */
26
- maxLines: number;
27
- /** The byte limit that was applied. */
28
- maxBytes: number;
29
- }
30
- /** The two independent limits a truncation call applies. */
31
- export interface TruncationOptions {
32
- /** Maximum number of lines; defaults to {@link DEFAULT_MAX_LINES}. */
33
- maxLines?: number;
34
- /** Maximum number of bytes; defaults to {@link DEFAULT_MAX_BYTES}. */
35
- maxBytes?: number;
36
- }
37
1
  /**
38
- * Minimal structural stand-in for pi's extension-registry type.
2
+ * dsh host shim for the host-neutral utilities the core uses.
39
3
  *
40
- * WHY: `src/edit-undo.ts` keeps the pi registration (and its `ExtensionAPI` type) next to the
41
- * host-agnostic use case the dsh undo tool calls. The dsh build resolves the pi package to this
42
- * shim and never invokes that registration, so the build typecheck needs only the single method
43
- * the registration calls; the root tsconfig still checks the registration against the real pi API.
44
- */
45
- export interface ExtensionAPI {
46
- /**
47
- * Register one pi tool definition. The argument is intentionally structural and loose: the dsh
48
- * program only needs the registration to compile, never to execute, and the loose `execute`
49
- * signature keeps the pi adapter's untyped callback out of the dsh build's implicit-any checks.
50
- */
51
- registerTool(definition: {
52
- execute: (...args: any[]) => unknown;
53
- [key: string]: unknown;
54
- }): void;
55
- }
56
- /**
57
- * Format bytes as a human-readable size (`512B`, `1.5KB`, `2.0MB`).
58
- * @param bytes - the byte count to render.
59
- * @returns the pi-compatible size label.
60
- */
61
- export declare function formatSize(bytes: number): string;
62
- /**
63
- * Truncate content from the head (keep the first N lines / bytes).
4
+ * WHY: the dsh bundle must not ship the pi agent package. The core's utility
5
+ * surface lives in `src/host-utils.ts`; this module keeps the historical
6
+ * `dsh/host-compat` import path (and the `@earendil-works/pi-coding-agent`
7
+ * esbuild/tsconfig alias) working for the dsh build.
64
8
  *
65
- * WHY: the read page's oversize and truncation warnings must agree with the pi
66
- * implementation the rest of the contract was written against, so this is a
67
- * faithful port including its two edge rules: complete lines only, and an
68
- * over-long first line yields empty content with `firstLineExceedsLimit`.
69
- * @param content - the text to truncate.
70
- * @param options - the line and byte limits.
71
- * @returns the truncation report.
72
- */
73
- export declare function truncateHead(content: string, options?: TruncationOptions): TruncationResult;
74
- /**
75
- * Serialize file mutation operations that target the same file; different files still run
76
- * in parallel. A faithful port of pi's queue so the core's cut transaction keeps its
77
- * lock ordering guarantees under the dsh host.
78
- * @param filePath - the file the mutation targets.
79
- * @param run - the critical section to run once the path is free.
80
- * @returns the critical section's result.
9
+ * @module dsh-hashline-edit/host-compat
81
10
  */
82
- export declare function withFileMutationQueue<T>(filePath: string, run: () => Promise<T>): Promise<T>;
11
+ export * from "../src/host-utils.js";
@@ -1,17 +1,18 @@
1
1
  /**
2
2
  * dsh-hashline-edit host plugin entry.
3
3
  *
4
- * On every `agent/created`, install the three hashline tools on the AGENT's
5
- * own scope layer so they shadow the preset's built-in `read`/`edit` for that
4
+ * On every `agent/created`, install the hashline tools on the AGENT's
5
+ * own scope layer so they shadow the preset's built-in `read` for that
6
6
  * agent (nearest layer wins in dsh's tool registry) and unwind automatically
7
- * when the agent is disposed. The built-in `write` stays in place; the
8
- * platform lane's write hook integrates with it.
7
+ * when the agent is disposed. The native `write`, `edit` and
8
+ * `str_replace_editor` schemas are hidden on the same scope so `apply_patch`
9
+ * is the only file-mutation entry point; the write hook still integrates a
10
+ * built-in `write` when a profile chooses to keep it visible.
9
11
  *
10
- * `inject` deliberately lists only `tools` and `systemPrompt`: M1 has no
11
12
  * `inject` deliberately lists only `tools` and `systemPrompt`: the read-path
12
13
  * `ctx.fs` bridge is optional and resolved with `ctx.get`, so a minimal
13
- * composition still loads with the local `node:fs` fallback. Edit/undo stay
14
- * local-only (the M5 bridge covers reads; M6 owns mutation).
14
+ * composition still loads with the local `node:fs` fallback. apply_patch and
15
+ * undo stay local-only (the M5 bridge covers reads; M6 owns mutation).
15
16
  * @module dsh-hashline-edit
16
17
  */
17
18
  import type { Context } from "@deepseek-ai/cordis";
@@ -24,6 +25,17 @@ export declare const name = "dsh-hashline-edit";
24
25
  * services are resolved with `ctx.get` inside `dsh/sandbox.ts` instead.
25
26
  */
26
27
  export declare const inject: string[];
28
+ /** The plugin configuration the loader passes as the second `apply` argument. */
29
+ export interface PluginConfig {
30
+ /**
31
+ * Hide the native `write`/`edit`/`str_replace_editor` tools so every file
32
+ * mutation goes through `apply_patch`. Defaults to true. A whole-file
33
+ * replacement is one `*** Update File:` identity hunk over the served range.
34
+ */
35
+ hideNativeWriteTools?: boolean;
36
+ /** Extra global tool names to hide alongside the native write set. */
37
+ hiddenToolNames?: readonly string[];
38
+ }
27
39
  /**
28
40
  * Install the hashline toolset for one agent inside its own effect, so the
29
41
  * registrations are disposed in reverse order when the agent scope unwinds.
@@ -31,6 +43,6 @@ export declare const inject: string[];
31
43
  * Exported separately from {@link apply} so the host test can drive one fake
32
44
  * agent directly and integration probes can reuse the exact install path.
33
45
  */
34
- export declare function installAgentTools(rootCtx: Context, agent: Agent, metrics?: ToolMetrics): void;
46
+ export declare function installAgentTools(rootCtx: Context, agent: Agent, metrics?: ToolMetrics, config?: PluginConfig): void;
35
47
  /** Mount the plugin: install the hashline toolset on every new agent. */
36
- export declare function apply(rootCtx: Context): void;
48
+ export declare function apply(rootCtx: Context, config?: PluginConfig): void;
@@ -1,4 +1,11 @@
1
- type ToolName = "read" | "edit" | "undo_last_edit" | "write" | "shell_view";
1
+ /**
2
+ * Every tool name this telemetry can carry.
3
+ *
4
+ * The named members are the tools this plugin measures itself; the open member
5
+ * lets the global observer record any other host tool — the built-in `write`,
6
+ * `pwsh`, `grep`, … — without widening this union once per tool.
7
+ */
8
+ type ToolName = "read" | "apply_patch" | "undo_last_edit" | "write" | "shell_view" | (string & {});
2
9
  /** One tool call, as written to the JSONL file. Character counts are exact; the
3
10
  * token estimate belongs to the summarizer so the ratio stays a reporting
4
11
  * choice instead of being frozen into the event stream. */
@@ -1,19 +1,18 @@
1
+ import type { MetricSink, ToolMetric } from "./events.js";
1
2
  /**
2
- * Shared emission helpers for the dsh tool-call telemetry.
3
- *
4
- * The tools and the shell hook call {@link createToolMetrics} at their boundary
5
- * with whatever counts they already hold; every extraction here is cheap
6
- * (string lengths, array lengths, one JSON stringify of the request). The
7
- * sink is optional: when no `DSH_HASHLINE_METRICS` file is configured the
8
- * callers skip the timer and this module is never reached, so the tool path
9
- * pays no allocation.
3
+ * The execution facts one measured call carries.
10
4
  *
11
- * Telemetry is observability only: a missing/disabled/throwing sink must
12
- * never fail or delay the call it measures.
13
- * @module dsh-hashline-edit/metrics/instrument
5
+ * Structural on purpose: a tool boundary hands over a `ToolRunContext` while the
6
+ * global observer sees the `tools/execute` view of the same call, and both expose
7
+ * the caller agent. Only the agent's id is read (the session key the event
8
+ * names), so neither caller has to manufacture a full `ToolExecution`.
14
9
  */
15
- import type { ToolExecution } from "@deepseek-ai/dsh-tools";
16
- import type { MetricSink, ToolMetric } from "./events.js";
10
+ interface MetricExecution {
11
+ /** The agent on whose behalf the call ran. */
12
+ readonly agent?: {
13
+ readonly id?: unknown;
14
+ } | undefined;
15
+ }
17
16
  /** One event's facts, as the tool boundary knows them. */
18
17
  interface MetricRecord {
19
18
  /** The metric tool key; `shell_view` is emitted only when the hook rewrites a result. */
@@ -23,7 +22,7 @@ interface MetricRecord {
23
22
  /** Wall time of the tool call (or of the shell hook's rewrite). */
24
23
  readonly durationMs: number;
25
24
  /** The dsh execution; supplies the session id. */
26
- readonly exec: ToolExecution;
25
+ readonly exec: MetricExecution;
27
26
  /** The accepted tool arguments, used for the exact request-payload character count. */
28
27
  readonly args?: unknown;
29
28
  /** Explicit request character count; when omitted, `JSON.stringify(args).length`. */
@@ -32,6 +31,11 @@ interface MetricRecord {
32
31
  readonly charsOut: number;
33
32
  /** The request path, when the call names one. */
34
33
  readonly path?: string | undefined;
34
+ /**
35
+ * Retry-chain discriminator for a call that names no path: a shell call's
36
+ * command, so re-running the same command is one chain.
37
+ */
38
+ readonly chain?: string | undefined;
35
39
  /** First `E_*`/`W_*` code on a refusal or a warning. */
36
40
  readonly code?: string | undefined;
37
41
  /** Final file line count, when the tool already computed it. */
@@ -45,6 +49,8 @@ interface MetricRecord {
45
49
  export declare function pathOf(args: unknown): string | undefined;
46
50
  /** The first `W_*` code among the supplied warning strings, if any. */
47
51
  export declare function warningCodeOf(values: readonly (string | undefined)[]): string | undefined;
52
+ /** The first `E_*`/`W_*` code carried by a settled result's content blocks. */
53
+ export declare function resultCodeOf(result: unknown): string | undefined;
48
54
  /** The registry code carried by a caught failure, if it carries one. */
49
55
  export declare function errorCodeOf(error: unknown): string | undefined;
50
56
  /** Exact text length of `{ content: [{text}], … }` tool envelopes (edit/undo). */
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Global tool-call telemetry for the dsh host layer.
3
+ *
4
+ * Before this observer the adapter measured only its own surface: `read`,
5
+ * `apply_patch` and `undo_last_edit` from the tool boundaries, plus `shell_view` from
6
+ * the shell hook when it rewrites a result. Every other call the host
7
+ * dispatched — the built-in `write` that sits next to this plugin's shadowed
8
+ * tools, `pwsh`, `grep`, … — left no event at all, so no baseline could say
9
+ * what those calls cost or how often they were retried. This listener closes
10
+ * that hole with the tool's own name as the event's `tool`.
11
+ *
12
+ * It hangs off `tools/execute`, the host's around-dispatch waterfall (the one
13
+ * the host documents for metrics), on the agent's own scope, so a
14
+ * scope-filtered dispatch sends it that agent's calls only. It is installed
15
+ * only when a sink exists: the unmeasured composition pays no listener and no
16
+ * allocation.
17
+ *
18
+ * @module dsh-hashline-edit/metrics/observer
19
+ */
20
+ import type { Context } from "@deepseek-ai/cordis";
21
+ import { type ToolMetrics } from "./instrument.js";
22
+ /**
23
+ * Install the global observer for one agent.
24
+ * @param agentCtx - the agent's scoped context; the listener sees only its calls.
25
+ * @param metrics - the shared recorder; `undefined` installs nothing at all.
26
+ * @returns the exact disposer that unregisters the listener.
27
+ */
28
+ export declare function registerToolObserver(agentCtx: Context, metrics: ToolMetrics | undefined): () => void;
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Mirrors the built-in `@deepseek-ai/dsh-tool-fs` controller (same
5
5
  * vocabulary, same fail-closed approval sequence, shared
6
- * `@deepseek-ai/dsh-sandbox` pieces) so `edit`/`undo_last_edit` ask for wider
6
+ * `@deepseek-ai/dsh-sandbox` pieces) so `apply_patch`/`undo_last_edit` ask for wider
7
7
  * access exactly like the built-in `write`/`edit`. The differences are
8
8
  * deliberate M1 facts:
9
9
  *
@@ -21,7 +21,7 @@ import type { ToolExecution } from "@deepseek-ai/dsh-tools";
21
21
  import type { SandboxMode } from "@deepseek-ai/dsh-sandbox";
22
22
  import type { SandboxArgs, SandboxController, SandboxDecision } from "./contracts.js";
23
23
  /**
24
- * The filesystem escalation controller shared by `edit` and `undo_last_edit`
24
+ * The filesystem escalation controller shared by `apply_patch` and `undo_last_edit`
25
25
  * for one agent install.
26
26
  */
27
27
  export declare class FsSandboxController implements SandboxController {
@@ -7,7 +7,7 @@
7
7
  * foreground stdout channel, or a shell view that refuses (not whitelisted /
8
8
  * ambiguous alignment) all pass the decision through untouched. Only a
9
9
  * successful, uniquely aligned view is rewritten, and its rows are served
10
- * through the read tool's seam so a following `edit` resolves the anchors.
10
+ * through the read tool's seam so a following `apply_patch` hunk resolves the anchors.
11
11
  *
12
12
  * Both foreground value shapes the host may project are understood:
13
13
  * `{ kind: "foreground", stdout: { text, truncated } }` (the canonical
@@ -19,13 +19,20 @@
19
19
  */
20
20
  import type { Context } from "@deepseek-ai/cordis";
21
21
  import type { PostToolDecision, ToolExecution, ToolExecutionResult } from "@deepseek-ai/dsh-tools";
22
- import type { ToolDependencies } from "./contracts.js";
22
+ import type { FsObservationChannel, ToolDependencies } from "./contracts.js";
23
23
  import type { ToolMetrics } from "./metrics/instrument.js";
24
- import { type ShellViewPlan, type ShellViewRequest } from "./shell-view.js";
24
+ import { type GrepViewPlan, type GrepViewRequest, type ShellViewPlan, type ShellViewRequest } from "./shell-view.js";
25
25
  /** Host seams one shell hook needs; a product of `ctx`/`deps` at apply time. */
26
26
  export interface ShellHookDeps {
27
27
  /** Plan anchored rows for a whitelisted command; `undefined` means pass through. */
28
28
  plan(request: ShellViewRequest): Promise<ShellViewPlan | undefined>;
29
+ /** Plan anchored rows for a built-in `grep` result; `undefined` means pass through. */
30
+ planGrep(request: GrepViewRequest): Promise<GrepViewPlan | undefined>;
31
+ /**
32
+ * Optional host observation channel: a granted view is a read of that file, so
33
+ * the host's read-before-write guard must see it.
34
+ */
35
+ readonly observations?: FsObservationChannel;
29
36
  /** Host diagnostic sink; the hook must never throw through this. */
30
37
  warn(message: string): void;
31
38
  /** Optional telemetry recorder; emits `shell_view` only when a result is rewritten. */
@@ -36,18 +43,20 @@ export interface ShellHooks {
36
43
  postExecute(exec: ToolExecution, result: Readonly<ToolExecutionResult>, next: () => Promise<PostToolDecision>): Promise<PostToolDecision>;
37
44
  }
38
45
  /**
39
- * Compose the shell hook around one dependency set. The returned function is
40
- * the test surface; {@link registerShellHook} is a thin `agentCtx.on` binding.
46
+ * Compose the shell and built-in-grep hooks around one dependency set. Both listen on
47
+ * `tools/post-execute` and share the serve/lease seam; this single entry point keeps the
48
+ * registration one listener. The returned function is the test surface;
49
+ * {@link registerShellHook} is a thin `agentCtx.on` binding.
41
50
  */
42
51
  export declare function createShellHooks(deps: ShellHookDeps): ShellHooks;
43
52
  /** The production dependency set: the shell-view planner plus the host diagnostic sink. */
44
53
  export declare function defaultShellHookDeps(deps: ToolDependencies, warn: (message: string) => void): ShellHookDeps;
45
54
  /**
46
- * Register the shell integration for one agent on its own scope layer.
55
+ * Register the shell and built-in-grep integrations for one agent on its own scope layer.
47
56
  *
48
57
  * @param rootCtx - host context, used only for diagnostics.
49
- * @param agentCtx - the agent's scoped context; the listener sees only that agent's shell calls.
50
- * @param deps - the host seams the shell view plans and serves through.
58
+ * @param agentCtx - the agent's scoped context; the listener sees only that agent's calls.
59
+ * @param deps - the host seams the shell view and the grep view plan and serve through.
51
60
  * @returns the exact disposer that unregisters the listener.
52
61
  */
53
62
  export declare function registerShellHook(rootCtx: Context, agentCtx: Context, deps: ToolDependencies): () => void;