dsh-doublecheck 0.9.12 → 0.9.14
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/CHANGELOG.md +45 -0
- package/README-es.md +50 -46
- package/README-hi.md +50 -46
- package/README-pt.md +50 -46
- package/README-zh.md +50 -46
- package/README.md +33 -21
- package/lib/events.d.ts +58 -0
- package/lib/events.js +38 -1
- package/lib/guard/command.js +2 -1
- package/lib/guard/gate.d.ts +12 -1
- package/lib/guard/gate.js +12 -1
- package/lib/guard/index.d.ts +88 -13
- package/lib/guard/index.js +86 -48
- package/package.json +19 -16
package/lib/events.d.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* @module dsh-doublecheck/events
|
|
14
14
|
*/
|
|
15
15
|
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
16
|
+
import type { ContextFormed } from '@deepseek-ai/dsh-llm';
|
|
16
17
|
import type { Session, SessionEventMap } from '@deepseek-ai/dsh-session';
|
|
17
18
|
import type { GrilledSpec, ReviewFinding, ReviewVerdict, ReportVerdict, VerifyCheck } from './domain/vocabulary.js';
|
|
18
19
|
import type { GateState, GateVerdict } from './domain/gate.js';
|
|
@@ -72,6 +73,18 @@ export interface GateAppend {
|
|
|
72
73
|
}
|
|
73
74
|
declare module '@deepseek-ai/dsh-llm' {
|
|
74
75
|
interface MessageSourceMap {
|
|
76
|
+
/**
|
|
77
|
+
* The discipline notices injected into the session — the grill/red/green
|
|
78
|
+
* reminders, the delivery-report expectation, the adversary steer, and the
|
|
79
|
+
* `/doublecheck on|off` switch notice. All carry `form: 'notice'` and a
|
|
80
|
+
* stable one-line `summary` the guard's durable `remindOnce` fold reads
|
|
81
|
+
* back (model-visible ⟺ logged). Producer-owned on purpose: the harness
|
|
82
|
+
* removed the shared catch-all `plugin` kind, and its physical-row
|
|
83
|
+
* admission refuses one outright, so a source must name this package.
|
|
84
|
+
*/
|
|
85
|
+
'dsh-doublecheck': {
|
|
86
|
+
kind: 'dsh-doublecheck';
|
|
87
|
+
} & ContextFormed;
|
|
75
88
|
/**
|
|
76
89
|
* The adversary review injected into the session: the durable record of
|
|
77
90
|
* one settled critique. The model-facing text is the rendered findings
|
|
@@ -200,3 +213,48 @@ declare module '@deepseek-ai/cordis' {
|
|
|
200
213
|
}): void;
|
|
201
214
|
}
|
|
202
215
|
}
|
|
216
|
+
/** The source shape an injected doublecheck notice carries, as read back from an unsettled log value. */
|
|
217
|
+
export interface NoticeSourceShape {
|
|
218
|
+
kind?: unknown;
|
|
219
|
+
plugin?: unknown;
|
|
220
|
+
form?: unknown;
|
|
221
|
+
summary?: unknown;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* The durable notice summaries the guard's `remindOnce` fold recognizes, and
|
|
225
|
+
* the prose each one announces. A summary is a stable wire constant, not
|
|
226
|
+
* prose: it identifies which reminder is already on record.
|
|
227
|
+
*/
|
|
228
|
+
export type NoticeSummary = 'requirements check' | 'red/green check' | 'green gate' | 'delivery report' | 'adversary review' | 'doublecheck state';
|
|
229
|
+
/**
|
|
230
|
+
* Build the producer-owned source for one doublecheck notice.
|
|
231
|
+
* @param summary - the stable notice summary the fold reads back.
|
|
232
|
+
* @returns the `MessageSourceMap` member this package declares.
|
|
233
|
+
*/
|
|
234
|
+
export declare function noticeSource(summary: NoticeSummary): {
|
|
235
|
+
kind: 'dsh-doublecheck';
|
|
236
|
+
form: 'notice';
|
|
237
|
+
summary: string;
|
|
238
|
+
};
|
|
239
|
+
/**
|
|
240
|
+
* Read the notice summary out of a durable `user/message` source, or undefined
|
|
241
|
+
* when the source is not a doublecheck notice.
|
|
242
|
+
*
|
|
243
|
+
* Three shapes are recognized, because the durable log outlives any one
|
|
244
|
+
* release line:
|
|
245
|
+
*
|
|
246
|
+
* - `{ kind: 'dsh-doublecheck', form: 'notice', summary }` — what this package
|
|
247
|
+
* writes now.
|
|
248
|
+
* - `{ kind: 'plugin:dsh-doublecheck', form: 'notice', summary }` — the host's
|
|
249
|
+
* V3→V4 migration lifts a released catch-all `plugin` wrapper to a
|
|
250
|
+
* `plugin:<name>` kind for producers it does not know, and preserves the
|
|
251
|
+
* rest of the source. A session recorded before this package adopted its own
|
|
252
|
+
* kind therefore arrives here.
|
|
253
|
+
* - `{ kind: 'plugin', plugin: 'dsh-doublecheck', form: 'notice', summary }` —
|
|
254
|
+
* the released wrapper itself, kept for a log value that has not been
|
|
255
|
+
* through that migration (an in-process array from an older composition).
|
|
256
|
+
*
|
|
257
|
+
* @param source - one `user/message` event's source, of unknown shape.
|
|
258
|
+
* @returns the summary when this is a doublecheck notice, else undefined.
|
|
259
|
+
*/
|
|
260
|
+
export declare function noticeSummaryOf(source: NoticeSourceShape): string | undefined;
|
package/lib/events.js
CHANGED
|
@@ -12,4 +12,41 @@
|
|
|
12
12
|
*
|
|
13
13
|
* @module dsh-doublecheck/events
|
|
14
14
|
*/
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Build the producer-owned source for one doublecheck notice.
|
|
17
|
+
* @param summary - the stable notice summary the fold reads back.
|
|
18
|
+
* @returns the `MessageSourceMap` member this package declares.
|
|
19
|
+
*/
|
|
20
|
+
export function noticeSource(summary) {
|
|
21
|
+
return { kind: 'dsh-doublecheck', form: 'notice', summary };
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Read the notice summary out of a durable `user/message` source, or undefined
|
|
25
|
+
* when the source is not a doublecheck notice.
|
|
26
|
+
*
|
|
27
|
+
* Three shapes are recognized, because the durable log outlives any one
|
|
28
|
+
* release line:
|
|
29
|
+
*
|
|
30
|
+
* - `{ kind: 'dsh-doublecheck', form: 'notice', summary }` — what this package
|
|
31
|
+
* writes now.
|
|
32
|
+
* - `{ kind: 'plugin:dsh-doublecheck', form: 'notice', summary }` — the host's
|
|
33
|
+
* V3→V4 migration lifts a released catch-all `plugin` wrapper to a
|
|
34
|
+
* `plugin:<name>` kind for producers it does not know, and preserves the
|
|
35
|
+
* rest of the source. A session recorded before this package adopted its own
|
|
36
|
+
* kind therefore arrives here.
|
|
37
|
+
* - `{ kind: 'plugin', plugin: 'dsh-doublecheck', form: 'notice', summary }` —
|
|
38
|
+
* the released wrapper itself, kept for a log value that has not been
|
|
39
|
+
* through that migration (an in-process array from an older composition).
|
|
40
|
+
*
|
|
41
|
+
* @param source - one `user/message` event's source, of unknown shape.
|
|
42
|
+
* @returns the summary when this is a doublecheck notice, else undefined.
|
|
43
|
+
*/
|
|
44
|
+
export function noticeSummaryOf(source) {
|
|
45
|
+
if (source.form !== 'notice' || typeof source.summary !== 'string')
|
|
46
|
+
return undefined;
|
|
47
|
+
if (source.kind === 'dsh-doublecheck' || source.kind === 'plugin:dsh-doublecheck')
|
|
48
|
+
return source.summary;
|
|
49
|
+
if (source.kind === 'plugin' && source.plugin === 'dsh-doublecheck')
|
|
50
|
+
return source.summary;
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
package/lib/guard/command.js
CHANGED
|
@@ -17,6 +17,7 @@ import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
|
17
17
|
import { Session, SessionId } from '@deepseek-ai/dsh-session';
|
|
18
18
|
import { sessionEvents } from "../session-events.js";
|
|
19
19
|
import { deriveReportVerdict, foldReportFacts, renderReportMarkdown } from "../domain/report.js";
|
|
20
|
+
import { noticeSource } from "../events.js";
|
|
20
21
|
import { PROSE } from "./prose.js";
|
|
21
22
|
/**
|
|
22
23
|
* Whether this host's `Session.append` stamps the `ignorable` envelope marker.
|
|
@@ -63,7 +64,7 @@ export function effectiveDoublecheckEnabled(events, fallback) {
|
|
|
63
64
|
}
|
|
64
65
|
/** The switch notice injected after `/doublecheck on|off`. */
|
|
65
66
|
function switchNotice(enabled, durable, prose) {
|
|
66
|
-
const source =
|
|
67
|
+
const source = noticeSource('doublecheck state');
|
|
67
68
|
const text = enabled
|
|
68
69
|
? durable ? prose.switchOnDurable : prose.switchOnLocal
|
|
69
70
|
: durable ? prose.switchOffDurable : prose.switchOffLocal;
|
package/lib/guard/gate.d.ts
CHANGED
|
@@ -89,7 +89,18 @@ export interface GateConfig {
|
|
|
89
89
|
}
|
|
90
90
|
/** The default gate configuration: every phase on, the six key questions required. */
|
|
91
91
|
export declare const DEFAULT_GATE_CONFIG: GateConfig;
|
|
92
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* The delivery-gate configuration schema (the pluggable checklist + knobs).
|
|
94
|
+
*
|
|
95
|
+
* Annotated with the bare `Schema<GateConfig>` alias on purpose. The guard row
|
|
96
|
+
* marks this whole block `.volatile()`, and that call's result is derived from
|
|
97
|
+
* the *declared* schema type — so the alias is what keeps `Config`'s emitted
|
|
98
|
+
* declaration nameable instead of pointing into the package manager's store.
|
|
99
|
+
* The alias carries no root-default mode, which is why the guard row's
|
|
100
|
+
* interface declares the field `Volatile<GateConfig | undefined>` and resolves
|
|
101
|
+
* the absence once, at the read: the runtime value still comes from the
|
|
102
|
+
* `.default(...)` below.
|
|
103
|
+
*/
|
|
93
104
|
export declare const GateConfigSchema: Schema<GateConfig>;
|
|
94
105
|
/**
|
|
95
106
|
* Validate the gate configuration fail-loud: checklist ids unique and
|
package/lib/guard/gate.js
CHANGED
|
@@ -67,7 +67,18 @@ export const DEFAULT_GATE_CONFIG = {
|
|
|
67
67
|
maxFindings: 5,
|
|
68
68
|
},
|
|
69
69
|
};
|
|
70
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* The delivery-gate configuration schema (the pluggable checklist + knobs).
|
|
72
|
+
*
|
|
73
|
+
* Annotated with the bare `Schema<GateConfig>` alias on purpose. The guard row
|
|
74
|
+
* marks this whole block `.volatile()`, and that call's result is derived from
|
|
75
|
+
* the *declared* schema type — so the alias is what keeps `Config`'s emitted
|
|
76
|
+
* declaration nameable instead of pointing into the package manager's store.
|
|
77
|
+
* The alias carries no root-default mode, which is why the guard row's
|
|
78
|
+
* interface declares the field `Volatile<GateConfig | undefined>` and resolves
|
|
79
|
+
* the absence once, at the read: the runtime value still comes from the
|
|
80
|
+
* `.default(...)` below.
|
|
81
|
+
*/
|
|
71
82
|
export const GateConfigSchema = z.object({
|
|
72
83
|
enabled: z.boolean().default(true),
|
|
73
84
|
planSuggestion: z.boolean().default(true),
|
package/lib/guard/index.d.ts
CHANGED
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
*
|
|
27
27
|
* @module dsh-doublecheck/guard
|
|
28
28
|
*/
|
|
29
|
-
import type { Context } from '@deepseek-ai/cordis';
|
|
29
|
+
import type { Context, Volatile } from '@deepseek-ai/cordis';
|
|
30
30
|
import type { SessionEvent } from '@deepseek-ai/dsh-session';
|
|
31
|
-
import
|
|
31
|
+
import z from '@deepseek-ai/schemastery';
|
|
32
32
|
import { type DisciplineState } from '../domain/stages.js';
|
|
33
33
|
import type { GuardIntensity } from '../events.js';
|
|
34
34
|
import { type ProseLanguage } from './prose.js';
|
|
@@ -36,18 +36,14 @@ import { type GateConfig } from './gate.js';
|
|
|
36
36
|
import { type GateVerdict } from '../domain/gate.js';
|
|
37
37
|
export declare const name = "doublecheck-guard";
|
|
38
38
|
export declare const inject: string[];
|
|
39
|
-
/**
|
|
40
|
-
* The gate's settings namespace. Hyphenated on purpose: the host's
|
|
41
|
-
* `NAMESPACE_PATTERN` (`/^[a-z][a-z0-9-]*$/`) rejects a dot with a
|
|
42
|
-
* `TypeError`, and a rejected registration never reaches
|
|
43
|
-
* `ctx.settings.describe()`, so no settings surface can see it. v0.9.8 and
|
|
44
|
-
* earlier registered `doublecheck.gate`, which never took effect anywhere.
|
|
45
|
-
*/
|
|
46
|
-
export declare const GATE_SETTINGS_NS = "doublecheck-gate";
|
|
47
39
|
/**
|
|
48
40
|
* Guard configuration. `intensity` is shared by all three gates; `modules`
|
|
49
41
|
* selects them. The `adversary` module (v0.3) dispatches a forked critic
|
|
50
42
|
* subagent at the turn boundary once the delivery reaches green.
|
|
43
|
+
*
|
|
44
|
+
* Only `gate` is a live field: the delivery gate's checklist is the one block
|
|
45
|
+
* users tune per project, and it is the block this row's settings card edits.
|
|
46
|
+
* Every other knob is ordinary composition config from the profile patch.
|
|
51
47
|
*/
|
|
52
48
|
export interface Config {
|
|
53
49
|
/** Enforcement strength of the grill, red/green, and review gates. */
|
|
@@ -84,10 +80,89 @@ export interface Config {
|
|
|
84
80
|
testCommandPatterns: string[];
|
|
85
81
|
/** Regexes identifying test-file paths, exempt from the red gate. */
|
|
86
82
|
testFilePatterns: string[];
|
|
87
|
-
/**
|
|
88
|
-
|
|
83
|
+
/**
|
|
84
|
+
* The delivery quality gate: the configurable checklist and the panel. A live
|
|
85
|
+
* field on a host that has them, so the block is edited from this row's
|
|
86
|
+
* settings card; on an older host line it stays ordinary composition config
|
|
87
|
+
* and there is no card. Either way it is read through
|
|
88
|
+
* {@link resolveGateBlock}; see {@link apply} for when the value is taken.
|
|
89
|
+
* `| undefined` is the schema alias's shape (see `GateConfigSchema`), not a
|
|
90
|
+
* real state: the block carries a root default.
|
|
91
|
+
*/
|
|
92
|
+
gate: Volatile<GateConfig | undefined> | GateConfig;
|
|
89
93
|
}
|
|
90
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Resolve the guard row's `gate` block from whatever shape the host produced:
|
|
96
|
+
* a `Volatile` reference on a host with live Config fields, the plain resolved
|
|
97
|
+
* object on one without. One build therefore spans the whole peer band.
|
|
98
|
+
* @param value - the resolved `gate` field.
|
|
99
|
+
* @returns the gate configuration block.
|
|
100
|
+
* @throws when the block is absent — the schema carries a root default, so this
|
|
101
|
+
* is a loud guard rather than a supported state.
|
|
102
|
+
*/
|
|
103
|
+
export declare function resolveGateBlock(value: Volatile<GateConfig | undefined> | GateConfig): GateConfig;
|
|
104
|
+
/**
|
|
105
|
+
* The guard row's Config schema.
|
|
106
|
+
*
|
|
107
|
+
* Deliberately NOT annotated with the `Schema<Config>` alias: a live field's
|
|
108
|
+
* value is a `Volatile` reference, and the alias's output mapping cannot
|
|
109
|
+
* express one (it would demand a plain `GateConfig`). The interface above
|
|
110
|
+
* documents what `apply` reads; this schema is the runtime contract — the same
|
|
111
|
+
* split the harness's own provider rows use (`@deepseek-ai/dsh-llm`'s
|
|
112
|
+
* deepseek row declares `retryPolicy: Volatile<...>` against an un-annotated
|
|
113
|
+
* schema).
|
|
114
|
+
*/
|
|
115
|
+
export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
116
|
+
intensity: z<"warn" | "remind" | "block", "warn" | "remind" | "block", "defined">;
|
|
117
|
+
modules: z<Schemastery.ObjectS<NoInfer<{
|
|
118
|
+
grill: z<boolean, boolean, "defined">;
|
|
119
|
+
tdd: z<boolean, boolean, "defined">;
|
|
120
|
+
adversary: z<boolean, boolean, "defined">;
|
|
121
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
122
|
+
grill: z<boolean, boolean, "defined">;
|
|
123
|
+
tdd: z<boolean, boolean, "defined">;
|
|
124
|
+
adversary: z<boolean, boolean, "defined">;
|
|
125
|
+
}>>, "defined">;
|
|
126
|
+
adversaryModel: z<string | null, string | null, "defined">;
|
|
127
|
+
adversaryProvider: z<string, string, "defined">;
|
|
128
|
+
adversaryMaxFindings: z<number, number, "defined">;
|
|
129
|
+
adversaryTools: z<string[], string[], "defined">;
|
|
130
|
+
adversaryTimeoutMs: z<number, number, "defined">;
|
|
131
|
+
guardTools: z<string[], string[], "defined">;
|
|
132
|
+
vagueTaskMaxChars: z<number, number, "defined">;
|
|
133
|
+
remindOnce: z<boolean, boolean, "defined">;
|
|
134
|
+
language: z<"en" | "zh", "en" | "zh", "defined">;
|
|
135
|
+
enableByDefault: z<boolean, boolean, "defined">;
|
|
136
|
+
testToolNames: z<string[], string[], "defined">;
|
|
137
|
+
testCommandPatterns: z<string[], string[], "defined">;
|
|
138
|
+
testFilePatterns: z<string[], string[], "defined">;
|
|
139
|
+
gate: z<GateConfig>;
|
|
140
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
141
|
+
intensity: z<"warn" | "remind" | "block", "warn" | "remind" | "block", "defined">;
|
|
142
|
+
modules: z<Schemastery.ObjectS<NoInfer<{
|
|
143
|
+
grill: z<boolean, boolean, "defined">;
|
|
144
|
+
tdd: z<boolean, boolean, "defined">;
|
|
145
|
+
adversary: z<boolean, boolean, "defined">;
|
|
146
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
147
|
+
grill: z<boolean, boolean, "defined">;
|
|
148
|
+
tdd: z<boolean, boolean, "defined">;
|
|
149
|
+
adversary: z<boolean, boolean, "defined">;
|
|
150
|
+
}>>, "defined">;
|
|
151
|
+
adversaryModel: z<string | null, string | null, "defined">;
|
|
152
|
+
adversaryProvider: z<string, string, "defined">;
|
|
153
|
+
adversaryMaxFindings: z<number, number, "defined">;
|
|
154
|
+
adversaryTools: z<string[], string[], "defined">;
|
|
155
|
+
adversaryTimeoutMs: z<number, number, "defined">;
|
|
156
|
+
guardTools: z<string[], string[], "defined">;
|
|
157
|
+
vagueTaskMaxChars: z<number, number, "defined">;
|
|
158
|
+
remindOnce: z<boolean, boolean, "defined">;
|
|
159
|
+
language: z<"en" | "zh", "en" | "zh", "defined">;
|
|
160
|
+
enableByDefault: z<boolean, boolean, "defined">;
|
|
161
|
+
testToolNames: z<string[], string[], "defined">;
|
|
162
|
+
testCommandPatterns: z<string[], string[], "defined">;
|
|
163
|
+
testFilePatterns: z<string[], string[], "defined">;
|
|
164
|
+
gate: z<GateConfig>;
|
|
165
|
+
}>>, "plain">;
|
|
91
166
|
/** Cached per-session guard facts, folded incrementally from the append-only log. */
|
|
92
167
|
export interface Snapshot {
|
|
93
168
|
/** The log snapshot this fold last consumed; an append yields a new one. */
|
package/lib/guard/index.js
CHANGED
|
@@ -32,6 +32,7 @@ import z from '@deepseek-ai/schemastery';
|
|
|
32
32
|
import { DEFAULT_MUTATION_TOOLS, DEFAULT_TEST_COMMAND_PATTERNS, DEFAULT_TEST_FILE_PATTERNS, DEFAULT_TEST_TOOL_NAMES, compileDetection, isTestFilePath, mutationTargetPath, parseRawArguments, } from "../domain/evidence.js";
|
|
33
33
|
import { emptyDisciplineState, foldDisciplineRange } from "../domain/stages.js";
|
|
34
34
|
import { isVagueTask } from "../domain/vagueness.js";
|
|
35
|
+
import { noticeSource, noticeSummaryOf } from "../events.js";
|
|
35
36
|
import { runAdversaryReview, reviewInjection } from "./review.js";
|
|
36
37
|
import { PROSE } from "./prose.js";
|
|
37
38
|
import { gateHandler, gateInjection, GateConfigSchema, runGate, settleGate, validateGateConfig, } from "./gate.js";
|
|
@@ -43,13 +44,47 @@ import { doublecheckHandler, hostStampsIgnorable } from "./command.js";
|
|
|
43
44
|
export const name = 'doublecheck-guard';
|
|
44
45
|
export const inject = ['commands'];
|
|
45
46
|
/**
|
|
46
|
-
* The gate
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
47
|
+
* The gate block as a live Config field where the host has one, else as plain
|
|
48
|
+
* composition config.
|
|
49
|
+
*
|
|
50
|
+
* `.volatile()` first exists in `@deepseek-ai/schemastery` 3.18.3, but the peer
|
|
51
|
+
* band still admits host lines that ship an older one. Detected once, at load:
|
|
52
|
+
* on such a host the block stays ordinary config and the settings card is
|
|
53
|
+
* simply absent — the row must not fail to mount over a missing optional
|
|
54
|
+
* surface.
|
|
55
|
+
*/
|
|
56
|
+
function gateField() {
|
|
57
|
+
const candidate = GateConfigSchema;
|
|
58
|
+
return typeof candidate.volatile === 'function' ? candidate.volatile() : GateConfigSchema;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Resolve the guard row's `gate` block from whatever shape the host produced:
|
|
62
|
+
* a `Volatile` reference on a host with live Config fields, the plain resolved
|
|
63
|
+
* object on one without. One build therefore spans the whole peer band.
|
|
64
|
+
* @param value - the resolved `gate` field.
|
|
65
|
+
* @returns the gate configuration block.
|
|
66
|
+
* @throws when the block is absent — the schema carries a root default, so this
|
|
67
|
+
* is a loud guard rather than a supported state.
|
|
68
|
+
*/
|
|
69
|
+
export function resolveGateBlock(value) {
|
|
70
|
+
const live = value;
|
|
71
|
+
const resolved = typeof live.get === 'function' ? live.get() : value;
|
|
72
|
+
if (resolved === undefined) {
|
|
73
|
+
throw new Error('dsh-doublecheck: gate config resolved to undefined; the Schema root default is missing');
|
|
74
|
+
}
|
|
75
|
+
return resolved;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The guard row's Config schema.
|
|
79
|
+
*
|
|
80
|
+
* Deliberately NOT annotated with the `Schema<Config>` alias: a live field's
|
|
81
|
+
* value is a `Volatile` reference, and the alias's output mapping cannot
|
|
82
|
+
* express one (it would demand a plain `GateConfig`). The interface above
|
|
83
|
+
* documents what `apply` reads; this schema is the runtime contract — the same
|
|
84
|
+
* split the harness's own provider rows use (`@deepseek-ai/dsh-llm`'s
|
|
85
|
+
* deepseek row declares `retryPolicy: Volatile<...>` against an un-annotated
|
|
86
|
+
* schema).
|
|
51
87
|
*/
|
|
52
|
-
export const GATE_SETTINGS_NS = 'doublecheck-gate';
|
|
53
88
|
export const Config = z.object({
|
|
54
89
|
intensity: z.union(['remind', 'warn', 'block']).default('remind'),
|
|
55
90
|
modules: z.object({
|
|
@@ -70,7 +105,7 @@ export const Config = z.object({
|
|
|
70
105
|
testToolNames: z.array(z.string()).default([...DEFAULT_TEST_TOOL_NAMES]),
|
|
71
106
|
testCommandPatterns: z.array(z.string()).default([...DEFAULT_TEST_COMMAND_PATTERNS]),
|
|
72
107
|
testFilePatterns: z.array(z.string()).default([...DEFAULT_TEST_FILE_PATTERNS]),
|
|
73
|
-
gate:
|
|
108
|
+
gate: gateField(),
|
|
74
109
|
});
|
|
75
110
|
/**
|
|
76
111
|
* Install the guard listeners.
|
|
@@ -80,34 +115,42 @@ export const Config = z.object({
|
|
|
80
115
|
export function apply(ctx, config) {
|
|
81
116
|
assertPositiveInteger('vagueTaskMaxChars', config.vagueTaskMaxChars);
|
|
82
117
|
assertGuardTools(config.guardTools);
|
|
83
|
-
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
118
|
+
// The effective gate config: the profile patch's `gate` block, layered over
|
|
119
|
+
// the Schema defaults (the host resolves exactly that order and hands the
|
|
120
|
+
// result over as this live field's value). Read ONCE here, deliberately:
|
|
121
|
+
// `gate.enabled` decides whether the turn-stopping notice is installed at
|
|
122
|
+
// load, so a live swap could not honor it. An edit made through the settings
|
|
123
|
+
// card lands in the profile patch and is picked up on the next load — the
|
|
124
|
+
// same restart-scoped semantics the removed `doublecheck-gate` namespace had
|
|
125
|
+
// (`applies: 'restart'`), minus the separate document that no longer exists.
|
|
126
|
+
//
|
|
127
|
+
// The one assertion in this module: on a host with live Config fields,
|
|
128
|
+
// `Volatile.get()` hands back a deeply readonly snapshot, and every consumer
|
|
129
|
+
// below (the command seam, the four gate phases, the report renderer) takes
|
|
130
|
+
// the hand-written `GateConfig`. Nothing in this package writes the block, so
|
|
131
|
+
// the snapshot's readonly wrapper is dropped once, at this single boundary —
|
|
132
|
+
// and on a host without live fields there is no wrapper to drop.
|
|
133
|
+
const gateConfig = resolveGateBlock(config.gate);
|
|
134
|
+
// Fail loud on an unactable checklist. The host validates the Config schema
|
|
135
|
+
// on every settings write, but this cross-field check (unique, non-empty,
|
|
136
|
+
// in-range checklist ids and the thresholds they bound) is this package's own
|
|
137
|
+
// rule, so it runs here — the write path cannot carry it.
|
|
138
|
+
validateGateConfig(gateConfig);
|
|
139
|
+
// Expose this row's live fields on the settings page. `auto: false` keeps the
|
|
140
|
+
// generated card off: the gate knobs are rendered from the schema by the
|
|
141
|
+
// form owner, and the package ships no custom page. The policy is an effect
|
|
142
|
+
// so it unwinds with the row. Feature-detected: the settings service of a
|
|
143
|
+
// host line without live Config fields has no `configure` at all, and the row
|
|
144
|
+
// must still mount there.
|
|
145
|
+
ctx.inject(['settings'], (child) => {
|
|
146
|
+
const settings = child.settings;
|
|
147
|
+
if (typeof settings.configure !== 'function')
|
|
148
|
+
return;
|
|
149
|
+
// Bound: the policy is read off the service instance, and the host's
|
|
150
|
+
// implementation (like this row's stand-in) keeps its state on `this`.
|
|
151
|
+
const configure = settings.configure.bind(settings);
|
|
152
|
+
child.effect(() => configure({ auto: false }, ctx.fiber));
|
|
153
|
+
});
|
|
111
154
|
if (config.modules.adversary) {
|
|
112
155
|
assertBoundedInteger('adversaryMaxFindings', config.adversaryMaxFindings, 20);
|
|
113
156
|
assertPositiveInteger('adversaryTimeoutMs', config.adversaryTimeoutMs);
|
|
@@ -156,14 +199,15 @@ export function apply(ctx, config) {
|
|
|
156
199
|
continue;
|
|
157
200
|
if (event.type === 'user/message') {
|
|
158
201
|
const source = event.data.source;
|
|
159
|
-
|
|
160
|
-
|
|
202
|
+
const summary = noticeSummaryOf(source);
|
|
203
|
+
if (summary !== undefined) {
|
|
204
|
+
if (summary === 'requirements check')
|
|
161
205
|
snapshot.grillReminded = true;
|
|
162
|
-
else if (
|
|
206
|
+
else if (summary === 'red/green check')
|
|
163
207
|
snapshot.redReminded = true;
|
|
164
|
-
else if (
|
|
208
|
+
else if (summary === 'green gate')
|
|
165
209
|
snapshot.greenReminded = true;
|
|
166
|
-
else if (
|
|
210
|
+
else if (summary === 'delivery report')
|
|
167
211
|
snapshot.reportReminded = true;
|
|
168
212
|
}
|
|
169
213
|
else if (source.kind === 'doublecheck-review') {
|
|
@@ -251,15 +295,9 @@ export function apply(ctx, config) {
|
|
|
251
295
|
}
|
|
252
296
|
return refold(session, events);
|
|
253
297
|
}
|
|
254
|
-
/** A
|
|
298
|
+
/** A producer-owned notice message carrying the given prose. */
|
|
255
299
|
function notice(summary, text) {
|
|
256
|
-
|
|
257
|
-
kind: 'plugin',
|
|
258
|
-
plugin: 'dsh-doublecheck',
|
|
259
|
-
form: 'notice',
|
|
260
|
-
summary,
|
|
261
|
-
};
|
|
262
|
-
return createUserMessage({ content: [{ type: 'text', text }], source });
|
|
300
|
+
return createUserMessage({ content: [{ type: 'text', text }], source: noticeSource(summary) });
|
|
263
301
|
}
|
|
264
302
|
/** The grill reminder for this reaction, or undefined when already reminded. */
|
|
265
303
|
function nextGrillReminder(snapshot) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-doublecheck",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.14",
|
|
4
4
|
"description": "Delivery quality gate for DeepSeek Harness: grill the requirements, test the implementation, prove the delivery, and gate the handoff with a deliverable/rework decision.",
|
|
5
5
|
"packageManager": "pnpm@11.7.0",
|
|
6
6
|
"repository": {
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
"test": "vitest run",
|
|
64
64
|
"test:coverage": "vitest run --coverage",
|
|
65
65
|
"verify:skills": "node scripts/verify-skills.mjs",
|
|
66
|
+
"check:lockfile": "node scripts/check-lockfile-drift.mjs",
|
|
66
67
|
"check:readmes": "node scripts/check-readme-sync.mjs",
|
|
67
68
|
"verify:self-contained": "node scripts/verify-self-contained.mjs",
|
|
68
69
|
"verify:artifacts": "node scripts/verify-artifacts.mjs",
|
|
@@ -115,7 +116,8 @@
|
|
|
115
116
|
"compatibility": {
|
|
116
117
|
"dshVersions": [
|
|
117
118
|
"0.1.2-rc.1",
|
|
118
|
-
"0.1.5-rc.2"
|
|
119
|
+
"0.1.5-rc.2",
|
|
120
|
+
"0.1.7-alpha.1"
|
|
119
121
|
]
|
|
120
122
|
},
|
|
121
123
|
"capability": {
|
|
@@ -148,21 +150,22 @@
|
|
|
148
150
|
"zod": "^4.4.3"
|
|
149
151
|
},
|
|
150
152
|
"devDependencies": {
|
|
151
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
153
|
+
"@deepseek-ai/cordis": "^4.0.3",
|
|
152
154
|
"@deepseek-ai/cordis-plugin-loader": "^1.0.3",
|
|
153
|
-
"@deepseek-ai/dsh-agent": "0.1.
|
|
154
|
-
"@deepseek-ai/dsh-commands": "0.1.
|
|
155
|
-
"@deepseek-ai/dsh-fs": "0.1.
|
|
156
|
-
"@deepseek-ai/dsh-
|
|
157
|
-
"@deepseek-ai/dsh-
|
|
158
|
-
"@deepseek-ai/dsh-session
|
|
159
|
-
"@deepseek-ai/dsh-
|
|
160
|
-
"@deepseek-ai/dsh-
|
|
161
|
-
"@deepseek-ai/dsh-
|
|
162
|
-
"@deepseek-ai/dsh-
|
|
163
|
-
"@deepseek-ai/dsh-
|
|
164
|
-
"@deepseek-ai/dsh-
|
|
165
|
-
"@deepseek-ai/
|
|
155
|
+
"@deepseek-ai/dsh-agent": "0.1.7-alpha.1",
|
|
156
|
+
"@deepseek-ai/dsh-commands": "0.1.7-alpha.1",
|
|
157
|
+
"@deepseek-ai/dsh-fs": "0.1.7-alpha.1",
|
|
158
|
+
"@deepseek-ai/dsh-invariants": "0.1.7-alpha.1",
|
|
159
|
+
"@deepseek-ai/dsh-llm": "0.1.7-alpha.1",
|
|
160
|
+
"@deepseek-ai/dsh-session": "0.1.7-alpha.1",
|
|
161
|
+
"@deepseek-ai/dsh-session-projection": "0.1.7-alpha.1",
|
|
162
|
+
"@deepseek-ai/dsh-settings": "0.1.7-alpha.1",
|
|
163
|
+
"@deepseek-ai/dsh-skill": "0.1.7-alpha.1",
|
|
164
|
+
"@deepseek-ai/dsh-subagent": "0.1.7-alpha.1",
|
|
165
|
+
"@deepseek-ai/dsh-system-prompt": "0.1.7-alpha.1",
|
|
166
|
+
"@deepseek-ai/dsh-tools": "0.1.7-alpha.1",
|
|
167
|
+
"@deepseek-ai/dsh-workflow": "0.1.7-alpha.1",
|
|
168
|
+
"@deepseek-ai/schemastery": "^3.18.3",
|
|
166
169
|
"@eslint/js": "^9.30.0",
|
|
167
170
|
"@types/node": "^22.15.0",
|
|
168
171
|
"@vitest/coverage-v8": "^3.2.0",
|