@agent_forge/forge-dsh 1.78.0 → 1.79.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -28
- package/lib/index.js +52 -2
- package/lib/runner.js +109 -34
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -25,25 +25,30 @@ the forge binary — the plugin itself has **zero runtime dependencies**.
|
|
|
25
25
|
| `agent/session-start` | `SessionStart`¹ | context via `agent.inject()` |
|
|
26
26
|
| `agent/turn-stopping` | `Stop` | `agent.steer(reason)` → another step |
|
|
27
27
|
|
|
28
|
-
¹ `source:'compact'` additionally fires forge's `PostCompact` group —
|
|
29
|
-
|
|
28
|
+
¹ `source:'compact'` additionally fires forge's `PostCompact` group — the preview
|
|
29
|
+
builds expose no dedicated compaction point.
|
|
30
30
|
|
|
31
31
|
DSH tool names map onto the Claude Code names forge dispatches on
|
|
32
|
-
(`write/edit/str_replace_editor→Write/Edit`, `bash/pwsh→Bash`, `read→Read`,
|
|
32
|
+
(`write/edit/str_replace_editor→Write/Edit`, `bash/pwsh→Bash`, `read/read_image→Read`,
|
|
33
33
|
`skill→Skill`); unmapped tools pass ungated. The wired hook roster mirrors
|
|
34
34
|
forge's canonical spec (`lib/spec.json`, drift-guarded by a Go test in the Forge
|
|
35
35
|
repo) — freeze-guard, task-guard, assertion-check, read-before-edit, bash-guard,
|
|
36
36
|
hazard-guard, auto-compile, workflow-test-guard, file-sentinel, tool-track,
|
|
37
37
|
test-nudge, failure-track (PostToolUseFailure), subagent-track (SubagentStop),
|
|
38
|
-
task-verify, review-stop, skill-trigger,
|
|
38
|
+
task-verify, review-stop, skill-trigger, doc-lint, conventions-write, gate-cmd-form,
|
|
39
|
+
task-drift, resume-reinject, compact-resume, conventions-context, and the
|
|
40
|
+
session-start group.
|
|
39
41
|
failure-track/subagent-track are mirrored in the spec but inert until DSH
|
|
40
|
-
exposes the corresponding event points (
|
|
42
|
+
exposes the corresponding event points (the preview builds have neither).
|
|
41
43
|
|
|
42
44
|
## Requirements
|
|
43
45
|
|
|
44
46
|
- The `forge` CLI on `PATH` (`npm install -g @agent_forge/forge`), with the project
|
|
45
47
|
initialized (`forge init`) for task gates to have state to enforce.
|
|
46
|
-
- DeepSeek Harness `0.1.0-rc.
|
|
48
|
+
- DeepSeek Harness `0.1.0-rc.7` – `0.2.1-rc.2`, Node.js ≥ 18. The bridge contract is
|
|
49
|
+
pinned to `0.1.0-rc.7` ([`contract.json`](./contract.json)); `0.2.1-rc.2` is the
|
|
50
|
+
desktop build this plugin runs on (pre-tool interception verified live there; a
|
|
51
|
+
full five-event regression across the range is still pending).
|
|
47
52
|
- Windows works out of the box: npm lays out `forge` as a `forge.cmd` shim, which
|
|
48
53
|
the plugin spawns through `cmd.exe` (a bare spawn cannot execute it); timeout
|
|
49
54
|
kills tear down the whole child tree via `taskkill /T`.
|
|
@@ -57,45 +62,67 @@ dsh plugin --profile web add @agent_forge/forge-dsh
|
|
|
57
62
|
# or straight from the Forge repo (subdirectory install)
|
|
58
63
|
dsh plugin --profile web add "github:MjxUpUp/Forge#main&path:/plugins/forge-dsh"
|
|
59
64
|
|
|
60
|
-
# local development
|
|
61
|
-
dsh plugin --profile web add "link
|
|
65
|
+
# local development (run from this directory; `link:.` works on Windows too)
|
|
66
|
+
dsh plugin --profile web add "link:."
|
|
62
67
|
```
|
|
63
68
|
|
|
64
|
-
|
|
69
|
+
The plugin hot-loads into a running dsh — adding or removing the bundle reloads the
|
|
70
|
+
profile. If a given build does not pick it up, restart the host once. Start a new
|
|
71
|
+
session to pick up the SessionStart context, and check the bridge's state with
|
|
72
|
+
`/forge-status` (probe result, wired groups, recent verdicts). To uninstall:
|
|
73
|
+
`dsh plugin --profile <name> remove @agent_forge/forge-dsh` (the desktop app's profile
|
|
74
|
+
is `desktop`), or keep it installed but inert with `enabled:false`.
|
|
65
75
|
|
|
66
76
|
## Config
|
|
67
77
|
|
|
78
|
+
Keys go on the bundle entry in the profile's `cordis.patch.yml` — the user-editable
|
|
79
|
+
patch layer. Do not edit the generated `cordis.yml`: the loader rewrites that root on
|
|
80
|
+
every boot, so anything written there is lost (the desktop app manages its own profile
|
|
81
|
+
under `~/.dsh/profiles/<profile>/`):
|
|
68
82
|
```yaml
|
|
69
|
-
# profile
|
|
70
|
-
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
debug: false # log fail-open hook errors to console.error
|
|
83
|
+
# ~/.dsh/profiles/<profile>/cordis.patch.yml
|
|
84
|
+
- id: forge-quality
|
|
85
|
+
name: "@agent_forge/forge-dsh"
|
|
86
|
+
config:
|
|
87
|
+
forgeBin: forge # binary name/path; FORGE_BIN only fills in when this key is absent
|
|
88
|
+
timeoutMs: 30000 # per-hook ceiling; timeout kills and FAILS OPEN
|
|
89
|
+
enabled: true # false unwires every listener
|
|
90
|
+
debug: false # log fail-open hook errors to console.error
|
|
78
91
|
```
|
|
79
92
|
|
|
80
93
|
## Fail-open contract
|
|
81
94
|
|
|
82
|
-
Blocks are read from forge's stdout JSON (`decision:"block"`), never from
|
|
83
|
-
codes
|
|
84
|
-
Every infrastructure failure (forge
|
|
85
|
-
timeout) **fails open**: a forge outage
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
95
|
+
Blocks are read from forge's stdout JSON (`decision:"block"`), never inferred from
|
|
96
|
+
exit codes: the verdict reason and its `additionalContext` exist only in that JSON,
|
|
97
|
+
and forge reuses exit 2 for hard errors as well. Every infrastructure failure (forge
|
|
98
|
+
missing, spawn error, unparseable output, timeout) **fails open**: a forge outage
|
|
99
|
+
never locks the agent out of its own tools. Such hook-run failures are silent by
|
|
100
|
+
design; surface them with `/forge-status` or `debug:true`.
|
|
101
|
+
|
|
102
|
+
A missing `forge` binary is the one exception to that silence: each session start
|
|
103
|
+
probes `forge --version` once (until a probe succeeds — the answer is then cached
|
|
104
|
+
for the process lifetime) and, when the probe fails, injects a visible notice
|
|
105
|
+
naming the configured `forgeBin`. Without it an uninstalled CLI would look exactly
|
|
106
|
+
like a healthy plugin gating nothing. The notice is informational, not a switch —
|
|
107
|
+
while the binary is missing, every hook attempt fails open on its own, so gates are
|
|
108
|
+
inactive whether or not the probe ran. Installing forge needs no host restart: the
|
|
109
|
+
next session probes again and gating resumes.
|
|
110
|
+
|
|
111
|
+
Two latency trade-offs to know: hooks run **serially** per event (up to six per
|
|
89
112
|
group), so a slow hook chain delays the tool call — and a gate that legitimately
|
|
90
113
|
exceeds `timeoutMs` (e.g. `auto-compile` cold-building a large project past 30s)
|
|
91
114
|
silently loses that run's feedback to fail-open (visible in `/forge-status`).
|
|
92
115
|
Raise `timeoutMs` on big projects.
|
|
93
116
|
|
|
117
|
+
`/forge-status` reports the probe line after the header: `probe: ok (forge version …)`,
|
|
118
|
+
`probe: FAILED — gates inactive (…)`, or `not run yet` before the process's first
|
|
119
|
+
session start.
|
|
120
|
+
|
|
94
121
|
## Known watch items (dsh preview)
|
|
95
122
|
|
|
96
|
-
- **No stop-hook loop guard** in rc.7
|
|
97
|
-
every stop boundary. forge's Stop gates
|
|
98
|
-
but a stuck gate loops the turn.
|
|
123
|
+
- **No stop-hook loop guard** in the preview builds (`0.1.0-rc.7` – `0.2.1-rc.2`) —
|
|
124
|
+
a permanently-failing Stop gate steers at every stop boundary. forge's Stop gates
|
|
125
|
+
are self-limiting once their gate passes, but a stuck gate loops the turn.
|
|
99
126
|
- **SessionStart context is best-effort** (emit mode, detached): it may miss the
|
|
100
127
|
first request of a very short-lived session — same limitation the official
|
|
101
128
|
bridges document (`TODO(session-start-gating)`).
|
package/lib/index.js
CHANGED
|
@@ -28,8 +28,9 @@
|
|
|
28
28
|
* @module @agent_forge/forge-dsh
|
|
29
29
|
*/
|
|
30
30
|
import { readFileSync } from "node:fs";
|
|
31
|
+
import { probeForge } from "./runner.js";
|
|
31
32
|
import { runHookGroup, preExecuteDecision, postExecuteDecision, preStepDecision, turnStoppingOutcome, sessionStartOutcome } from "./decisions.js";
|
|
32
|
-
import { buildEventPayload, buildToolPayload, matchedCommands, promptText, toCCToolName } from "./tools.js";
|
|
33
|
+
import { buildEventPayload, buildToolPayload, matchedCommands, pluginMessage, promptText, toCCToolName } from "./tools.js";
|
|
33
34
|
|
|
34
35
|
const spec = JSON.parse(readFileSync(new URL("./spec.json", import.meta.url), "utf8"));
|
|
35
36
|
|
|
@@ -66,6 +67,49 @@ function apply(ctx, config = {}) {
|
|
|
66
67
|
}
|
|
67
68
|
};
|
|
68
69
|
|
|
70
|
+
// A missing forge binary used to be completely silent: every hook failed
|
|
71
|
+
// open, so the plugin looked installed but gated nothing. Probe once per
|
|
72
|
+
// session start (retrying on later sessions, since installing forge needs no
|
|
73
|
+
// host restart) and surface the failure through agent.inject — the same
|
|
74
|
+
// channel forge's own advisories use. Once a probe succeeds the answer is
|
|
75
|
+
// cached for the process lifetime; forgeBin is static config per plugin
|
|
76
|
+
// instance, so it cannot go stale.
|
|
77
|
+
let forgeReady = false;
|
|
78
|
+
let lastProbe;
|
|
79
|
+
let probeInFlight;
|
|
80
|
+
const noticedAgents = new WeakSet();
|
|
81
|
+
const missingForgeNotice = (error) =>
|
|
82
|
+
`[forge-dsh] forge CLI not found (forgeBin: ${opts.forgeBin}). ` +
|
|
83
|
+
"Forge gates are OFF for this session — every action runs unguarded (fail-open). " +
|
|
84
|
+
"Install it with `npm install -g @agent_forge/forge`, then start a new session; " +
|
|
85
|
+
"if it lives elsewhere, set forgeBin in the profile patch." +
|
|
86
|
+
(error === undefined ? "" : `\nProbe error: ${error}`);
|
|
87
|
+
const ensureForge = async (agent) => {
|
|
88
|
+
if (forgeReady) return true;
|
|
89
|
+
if (probeInFlight === undefined) {
|
|
90
|
+
probeInFlight = probeForge({ forgeBin: opts.forgeBin, timeoutMs: Math.min(opts.timeoutMs, 5000) })
|
|
91
|
+
.finally(() => {
|
|
92
|
+
probeInFlight = undefined;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
const probe = await probeInFlight;
|
|
96
|
+
lastProbe = probe;
|
|
97
|
+
if (probe.ok) {
|
|
98
|
+
forgeReady = true;
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
if (debug) console.error(`[forge-dsh] forge probe failed: ${probe.error}`);
|
|
102
|
+
if (agent != null && !noticedAgents.has(agent)) {
|
|
103
|
+
noticedAgents.add(agent);
|
|
104
|
+
try {
|
|
105
|
+
await agent.inject?.(pluginMessage(missingForgeNotice(probe.error)));
|
|
106
|
+
} catch {
|
|
107
|
+
// a throwing (or rejecting) inject must never interrupt session start
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
};
|
|
112
|
+
|
|
69
113
|
// PreToolUse — deny short-circuits the tool call.
|
|
70
114
|
ctx.effect(() => ctx.on("tools/pre-execute", async (exec, next) => {
|
|
71
115
|
const commands = matchedCommands(spec.PreToolUse, toCCToolName(exec?.name ?? ""));
|
|
@@ -102,6 +146,7 @@ function apply(ctx, config = {}) {
|
|
|
102
146
|
const agent = payload?.agent;
|
|
103
147
|
const source = payload?.source;
|
|
104
148
|
(async () => {
|
|
149
|
+
await ensureForge(agent);
|
|
105
150
|
const start = await runHookGroup(
|
|
106
151
|
matchedCommands(spec.SessionStart, ""),
|
|
107
152
|
buildEventPayload("SessionStart", agent, { source }),
|
|
@@ -170,9 +215,14 @@ function apply(ctx, config = {}) {
|
|
|
170
215
|
if (r.errors.length > 0) parts.push(`FAIL-OPEN: ${r.errors.join("; ")}`);
|
|
171
216
|
return parts.join(" ");
|
|
172
217
|
}).join("\n");
|
|
218
|
+
const probeLine = lastProbe === undefined
|
|
219
|
+
? "not run yet (no session start in this process)"
|
|
220
|
+
: lastProbe.ok
|
|
221
|
+
? `ok${lastProbe.version === undefined ? "" : ` (${lastProbe.version.slice(0, 80)})`}`
|
|
222
|
+
: `FAILED — gates inactive (${lastProbe.error})`;
|
|
173
223
|
return {
|
|
174
224
|
kind: "success",
|
|
175
|
-
text: `# Forge quality gates\n\nforgeBin: ${opts.forgeBin} timeoutMs: ${opts.timeoutMs}\n\n## Wired groups\n${groups}\n\n## Verdict summary (last ${recentRuns.length} runs)\n runs=${summary.runs} blocked=${summary.blocked} fail-open=${summary.failOpen} contexts=${summary.contexts}\n\n## Recent runs (last 10)\n${runs}`,
|
|
225
|
+
text: `# Forge quality gates\n\nforgeBin: ${opts.forgeBin} timeoutMs: ${opts.timeoutMs}\nprobe: ${probeLine}\n\n## Wired groups\n${groups}\n\n## Verdict summary (last ${recentRuns.length} runs)\n runs=${summary.runs} blocked=${summary.blocked} fail-open=${summary.failOpen} contexts=${summary.contexts}\n\n## Recent runs (last 10)\n${runs}`,
|
|
176
226
|
};
|
|
177
227
|
},
|
|
178
228
|
}), "forge: status command");
|
package/lib/runner.js
CHANGED
|
@@ -108,40 +108,7 @@ export function runForgeHook(command, payload, opts = {}) {
|
|
|
108
108
|
const timer = setTimeout(() => {
|
|
109
109
|
if (settled) return;
|
|
110
110
|
settled = true;
|
|
111
|
-
|
|
112
|
-
// grandchild holds the task index lock AND the inherited stdio pipes:
|
|
113
|
-
// killing only the shell ORPHANS the grandchild (a dead parent's tree
|
|
114
|
-
// is unfindable for /T) — it survives with the pipes open, pinning this
|
|
115
|
-
// process's event loop forever. taskkill /T /F is therefore the primary
|
|
116
|
-
// kill: it tears down shell + descendants atomically. kill() (wrapper
|
|
117
|
-
// only) is the FALLBACK for when taskkill itself fails — spawn error
|
|
118
|
-
// (not on PATH) or nonzero exit (access denied); killing just the
|
|
119
|
-
// wrapper then still beats killing nothing. Non-shell routes kill the
|
|
120
|
-
// forge process directly.
|
|
121
|
-
if (plan.shell && child.pid) {
|
|
122
|
-
const fallback = () => {
|
|
123
|
-
try {
|
|
124
|
-
child.kill();
|
|
125
|
-
} catch {
|
|
126
|
-
// already exited — nothing to kill
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
try {
|
|
130
|
-
const killer = spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });
|
|
131
|
-
killer.on("error", fallback);
|
|
132
|
-
killer.on("close", (c) => {
|
|
133
|
-
if (c !== 0) fallback();
|
|
134
|
-
});
|
|
135
|
-
} catch {
|
|
136
|
-
fallback();
|
|
137
|
-
}
|
|
138
|
-
} else {
|
|
139
|
-
try {
|
|
140
|
-
child.kill();
|
|
141
|
-
} catch {
|
|
142
|
-
// already exited — nothing to kill
|
|
143
|
-
}
|
|
144
|
-
}
|
|
111
|
+
killTree(child, plan.shell);
|
|
145
112
|
resolve({ block: false, error: `timeout after ${timeoutMs}ms` });
|
|
146
113
|
}, timeoutMs);
|
|
147
114
|
const settle = (verdict) => {
|
|
@@ -199,6 +166,114 @@ export function runForgeHook(command, payload, opts = {}) {
|
|
|
199
166
|
});
|
|
200
167
|
}
|
|
201
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Kill a spawned forge and, on the win32 shell route, its whole process tree.
|
|
171
|
+
*
|
|
172
|
+
* On the win32 shell route the direct child is cmd.exe and the forge
|
|
173
|
+
* grandchild holds the task index lock AND the inherited stdio pipes: killing
|
|
174
|
+
* only the shell ORPHANS the grandchild (a dead parent's tree is unfindable
|
|
175
|
+
* for /T) — it survives with the pipes open, pinning this process's event
|
|
176
|
+
* loop forever. taskkill /T /F is therefore the primary kill: it tears down
|
|
177
|
+
* shell + descendants atomically. kill() (wrapper only) is the FALLBACK for
|
|
178
|
+
* when taskkill itself fails — spawn error (not on PATH) or nonzero exit
|
|
179
|
+
* (access denied); killing just the wrapper then still beats killing nothing.
|
|
180
|
+
* Non-shell routes kill the forge process directly.
|
|
181
|
+
*
|
|
182
|
+
* @param {import("node:child_process").ChildProcess} child
|
|
183
|
+
* @param {boolean} shell - planSpawn's shell flag for this child.
|
|
184
|
+
*/
|
|
185
|
+
function killTree(child, shell) {
|
|
186
|
+
if (shell && child.pid) {
|
|
187
|
+
const fallback = () => {
|
|
188
|
+
try {
|
|
189
|
+
child.kill();
|
|
190
|
+
} catch {
|
|
191
|
+
// already exited — nothing to kill
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
try {
|
|
195
|
+
const killer = spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });
|
|
196
|
+
killer.on("error", fallback);
|
|
197
|
+
killer.on("close", (code) => {
|
|
198
|
+
if (code !== 0) fallback();
|
|
199
|
+
});
|
|
200
|
+
} catch {
|
|
201
|
+
fallback();
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
try {
|
|
205
|
+
child.kill();
|
|
206
|
+
} catch {
|
|
207
|
+
// already exited — nothing to kill
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Probe whether the configured forge binary is runnable: one `forge --version`
|
|
214
|
+
* spawn, stdout captured, bounded by `timeoutMs` (default 5000 — a probe must
|
|
215
|
+
* never hold up session start for a hook-sized 30s). Fail-open like the hook
|
|
216
|
+
* path: resolves `{ok:false, error}` instead of throwing, so a missing or
|
|
217
|
+
* broken binary can be reported to the user rather than silently voiding every
|
|
218
|
+
* gate.
|
|
219
|
+
*
|
|
220
|
+
* The `--version` subcommand does not read stdin (verified against forge
|
|
221
|
+
* 1.79.0), so stdin is ignored; a hung probe is killed with the same
|
|
222
|
+
* tree-safe kill as a hook.
|
|
223
|
+
*
|
|
224
|
+
* @param {object} [opts]
|
|
225
|
+
* @param {string} [opts.forgeBin="forge"]
|
|
226
|
+
* @param {number} [opts.timeoutMs=5000]
|
|
227
|
+
* @returns {Promise<{ok: boolean, version?: string, error?: string}>}
|
|
228
|
+
*/
|
|
229
|
+
export function probeForge(opts = {}) {
|
|
230
|
+
const forgeBin = opts.forgeBin ?? "forge";
|
|
231
|
+
const timeoutMs = opts.timeoutMs ?? 5000;
|
|
232
|
+
const plan = planSpawn(forgeBin, ["--version"]);
|
|
233
|
+
return new Promise((resolve) => {
|
|
234
|
+
let child;
|
|
235
|
+
try {
|
|
236
|
+
const args = plan.shell ? [[plan.file, ...plan.args].join(" ")] : [plan.file, plan.args];
|
|
237
|
+
child = spawn(...args, {
|
|
238
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
239
|
+
...(plan.shell ? { shell: true } : {}),
|
|
240
|
+
});
|
|
241
|
+
} catch (error) {
|
|
242
|
+
resolve({ ok: false, error: note(error) });
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
let out = "";
|
|
246
|
+
let err = "";
|
|
247
|
+
let settled = false;
|
|
248
|
+
let timer;
|
|
249
|
+
const finish = (verdict) => {
|
|
250
|
+
if (settled) return;
|
|
251
|
+
settled = true;
|
|
252
|
+
clearTimeout(timer);
|
|
253
|
+
resolve(verdict);
|
|
254
|
+
};
|
|
255
|
+
timer = setTimeout(() => {
|
|
256
|
+
killTree(child, plan.shell);
|
|
257
|
+
finish({ ok: false, error: `timeout after ${timeoutMs}ms` });
|
|
258
|
+
}, timeoutMs);
|
|
259
|
+
child.stdout.on("data", (d) => (out += d.toString()));
|
|
260
|
+
child.stderr.on("data", (d) => (err += d.toString()));
|
|
261
|
+
child.on("error", (error) => finish({ ok: false, error: note(error) }));
|
|
262
|
+
child.on("close", (code) => {
|
|
263
|
+
if (code === 0) {
|
|
264
|
+
const version = out.trim();
|
|
265
|
+
finish(version === "" ? { ok: true } : { ok: true, version });
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
// A missing binary on the win32 cmd.exe route exits nonzero with its
|
|
269
|
+
// message on stderr (cmd.exe itself spawned fine, so no 'error' event);
|
|
270
|
+
// keep that tail — it is the only diagnostic the user sees.
|
|
271
|
+
const tail = err.trim().slice(0, 200);
|
|
272
|
+
finish({ ok: false, error: `forge --version exited with code ${code}${tail === "" ? "" : `: ${tail}`}` });
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
202
277
|
function note(error) {
|
|
203
278
|
return (error instanceof Error ? error.message : String(error)).slice(0, 300);
|
|
204
279
|
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent_forge/forge-dsh",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.79.1",
|
|
4
4
|
"description": "Forge quality gates for DeepSeek Harness (dsh): task gates, read-before-edit, bash hazard interception and quality scoring, driven by the forge CLI through DSH's typed interception points.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/MjxUpUp/Forge",
|
|
8
8
|
"directory": "plugins/forge-dsh"
|
|
9
9
|
},
|
|
10
|
+
"homepage": "https://github.com/MjxUpUp/Forge/tree/main/plugins/forge-dsh",
|
|
10
11
|
"type": "module",
|
|
11
12
|
"main": "lib/index.js",
|
|
12
13
|
"files": [
|