dsh-doublecheck 0.9.13 → 0.9.15

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/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
- export {};
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
+ }
@@ -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 = { kind: 'plugin', plugin: 'dsh-doublecheck', form: 'notice', summary: 'doublecheck state' };
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;
@@ -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
- /** The delivery-gate configuration schema (the pluggable checklist + knobs). */
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
- /** The delivery-gate configuration schema (the pluggable checklist + knobs). */
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),
@@ -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 type Schema from '@deepseek-ai/schemastery';
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
- /** The delivery quality gate: the configurable checklist and the panel. */
88
- gate: GateConfig;
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
- export declare const Config: Schema<Config>;
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. */
@@ -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's settings namespace. Hyphenated on purpose: the host's
47
- * `NAMESPACE_PATTERN` (`/^[a-z][a-z0-9-]*$/`) rejects a dot with a
48
- * `TypeError`, and a rejected registration never reaches
49
- * `ctx.settings.describe()`, so no settings surface can see it. v0.9.8 and
50
- * earlier registered `doublecheck.gate`, which never took effect anywhere.
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: GateConfigSchema,
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
- validateGateConfig(config.gate);
84
- // The effective gate config: the user's stored `doublecheck-gate` section
85
- // overrides this row's composition entry, which overrides the Schema
86
- // defaults (the settings service resolves exactly that order). The service
87
- // is a weak seam — without it, or when the stored section is unusable, the
88
- // composition entry stands and the reason is logged. `applies: 'restart'`
89
- // is deliberate: `gate.enabled` decides whether the turn-stopping notice is
90
- // installed at load, so a live swap could not honor it; the resolved value
91
- // is therefore read once here and an edit takes effect on the next load.
92
- const settings = ctx.get('settings');
93
- let gateConfig = config.gate;
94
- if (settings !== undefined) {
95
- try {
96
- const resolved = settings.register(GATE_SETTINGS_NS, GateConfigSchema, {
97
- base: config.gate,
98
- applies: 'restart',
99
- validate: validateGateConfig,
100
- }).get();
101
- // The host validates the resolved section at registration, so this only
102
- // fires on a host that ignores the `validate` option; degrade rather
103
- // than fail the whole row over a settings typo.
104
- validateGateConfig(resolved);
105
- gateConfig = resolved;
106
- }
107
- catch (error) {
108
- ctx.logger.warn(`dsh-doublecheck: gate settings namespace skipped, using the composition entry: ${String(error)}`);
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
- if (source.kind === 'plugin' && source.plugin === 'dsh-doublecheck' && source.form === 'notice') {
160
- if (source.summary === 'requirements check')
202
+ const summary = noticeSummaryOf(source);
203
+ if (summary !== undefined) {
204
+ if (summary === 'requirements check')
161
205
  snapshot.grillReminded = true;
162
- else if (source.summary === 'red/green check')
206
+ else if (summary === 'red/green check')
163
207
  snapshot.redReminded = true;
164
- else if (source.summary === 'green gate')
208
+ else if (summary === 'green gate')
165
209
  snapshot.greenReminded = true;
166
- else if (source.summary === 'delivery report')
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 plugin-sourced notice message carrying the given prose. */
298
+ /** A producer-owned notice message carrying the given prose. */
255
299
  function notice(summary, text) {
256
- const source = {
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.13",
3
+ "version": "0.9.15",
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": {
@@ -84,7 +84,7 @@
84
84
  "license": "Apache-2.0",
85
85
  "engines": {
86
86
  "node": "^22.19.0 || >=24.0.0",
87
- "dsh": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0"
87
+ "dsh": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0 || >=0.1.7-0 <0.2.0"
88
88
  },
89
89
  "dsh": {
90
90
  "manifestVersion": 1,
@@ -116,7 +116,9 @@
116
116
  "compatibility": {
117
117
  "dshVersions": [
118
118
  "0.1.2-rc.1",
119
- "0.1.5-rc.2"
119
+ "0.1.5-rc.2",
120
+ "0.1.7-alpha.1",
121
+ "0.1.7-alpha.2"
120
122
  ]
121
123
  },
122
124
  "capability": {
@@ -133,15 +135,15 @@
133
135
  }
134
136
  },
135
137
  "peerDependencies": {
136
- "@deepseek-ai/cordis": "^4.0.2",
137
- "@deepseek-ai/dsh-agent": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0",
138
- "@deepseek-ai/dsh-commands": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0",
139
- "@deepseek-ai/dsh-invariants": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0",
140
- "@deepseek-ai/dsh-llm": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0",
141
- "@deepseek-ai/dsh-session": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0",
142
- "@deepseek-ai/dsh-session-projection": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0",
143
- "@deepseek-ai/dsh-skill": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0",
144
- "@deepseek-ai/dsh-tools": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0",
138
+ "@deepseek-ai/cordis": "^4.0.4",
139
+ "@deepseek-ai/dsh-agent": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0 || >=0.1.7-0 <0.2.0",
140
+ "@deepseek-ai/dsh-commands": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0 || >=0.1.7-0 <0.2.0",
141
+ "@deepseek-ai/dsh-invariants": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0 || >=0.1.7-0 <0.2.0",
142
+ "@deepseek-ai/dsh-llm": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0 || >=0.1.7-0 <0.2.0",
143
+ "@deepseek-ai/dsh-session": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0 || >=0.1.7-0 <0.2.0",
144
+ "@deepseek-ai/dsh-session-projection": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0 || >=0.1.7-0 <0.2.0",
145
+ "@deepseek-ai/dsh-skill": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0 || >=0.1.7-0 <0.2.0",
146
+ "@deepseek-ai/dsh-tools": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0 || >=0.1.7-0 <0.2.0",
145
147
  "@deepseek-ai/schemastery": "^3.18.2"
146
148
  },
147
149
  "dependencies": {
@@ -149,21 +151,22 @@
149
151
  "zod": "^4.4.3"
150
152
  },
151
153
  "devDependencies": {
152
- "@deepseek-ai/cordis": "^4.0.2",
154
+ "@deepseek-ai/cordis": "^4.0.4",
153
155
  "@deepseek-ai/cordis-plugin-loader": "^1.0.3",
154
- "@deepseek-ai/dsh-agent": "0.1.5-rc.2",
155
- "@deepseek-ai/dsh-commands": "0.1.5-rc.2",
156
- "@deepseek-ai/dsh-fs": "0.1.5-rc.2",
157
- "@deepseek-ai/dsh-llm": "0.1.5-rc.2",
158
- "@deepseek-ai/dsh-session": "0.1.5-rc.2",
159
- "@deepseek-ai/dsh-session-projection": "0.1.5-rc.2",
160
- "@deepseek-ai/dsh-settings": "0.1.5-rc.2",
161
- "@deepseek-ai/dsh-skill": "0.1.5-rc.2",
162
- "@deepseek-ai/dsh-subagent": "0.1.5-rc.2",
163
- "@deepseek-ai/dsh-system-prompt": "0.1.5-rc.2",
164
- "@deepseek-ai/dsh-tools": "0.1.5-rc.2",
165
- "@deepseek-ai/dsh-workflow": "0.1.5-rc.2",
166
- "@deepseek-ai/schemastery": "^3.18.2",
156
+ "@deepseek-ai/dsh-agent": "0.1.7-alpha.2",
157
+ "@deepseek-ai/dsh-commands": "0.1.7-alpha.2",
158
+ "@deepseek-ai/dsh-fs": "0.1.7-alpha.2",
159
+ "@deepseek-ai/dsh-invariants": "0.1.7-alpha.2",
160
+ "@deepseek-ai/dsh-llm": "0.1.7-alpha.2",
161
+ "@deepseek-ai/dsh-session": "0.1.7-alpha.2",
162
+ "@deepseek-ai/dsh-session-projection": "0.1.7-alpha.2",
163
+ "@deepseek-ai/dsh-settings": "0.1.7-alpha.2",
164
+ "@deepseek-ai/dsh-skill": "0.1.7-alpha.2",
165
+ "@deepseek-ai/dsh-subagent": "0.1.7-alpha.2",
166
+ "@deepseek-ai/dsh-system-prompt": "0.1.7-alpha.2",
167
+ "@deepseek-ai/dsh-tools": "0.1.7-alpha.2",
168
+ "@deepseek-ai/dsh-workflow": "0.1.7-alpha.2",
169
+ "@deepseek-ai/schemastery": "^3.18.3",
167
170
  "@eslint/js": "^9.30.0",
168
171
  "@types/node": "^22.15.0",
169
172
  "@vitest/coverage-v8": "^3.2.0",