dsh-doublecheck 0.5.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/CHANGELOG.md +65 -0
- package/LICENSE +201 -0
- package/README.es.md +252 -0
- package/README.hi.md +252 -0
- package/README.md +268 -0
- package/README.pt.md +252 -0
- package/README.zh.md +266 -0
- package/cordis.patch.yml +19 -0
- package/lib/domain/evidence.d.ts +100 -0
- package/lib/domain/evidence.js +180 -0
- package/lib/domain/projection.d.ts +60 -0
- package/lib/domain/projection.js +132 -0
- package/lib/domain/report.d.ts +93 -0
- package/lib/domain/report.js +266 -0
- package/lib/domain/stages.d.ts +98 -0
- package/lib/domain/stages.js +192 -0
- package/lib/domain/vagueness.d.ts +25 -0
- package/lib/domain/vagueness.js +56 -0
- package/lib/domain/vocabulary.d.ts +50 -0
- package/lib/domain/vocabulary.js +16 -0
- package/lib/events.d.ts +155 -0
- package/lib/events.js +15 -0
- package/lib/grill/index.d.ts +49 -0
- package/lib/grill/index.js +495 -0
- package/lib/grill/provider.d.ts +56 -0
- package/lib/grill/provider.js +189 -0
- package/lib/guard/command.d.ts +47 -0
- package/lib/guard/command.js +132 -0
- package/lib/guard/index.d.ts +111 -0
- package/lib/guard/index.js +516 -0
- package/lib/guard/prose.d.ts +34 -0
- package/lib/guard/prose.js +80 -0
- package/lib/guard/review.d.ts +67 -0
- package/lib/guard/review.js +184 -0
- package/lib/index.d.ts +17 -0
- package/lib/index.js +15 -0
- package/lib/invariant.d.ts +56 -0
- package/lib/invariant.js +112 -0
- package/lib/types.d.ts +39 -0
- package/lib/types.js +18 -0
- package/package.json +109 -0
- package/skills/delivery-proof/SKILL.md +52 -0
- package/skills/delivery-review/SKILL.md +49 -0
- package/skills/grill-requirements/SKILL.md +98 -0
- package/skills/red-green-tdd/SKILL.md +57 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundled skill provider for dsh-doublecheck's own `skills/` directory.
|
|
3
|
+
*
|
|
4
|
+
* The directory keeps the generic Agent Skills layout (`<name>/SKILL.md` with
|
|
5
|
+
* YAML frontmatter) so the assets stay liftable into any other Agent Skills
|
|
6
|
+
* ecosystem; this provider is the DSH-facing adapter that publishes them on
|
|
7
|
+
* `ctx.skills`.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-doublecheck/grill/provider
|
|
10
|
+
*/
|
|
11
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { BUNDLED_SKILL_RANK, isSkillName, } from '@deepseek-ai/dsh-skill';
|
|
15
|
+
/** Provider name under which the registry files these skills. */
|
|
16
|
+
export const PROVIDER_NAME = 'doublecheck';
|
|
17
|
+
/** Skill origin bucket: these are host-bundled package assets. */
|
|
18
|
+
const SOURCE = 'bundled';
|
|
19
|
+
/** Invocation policy: both the model and the human slash-command surface may use these skills. */
|
|
20
|
+
const INVOCATION = Object.freeze({ modelInvocable: true, userInvocable: true });
|
|
21
|
+
/** Package skills root, resolved identically from `src/` and the built `lib/`. */
|
|
22
|
+
const SKILLS_ROOT = fileURLToPath(new URL('../../skills/', import.meta.url));
|
|
23
|
+
/**
|
|
24
|
+
* SkillProvider publishing the package's bundled `skills/` directory. List
|
|
25
|
+
* parses the frontmatter of every skill's SKILL.md; get re-reads the body so
|
|
26
|
+
* on-disk edits appear on the next load. A malformed asset is reported as an
|
|
27
|
+
* unreadable skill (skipped, logged), matching the registry's provider
|
|
28
|
+
* containment contract.
|
|
29
|
+
*/
|
|
30
|
+
export class BundledSkillProvider {
|
|
31
|
+
name = PROVIDER_NAME;
|
|
32
|
+
ctx;
|
|
33
|
+
constructor(ctx) {
|
|
34
|
+
this.ctx = ctx;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Discover the bundled skill candidates.
|
|
38
|
+
* @param options - lookup options; `signal` cancels directory and file reads.
|
|
39
|
+
* @returns the complete candidate list for the bundled root.
|
|
40
|
+
*/
|
|
41
|
+
async list(options) {
|
|
42
|
+
// A bundled-root listing is a single directory read plus one parallel file
|
|
43
|
+
// read per skill; the abort signal still guards every read through `parse`.
|
|
44
|
+
const entries = await readdir(SKILLS_ROOT, { withFileTypes: true });
|
|
45
|
+
const parsedEntries = await Promise.all(entries
|
|
46
|
+
.filter(entry => entry.isDirectory())
|
|
47
|
+
.map(async (entry) => {
|
|
48
|
+
const directory = join(SKILLS_ROOT, entry.name);
|
|
49
|
+
const file = join(directory, 'SKILL.md');
|
|
50
|
+
const parsed = await this.parse(file, options.signal);
|
|
51
|
+
return parsed === undefined ? undefined : { parsed, file, directory };
|
|
52
|
+
}));
|
|
53
|
+
const candidates = [];
|
|
54
|
+
for (const entry of parsedEntries) {
|
|
55
|
+
if (entry === undefined)
|
|
56
|
+
continue;
|
|
57
|
+
const { parsed, file, directory } = entry;
|
|
58
|
+
candidates.push({
|
|
59
|
+
name: parsed.name,
|
|
60
|
+
description: parsed.description,
|
|
61
|
+
...parsed.whenToUse !== undefined ? { whenToUse: parsed.whenToUse } : {},
|
|
62
|
+
invocation: INVOCATION,
|
|
63
|
+
provider: PROVIDER_NAME,
|
|
64
|
+
source: SOURCE,
|
|
65
|
+
rank: BUNDLED_SKILL_RANK,
|
|
66
|
+
locator: { file, directory },
|
|
67
|
+
resourceBase: { kind: 'directory', path: directory },
|
|
68
|
+
path: file,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return candidates.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Load a complete bundled skill body.
|
|
75
|
+
* @param candidate - a candidate previously returned by {@link list}.
|
|
76
|
+
* @param options - lookup options; `signal` cancels the read.
|
|
77
|
+
* @returns the full skill definition, or `undefined` when the file no longer parses.
|
|
78
|
+
*/
|
|
79
|
+
async get(candidate, options) {
|
|
80
|
+
const locator = candidate.locator;
|
|
81
|
+
const parsed = await this.parse(locator.file, options.signal);
|
|
82
|
+
if (parsed === undefined)
|
|
83
|
+
return undefined;
|
|
84
|
+
return {
|
|
85
|
+
name: parsed.name,
|
|
86
|
+
description: parsed.description,
|
|
87
|
+
...parsed.whenToUse !== undefined ? { whenToUse: parsed.whenToUse } : {},
|
|
88
|
+
invocation: INVOCATION,
|
|
89
|
+
provider: PROVIDER_NAME,
|
|
90
|
+
source: SOURCE,
|
|
91
|
+
resourceBase: { kind: 'directory', path: locator.directory },
|
|
92
|
+
path: locator.file,
|
|
93
|
+
content: parsed.content,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
async parse(file, signal) {
|
|
97
|
+
let raw;
|
|
98
|
+
try {
|
|
99
|
+
raw = await readFile(file, { encoding: 'utf8', signal });
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (signal?.aborted === true)
|
|
103
|
+
throw error;
|
|
104
|
+
// A directory entry without SKILL.md is not a skill; any other read
|
|
105
|
+
// failure on our own bundled asset is an installation defect and
|
|
106
|
+
// propagates to the registry's provider containment.
|
|
107
|
+
if (hasErrorCode(error, 'ENOENT') || hasErrorCode(error, 'ENOTDIR'))
|
|
108
|
+
return undefined;
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
const parsed = parseSkillAsset(raw);
|
|
112
|
+
if (parsed === undefined) {
|
|
113
|
+
this.ctx.logger.warn(`dsh-doublecheck: bundled skill file ${file} is malformed and was skipped`);
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
if (!isSkillName(parsed.name)) {
|
|
117
|
+
this.ctx.logger.warn(`dsh-doublecheck: bundled skill file ${file} has invalid skill name "${parsed.name}" and was skipped`);
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
return parsed;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Parse a bundled SKILL.md asset: YAML-frontmatter scalars plus the body.
|
|
125
|
+
* Only the three fields this package ships are read; the frontmatter itself
|
|
126
|
+
* stays standard Agent Skills YAML.
|
|
127
|
+
* @param raw - the full file text.
|
|
128
|
+
* @returns the parsed skill, or `undefined` when the file has no valid frontmatter.
|
|
129
|
+
*/
|
|
130
|
+
export function parseSkillAsset(raw) {
|
|
131
|
+
const firstLineEnd = raw.indexOf('\n');
|
|
132
|
+
if (firstLineEnd < 0)
|
|
133
|
+
return undefined;
|
|
134
|
+
const firstLine = raw.slice(0, firstLineEnd).replace(/\r$/, '');
|
|
135
|
+
if (firstLine !== '---')
|
|
136
|
+
return undefined;
|
|
137
|
+
const closing = findClosingFence(raw, firstLineEnd + 1);
|
|
138
|
+
if (closing === undefined)
|
|
139
|
+
return undefined;
|
|
140
|
+
const frontmatter = raw.slice(firstLineEnd + 1, closing.start);
|
|
141
|
+
const name = scalarField(frontmatter, 'name');
|
|
142
|
+
const description = scalarField(frontmatter, 'description');
|
|
143
|
+
if (name === undefined || description === undefined)
|
|
144
|
+
return undefined;
|
|
145
|
+
const whenToUse = scalarField(frontmatter, 'whenToUse');
|
|
146
|
+
return {
|
|
147
|
+
name,
|
|
148
|
+
description,
|
|
149
|
+
...whenToUse !== undefined ? { whenToUse } : {},
|
|
150
|
+
content: raw.slice(closing.bodyStart).trim(),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/** Locate the closing `---` fence line and the body start after it. */
|
|
154
|
+
function findClosingFence(raw, start) {
|
|
155
|
+
let lineStart = start;
|
|
156
|
+
while (lineStart <= raw.length) {
|
|
157
|
+
const nextNewline = raw.indexOf('\n', lineStart);
|
|
158
|
+
const lineEnd = nextNewline < 0 ? raw.length : nextNewline;
|
|
159
|
+
const line = raw.slice(lineStart, lineEnd).replace(/\r$/, '');
|
|
160
|
+
if (line === '---') {
|
|
161
|
+
return { start: lineStart, bodyStart: nextNewline < 0 ? raw.length : nextNewline + 1 };
|
|
162
|
+
}
|
|
163
|
+
if (nextNewline < 0)
|
|
164
|
+
return undefined;
|
|
165
|
+
lineStart = nextNewline + 1;
|
|
166
|
+
}
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
/** Read one `key: value` scalar line, stripping one layer of matching quotes. */
|
|
170
|
+
function scalarField(frontmatter, key) {
|
|
171
|
+
for (const line of frontmatter.split('\n')) {
|
|
172
|
+
const match = /^([A-Za-z][\w-]*):\s*(.*)$/.exec(line);
|
|
173
|
+
if (match === null || match[1] !== key)
|
|
174
|
+
continue;
|
|
175
|
+
const value = match[2].trim();
|
|
176
|
+
if (value.length === 0)
|
|
177
|
+
return undefined;
|
|
178
|
+
const quote = value[0];
|
|
179
|
+
if ((quote === '"' || quote === "'") && value.endsWith(quote) && value.length >= 2) {
|
|
180
|
+
return value.slice(1, -1);
|
|
181
|
+
}
|
|
182
|
+
return value;
|
|
183
|
+
}
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
/** Whether an arbitrary thrown value carries the given Node error code. */
|
|
187
|
+
function hasErrorCode(error, code) {
|
|
188
|
+
return typeof error === 'object' && error !== null && 'code' in error && error.code === code;
|
|
189
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `/doublecheck` session command: inspect and steer the discipline gates
|
|
3
|
+
* from the conversation.
|
|
4
|
+
*
|
|
5
|
+
* - `status` — effective switch, configured modules, and the folded stage
|
|
6
|
+
* facts (spec committed, red/green color, review on record).
|
|
7
|
+
* - `report` — folds the delivery report on the spot from the same durable
|
|
8
|
+
* evidence the `doublecheck_report` tool reads (no verification workflow:
|
|
9
|
+
* the report tool owns that path).
|
|
10
|
+
* - `on` / `off` — writes the durable `doublecheck/state` override (the fold
|
|
11
|
+
* survives restarts, resumes, and forks — replay IS the state) and injects
|
|
12
|
+
* a model-visible switch notice (`user/message`, plugin source).
|
|
13
|
+
*
|
|
14
|
+
* @module dsh-doublecheck/guard/command
|
|
15
|
+
*/
|
|
16
|
+
import type { CommandInvocation, CommandResult } from '@deepseek-ai/dsh-commands';
|
|
17
|
+
import { Session } from '@deepseek-ai/dsh-session';
|
|
18
|
+
import type { SessionEvent } from '@deepseek-ai/dsh-session';
|
|
19
|
+
import type { TestRunDetection } from '../domain/evidence.ts';
|
|
20
|
+
import type { Config, Snapshot } from './index.ts';
|
|
21
|
+
export declare function hostStampsIgnorable(): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* The session's doublecheck master switch: the last `doublecheck/state` event,
|
|
24
|
+
* or the configured default when none is on record.
|
|
25
|
+
* @param events - the session log.
|
|
26
|
+
* @param fallback - `enableByDefault` from the guard config.
|
|
27
|
+
* @returns whether the discipline gates are enabled for this session.
|
|
28
|
+
*/
|
|
29
|
+
export declare function effectiveDoublecheckEnabled(events: readonly SessionEvent[], fallback: boolean): boolean;
|
|
30
|
+
/** The closures the command reads; all live inside the guard's `apply` scope. */
|
|
31
|
+
export interface CommandDeps {
|
|
32
|
+
config: Config;
|
|
33
|
+
/** The folded guard facts for a session (incremental fold, log-derived). */
|
|
34
|
+
snapshotOf: (session: Session) => Snapshot;
|
|
35
|
+
/** The compiled test-run detection shared with the tdd/adversary gates. */
|
|
36
|
+
detection: TestRunDetection;
|
|
37
|
+
/** Whether the host append surface stamps `ignorable` (see {@link hostStampsIgnorable}). */
|
|
38
|
+
stampsIgnorable: () => boolean;
|
|
39
|
+
/** Record a process-local switch override when the durable write is unavailable. */
|
|
40
|
+
setLocalOverride: (session: Session, enabled: boolean) => void;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Execute the `/doublecheck` command.
|
|
44
|
+
* @param deps - the guard closures the handler reads.
|
|
45
|
+
* @returns the command handler for `ctx.commands.register`.
|
|
46
|
+
*/
|
|
47
|
+
export declare function doublecheckHandler(deps: CommandDeps): (invocation: CommandInvocation) => CommandResult;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `/doublecheck` session command: inspect and steer the discipline gates
|
|
3
|
+
* from the conversation.
|
|
4
|
+
*
|
|
5
|
+
* - `status` — effective switch, configured modules, and the folded stage
|
|
6
|
+
* facts (spec committed, red/green color, review on record).
|
|
7
|
+
* - `report` — folds the delivery report on the spot from the same durable
|
|
8
|
+
* evidence the `doublecheck_report` tool reads (no verification workflow:
|
|
9
|
+
* the report tool owns that path).
|
|
10
|
+
* - `on` / `off` — writes the durable `doublecheck/state` override (the fold
|
|
11
|
+
* survives restarts, resumes, and forks — replay IS the state) and injects
|
|
12
|
+
* a model-visible switch notice (`user/message`, plugin source).
|
|
13
|
+
*
|
|
14
|
+
* @module dsh-doublecheck/guard/command
|
|
15
|
+
*/
|
|
16
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
17
|
+
import { Session, SessionId } from '@deepseek-ai/dsh-session';
|
|
18
|
+
import { deriveReportVerdict, foldReportFacts, renderReportMarkdown } from "../domain/report.js";
|
|
19
|
+
/**
|
|
20
|
+
* Whether this host's `Session.append` stamps the `ignorable` envelope marker.
|
|
21
|
+
* Detected once per process on a detached probe session: hosts without the
|
|
22
|
+
* surface (rc.6) accept and ignore the options bag, and writing the durable
|
|
23
|
+
* state event unmarked would make the session log unreadable to first-party
|
|
24
|
+
* readers — so the switch command falls back to in-memory switching there.
|
|
25
|
+
* @returns true when a `doublecheck/state` append comes back with the marker.
|
|
26
|
+
*/
|
|
27
|
+
let ignorableCapability;
|
|
28
|
+
export function hostStampsIgnorable() {
|
|
29
|
+
if (ignorableCapability === undefined) {
|
|
30
|
+
try {
|
|
31
|
+
const probe = Session.create(SessionId('doublecheck-append-probe'));
|
|
32
|
+
const event = probe.append('doublecheck/state', { enabled: true }, { ignorable: true });
|
|
33
|
+
ignorableCapability = event.ignorable === true;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
ignorableCapability = false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return ignorableCapability;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The session's doublecheck master switch: the last `doublecheck/state` event,
|
|
43
|
+
* or the configured default when none is on record.
|
|
44
|
+
* @param events - the session log.
|
|
45
|
+
* @param fallback - `enableByDefault` from the guard config.
|
|
46
|
+
* @returns whether the discipline gates are enabled for this session.
|
|
47
|
+
*/
|
|
48
|
+
export function effectiveDoublecheckEnabled(events, fallback) {
|
|
49
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
50
|
+
const event = events[index];
|
|
51
|
+
if (event?.type === 'doublecheck/state')
|
|
52
|
+
return event.data.enabled;
|
|
53
|
+
}
|
|
54
|
+
return fallback;
|
|
55
|
+
}
|
|
56
|
+
/** The switch notice injected after `/doublecheck on|off`. */
|
|
57
|
+
function switchNotice(enabled, durable) {
|
|
58
|
+
const source = { kind: 'plugin', plugin: 'dsh-doublecheck', form: 'notice', summary: 'doublecheck state' };
|
|
59
|
+
const scope = durable
|
|
60
|
+
? 'for this session'
|
|
61
|
+
: 'for this process only (this harness build predates the ignorable append surface, so the switch is not durable)';
|
|
62
|
+
const text = enabled
|
|
63
|
+
? `Double-check discipline was switched ON ${scope} (changed by the user): the requirements grill, the red/green gates, and the delivery review are active again.`
|
|
64
|
+
: `Double-check discipline was switched OFF ${scope} (changed by the user): the discipline gates defer to the human approval chain until switched back on.`;
|
|
65
|
+
return createUserMessage({ content: [{ type: 'text', text }], source });
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Execute the `/doublecheck` command.
|
|
69
|
+
* @param deps - the guard closures the handler reads.
|
|
70
|
+
* @returns the command handler for `ctx.commands.register`.
|
|
71
|
+
*/
|
|
72
|
+
export function doublecheckHandler(deps) {
|
|
73
|
+
const { config, snapshotOf, detection, stampsIgnorable, setLocalOverride } = deps;
|
|
74
|
+
return (invocation) => {
|
|
75
|
+
const agent = invocation.agent;
|
|
76
|
+
if (agent === undefined) {
|
|
77
|
+
return { kind: 'error', text: '/doublecheck needs an agent session to inspect.' };
|
|
78
|
+
}
|
|
79
|
+
const session = agent.session;
|
|
80
|
+
const input = invocation.rawInput.trim().toLowerCase();
|
|
81
|
+
const enabled = effectiveDoublecheckEnabled(session.events, config.enableByDefault);
|
|
82
|
+
if (input === 'status' || input === '') {
|
|
83
|
+
const snapshot = snapshotOf(session);
|
|
84
|
+
const discipline = snapshot.discipline;
|
|
85
|
+
return {
|
|
86
|
+
kind: 'success',
|
|
87
|
+
text: [
|
|
88
|
+
`Double-check discipline is ${enabled ? 'ON' : 'OFF'} for this session. (/doublecheck on|off)`,
|
|
89
|
+
`Modules: grill=${config.modules.grill ? 'on' : 'off'}, tdd=${config.modules.tdd ? 'on' : 'off'}, adversary=${config.modules.adversary ? 'on' : 'off'} (cordis.yml).`,
|
|
90
|
+
`Stage: spec=${discipline.hasSpec ? 'committed' : 'missing'}, tests=${discipline.color}, review=${snapshot.lastReviewSeq >= 0 ? 'on record' : 'not run'}.`,
|
|
91
|
+
'Usage: /doublecheck status|report|on|off',
|
|
92
|
+
].join('\n'),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (input === 'report') {
|
|
96
|
+
const facts = foldReportFacts(session.events, detection);
|
|
97
|
+
const report = {
|
|
98
|
+
...facts,
|
|
99
|
+
verdict: deriveReportVerdict(facts, null),
|
|
100
|
+
verification: null,
|
|
101
|
+
path: null,
|
|
102
|
+
written: false,
|
|
103
|
+
};
|
|
104
|
+
return { kind: 'success', text: renderReportMarkdown(report) };
|
|
105
|
+
}
|
|
106
|
+
if (input !== 'on' && input !== 'off') {
|
|
107
|
+
return { kind: 'error', text: `Unknown /doublecheck argument "${invocation.rawInput.trim()}". Usage: /doublecheck status|report|on|off` };
|
|
108
|
+
}
|
|
109
|
+
const target = input === 'on';
|
|
110
|
+
if (enabled === target) {
|
|
111
|
+
return { kind: 'success', text: `Double-check discipline is already ${input.toUpperCase()} for this session.` };
|
|
112
|
+
}
|
|
113
|
+
// Adaptive durable write: only hosts that stamp `ignorable` may write the
|
|
114
|
+
// state event; on rc.6 the options bag is ignored and an unmarked foreign
|
|
115
|
+
// event would make the session log unreadable to first-party readers.
|
|
116
|
+
const durable = stampsIgnorable();
|
|
117
|
+
if (durable) {
|
|
118
|
+
;
|
|
119
|
+
session.append('doublecheck/state', { enabled: target }, { ignorable: true });
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
setLocalOverride(session, target);
|
|
123
|
+
}
|
|
124
|
+
agent.inject(switchNotice(target, durable));
|
|
125
|
+
return {
|
|
126
|
+
kind: 'success',
|
|
127
|
+
text: durable
|
|
128
|
+
? `Double-check discipline ${input.toUpperCase()} for this session.`
|
|
129
|
+
: `Double-check discipline ${input.toUpperCase()} for this process only (not durable on this harness).`,
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The discipline guard: dsh-doublecheck's soft enforcement plugin.
|
|
3
|
+
*
|
|
4
|
+
* Two gates compose on the documented `tools/pre-execute` /
|
|
5
|
+
* `tools/post-execute` / `agent/turn-stopping` extension points, both reading
|
|
6
|
+
* their facts from the durable session log alone:
|
|
7
|
+
*
|
|
8
|
+
* - **Grill gate** (`modules.grill`): a vague task with no committed
|
|
9
|
+
* `doublecheck_spec` may not mutate implementation files.
|
|
10
|
+
* - **Red/green gate** (`modules.tdd`, v0.2): an implementation edit requires
|
|
11
|
+
* a failing test run on record since the last passing run (the red step);
|
|
12
|
+
* at turn end, edits without a passing run re-arm the green reminder.
|
|
13
|
+
* Writing test files is always allowed — that is how the red step happens.
|
|
14
|
+
*
|
|
15
|
+
* The configured `intensity` picks the consequence for both gates:
|
|
16
|
+
*
|
|
17
|
+
* - `remind`: the call proceeds; a reminder rides the call's
|
|
18
|
+
* `additionalContexts`, so the agent loop records it as a `user/message`
|
|
19
|
+
* session event (model-visible ⟺ logged).
|
|
20
|
+
* - `warn`: the call is held for human approval through the approval seam
|
|
21
|
+
* (`ask`); without an approval channel it denies.
|
|
22
|
+
* - `block`: the call is denied with corrective feedback.
|
|
23
|
+
*
|
|
24
|
+
* Resumed and forked sessions enforce identically. The package-internal
|
|
25
|
+
* `doublecheck/reminder` event announces each reaction for observers.
|
|
26
|
+
*
|
|
27
|
+
* @module dsh-doublecheck/guard
|
|
28
|
+
*/
|
|
29
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
30
|
+
import type { SessionEvent } from '@deepseek-ai/dsh-session';
|
|
31
|
+
import type Schema from '@deepseek-ai/schemastery';
|
|
32
|
+
import { type DisciplineState } from '../domain/stages.ts';
|
|
33
|
+
import type { GuardIntensity } from '../events.ts';
|
|
34
|
+
import { type ProseLanguage } from './prose.ts';
|
|
35
|
+
export declare const name = "doublecheck-guard";
|
|
36
|
+
export declare const inject: string[];
|
|
37
|
+
/**
|
|
38
|
+
* Guard configuration. `intensity` is shared by all three gates; `modules`
|
|
39
|
+
* selects them. The `adversary` module (v0.3) dispatches a forked critic
|
|
40
|
+
* subagent at the turn boundary once the delivery reaches green.
|
|
41
|
+
*/
|
|
42
|
+
export interface Config {
|
|
43
|
+
/** Enforcement strength of the grill, red/green, and review gates. */
|
|
44
|
+
intensity: GuardIntensity;
|
|
45
|
+
/** Discipline module switches. */
|
|
46
|
+
modules: {
|
|
47
|
+
grill: boolean;
|
|
48
|
+
tdd: boolean;
|
|
49
|
+
adversary: boolean;
|
|
50
|
+
};
|
|
51
|
+
/** Model route for the adversary critic; null means the main model self-reviews. */
|
|
52
|
+
adversaryModel: string | null;
|
|
53
|
+
/** Subagent provider name the critic runs on (default `fork`). */
|
|
54
|
+
adversaryProvider: string;
|
|
55
|
+
/** Maximum structured findings injected into the session. */
|
|
56
|
+
adversaryMaxFindings: number;
|
|
57
|
+
/** Tools the critic may call (read-only by default; never mutation tools). */
|
|
58
|
+
adversaryTools: string[];
|
|
59
|
+
/** Hard time budget for one critic run before it settles as unavailable. */
|
|
60
|
+
adversaryTimeoutMs: number;
|
|
61
|
+
/** Mutation tool names both gates watch (default `edit`, `write`). */
|
|
62
|
+
guardTools: string[];
|
|
63
|
+
/** Task text longer than this many characters is never treated as vague. */
|
|
64
|
+
vagueTaskMaxChars: number;
|
|
65
|
+
/** Inject each gate's reminder at most once per session. */
|
|
66
|
+
remindOnce: boolean;
|
|
67
|
+
/** Language of the injected reminder/deny/review prose. */
|
|
68
|
+
language: ProseLanguage;
|
|
69
|
+
/** Master switch for sessions without a `doublecheck/state` override. */
|
|
70
|
+
enableByDefault: boolean;
|
|
71
|
+
/** Shell tool names that can run tests (default `bash`, `pwsh`). */
|
|
72
|
+
testToolNames: string[];
|
|
73
|
+
/** Regexes a shell command must match to count as a test run. */
|
|
74
|
+
testCommandPatterns: string[];
|
|
75
|
+
/** Regexes identifying test-file paths, exempt from the red gate. */
|
|
76
|
+
testFilePatterns: string[];
|
|
77
|
+
}
|
|
78
|
+
export declare const Config: Schema<Config>;
|
|
79
|
+
/** Cached per-session guard facts, folded incrementally from the append-only log. */
|
|
80
|
+
export interface Snapshot {
|
|
81
|
+
/** The log snapshot this fold last consumed; an append yields a new one. */
|
|
82
|
+
events: readonly SessionEvent[];
|
|
83
|
+
/** Number of events already folded. */
|
|
84
|
+
scanned: number;
|
|
85
|
+
/** Latest direct-user task text folded so far. */
|
|
86
|
+
latestUserText: string;
|
|
87
|
+
/** `isVagueTask` applied to `latestUserText`. */
|
|
88
|
+
vague: boolean;
|
|
89
|
+
/** Seq of the latest direct-user task message, or 0 before any. */
|
|
90
|
+
lastTaskSeq: number;
|
|
91
|
+
/** The discipline fold (spec, red/green color, green gate, pending pairs). */
|
|
92
|
+
discipline: DisciplineState;
|
|
93
|
+
/** A grill reminder is already on record in the log (durable `remindOnce`). */
|
|
94
|
+
grillReminded: boolean;
|
|
95
|
+
/** A red-gate reminder is already on record in the log (durable `remindOnce`). */
|
|
96
|
+
redReminded: boolean;
|
|
97
|
+
/** A green-gate reminder is already on record in the log (durable `remindOnce`). */
|
|
98
|
+
greenReminded: boolean;
|
|
99
|
+
/** A delivery-report reminder is already on record in the log (durable `remindOnce`). */
|
|
100
|
+
reportReminded: boolean;
|
|
101
|
+
/** Seq of the latest `doublecheck-review` source record, or -1 before any. */
|
|
102
|
+
lastReviewSeq: number;
|
|
103
|
+
/** Implementation edits folded after {@link lastReviewSeq}. */
|
|
104
|
+
editsAfterReview: number;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Install the guard listeners.
|
|
108
|
+
* @param ctx - plugin context; listeners unwind with it.
|
|
109
|
+
* @param config - validated {@link Config}; reserved-module misuse fails loud here.
|
|
110
|
+
*/
|
|
111
|
+
export declare function apply(ctx: Context, config: Config): void;
|