dsh-doublecheck 0.9.24 → 0.9.27
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 +30 -0
- package/README-es.md +143 -53
- package/README-hi.md +143 -53
- package/README-pt.md +134 -44
- package/README-zh.md +134 -44
- package/README.md +141 -51
- package/lib/domain/projection.d.ts +54 -1
- package/lib/domain/projection.js +149 -11
- package/lib/guard/activation.d.ts +31 -0
- package/lib/guard/activation.js +1 -0
- package/lib/guard/gate.d.ts +2 -0
- package/lib/guard/gate.js +33 -9
- package/lib/guard/index.d.ts +4 -0
- package/lib/guard/index.js +14 -2
- package/lib/guard/review.d.ts +2 -0
- package/lib/guard/review.js +31 -9
- package/lib/types.d.ts +17 -31
- package/lib/types.js +4 -10
- package/package.json +1 -1
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
* bump the edit count, and a `doublecheck-review` source record marks the
|
|
9
9
|
* delivery reviewed. Resumed and forked sessions fold to the same view.
|
|
10
10
|
*
|
|
11
|
+
* The WIRE value is not this state: it is one bounded single-line string this
|
|
12
|
+
* module renders (`renderDoublecheckLine`), because a JSON object of these
|
|
13
|
+
* eight fields is at best 126 characters and every generic one-line renderer
|
|
14
|
+
* truncates it — the old object view lost `gateVerdict`/`gateRedCount`
|
|
15
|
+
* outright. See {@link DOUBLECHECK_LINE_MAX}.
|
|
16
|
+
*
|
|
11
17
|
* @module dsh-doublecheck/domain/projection
|
|
12
18
|
*/
|
|
13
19
|
import type { SessionEvent } from '@deepseek-ai/dsh-session';
|
|
@@ -15,6 +21,28 @@ import { type TestRunDetection } from './evidence.js';
|
|
|
15
21
|
import { type DisciplineStage, type TestColor } from './stages.js';
|
|
16
22
|
import { type GateVerdict } from './gate.js';
|
|
17
23
|
import type { DoublecheckView } from '../types.js';
|
|
24
|
+
/**
|
|
25
|
+
* The hard budget of the wire line, in UTF-16 code units — the same unit
|
|
26
|
+
* `String#length` and every one-line renderer measure.
|
|
27
|
+
*
|
|
28
|
+
* 120 is not arbitrary: the WeChat bridge's generic projection renderer
|
|
29
|
+
* (`dsh-wechat` `renderProjectionValue`) keeps a value only while
|
|
30
|
+
* `value.length <= 120` and otherwise cuts it to 117 units plus `…`. The
|
|
31
|
+
* object-shaped view this projection used to emit was 131 units when EMPTY
|
|
32
|
+
* and 139 with a short goal, so it was truncated for every possible value and
|
|
33
|
+
* the two fields a user reads on a phone (`gateVerdict`, `gateRedCount`) were
|
|
34
|
+
* exactly the ones cut off. A single bounded line keeps the whole payload
|
|
35
|
+
* inside every renderer's first line.
|
|
36
|
+
*/
|
|
37
|
+
export declare const DOUBLECHECK_LINE_MAX = 120;
|
|
38
|
+
/**
|
|
39
|
+
* Longest `goal:` excerpt kept on the line, in UTF-16 code units. A goal is
|
|
40
|
+
* free text and is the one lossy field: it is sanitized to one line and
|
|
41
|
+
* clipped (never dropped silently — a clipped excerpt ends with `…`). The
|
|
42
|
+
* three fields a user reads first (`gate:`, `stage:`, `r<red>`) are never
|
|
43
|
+
* clipped.
|
|
44
|
+
*/
|
|
45
|
+
export declare const DOUBLECHECK_GOAL_MAX = 40;
|
|
18
46
|
/** The projection unit's plain-JSON fold state. */
|
|
19
47
|
export interface DoublecheckProjectionState {
|
|
20
48
|
/** The last discipline stage a successful call or test run reached. */
|
|
@@ -58,8 +86,33 @@ export declare function applyDoublecheckEvent(state: DoublecheckProjectionState,
|
|
|
58
86
|
*/
|
|
59
87
|
export declare function foldDoublecheckState(events: readonly SessionEvent[], detection: TestRunDetection): DoublecheckProjectionState;
|
|
60
88
|
/**
|
|
61
|
-
* Project the folded state to the wire payload.
|
|
89
|
+
* Project the folded state to the wire payload: one status line.
|
|
90
|
+
*
|
|
91
|
+
* The line is built in priority order and every part is bounded, so the total
|
|
92
|
+
* can never exceed {@link DOUBLECHECK_LINE_MAX}:
|
|
93
|
+
*
|
|
94
|
+
* 1. `gate:<verdict> r<red>` — the latest gate verdict and its red-light count.
|
|
95
|
+
* 2. `stage:<stage>` — the discipline stage reached on durable evidence.
|
|
96
|
+
* 3. `test:<color> edits:<n> spec:<yes|no> review:<yes|no>` — the supporting facts.
|
|
97
|
+
* 4. `goal:<excerpt>` — only when a spec exists and the excerpt is non-empty;
|
|
98
|
+
* clipped to whatever budget parts 1–3 left, at most
|
|
99
|
+
* {@link DOUBLECHECK_GOAL_MAX} units.
|
|
100
|
+
*
|
|
101
|
+
* The worst case of parts 1–3 is 84 units (`gate:deliverable r9999+`,
|
|
102
|
+
* `stage:design`, `test:green edits:99999+ spec:yes review:yes`), leaving at
|
|
103
|
+
* least 28 units for the goal excerpt — so no part is ever dropped to make
|
|
104
|
+
* room, and a final hard clamp backstops the invariant.
|
|
62
105
|
* @param state - the folded state.
|
|
63
106
|
* @returns the whole current value for the `doublecheck` projection key.
|
|
64
107
|
*/
|
|
65
108
|
export declare function viewDoublecheck(state: DoublecheckProjectionState): DoublecheckView;
|
|
109
|
+
/**
|
|
110
|
+
* Render the fold state as the bounded single-line wire value.
|
|
111
|
+
*
|
|
112
|
+
* Pure and total: any state (including a hand-built or version-mismatched one
|
|
113
|
+
* with absurd counts) yields a one-line string of at most
|
|
114
|
+
* {@link DOUBLECHECK_LINE_MAX} code units, never a throw.
|
|
115
|
+
* @param state - the folded state.
|
|
116
|
+
* @returns one line, `<= DOUBLECHECK_LINE_MAX` code units, with no newline.
|
|
117
|
+
*/
|
|
118
|
+
export declare function renderDoublecheckLine(state: DoublecheckProjectionState): string;
|
package/lib/domain/projection.js
CHANGED
|
@@ -8,11 +8,46 @@
|
|
|
8
8
|
* bump the edit count, and a `doublecheck-review` source record marks the
|
|
9
9
|
* delivery reviewed. Resumed and forked sessions fold to the same view.
|
|
10
10
|
*
|
|
11
|
+
* The WIRE value is not this state: it is one bounded single-line string this
|
|
12
|
+
* module renders (`renderDoublecheckLine`), because a JSON object of these
|
|
13
|
+
* eight fields is at best 126 characters and every generic one-line renderer
|
|
14
|
+
* truncates it — the old object view lost `gateVerdict`/`gateRedCount`
|
|
15
|
+
* outright. See {@link DOUBLECHECK_LINE_MAX}.
|
|
16
|
+
*
|
|
11
17
|
* @module dsh-doublecheck/domain/projection
|
|
12
18
|
*/
|
|
13
19
|
import { isTestCommand, isTestFilePath, joinTextBlocks, mutationTargetPath, parseRawArguments, ptcSettle, shellCommand, testOutcome, } from "./evidence.js";
|
|
14
20
|
import { SPEC_TOOL_NAME } from "./stages.js";
|
|
15
21
|
import { countRedChecks } from "./gate.js";
|
|
22
|
+
/**
|
|
23
|
+
* The hard budget of the wire line, in UTF-16 code units — the same unit
|
|
24
|
+
* `String#length` and every one-line renderer measure.
|
|
25
|
+
*
|
|
26
|
+
* 120 is not arbitrary: the WeChat bridge's generic projection renderer
|
|
27
|
+
* (`dsh-wechat` `renderProjectionValue`) keeps a value only while
|
|
28
|
+
* `value.length <= 120` and otherwise cuts it to 117 units plus `…`. The
|
|
29
|
+
* object-shaped view this projection used to emit was 131 units when EMPTY
|
|
30
|
+
* and 139 with a short goal, so it was truncated for every possible value and
|
|
31
|
+
* the two fields a user reads on a phone (`gateVerdict`, `gateRedCount`) were
|
|
32
|
+
* exactly the ones cut off. A single bounded line keeps the whole payload
|
|
33
|
+
* inside every renderer's first line.
|
|
34
|
+
*/
|
|
35
|
+
export const DOUBLECHECK_LINE_MAX = 120;
|
|
36
|
+
/**
|
|
37
|
+
* Longest `goal:` excerpt kept on the line, in UTF-16 code units. A goal is
|
|
38
|
+
* free text and is the one lossy field: it is sanitized to one line and
|
|
39
|
+
* clipped (never dropped silently — a clipped excerpt ends with `…`). The
|
|
40
|
+
* three fields a user reads first (`gate:`, `stage:`, `r<red>`) are never
|
|
41
|
+
* clipped.
|
|
42
|
+
*/
|
|
43
|
+
export const DOUBLECHECK_GOAL_MAX = 40;
|
|
44
|
+
/** Segment separator: visually distinct on a phone and outside the goal word charset. */
|
|
45
|
+
const SEGMENT = ' | ';
|
|
46
|
+
/** Marks a value this line deliberately shortened. */
|
|
47
|
+
const ELLIPSIS = '…';
|
|
48
|
+
/** Above these counts the line prints `<max>+` instead of the exact number. */
|
|
49
|
+
const RED_COUNT_MAX = 9999;
|
|
50
|
+
const EDIT_COUNT_MAX = 99999;
|
|
16
51
|
/** State for the empty log. */
|
|
17
52
|
export function emptyDoublecheckState() {
|
|
18
53
|
return {
|
|
@@ -130,19 +165,122 @@ export function foldDoublecheckState(events, detection) {
|
|
|
130
165
|
return state;
|
|
131
166
|
}
|
|
132
167
|
/**
|
|
133
|
-
* Project the folded state to the wire payload.
|
|
168
|
+
* Project the folded state to the wire payload: one status line.
|
|
169
|
+
*
|
|
170
|
+
* The line is built in priority order and every part is bounded, so the total
|
|
171
|
+
* can never exceed {@link DOUBLECHECK_LINE_MAX}:
|
|
172
|
+
*
|
|
173
|
+
* 1. `gate:<verdict> r<red>` — the latest gate verdict and its red-light count.
|
|
174
|
+
* 2. `stage:<stage>` — the discipline stage reached on durable evidence.
|
|
175
|
+
* 3. `test:<color> edits:<n> spec:<yes|no> review:<yes|no>` — the supporting facts.
|
|
176
|
+
* 4. `goal:<excerpt>` — only when a spec exists and the excerpt is non-empty;
|
|
177
|
+
* clipped to whatever budget parts 1–3 left, at most
|
|
178
|
+
* {@link DOUBLECHECK_GOAL_MAX} units.
|
|
179
|
+
*
|
|
180
|
+
* The worst case of parts 1–3 is 84 units (`gate:deliverable r9999+`,
|
|
181
|
+
* `stage:design`, `test:green edits:99999+ spec:yes review:yes`), leaving at
|
|
182
|
+
* least 28 units for the goal excerpt — so no part is ever dropped to make
|
|
183
|
+
* room, and a final hard clamp backstops the invariant.
|
|
134
184
|
* @param state - the folded state.
|
|
135
185
|
* @returns the whole current value for the `doublecheck` projection key.
|
|
136
186
|
*/
|
|
137
187
|
export function viewDoublecheck(state) {
|
|
138
|
-
return
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
188
|
+
return renderDoublecheckLine(state);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Render the fold state as the bounded single-line wire value.
|
|
192
|
+
*
|
|
193
|
+
* Pure and total: any state (including a hand-built or version-mismatched one
|
|
194
|
+
* with absurd counts) yields a one-line string of at most
|
|
195
|
+
* {@link DOUBLECHECK_LINE_MAX} code units, never a throw.
|
|
196
|
+
* @param state - the folded state.
|
|
197
|
+
* @returns one line, `<= DOUBLECHECK_LINE_MAX` code units, with no newline.
|
|
198
|
+
*/
|
|
199
|
+
export function renderDoublecheckLine(state) {
|
|
200
|
+
const head = [
|
|
201
|
+
`gate:${state.gateVerdict} r${boundedCount(state.gateRedCount, RED_COUNT_MAX)}`,
|
|
202
|
+
`stage:${state.stage}`,
|
|
203
|
+
`test:${state.color} edits:${boundedCount(state.editCount, EDIT_COUNT_MAX)}`
|
|
204
|
+
+ ` spec:${state.hasSpec ? 'yes' : 'no'} review:${state.reviewed ? 'yes' : 'no'}`,
|
|
205
|
+
].join(SEGMENT);
|
|
206
|
+
// A goal without a committed spec would contradict `spec:no`.
|
|
207
|
+
const goal = state.hasSpec ? sanitizeGoal(state.specGoal) : '';
|
|
208
|
+
if (goal === '')
|
|
209
|
+
return clampLine(head);
|
|
210
|
+
const budget = DOUBLECHECK_LINE_MAX - head.length - SEGMENT.length - 'goal:'.length;
|
|
211
|
+
if (budget <= 0)
|
|
212
|
+
return clampLine(head);
|
|
213
|
+
const excerpt = capText(goal, Math.min(budget, DOUBLECHECK_GOAL_MAX));
|
|
214
|
+
if (excerpt === '')
|
|
215
|
+
return clampLine(head);
|
|
216
|
+
return clampLine(`${head}${SEGMENT}goal:${excerpt}`);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Render a count for the line: non-finite and negative inputs fold to `0`, and
|
|
220
|
+
* a count above `maximum` prints `<maximum>+` so the field stays bounded.
|
|
221
|
+
* @param value - the folded count.
|
|
222
|
+
* @param maximum - the largest exact count the line prints.
|
|
223
|
+
* @returns a bounded decimal string.
|
|
224
|
+
*/
|
|
225
|
+
function boundedCount(value, maximum) {
|
|
226
|
+
if (!Number.isFinite(value))
|
|
227
|
+
return '0';
|
|
228
|
+
const count = Math.max(0, Math.trunc(value));
|
|
229
|
+
return count > maximum ? `${maximum}+` : String(count);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Flatten a free-text goal to one safe line: control characters (including
|
|
233
|
+
* newlines and the C1 range) become spaces, the segment separator becomes `/`,
|
|
234
|
+
* whitespace runs collapse, and the result is trimmed. The wire value must be
|
|
235
|
+
* ONE line — the WeChat renderer keeps only the first line of a multi-line
|
|
236
|
+
* value. `\p{Cc}` is the Unicode control category (U+0000–U+001F and
|
|
237
|
+
* U+007F–U+009F), so no literal control character appears in this pattern.
|
|
238
|
+
* @param goal - the raw committed spec goal.
|
|
239
|
+
* @returns the sanitized single-line goal, possibly empty.
|
|
240
|
+
*/
|
|
241
|
+
function sanitizeGoal(goal) {
|
|
242
|
+
return goal
|
|
243
|
+
.replace(/\p{Cc}/gu, ' ')
|
|
244
|
+
.replace(/\|/gu, '/')
|
|
245
|
+
.replace(/\s+/gu, ' ')
|
|
246
|
+
.trim();
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Cut `text` to at most `budget` code units, marking a real cut with `…` and
|
|
250
|
+
* never splitting a surrogate pair.
|
|
251
|
+
* @param text - the text to bound.
|
|
252
|
+
* @param budget - the maximum length of the result.
|
|
253
|
+
* @returns the text, or a `…`-terminated prefix of it.
|
|
254
|
+
*/
|
|
255
|
+
function capText(text, budget) {
|
|
256
|
+
if (budget <= 0)
|
|
257
|
+
return '';
|
|
258
|
+
if (text.length <= budget)
|
|
259
|
+
return text;
|
|
260
|
+
if (budget <= ELLIPSIS.length)
|
|
261
|
+
return ELLIPSIS;
|
|
262
|
+
return `${text.slice(0, safeEnd(text, budget - ELLIPSIS.length))}${ELLIPSIS}`;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Backstop for {@link DOUBLECHECK_LINE_MAX}: the line is bounded by
|
|
266
|
+
* construction, so this is unreachable today and exists so a future field
|
|
267
|
+
* cannot escape the budget silently.
|
|
268
|
+
* @param line - the assembled line.
|
|
269
|
+
* @returns the line, or a `…`-terminated prefix within budget.
|
|
270
|
+
*/
|
|
271
|
+
function clampLine(line) {
|
|
272
|
+
return capText(line, DOUBLECHECK_LINE_MAX);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* The largest cut index that does not split a UTF-16 surrogate pair: a lone
|
|
276
|
+
* surrogate is rejected by the WeChat transport (`ret=-1 invalid request`).
|
|
277
|
+
* @param text - the text being cut.
|
|
278
|
+
* @param end - the proposed cut index.
|
|
279
|
+
* @returns `end`, or `end - 1` when it would split a pair.
|
|
280
|
+
*/
|
|
281
|
+
function safeEnd(text, end) {
|
|
282
|
+
if (end <= 0)
|
|
283
|
+
return 0;
|
|
284
|
+
const previous = text.charCodeAt(end - 1);
|
|
285
|
+
return previous >= 0xD800 && previous <= 0xDBFF ? end - 1 : end;
|
|
148
286
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal shape of the 0.2.1+ `subagents.startActivation` surface. The alpha
|
|
3
|
+
* host line only exposes `start(provider, request)`; keeping this structural
|
|
4
|
+
* lets the same guard code compile against both.
|
|
5
|
+
*/
|
|
6
|
+
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
7
|
+
import type { ContentBlock } from '@deepseek-ai/dsh-llm';
|
|
8
|
+
import type { SessionId } from '@deepseek-ai/dsh-session';
|
|
9
|
+
export interface GuardActivationService {
|
|
10
|
+
startActivation?: (spec: {
|
|
11
|
+
provider: string;
|
|
12
|
+
label: string;
|
|
13
|
+
request: {
|
|
14
|
+
prompt: ContentBlock[];
|
|
15
|
+
parent: Agent;
|
|
16
|
+
outputSchema: unknown;
|
|
17
|
+
maxDepth?: number;
|
|
18
|
+
agentOptions?: Record<string, unknown>;
|
|
19
|
+
agentPreset: string;
|
|
20
|
+
};
|
|
21
|
+
signal: AbortSignal;
|
|
22
|
+
delivery: 'caller';
|
|
23
|
+
}) => Promise<{
|
|
24
|
+
childId: SessionId;
|
|
25
|
+
result: Promise<{
|
|
26
|
+
stopReason: string;
|
|
27
|
+
structured?: unknown;
|
|
28
|
+
}>;
|
|
29
|
+
dispose(): Promise<void>;
|
|
30
|
+
}>;
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/lib/guard/gate.d.ts
CHANGED
|
@@ -38,6 +38,8 @@ export interface GateReviewerConfig {
|
|
|
38
38
|
provider: string;
|
|
39
39
|
/** Model route for the local reviewer; null means the main model reviews. */
|
|
40
40
|
model: string | null;
|
|
41
|
+
/** Read-only Agent Preset for the startActivation path. */
|
|
42
|
+
preset?: string;
|
|
41
43
|
/** Tools the local reviewer may call (read-only by default). */
|
|
42
44
|
tools: string[];
|
|
43
45
|
/** Hard time budget for one local reviewer run. */
|
package/lib/guard/gate.js
CHANGED
|
@@ -114,6 +114,7 @@ export const GateConfigSchema = z.object({
|
|
|
114
114
|
consistency: z.object({
|
|
115
115
|
enabled: z.boolean().default(true),
|
|
116
116
|
provider: z.string().min(1).default('fork'),
|
|
117
|
+
preset: z.string(),
|
|
117
118
|
model: z.union([z.string(), z.const(null)]).default(null),
|
|
118
119
|
tools: z.array(z.string()).default(['read', 'glob', 'grep']),
|
|
119
120
|
timeoutMs: z.number().default(120000),
|
|
@@ -123,6 +124,7 @@ export const GateConfigSchema = z.object({
|
|
|
123
124
|
enabled: z.boolean().default(true),
|
|
124
125
|
engine: z.union(['auto', 'local']).default('auto'),
|
|
125
126
|
provider: z.string().min(1).default('fork'),
|
|
127
|
+
preset: z.string(),
|
|
126
128
|
model: z.union([z.string(), z.const(null)]).default(null),
|
|
127
129
|
tools: z.array(z.string()).default(['read', 'glob', 'grep']),
|
|
128
130
|
timeoutMs: z.number().default(120000),
|
|
@@ -248,15 +250,37 @@ async function runLocalReviewer(ctx, reviewer, agent, task, label, signal) {
|
|
|
248
250
|
const runSignal = AbortSignal.any([signal, timeout]);
|
|
249
251
|
let run;
|
|
250
252
|
try {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
253
|
+
const activation = subagents;
|
|
254
|
+
if (typeof activation.startActivation === 'function') {
|
|
255
|
+
if (reviewer.preset === undefined) {
|
|
256
|
+
return { findings: null, unavailable: 'this host uses subagents.startActivation; set the reviewer preset to a read-only Agent Preset' };
|
|
257
|
+
}
|
|
258
|
+
const started = await activation.startActivation({
|
|
259
|
+
provider: reviewer.provider,
|
|
260
|
+
label,
|
|
261
|
+
request: {
|
|
262
|
+
prompt: [{ type: 'text', text: task }],
|
|
263
|
+
parent: agent,
|
|
264
|
+
outputSchema: GATE_FINDINGS_SCHEMA,
|
|
265
|
+
agentPreset: reviewer.preset,
|
|
266
|
+
...reviewer.model !== null ? { agentOptions: { model: reviewer.model } } : {},
|
|
267
|
+
},
|
|
268
|
+
signal: runSignal,
|
|
269
|
+
delivery: 'caller',
|
|
270
|
+
});
|
|
271
|
+
run = started;
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
run = await subagents.start(reviewer.provider, {
|
|
275
|
+
label,
|
|
276
|
+
prompt: [{ type: 'text', text: task }],
|
|
277
|
+
parent: agent,
|
|
278
|
+
signal: runSignal,
|
|
279
|
+
...reviewer.model !== null ? { agentOptions: { model: reviewer.model } } : {},
|
|
280
|
+
outputSchema: GATE_FINDINGS_SCHEMA,
|
|
281
|
+
toolFilter: { allow: reviewer.tools },
|
|
282
|
+
});
|
|
283
|
+
}
|
|
260
284
|
}
|
|
261
285
|
catch (error) {
|
|
262
286
|
return { findings: null, unavailable: String(error) };
|
package/lib/guard/index.d.ts
CHANGED
|
@@ -58,6 +58,8 @@ export interface Config {
|
|
|
58
58
|
adversaryModel: string | null;
|
|
59
59
|
/** Subagent provider name the critic runs on (default `fork`). */
|
|
60
60
|
adversaryProvider: string;
|
|
61
|
+
/** Read-only Agent Preset for hosts that use startActivation. */
|
|
62
|
+
adversaryPreset?: string;
|
|
61
63
|
/** Maximum structured findings injected into the session. */
|
|
62
64
|
adversaryMaxFindings: number;
|
|
63
65
|
/** Tools the critic may call (read-only by default; never mutation tools). */
|
|
@@ -125,6 +127,7 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
125
127
|
}>>, "defined">;
|
|
126
128
|
adversaryModel: z<string | null, string | null, "defined">;
|
|
127
129
|
adversaryProvider: z<string, string, "defined">;
|
|
130
|
+
adversaryPreset: z<string, string, "plain">;
|
|
128
131
|
adversaryMaxFindings: z<number, number, "defined">;
|
|
129
132
|
adversaryTools: z<string[], string[], "defined">;
|
|
130
133
|
adversaryTimeoutMs: z<number, number, "defined">;
|
|
@@ -150,6 +153,7 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
150
153
|
}>>, "defined">;
|
|
151
154
|
adversaryModel: z<string | null, string | null, "defined">;
|
|
152
155
|
adversaryProvider: z<string, string, "defined">;
|
|
156
|
+
adversaryPreset: z<string, string, "plain">;
|
|
153
157
|
adversaryMaxFindings: z<number, number, "defined">;
|
|
154
158
|
adversaryTools: z<string[], string[], "defined">;
|
|
155
159
|
adversaryTimeoutMs: z<number, number, "defined">;
|
package/lib/guard/index.js
CHANGED
|
@@ -94,6 +94,7 @@ export const Config = z.object({
|
|
|
94
94
|
}).default({ grill: true, tdd: true, adversary: false }),
|
|
95
95
|
adversaryModel: z.union([z.string(), z.const(null)]).default(null),
|
|
96
96
|
adversaryProvider: z.string().min(1).default('fork'),
|
|
97
|
+
adversaryPreset: z.string(),
|
|
97
98
|
adversaryMaxFindings: z.number().default(5),
|
|
98
99
|
adversaryTools: z.array(z.string()).default(['read', 'glob', 'grep']),
|
|
99
100
|
adversaryTimeoutMs: z.number().default(120000),
|
|
@@ -577,7 +578,8 @@ export function apply(ctx, config) {
|
|
|
577
578
|
}
|
|
578
579
|
});
|
|
579
580
|
// The `doublecheck` session projection: folds the same durable discipline
|
|
580
|
-
// facts the guard reads into
|
|
581
|
+
// facts the guard reads into the wire value a client renders — one bounded
|
|
582
|
+
// status line (`renderDoublecheckLine`), not a JSON object.
|
|
581
583
|
// Activates only when a projection registry is composed.
|
|
582
584
|
ctx.inject(['sessionProjections'], (projectionCtx) => {
|
|
583
585
|
const registry = projectionCtx.get('sessionProjections');
|
|
@@ -597,7 +599,16 @@ export function apply(ctx, config) {
|
|
|
597
599
|
viewSchema: doublecheckViewSchema,
|
|
598
600
|
view: viewDoublecheck,
|
|
599
601
|
},
|
|
600
|
-
|
|
602
|
+
// v3: the wire value changed type (object -> one bounded line). The
|
|
603
|
+
// bump is the persisted-cache invalidation the registry documents, and
|
|
604
|
+
// it IS the backward-compatibility story: the state fields themselves
|
|
605
|
+
// are unchanged and still pass `doublecheckStateSchema`, but a row
|
|
606
|
+
// written at `ver: 2` is discarded on a `ver: 3` unit and the key
|
|
607
|
+
// refolds from `init` over the full log (the host's `restoreFloor`
|
|
608
|
+
// drops to seq 0 on any version mismatch). No migration code, and a
|
|
609
|
+
// pre-upgrade session degrades to a recomputed line instead of a
|
|
610
|
+
// stale one or a crash.
|
|
611
|
+
stateVersion: 3,
|
|
601
612
|
}), 'dsh-doublecheck: projection');
|
|
602
613
|
});
|
|
603
614
|
// The invariant companion, registered from the main plugin so its checks
|
|
@@ -616,6 +627,7 @@ export function apply(ctx, config) {
|
|
|
616
627
|
function adversaryConfig(config) {
|
|
617
628
|
return {
|
|
618
629
|
adversaryProvider: config.adversaryProvider,
|
|
630
|
+
adversaryPreset: config.adversaryPreset,
|
|
619
631
|
adversaryModel: config.adversaryModel ?? null,
|
|
620
632
|
adversaryMaxFindings: config.adversaryMaxFindings,
|
|
621
633
|
adversaryTools: config.adversaryTools,
|
package/lib/guard/review.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ import { type GuardProse, type ProseLanguage } from './prose.js';
|
|
|
21
21
|
/** The adversary knobs the review runner reads. */
|
|
22
22
|
export interface AdversaryConfig {
|
|
23
23
|
adversaryProvider: string;
|
|
24
|
+
/** Read-only Agent Preset for the startActivation path. */
|
|
25
|
+
adversaryPreset?: string;
|
|
24
26
|
adversaryModel: string | null;
|
|
25
27
|
adversaryMaxFindings: number;
|
|
26
28
|
adversaryTools: string[];
|
package/lib/guard/review.js
CHANGED
|
@@ -99,15 +99,37 @@ export async function runAdversaryReview(ctx, config, agent, turnSignal) {
|
|
|
99
99
|
const signal = turnSignal === undefined ? timeout : AbortSignal.any([turnSignal, timeout]);
|
|
100
100
|
let run;
|
|
101
101
|
try {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
102
|
+
const activation = subagents;
|
|
103
|
+
if (typeof activation.startActivation === 'function') {
|
|
104
|
+
if (config.adversaryPreset === undefined) {
|
|
105
|
+
return { verdict: 'unavailable', findings: [], text: prose.reviewUnavailableFailed('this host uses subagents.startActivation; set adversaryPreset to a read-only Agent Preset') };
|
|
106
|
+
}
|
|
107
|
+
const started = await activation.startActivation({
|
|
108
|
+
provider: config.adversaryProvider,
|
|
109
|
+
label: 'doublecheck-adversary',
|
|
110
|
+
request: {
|
|
111
|
+
prompt: [{ type: 'text', text: prose.criticTask }],
|
|
112
|
+
parent: agent,
|
|
113
|
+
outputSchema: REVIEW_OUTPUT_SCHEMA,
|
|
114
|
+
agentPreset: config.adversaryPreset,
|
|
115
|
+
...config.adversaryModel !== null ? { agentOptions: { model: config.adversaryModel } } : {},
|
|
116
|
+
},
|
|
117
|
+
signal,
|
|
118
|
+
delivery: 'caller',
|
|
119
|
+
});
|
|
120
|
+
run = started;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
run = await subagents.start(config.adversaryProvider, {
|
|
124
|
+
label: 'doublecheck-adversary',
|
|
125
|
+
prompt: [{ type: 'text', text: prose.criticTask }],
|
|
126
|
+
parent: agent,
|
|
127
|
+
signal,
|
|
128
|
+
...config.adversaryModel !== null ? { agentOptions: { model: config.adversaryModel } } : {},
|
|
129
|
+
outputSchema: REVIEW_OUTPUT_SCHEMA,
|
|
130
|
+
toolFilter: { allow: config.adversaryTools },
|
|
131
|
+
});
|
|
132
|
+
}
|
|
111
133
|
}
|
|
112
134
|
catch (error) {
|
|
113
135
|
// Provider absence, capability rejection, or a child that could not be
|
package/lib/types.d.ts
CHANGED
|
@@ -7,39 +7,25 @@
|
|
|
7
7
|
* @module dsh-doublecheck/types
|
|
8
8
|
*/
|
|
9
9
|
import { z as zod } from 'zod';
|
|
10
|
-
import type { DisciplineStage, TestColor } from './domain/stages.js';
|
|
11
|
-
import type { GateVerdict } from './domain/gate.js';
|
|
12
10
|
import type { DoublecheckProjectionState } from './domain/projection.js';
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
/** Red-light (failing) checks of the latest gate run. */
|
|
30
|
-
gateRedCount: number;
|
|
31
|
-
}
|
|
11
|
+
/**
|
|
12
|
+
* The whole wire value of the `doublecheck` session projection: ONE bounded
|
|
13
|
+
* status line, e.g.
|
|
14
|
+
*
|
|
15
|
+
* ```text
|
|
16
|
+
* gate:rework r2 | stage:green | test:green edits:12 spec:yes review:yes | goal:ship parser fix
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* It is a string rather than an object on purpose. The eight state fields cost
|
|
20
|
+
* 126 characters as compact JSON even at their shortest possible values, so no
|
|
21
|
+
* object-shaped payload can survive a renderer that keeps one line under 121
|
|
22
|
+
* characters — which is exactly how `gateVerdict` and `gateRedCount` used to
|
|
23
|
+
* disappear. `renderDoublecheckLine` (`./domain/projection.ts`) owns the
|
|
24
|
+
* format and the {@link DOUBLECHECK_LINE_MAX} budget.
|
|
25
|
+
*/
|
|
26
|
+
export type DoublecheckView = string;
|
|
32
27
|
/** Validates the `doublecheck` projection's wire payload before it leaves the host. */
|
|
33
|
-
export declare const doublecheckViewSchema: zod.
|
|
34
|
-
stage: zod.ZodUnion<readonly [zod.ZodLiteral<"grill">, zod.ZodLiteral<"design">, zod.ZodLiteral<"red">, zod.ZodLiteral<"green">, zod.ZodLiteral<"review">, zod.ZodLiteral<"verify">]>;
|
|
35
|
-
color: zod.ZodUnion<readonly [zod.ZodLiteral<"none">, zod.ZodLiteral<"red">, zod.ZodLiteral<"green">]>;
|
|
36
|
-
hasSpec: zod.ZodBoolean;
|
|
37
|
-
specGoal: zod.ZodString;
|
|
38
|
-
reviewed: zod.ZodBoolean;
|
|
39
|
-
editCount: zod.ZodNumber;
|
|
40
|
-
gateVerdict: zod.ZodUnion<readonly [zod.ZodLiteral<"none">, zod.ZodLiteral<"deliverable">, zod.ZodLiteral<"rework">]>;
|
|
41
|
-
gateRedCount: zod.ZodNumber;
|
|
42
|
-
}, zod.core.$strip>;
|
|
28
|
+
export declare const doublecheckViewSchema: zod.ZodString;
|
|
43
29
|
/** Validates the persisted `doublecheck` projection state before it seeds a fold. */
|
|
44
30
|
export declare const doublecheckStateSchema: zod.ZodObject<{
|
|
45
31
|
stage: zod.ZodUnion<readonly [zod.ZodLiteral<"grill">, zod.ZodLiteral<"design">, zod.ZodLiteral<"red">, zod.ZodLiteral<"green">, zod.ZodLiteral<"review">, zod.ZodLiteral<"verify">]>;
|
package/lib/types.js
CHANGED
|
@@ -7,17 +7,11 @@
|
|
|
7
7
|
* @module dsh-doublecheck/types
|
|
8
8
|
*/
|
|
9
9
|
import { z as zod } from 'zod';
|
|
10
|
+
// Value import (the one runtime edge in this type table): the wire line's
|
|
11
|
+
// budget is declared once, next to the renderer that must honor it.
|
|
12
|
+
import { DOUBLECHECK_LINE_MAX } from "./domain/projection.js";
|
|
10
13
|
/** Validates the `doublecheck` projection's wire payload before it leaves the host. */
|
|
11
|
-
export const doublecheckViewSchema = zod.
|
|
12
|
-
stage: zod.union([zod.literal('grill'), zod.literal('design'), zod.literal('red'), zod.literal('green'), zod.literal('review'), zod.literal('verify')]),
|
|
13
|
-
color: zod.union([zod.literal('none'), zod.literal('red'), zod.literal('green')]),
|
|
14
|
-
hasSpec: zod.boolean(),
|
|
15
|
-
specGoal: zod.string(),
|
|
16
|
-
reviewed: zod.boolean(),
|
|
17
|
-
editCount: zod.number(),
|
|
18
|
-
gateVerdict: zod.union([zod.literal('none'), zod.literal('deliverable'), zod.literal('rework')]),
|
|
19
|
-
gateRedCount: zod.number(),
|
|
20
|
-
});
|
|
14
|
+
export const doublecheckViewSchema = zod.string().max(DOUBLECHECK_LINE_MAX);
|
|
21
15
|
/** Validates the persisted `doublecheck` projection state before it seeds a fold. */
|
|
22
16
|
export const doublecheckStateSchema = zod.object({
|
|
23
17
|
stage: zod.union([zod.literal('grill'), zod.literal('design'), zod.literal('red'), zod.literal('green'), zod.literal('review'), zod.literal('verify')]),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-doublecheck",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.27",
|
|
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": {
|