@agent_forge/forge-dsh 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 MjxUpUp
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # @agent_forge/forge-dsh
2
+
3
+ Forge quality gates for **DeepSeek Harness (dsh)** — task gates, read-before-edit,
4
+ bash hazard interception, and quality scoring, enforced inside DSH sessions through
5
+ the harness's typed interception points.
6
+
7
+ [Forge](https://github.com/MjxUpUp/Forge) is an AI-code quality gate engine. This
8
+ plugin is a thin Cordis wrapper: it forwards DSH tool/session events to the `forge`
9
+ CLI and translates forge's verdicts back into DSH decisions. All gate logic lives in
10
+ the forge binary — the plugin itself has **zero runtime dependencies**.
11
+
12
+ ## How it works
13
+
14
+ | DSH interception point | forge hook event | A forge block becomes |
15
+ |---|---|---|
16
+ | `tools/pre-execute` | `PreToolUse` | `{kind:'deny', reason}` |
17
+ | `tools/post-execute` | `PostToolUse` | `{kind:'block', feedback}` (error result) |
18
+ | `agent/pre-step` | `UserPromptSubmit` | `{kind:'reject'}` |
19
+ | `agent/session-start` | `SessionStart`¹ | context via `agent.inject()` |
20
+ | `agent/turn-stopping` | `Stop` | `agent.steer(reason)` → another step |
21
+
22
+ ¹ `source:'compact'` additionally fires forge's `PostCompact` group — DSH rc.7
23
+ exposes no dedicated compaction point.
24
+
25
+ DSH tool names map onto the Claude Code names forge dispatches on
26
+ (`write/edit/str_replace_editor→Write/Edit`, `bash/pwsh→Bash`, `read→Read`,
27
+ `skill→Skill`); unmapped tools pass ungated. The wired hook roster mirrors
28
+ forge's canonical spec (`lib/spec.json`, drift-guarded by a Go test in the Forge
29
+ repo) — freeze-guard, task-guard, assertion-check, read-before-edit, bash-guard,
30
+ hazard-guard, auto-compile, workflow-test-guard, file-sentinel, tool-track,
31
+ task-verify, review-stop, skill-trigger, and the session-start group.
32
+
33
+ ## Requirements
34
+
35
+ - The `forge` CLI on `PATH` (`npm install -g @agent_forge/forge`), with the project
36
+ initialized (`forge init`) for task gates to have state to enforce.
37
+ - DeepSeek Harness `0.1.0-rc.x` (verified against `0.1.0-rc.7`), Node.js ≥ 18.
38
+
39
+ ## Install
40
+
41
+ ```sh
42
+ # from npm
43
+ dsh plugin --profile web add @agent_forge/forge-dsh
44
+
45
+ # or straight from the Forge repo (subdirectory install)
46
+ dsh plugin --profile web add "github:MjxUpUp/Forge#main&path:/plugins/forge-dsh"
47
+
48
+ # local development
49
+ dsh plugin --profile web add "link:$(pwd)"
50
+ ```
51
+
52
+ Restart `dsh web` after install. Check status inside a session with `/forge-status`.
53
+
54
+ ## Config
55
+
56
+ ```yaml
57
+ # profile patch
58
+ - insert:
59
+ - id: forge-quality
60
+ name: "@agent_forge/forge-dsh"
61
+ config:
62
+ forgeBin: forge # binary name/path (env FORGE_BIN overrides the default)
63
+ timeoutMs: 30000 # per-hook ceiling; timeout kills and FAILS OPEN
64
+ enabled: true # false unwires every listener
65
+ debug: false # log fail-open hook errors to console.error
66
+ ```
67
+
68
+ ## Fail-open contract
69
+
70
+ Blocks are read from forge's stdout JSON (`decision:"block"`), never from exit
71
+ codes — a forge internal error (exit 1) is indistinguishable from a deny otherwise.
72
+ Every infrastructure failure (forge missing, spawn error, unparseable output,
73
+ timeout) **fails open**: a forge outage never locks the agent out of its own tools.
74
+ Such failures are silent by design; surface them with `/forge-status` or `debug:true`.
75
+
76
+ Two latency trade-offs to know: hooks run **serially** per event (up to five per
77
+ group), so a slow hook chain delays the tool call — and a gate that legitimately
78
+ exceeds `timeoutMs` (e.g. `auto-compile` cold-building a large project past 30s)
79
+ silently loses that run's feedback to fail-open (visible in `/forge-status`).
80
+ Raise `timeoutMs` on big projects.
81
+
82
+ ## Known watch items (dsh preview)
83
+
84
+ - **No stop-hook loop guard** in rc.7 — a permanently-failing Stop gate steers at
85
+ every stop boundary. forge's Stop gates are self-limiting once their gate passes,
86
+ but a stuck gate loops the turn.
87
+ - **SessionStart context is best-effort** (emit mode, detached): it may miss the
88
+ first request of a very short-lived session — same limitation the official
89
+ bridges document (`TODO(session-start-gating)`).
90
+ - DSH is a developer preview; pin this plugin's version alongside your dsh version.
91
+
92
+ ## Development
93
+
94
+ ```sh
95
+ npm install # dev-only: @deepseek-ai/cordis for the wiring tests
96
+ npm test # 33 tests: mapping, runner, decision folding, real-cordis wiring
97
+ ```
98
+
99
+ `test/doubles/fake-forge.mjs` stands in for the forge binary; the wiring suite
100
+ boots a real cordis runtime and asserts dispatch order, short-circuit semantics,
101
+ payload shape, and every decision mapping.
@@ -0,0 +1,3 @@
1
+ - insert:
2
+ - id: forge-quality
3
+ name: "@agent_forge/forge-dsh"
@@ -0,0 +1,185 @@
1
+ /**
2
+ * @agent_forge/forge-dsh — decision folding (pure logic, no I/O).
3
+ *
4
+ * One forge hook group (a spec event's matched commands) runs serially and
5
+ * collapses to `{ blocked?, contexts, errors }`:
6
+ * - the first block wins and short-circuits the rest of the group (the
7
+ * spec's own ordering contract — freeze-guard runs first precisely so its
8
+ * deny pre-empts task-guard's);
9
+ * - allow-path additionalContext strings accumulate in order;
10
+ * - infrastructure failures (fail-open verdicts) are collected for
11
+ * observability but never influence the outcome.
12
+ *
13
+ * The collapse then maps onto DSH's typed decisions per interception point
14
+ * (see index.js): deny for tools/pre-execute, block+feedback for
15
+ * tools/post-execute, reject for agent/pre-step, steering for
16
+ * agent/turn-stopping.
17
+ *
18
+ * @module decisions
19
+ */
20
+
21
+ import { runForgeHook } from "./runner.js";
22
+ import { pluginMessage } from "./tools.js";
23
+
24
+ /**
25
+ * @typedef {object} GroupOutcome
26
+ * @property {{reason: string, command: string}|undefined} blocked
27
+ * @property {string[]} contexts - allow-path additionalContext, in order.
28
+ * @property {string[]} errors - fail-open infrastructure notes.
29
+ */
30
+
31
+ /**
32
+ * Run one matched hook group serially against one payload.
33
+ *
34
+ * @param {string[]} commands - `forge hook <name>` commands, spec order.
35
+ * @param {object} payload - Claude-Code-shape stdin payload (shared by the
36
+ * whole group, as in Claude Code).
37
+ * @param {object} [opts] - forwarded to runForgeHook (forgeBin/timeoutMs/cwd).
38
+ * @returns {Promise<GroupOutcome>}
39
+ */
40
+ export async function runHookGroup(commands, payload, opts = {}) {
41
+ const run = opts.runner ?? runForgeHook; // DI seam for tests
42
+ const outcome = { blocked: undefined, contexts: [], errors: [] };
43
+ for (const command of commands) {
44
+ const verdict = await run(command, payload, opts);
45
+ if (verdict.error !== undefined) outcome.errors.push(`${command}: ${verdict.error}`);
46
+ if (verdict.block) {
47
+ outcome.blocked = { reason: verdict.reason ?? "denied", command };
48
+ break;
49
+ }
50
+ if (verdict.context !== undefined) outcome.contexts.push(verdict.context);
51
+ }
52
+ return outcome;
53
+ }
54
+
55
+ /**
56
+ * tools/pre-execute: forge block → deny; otherwise delegate. Allow-path
57
+ * context has no channel on PreToolDecision (allow carries nothing), so it is
58
+ * queued into the agent's next pre-step via agent.inject — the seam dsh-agent
59
+ * documents for exactly this ("Queue model-facing context for the next
60
+ * pre-step without waking the driver").
61
+ *
62
+ * @param {GroupOutcome} outcome
63
+ * @param {object|undefined} agent
64
+ * @param {() => Promise<object>} next
65
+ * @returns {Promise<object>} PreToolDecision
66
+ */
67
+ export async function preExecuteDecision(outcome, agent, next) {
68
+ if (outcome.blocked !== undefined) {
69
+ return { kind: "deny", reason: outcome.blocked.reason };
70
+ }
71
+ injectContexts(agent, outcome.contexts);
72
+ return next();
73
+ }
74
+
75
+ /**
76
+ * tools/post-execute: forge block → block with the reason as corrective
77
+ * feedback (the post-execute "block turns corrective feedback into an error
78
+ * result" channel). The block variant of PostToolDecision carries NO
79
+ * additionalContexts (that field belongs to accept), so contexts gathered
80
+ * before the block are queued via agent.inject instead of being attached to
81
+ * a decision shape the runtime would drop (or worse, reject on a stricter
82
+ * schema check). Allow-path context folds into the downstream decision AFTER
83
+ * delegating (delegate-then-prepend: returning an enter-style decision
84
+ * without next() would short-circuit every later listener).
85
+ *
86
+ * @param {GroupOutcome} outcome
87
+ * @param {() => Promise<object>} next
88
+ * @param {object|undefined} [agent] - for the block-path context fallback.
89
+ * @returns {Promise<object>} PostToolDecision
90
+ */
91
+ export async function postExecuteDecision(outcome, next, agent) {
92
+ if (outcome.blocked !== undefined) {
93
+ injectContexts(agent, outcome.contexts);
94
+ return {
95
+ kind: "block",
96
+ feedback: [{ type: "text", text: outcome.blocked.reason }],
97
+ };
98
+ }
99
+ const decision = await next();
100
+ if (outcome.contexts.length === 0) return decision;
101
+ return {
102
+ ...decision,
103
+ additionalContexts: [
104
+ ...outcome.contexts.map(pluginMessage),
105
+ ...(decision?.additionalContexts ?? []),
106
+ ],
107
+ };
108
+ }
109
+
110
+ /**
111
+ * agent/pre-step (UserPromptSubmit): forge block → reject the step. PreStepDecision's
112
+ * reject variant carries no reason, so the gate's corrective feedback (and any
113
+ * same-group contexts) is queued via agent.inject first — on Claude Code the
114
+ * UserPromptSubmit block reason reaches the user, and it must not silently
115
+ * vanish here. Allow-path context prepends plugin messages into the
116
+ * downstream enter decision (reject passes through untouched).
117
+ *
118
+ * @param {GroupOutcome} outcome
119
+ * @param {() => Promise<object>} next
120
+ * @param {object|undefined} [agent] - for the reject-path reason/context fallback.
121
+ * @returns {Promise<object>} PreStepDecision
122
+ */
123
+ export async function preStepDecision(outcome, next, agent) {
124
+ if (outcome.blocked !== undefined) {
125
+ injectContexts(agent, [outcome.blocked.reason, ...outcome.contexts]);
126
+ return { kind: "reject" };
127
+ }
128
+ const decision = await next();
129
+ if (outcome.contexts.length === 0 || decision?.kind !== "enter") return decision;
130
+ return {
131
+ kind: "enter",
132
+ messages: [...outcome.contexts.map(pluginMessage), ...decision.messages],
133
+ };
134
+ }
135
+
136
+ /**
137
+ * agent/turn-stopping (Stop): forge block steers the agent into another step
138
+ * (dsh-agent: "a listener that objects steers and the machine re-reads its
139
+ * inbox: fresh steering runs another step, none closes the turn"); allow-path
140
+ * context is queued via inject.
141
+ *
142
+ * NOTE: DSH rc.7 has no stop-hook loop guard (the official bridges always
143
+ * report stop_hook_active:false for the same reason). forge's Stop hooks are
144
+ * gate-driven and self-limiting (a passing gate stops blocking), but a
145
+ * permanently-failing gate will steer every stop boundary — surfaced in the
146
+ * README as a known watch item.
147
+ *
148
+ * @param {GroupOutcome} outcome
149
+ * @param {object} agent
150
+ * @returns {Promise<void>}
151
+ */
152
+ export async function turnStoppingOutcome(outcome, agent) {
153
+ if (outcome.blocked !== undefined) {
154
+ agent?.steer?.(pluginMessage(outcome.blocked.reason));
155
+ return;
156
+ }
157
+ injectContexts(agent, outcome.contexts);
158
+ }
159
+
160
+ /**
161
+ * agent/session-start (emit): every collected context is queued via inject.
162
+ * Best-effort by design — emit listeners are detached, so context may miss
163
+ * the first request of a short-lived session (the official bridges document
164
+ * the same TODO(session-start-gating) limitation).
165
+ *
166
+ * @param {GroupOutcome} outcome
167
+ * @param {object} agent
168
+ */
169
+ export function sessionStartOutcome(outcome, agent) {
170
+ injectContexts(agent, outcome.contexts);
171
+ }
172
+
173
+ /** Queue each context string as one plugin-sourced message. */
174
+ function injectContexts(agent, contexts) {
175
+ if (contexts.length === 0) return;
176
+ const inject = agent?.inject;
177
+ if (typeof inject !== "function") return;
178
+ for (const text of contexts) {
179
+ try {
180
+ inject.call(agent, pluginMessage(text));
181
+ } catch {
182
+ // a throwing inject must never interrupt the session (bridge isolation rule)
183
+ }
184
+ }
185
+ }
package/lib/index.js ADDED
@@ -0,0 +1,167 @@
1
+ /**
2
+ * @agent_forge/forge-dsh — Forge quality gates for DeepSeek Harness.
3
+ *
4
+ * Wires forge's Claude-Code hook roster (mirrored in lib/spec.json from
5
+ * internal/hooks/settings.go ForgeHookSpec — drift-guarded by a Go test in
6
+ * the Forge repo) onto DSH's typed interception points:
7
+ *
8
+ * tools/pre-execute → PreToolUse (block → {kind:'deny'})
9
+ * tools/post-execute → PostToolUse (block → {kind:'block', feedback})
10
+ * agent/pre-step → UserPromptSubmit (block → {kind:'reject'})
11
+ * agent/session-start → SessionStart (emit; source 'compact' also fires
12
+ * the PostCompact group — DSH rc.7 exposes no
13
+ * separate compaction point)
14
+ * agent/turn-stopping → Stop (block → agent.steer(reason))
15
+ *
16
+ * Each hook runs as `forge hook <name>` with a Claude-Code-shape stdin
17
+ * payload and speaks forge's claude stdout dialect back (block read from the
18
+ * JSON decision field; every infrastructure failure fails open — a forge
19
+ * outage never locks the agent out of its tools). Verified against
20
+ * @deepseek-ai/dsh 0.1.0-rc.7 type surface.
21
+ *
22
+ * Config (profile patch):
23
+ * forgeBin - forge binary name/path (default "forge", env FORGE_BIN wins over default)
24
+ * timeoutMs - per-hook ceiling, kill + fail open (default 30000)
25
+ * enabled - false disables every listener (default true)
26
+ * debug - log hook failures to console.error (default false)
27
+ *
28
+ * @module @agent_forge/forge-dsh
29
+ */
30
+ import { readFileSync } from "node:fs";
31
+ import { runHookGroup, preExecuteDecision, postExecuteDecision, preStepDecision, turnStoppingOutcome, sessionStartOutcome } from "./decisions.js";
32
+ import { buildEventPayload, buildToolPayload, matchedCommands, promptText, toCCToolName } from "./tools.js";
33
+
34
+ const spec = JSON.parse(readFileSync(new URL("./spec.json", import.meta.url), "utf8"));
35
+
36
+ /** Stable Cordis plugin name. */
37
+ const name = "forge-quality";
38
+ /** Hard dependency: the tool registry (pre/post-execute waterfalls). */
39
+ const inject = ["tools"];
40
+
41
+ /**
42
+ * @param {object} ctx - Cordis plugin context.
43
+ * @param {object} [config] - { forgeBin?, timeoutMs?, enabled?, debug? }.
44
+ */
45
+ function apply(ctx, config = {}) {
46
+ if (config.enabled === false) return;
47
+ const opts = {
48
+ forgeBin: config.forgeBin ?? process.env.FORGE_BIN ?? "forge",
49
+ timeoutMs: config.timeoutMs ?? 30000,
50
+ };
51
+ const debug = config.debug === true;
52
+ // Recent-run ring buffer for /forge-status — fail-open verdicts are silent
53
+ // by design, so this (plus debug logging) is the only way to see them.
54
+ const recentRuns = [];
55
+ const noteErrors = (label, outcome) => {
56
+ recentRuns.push({
57
+ label,
58
+ blocked: outcome.blocked?.command ?? null,
59
+ contexts: outcome.contexts.length,
60
+ errors: outcome.errors,
61
+ at: Date.now(),
62
+ });
63
+ if (recentRuns.length > 50) recentRuns.shift();
64
+ if (debug && outcome.errors.length > 0) {
65
+ console.error(`[forge-dsh] ${label} fail-open: ${outcome.errors.join("; ")}`);
66
+ }
67
+ };
68
+
69
+ // PreToolUse — deny short-circuits the tool call.
70
+ ctx.effect(() => ctx.on("tools/pre-execute", async (exec, next) => {
71
+ const commands = matchedCommands(spec.PreToolUse, toCCToolName(exec?.name ?? ""));
72
+ if (commands.length === 0) return next();
73
+ const outcome = await runHookGroup(commands, buildToolPayload(exec, "PreToolUse"), opts);
74
+ noteErrors("pre-execute", outcome);
75
+ return preExecuteDecision(outcome, exec?.agent, next);
76
+ }), "forge: pre-execute");
77
+
78
+ // PostToolUse — block turns the gate's reason into an error result.
79
+ ctx.effect(() => ctx.on("tools/post-execute", async (exec, result, next) => {
80
+ const commands = matchedCommands(spec.PostToolUse, toCCToolName(exec?.name ?? ""));
81
+ if (commands.length === 0) return next();
82
+ const outcome = await runHookGroup(commands, buildToolPayload(exec, "PostToolUse"), opts);
83
+ noteErrors("post-execute", outcome);
84
+ return postExecuteDecision(outcome, next, exec?.agent);
85
+ }), "forge: post-execute");
86
+
87
+ // UserPromptSubmit — the prompt rides the step's claimed messages.
88
+ ctx.effect(() => ctx.on("agent/pre-step", async (payload, next) => {
89
+ const commands = matchedCommands(spec.UserPromptSubmit, "");
90
+ if (commands.length === 0) return next();
91
+ const eventPayload = buildEventPayload("UserPromptSubmit", payload?.agent, {
92
+ prompt: promptText(payload?.messages),
93
+ });
94
+ const outcome = await runHookGroup(commands, eventPayload, opts);
95
+ noteErrors("pre-step", outcome);
96
+ return preStepDecision(outcome, next, payload?.agent);
97
+ }), "forge: pre-step");
98
+
99
+ // SessionStart — emit mode: detached, contexts queued via agent.inject.
100
+ // source 'compact' doubles as PostCompact (DSH exposes no dedicated point).
101
+ ctx.effect(() => ctx.on("agent/session-start", (payload) => {
102
+ const agent = payload?.agent;
103
+ const source = payload?.source;
104
+ (async () => {
105
+ const start = await runHookGroup(
106
+ matchedCommands(spec.SessionStart, ""),
107
+ buildEventPayload("SessionStart", agent, { source }),
108
+ opts,
109
+ );
110
+ noteErrors("session-start", start);
111
+ sessionStartOutcome(start, agent);
112
+ if (source === "compact") {
113
+ const compact = await runHookGroup(
114
+ matchedCommands(spec.PostCompact, ""),
115
+ buildEventPayload("PostCompact", agent, {}),
116
+ opts,
117
+ );
118
+ noteErrors("post-compact", compact);
119
+ sessionStartOutcome(compact, agent);
120
+ }
121
+ })().catch((error) => {
122
+ if (debug) console.error(`[forge-dsh] session-start failed open: ${error}`);
123
+ });
124
+ }), "forge: session-start");
125
+
126
+ // Stop — a blocking gate steers the agent into another step.
127
+ ctx.effect(() => ctx.on("agent/turn-stopping", async (payload) => {
128
+ const agent = payload?.agent;
129
+ const outcome = await runHookGroup(
130
+ matchedCommands(spec.Stop, ""),
131
+ buildEventPayload("Stop", agent, { stop_hook_active: false }),
132
+ opts,
133
+ );
134
+ noteErrors("turn-stopping", outcome);
135
+ return turnStoppingOutcome(outcome, agent);
136
+ }), "forge: turn-stopping");
137
+
138
+ // /forge-status — wired groups + recent runs (the only place fail-open
139
+ // infrastructure errors surface without debug:true).
140
+ const commands = ctx.get("commands");
141
+ if (commands !== undefined) {
142
+ ctx.effect(() => commands.register({
143
+ name: "forge-status",
144
+ description: "Forge quality gates: wired hook groups and recent runs",
145
+ handler: async () => {
146
+ const groups = Object.entries(spec)
147
+ .map(([event, gs]) => ` ${event}: ${gs.flatMap((g) => g.hooks.map((h) => h.command.replace("forge hook ", ""))).join(", ")}`)
148
+ .join("\n");
149
+ const runs = recentRuns.length === 0
150
+ ? " (no hook runs yet)"
151
+ : recentRuns.slice(-10).map((r) => {
152
+ const parts = [` ${new Date(r.at).toISOString()} ${r.label}`];
153
+ if (r.blocked) parts.push(`blocked-by=${r.blocked}`);
154
+ if (r.contexts > 0) parts.push(`contexts=${r.contexts}`);
155
+ if (r.errors.length > 0) parts.push(`FAIL-OPEN: ${r.errors.join("; ")}`);
156
+ return parts.join(" ");
157
+ }).join("\n");
158
+ return {
159
+ kind: "success",
160
+ text: `# Forge quality gates\n\nforgeBin: ${opts.forgeBin} timeoutMs: ${opts.timeoutMs}\n\n## Wired groups\n${groups}\n\n## Recent runs (last 10)\n${runs}`,
161
+ };
162
+ },
163
+ }), "forge: status command");
164
+ }
165
+ }
166
+
167
+ export { apply, inject, name };
package/lib/runner.js ADDED
@@ -0,0 +1,121 @@
1
+ /**
2
+ * @agent_forge/forge-dsh — spawn layer.
3
+ *
4
+ * Runs one `forge hook <name>` command with a Claude-Code-shape JSON payload on
5
+ * stdin and parses forge's verdict from its stdout JSON. The contract is the
6
+ * one forge's claude emitter writes (internal/cli/hook.go emitClaudeOutput):
7
+ *
8
+ * pass, no detail : exit 0, empty stdout
9
+ * pass + detail : exit 0, {"hookSpecificOutput":{"hookEventName","additionalContext"}}
10
+ * block : exit 2, {"decision":"block","reason":...,"hookSpecificOutput":{...}}
11
+ * plus the reason on stderr
12
+ *
13
+ * Block is read from the JSON `decision` field — never from the exit code —
14
+ * because cobra reports forge's internal errors as exit 1, indistinguishable
15
+ * from a deny. Every infrastructure failure (forge missing, spawn error, JSON
16
+ * parse failure, timeout) FAILS OPEN so a forge outage never locks the agent
17
+ * out of its own tools. Same contract as the opencode/pi translators
18
+ * (internal/agentbridge/forge_spawn.ts).
19
+ *
20
+ * @module runner
21
+ */
22
+ import { spawn } from "node:child_process";
23
+
24
+ /**
25
+ * @typedef {object} HookVerdict
26
+ * @property {boolean} block - forge denied the action.
27
+ * @property {string} [reason] - block reason (shown to the model).
28
+ * @property {string} [context] - allow-path additionalContext to inject.
29
+ * @property {string} [error] - infrastructure failure note (fail-open path).
30
+ */
31
+
32
+ /**
33
+ * Run one forge hook command.
34
+ *
35
+ * @param {string} command - spec command, e.g. "forge hook task-guard".
36
+ * @param {object} payload - Claude-Code-shape hook stdin payload.
37
+ * @param {object} [opts]
38
+ * @param {string} [opts.forgeBin="forge"] - forge binary (path or PATH name).
39
+ * @param {number} [opts.timeoutMs=30000] - kill + fail open past this.
40
+ * @param {string} [opts.cwd] - working directory for the hook.
41
+ * @returns {Promise<HookVerdict>}
42
+ */
43
+ export function runForgeHook(command, payload, opts = {}) {
44
+ const forgeBin = opts.forgeBin ?? "forge";
45
+ const timeoutMs = opts.timeoutMs ?? 30000;
46
+ const parts = command.split(" "); // ["forge","hook","task-guard"]
47
+ const argv = [forgeBin, ...parts.slice(1)];
48
+ return new Promise((resolve) => {
49
+ let child;
50
+ try {
51
+ child = spawn(argv[0], argv.slice(1), {
52
+ stdio: ["pipe", "pipe", "pipe"],
53
+ cwd: opts.cwd,
54
+ });
55
+ } catch (error) {
56
+ resolve({ block: false, error: note(error) });
57
+ return;
58
+ }
59
+ let out = "";
60
+ let settled = false;
61
+ // 30s default ceiling: a hung forge process (index.lock, blocked stdin,
62
+ // AV scan) would otherwise never resolve this Promise and freeze the
63
+ // agent's tool call forever. Timeout → kill + fail open.
64
+ const timer = setTimeout(() => {
65
+ if (settled) return;
66
+ settled = true;
67
+ try {
68
+ child.kill();
69
+ } catch {
70
+ // already exited — nothing to kill
71
+ }
72
+ resolve({ block: false, error: `timeout after ${timeoutMs}ms` });
73
+ }, timeoutMs);
74
+ const settle = (verdict) => {
75
+ if (settled) return;
76
+ settled = true;
77
+ clearTimeout(timer);
78
+ resolve(verdict);
79
+ };
80
+ child.stdout.on("data", (d) => (out += d.toString()));
81
+ child.on("error", (error) => settle({ block: false, error: note(error) }));
82
+ child.on("close", () => {
83
+ const text = out.trim();
84
+ if (text === "") return settle({ block: false });
85
+ try {
86
+ const j = JSON.parse(text);
87
+ const context = j?.hookSpecificOutput?.additionalContext;
88
+ if (j?.decision === "block") {
89
+ return settle({
90
+ block: true,
91
+ reason: j?.reason ?? context ?? "denied",
92
+ });
93
+ }
94
+ return settle({
95
+ block: false,
96
+ context: typeof context === "string" && context !== "" ? context : undefined,
97
+ });
98
+ } catch {
99
+ settle({ block: false, error: "unparseable forge stdout" });
100
+ }
101
+ });
102
+ // stdin error guard: a forge that exits WITHOUT reading stdin (unknown
103
+ // hook name after a version drift, a broken FORGE_BIN shim, an early
104
+ // panic) makes this write raise EPIPE on the stdin stream — an unhandled
105
+ // 'error' event here would crash the DSH HOST PROCESS, the exact opposite
106
+ // of the fail-open contract. The verdict itself still comes from
107
+ // close/stdout above; swallowing the stream error is sufficient.
108
+ //
109
+ // stdin 错误护栏:forge 不读 stdin 就提前退出(版本漂移后的未知 hook 名、
110
+ // 坏掉的 FORGE_BIN shim、早期 panic)会让这次写入在 stdin 流上抛
111
+ // EPIPE——此处未处理的 'error' 事件会**崩溃 DSH 宿主进程**,与
112
+ // fail-open 契约正好相反。判定仍由上面的 close/stdout 路径产出,吞掉
113
+ // 流错误即可。
114
+ child.stdin.on("error", () => {});
115
+ child.stdin.end(JSON.stringify(payload) + "\n");
116
+ });
117
+ }
118
+
119
+ function note(error) {
120
+ return (error instanceof Error ? error.message : String(error)).slice(0, 300);
121
+ }
package/lib/spec.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "PostToolUse": [
3
+ {
4
+ "matcher": "Write|Edit",
5
+ "hooks": [
6
+ { "type": "command", "command": "forge hook auto-compile" },
7
+ { "type": "command", "command": "forge hook workflow-test-guard" },
8
+ { "type": "command", "command": "forge hook skill-trigger" }
9
+ ]
10
+ },
11
+ {
12
+ "matcher": "Bash",
13
+ "hooks": [
14
+ { "type": "command", "command": "forge hook file-sentinel" },
15
+ { "type": "command", "command": "forge hook skill-trigger" }
16
+ ]
17
+ },
18
+ {
19
+ "matcher": "Read|Skill|Agent",
20
+ "hooks": [
21
+ { "type": "command", "command": "forge hook tool-track" }
22
+ ]
23
+ }
24
+ ],
25
+ "PreToolUse": [
26
+ {
27
+ "matcher": "Write|Edit",
28
+ "hooks": [
29
+ { "type": "command", "command": "forge hook freeze-guard" },
30
+ { "type": "command", "command": "forge hook task-guard" },
31
+ { "type": "command", "command": "forge hook assertion-check" },
32
+ { "type": "command", "command": "forge hook read-before-edit" },
33
+ { "type": "command", "command": "forge hook skill-trigger" }
34
+ ]
35
+ },
36
+ {
37
+ "matcher": "Bash",
38
+ "hooks": [
39
+ { "type": "command", "command": "forge hook bash-guard" },
40
+ { "type": "command", "command": "forge hook hazard-guard" },
41
+ { "type": "command", "command": "forge hook skill-trigger" }
42
+ ]
43
+ }
44
+ ],
45
+ "Stop": [
46
+ {
47
+ "hooks": [
48
+ { "type": "command", "command": "forge hook task-verify" },
49
+ { "type": "command", "command": "forge hook review-stop" },
50
+ { "type": "command", "command": "forge hook skill-trigger" }
51
+ ]
52
+ }
53
+ ],
54
+ "SessionStart": [
55
+ {
56
+ "hooks": [
57
+ { "type": "command", "command": "forge hook skill-scan" },
58
+ { "type": "command", "command": "forge hook mcp-scan" },
59
+ { "type": "command", "command": "forge hook init-suggest" },
60
+ { "type": "command", "command": "forge hook task-resume" },
61
+ { "type": "command", "command": "forge hook skill-trigger" }
62
+ ]
63
+ }
64
+ ],
65
+ "PostCompact": [
66
+ {
67
+ "hooks": [
68
+ { "type": "command", "command": "forge hook compact-resume" }
69
+ ]
70
+ }
71
+ ],
72
+ "UserPromptSubmit": [
73
+ {
74
+ "hooks": [
75
+ { "type": "command", "command": "forge hook resume-reinject" },
76
+ { "type": "command", "command": "forge hook skill-trigger" }
77
+ ]
78
+ }
79
+ ]
80
+ }
package/lib/tools.js ADDED
@@ -0,0 +1,187 @@
1
+ /**
2
+ * @agent_forge/forge-dsh — DSH → Claude-Code vocabulary mapping.
3
+ *
4
+ * forge's hook dispatch keys on Claude Code tool names (Write/Edit/Bash/Read/
5
+ * Skill/Agent) and Claude-shape payloads (HookInput in internal/cli/hook.go).
6
+ * DSH's built-in fs tools already use Claude-style argument field names
7
+ * (file_path/content/old_string/new_string — verified against
8
+ * @deepseek-ai/dsh-tool-fs 0.1.0-rc.7), so tool_input is a light alias pass
9
+ * rather than a deep translation.
10
+ *
11
+ * @module tools
12
+ */
13
+
14
+ /** DSH built-in tool name → Claude Code tool name forge dispatches on. */
15
+ export const CC_TOOL_NAME = {
16
+ write: "Write",
17
+ edit: "Edit",
18
+ str_replace_editor: "Edit", // Minimal preset's editor tool
19
+ bash: "Bash",
20
+ pwsh: "Bash",
21
+ read: "Read",
22
+ read_image: "Read",
23
+ skill: "Skill",
24
+ };
25
+
26
+ /**
27
+ * Map a DSH tool name to its Claude Code equivalent. Unknown names pass
28
+ * through unchanged — no spec matcher will match them, so they sail through
29
+ * the waterfalls ungated (same as an unmatched tool in Claude Code).
30
+ *
31
+ * @param {string} dshName
32
+ * @returns {string}
33
+ */
34
+ export function toCCToolName(dshName) {
35
+ return CC_TOOL_NAME[dshName] ?? dshName;
36
+ }
37
+
38
+ /** Field aliases from non-CC argument shapes to Claude's tool_input keys. */
39
+ const INPUT_ALIASES = {
40
+ filePath: "file_path",
41
+ path: "file_path",
42
+ newText: "content",
43
+ };
44
+
45
+ /**
46
+ * Normalize one tool's argument object into Claude tool_input shape. DSH fs
47
+ * tools already use file_path/content/old_string/new_string, so this mostly
48
+ * copies; aliases cover camelCase variants seen in community tools. The
49
+ * original arguments object is never mutated.
50
+ *
51
+ * @param {unknown} args
52
+ * @returns {Record<string, unknown>}
53
+ */
54
+ export function normalizeToolInput(args) {
55
+ const out = {};
56
+ if (args === null || typeof args !== "object" || Array.isArray(args)) return out;
57
+ for (const [key, value] of Object.entries(args)) {
58
+ out[INPUT_ALIASES[key] ?? key] = value;
59
+ }
60
+ // A bare alias must not shadow an already-correct CC field (args carrying
61
+ // both file_path and path: file_path wins regardless of iteration order).
62
+ for (const [alias, canonical] of Object.entries(INPUT_ALIASES)) {
63
+ if (args[alias] !== undefined && args[canonical] !== undefined) {
64
+ out[canonical] = args[canonical];
65
+ }
66
+ }
67
+ return out;
68
+ }
69
+
70
+ /**
71
+ * Collect the spec hook commands of one event whose matcher accepts ccName.
72
+ * A group with an empty/absent matcher always matches; otherwise the matcher
73
+ * is an alternation regex tested against the Claude tool name (identical to
74
+ * Claude Code's matcher semantics for forge's own spec — every matcher in
75
+ * spec.json is a plain alternation like "Write|Edit").
76
+ *
77
+ * @param {Array<{matcher?: string, hooks: Array<{command: string}>}>} groups
78
+ * @param {string} ccName
79
+ * @returns {string[]} matched commands, in spec order
80
+ */
81
+ export function matchedCommands(groups, ccName) {
82
+ const commands = [];
83
+ for (const group of groups ?? []) {
84
+ if (group.matcher !== undefined && group.matcher !== "") {
85
+ if (!new RegExp(`^(?:${group.matcher})$`).test(ccName)) continue;
86
+ }
87
+ for (const hook of group.hooks ?? []) commands.push(hook.command);
88
+ }
89
+ return commands;
90
+ }
91
+
92
+ /**
93
+ * Extract the session identity forge expects from a DSH agent handle.
94
+ * Falls back to process cwd and an empty session id (forge degrades to its
95
+ * legacy global state file for an empty session id — same degradation the
96
+ * official bridges accept when no transcript locator exists).
97
+ *
98
+ * @param {object|undefined} agent
99
+ * @returns {{ sessionId: string, cwd: string }}
100
+ */
101
+ export function sessionBits(agent) {
102
+ const session = agent?.session;
103
+ return {
104
+ sessionId: session?.id ?? "",
105
+ cwd: session?.header?.cwd ?? process.cwd(),
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Build the Claude-Code-shape stdin payload for one tool event.
111
+ *
112
+ * @param {object} exec - DSH ToolExecution ({name, arguments, agent?}).
113
+ * @param {"PreToolUse"|"PostToolUse"} event
114
+ * @returns {object}
115
+ */
116
+ export function buildToolPayload(exec, event) {
117
+ const { sessionId, cwd } = sessionBits(exec?.agent);
118
+ return {
119
+ session_id: sessionId,
120
+ transcript_path: "",
121
+ cwd,
122
+ hook_event_name: event,
123
+ tool_name: toCCToolName(exec?.name ?? ""),
124
+ tool_input: normalizeToolInput(exec?.arguments),
125
+ forge_agent: "dsh",
126
+ };
127
+ }
128
+
129
+ /**
130
+ * Build the stdin payload for a non-tool event (Stop / SessionStart /
131
+ * UserPromptSubmit / PostCompact).
132
+ *
133
+ * @param {string} event
134
+ * @param {object|undefined} agent
135
+ * @param {object} [extra] - extra fields (prompt, source, stop_hook_active…).
136
+ * @returns {object}
137
+ */
138
+ export function buildEventPayload(event, agent, extra = {}) {
139
+ const { sessionId, cwd } = sessionBits(agent);
140
+ return {
141
+ session_id: sessionId,
142
+ transcript_path: "",
143
+ cwd,
144
+ hook_event_name: event,
145
+ forge_agent: "dsh",
146
+ ...extra,
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Concatenate the visible text of user messages into one prompt string
152
+ * (forge's UserPromptSubmit consumers read a flat `prompt` field).
153
+ *
154
+ * @param {Array<{content?: Array<{type: string, text?: string}>}>} messages
155
+ * @returns {string}
156
+ */
157
+ export function promptText(messages) {
158
+ const parts = [];
159
+ for (const message of messages ?? []) {
160
+ for (const block of message?.content ?? []) {
161
+ if (block?.type === "text" && typeof block.text === "string") parts.push(block.text);
162
+ }
163
+ }
164
+ return parts.join("\n");
165
+ }
166
+
167
+ let messageSeq = 0;
168
+
169
+ /**
170
+ * Build one plugin-sourced user message (the shape dsh-llm's
171
+ * createUserMessage freezes: role 'user', text content, plugin source).
172
+ * Constructed literally to keep this package dependency-free — importing
173
+ * @deepseek-ai/dsh-llm would only resolve when the install layout happens to
174
+ * nest it above us.
175
+ *
176
+ * @param {string} text
177
+ * @returns {{id: string, role: "user", content: Array<{type: "text", text: string}>, source: {kind: "plugin", plugin: string}}}
178
+ */
179
+ export function pluginMessage(text) {
180
+ messageSeq += 1;
181
+ return {
182
+ id: `forge-dsh-${Date.now()}-${messageSeq}`,
183
+ role: "user",
184
+ content: [{ type: "text", text }],
185
+ source: { kind: "plugin", plugin: "forge-quality" },
186
+ };
187
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@agent_forge/forge-dsh",
3
+ "version": "0.1.0",
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
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/MjxUpUp/Forge",
8
+ "directory": "plugins/forge-dsh"
9
+ },
10
+ "type": "module",
11
+ "main": "lib/index.js",
12
+ "files": [
13
+ "lib/index.js",
14
+ "lib/runner.js",
15
+ "lib/decisions.js",
16
+ "lib/tools.js",
17
+ "lib/spec.json",
18
+ "cordis.patch.yml"
19
+ ],
20
+ "dsh": {
21
+ "bundle": {
22
+ "patch": "./cordis.patch.yml"
23
+ }
24
+ },
25
+ "scripts": {
26
+ "test": "node --test lib/*.test.js"
27
+ },
28
+ "keywords": [
29
+ "dsh",
30
+ "dsh-plugin",
31
+ "deepseek-harness",
32
+ "quality-gates",
33
+ "hooks",
34
+ "ai-code-quality"
35
+ ],
36
+ "license": "Apache-2.0",
37
+ "engines": {
38
+ "node": ">=18"
39
+ },
40
+ "devDependencies": {
41
+ "@deepseek-ai/cordis": "^4.0.1"
42
+ }
43
+ }