dsh-logicprobe 0.6.6 → 0.6.8

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/src/index.ts CHANGED
@@ -1,349 +1,369 @@
1
- /**
2
- * logicprobe — DeepSeek Harness native plugin for the Logic Probe toolbox.
3
- * Injects the session-start gate text (claim-verification doctrine, 1% Rule,
4
- * Red Flags, proactive suggestion) into the first model step of every agent
5
- * session, mirroring the SessionStart hook the Claude Code plugin installs.
6
- * The skill ships in this package's `skills/` directory and is registered at
7
- * apply time into dsh's `ctx.skills` registry through the standard filesystem
8
- * provider, so it appears in every session catalog without a manual copy step.
9
- *
10
- * Injection listens on agent/pre-step and appends the gate to the FIRST
11
- * model step that runs, once per session (guarded by the session's durable
12
- * history). Session-start inbox injection was dropped: a blank-session preset
13
- * switch (agentPreset.select -> recompose) can clear the inbox before the
14
- * first step, losing the gate for the whole session. The pre-step decision is
15
- * the durable path - anchored/bootstrap presets that strip first-step injected
16
- * reminders (skill catalog, AGENTS.md, gate plugins) simply defer this message
17
- * to the first step after their promotion, and the history guard re-injects it
18
- * there. The default gate text is the dsh-native adaptation of
19
- * `hooks/session-start-content.md`: behavior rules
20
- * (1% Rule / Red Flags / proactive suggestion) stay in sync, while
21
- * presentation is adapted to dsh's native skill catalog — the trigger list
22
- * lives in the skill description, not duplicated in the gate. Deployments
23
- * override via Config.
24
- *
25
- * @module logicprobe-dsh
26
- */
27
-
28
- import { fileURLToPath } from 'node:url'
29
- import type { Context } from '@deepseek-ai/cordis'
30
- import z from '@deepseek-ai/schemastery'
31
- import { createUserMessage } from '@deepseek-ai/dsh-llm'
32
- import type { Session, UserMessage } from '@deepseek-ai/dsh-session'
33
- import type { HostCordisInspectProviderRegistration } from '@deepseek-ai/dsh-cordis-host-runner'
34
- import type { AssembleContext, PromptContext } from '@deepseek-ai/dsh-system-prompt'
35
- import { FileSystemSkillProvider } from '@deepseek-ai/dsh-skill-filesystem'
36
- import { logicProbeVerifyTool } from './tool.js'
37
- import { logicProbeDataModelVerifyTool, DATA_ENGINE_SCHEMA_VERSION } from './data-tool.js'
38
- import { logicProbeConcurrencyScanTool } from './concurrency-tool.js'
39
- import { logicProbeComposeTool } from './compose-tool.js'
40
- import { logicProbeExportTool } from './export-tool.js'
41
- import { ENGINE_SCHEMA_VERSION } from './engine.js'
42
-
43
- export const name = 'logicprobe'
44
-
45
- // Skills are contributed through the registry service, which dsh-base always
46
- // mounts before bundle rows such as this one apply.
47
- export const inject = ['skills']
48
-
49
- // Absolute path of the package's shipped skills directory. `lib/index.js`
50
- // lives one level below the package root, so `../skills` from the module URL
51
- // lands on `<package>/skills` regardless of where the package was installed.
52
- const SKILLS_DIR = fileURLToPath(new URL('../skills', import.meta.url))
53
-
54
- const GATE_PLUGIN_ID = 'logicprobe'
55
-
56
- export type InteractionMode = 'ask' | 'auto' | 'follow-approval'
57
-
58
- const DEFAULT_GATE_CONTENT = `<EXTREMELY_IMPORTANT>
59
- Plugin logicprobe is active. Documents are not truth — code is. Verify every verifiable claim before accepting or acting on any design.
60
-
61
- **1% Rule**: If there is even a 1% chance the logicprobe skill applies — reviewing design documents, architecture specs, technical proposals, or refactoring plans that make claims about API names, file locations, enum values, mechanism feasibility, state machines, protocol logic, data models, schema migrations, data invariants, or behavioral guarantees ("always"/"never"/"guaranteed") — load it with the skill tool before responding. The cost of loading is trivial compared to the cost of a false claim.
62
-
63
- **Red Flags** — if you think any of these, STOP. You are rationalizing:
64
-
65
- | You think | Reality |
66
- |-----------|---------|
67
- | "This plan is too simple to verify" | The skill auto-classifies depth (LIGHTWEIGHT / STANDARD / ESCALATED). You don't decide. |
68
- | "I already know the file paths are correct" | Organic verification leaves no audit trail. Run Phase 0, append the "## Plan Verification" block. |
69
- | "I'll verify while implementing" | Verification happens before implementation, not during. |
70
- | "I can check this with reasoning alone" | Behavioral claims are verified with code/models, not intuition. One counter-example refutes a universal claim. |
71
-
72
- **Native verification path**: In dsh, prefer the \`logicprobe_verify\` tool for state-machine checks and \`logicprobe_datamodel_verify\` for data-model/schema migration checks. Both support before/after regression and common domain constraints (idempotency, monotonic, sequence, leads-to, atomicity). Python harnesses remain the fallback for non-dsh hosts.
73
-
74
- **Proactive suggestion**: When a user asks code-level behavioral questions — "could this state machine deadlock", "is this retry limit safe", "check this timing sequence for bugs", "is this migration non-breaking", "does this copy cover all required fields" — suggest logicprobe as an optional verification pass (do not auto-escalate).
75
- </EXTREMELY_IMPORTANT>`
76
-
77
- export interface Config {
78
- enabled: boolean
79
- gateContent: string
80
- interaction: InteractionMode
81
- }
82
-
83
- export const Config = z.object({
84
- enabled: z.boolean().default(true),
85
- gateContent: z.string().default(DEFAULT_GATE_CONTENT),
86
- interaction: z.union(['ask', 'auto', 'follow-approval']).default('follow-approval'),
87
- })
88
-
89
- function gateMessage(text: string): UserMessage {
90
- return createUserMessage({
91
- content: [{ type: 'text', text }],
92
- // `form` omitted — an undeclared context is the documented default.
93
- source: { kind: 'plugin', plugin: GATE_PLUGIN_ID },
94
- })
95
- }
96
-
97
- interface SessionEventLike {
98
- type: string
99
- data?: Record<string, unknown>
100
- }
101
-
102
- /**
103
- * Minimum session-store surface this plugin reads. DSH 0.1.2-alpha.4 replaced
104
- * the `Session.events` getter with on-demand reads (`seq`, `eventAt()`,
105
- * `snapshotEvents()`); releases up to 0.1.2-alpha.3 expose `events` as the
106
- * full log snapshot. Read through a structural union so one build serves every
107
- * declared DSH release.
108
- */
109
- interface SessionEventSource {
110
- events?: readonly SessionEventLike[]
111
- snapshotEvents?: () => readonly SessionEventLike[]
112
- }
113
-
114
- function readSessionEvents(session: Session): readonly SessionEventLike[] {
115
- const source = session as unknown as SessionEventSource
116
- if (typeof source.snapshotEvents === 'function') {
117
- const snapshot = source.snapshotEvents()
118
- if (Array.isArray(snapshot)) return snapshot
119
- }
120
- if (Array.isArray(source.events)) return source.events
121
- return []
122
- }
123
-
124
- function lastApprovalPolicy(session: Session): 'ask' | 'never' | undefined {
125
- const events = readSessionEvents(session)
126
- for (let index = events.length - 1; index >= 0; index -= 1) {
127
- const event = events[index]
128
- if (event.type === 'approval/policy') {
129
- return event.data?.policy === 'never' ? 'never' : 'ask'
130
- }
131
- }
132
- return undefined
133
- }
134
-
135
- function planModeActive(session: Session): boolean {
136
- const events = readSessionEvents(session)
137
- for (let index = events.length - 1; index >= 0; index -= 1) {
138
- const event = events[index]
139
- if (event.type === 'plan/mode') return event.data?.active === true
140
- }
141
- return false
142
- }
143
-
144
- function resolveInteraction(config: Config, session: Session): 'ask' | 'auto' {
145
- if (config.interaction === 'ask' || config.interaction === 'auto') return config.interaction
146
- return lastApprovalPolicy(session) === 'never' ? 'auto' : 'ask'
147
- }
148
-
149
- function modeContextText(config: Config, session: Session): string {
150
- const interaction = resolveInteraction(config, session)
151
- const lines = [
152
- 'logicprobe: use `logicprobe_verify` for state machines and `logicprobe_datamodel_verify` for data models; both cover before/after regression and common domain constraints.',
153
- interaction === 'auto'
154
- ? 'logicprobe interaction=auto: do NOT call ask_user_question for model confirmation; run round-trip validation of the extracted transition table and mark the result UNCONFIRMED.'
155
- : 'logicprobe interaction=ask: show the extracted transition table and get user confirmation before running verification.',
156
- ]
157
- if (planModeActive(session)) {
158
- lines.push('Plan mode active: before exit_plan_mode, run logicprobe Phase 0 and append the "## Plan Verification" block to the plan file.')
159
- }
160
- return lines.join(' ')
161
- }
162
-
163
- interface ToolRegistryLike {
164
- register(definition: unknown): () => void
165
- }
166
-
167
- interface SystemPromptLike {
168
- context(contribution: PromptContext): () => void
169
- }
170
-
171
- /**
172
- * Model-visible catalog entry (cordis_inspect_list / cordis_inspect_query):
173
- * lets the model read this plugin's runtime status without guessing. Mirrors
174
- * the registration pattern of the official dsh-tool-cordis host providers.
175
- */
176
- function inspectProvider(config: Config, isToolRegistered: () => boolean, isDataToolRegistered: () => boolean, isConcurrencyToolRegistered: () => boolean, isComposeToolRegistered: () => boolean, isExportToolRegistered: () => boolean): HostCordisInspectProviderRegistration {
177
- return {
178
- manifest: {
179
- id: 'logicprobe',
180
- description: 'Session-start gate injection and native verification tooling for the Logic Probe toolbox — folds the claim-verification doctrine (1% Rule / Red Flags / proactive suggestion) into the first model step of every agent session and registers the logicprobe_verify tool.',
181
- methods: [
182
- {
183
- name: 'status',
184
- description: 'Read gate injection status, interaction mode, tool registration state, and engine schema version.',
185
- inputSchema: {
186
- type: 'object',
187
- properties: {},
188
- additionalProperties: false,
189
- },
190
- outputSchema: {
191
- type: 'object',
192
- description: 'Gate-injection plugin status.',
193
- properties: {
194
- enabled: { type: 'boolean', description: 'Whether the gate folds into the first model step.' },
195
- gateContentLength: { type: 'integer', description: 'Length in characters of the injected gate text.' },
196
- interaction: { type: 'string', enum: ['ask', 'auto', 'follow-approval'], description: 'Configured interaction mode. follow-approval resolves per session from approval/policy.' },
197
- toolRegistered: { type: 'boolean', description: 'Whether the logicprobe_verify tool is registered on ctx.tools.' },
198
- dataToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_datamodel_verify tool is registered on ctx.tools.' },
199
- concurrencyToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_concurrency_scan tool is registered on ctx.tools.' },
200
- composeToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_compose_verify tool is registered on ctx.tools.' },
201
- exportToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_export tool is registered on ctx.tools.' },
202
- engineSchemaVersion: { type: 'integer', description: 'Model schema version the bundled state-machine verification engine accepts.' },
203
- dataEngineSchemaVersion: { type: 'integer', description: 'Model schema version the bundled data-model verification engine accepts.' },
204
- },
205
- required: ['enabled', 'gateContentLength', 'interaction', 'toolRegistered', 'dataToolRegistered', 'concurrencyToolRegistered', 'composeToolRegistered', 'exportToolRegistered', 'engineSchemaVersion', 'dataEngineSchemaVersion'],
206
- additionalProperties: false,
207
- },
208
- },
209
- ],
210
- },
211
- query: async (method) => {
212
- if (method === 'status') {
213
- return {
214
- enabled: config.enabled,
215
- gateContentLength: config.gateContent.length,
216
- interaction: config.interaction,
217
- toolRegistered: isToolRegistered(),
218
- dataToolRegistered: isDataToolRegistered(),
219
- concurrencyToolRegistered: isConcurrencyToolRegistered(),
220
- composeToolRegistered: isComposeToolRegistered(),
221
- exportToolRegistered: isExportToolRegistered(),
222
- engineSchemaVersion: ENGINE_SCHEMA_VERSION,
223
- dataEngineSchemaVersion: DATA_ENGINE_SCHEMA_VERSION,
224
- }
225
- }
226
- return null
227
- },
228
- }
229
- }
230
-
231
- export function apply(ctx: Context, config: Config): void {
232
- // Optional services are registered opportunistically; base-bundle rows can
233
- // mount after this row applies, so registration is retried on the first
234
- // agent/pre-step — by then the app is fully booted.
235
- let providerRegistered = false
236
- let toolRegistered = false
237
- let dataToolRegistered = false
238
- let concurrencyToolRegistered = false
239
- let composeToolRegistered = false
240
- let exportToolRegistered = false
241
- let modeContextRegistered = false
242
- const registerProvider = (): void => {
243
- if (providerRegistered) return
244
- const inspect = ctx.get('cordisInspect')
245
- if (inspect === undefined) return
246
- try {
247
- ctx.effect(() => inspect.register(inspectProvider(config, () => toolRegistered, () => dataToolRegistered, () => concurrencyToolRegistered, () => composeToolRegistered, () => exportToolRegistered)), 'logicprobe: inspect provider')
248
- providerRegistered = true
249
- } catch (err) {
250
- console.warn('[logicprobe] inspect provider registration failed', err)
251
- }
252
- }
253
- const registerTool = (): void => {
254
- if (toolRegistered) return
255
- const tools = ctx.get('tools') as ToolRegistryLike | undefined
256
- if (tools === undefined) return
257
- try {
258
- ctx.effect(() => tools.register(logicProbeVerifyTool), 'logicprobe: verify tool')
259
- ctx.effect(() => tools.register(logicProbeDataModelVerifyTool), 'logicprobe: data verify tool')
260
- ctx.effect(() => tools.register(logicProbeConcurrencyScanTool), 'logicprobe: concurrency scan tool')
261
- ctx.effect(() => tools.register(logicProbeComposeTool), 'logicprobe: compose tool')
262
- ctx.effect(() => tools.register(logicProbeExportTool), 'logicprobe: export tool')
263
- toolRegistered = true
264
- dataToolRegistered = true
265
- concurrencyToolRegistered = true
266
- composeToolRegistered = true
267
- exportToolRegistered = true
268
- } catch (err) {
269
- console.warn('[logicprobe] logicprobe_verify/logicprobe_datamodel_verify tool registration failed', err)
270
- }
271
- }
272
- const registerModeContext = (): void => {
273
- if (modeContextRegistered) return
274
- const systemPrompt = ctx.get('systemPrompt') as SystemPromptLike | undefined
275
- if (systemPrompt === undefined) return
276
- try {
277
- ctx.effect(() => systemPrompt.context({
278
- name: 'logicprobe:mode',
279
- order: 118,
280
- text: (context: AssembleContext) => {
281
- const agent = (context as AssembleContext & { agent?: { session: Session } }).agent
282
- if (agent === undefined) return ''
283
- return modeContextText(config, agent.session)
284
- },
285
- }), 'logicprobe: system prompt context')
286
- modeContextRegistered = true
287
- } catch (err) {
288
- console.warn('[logicprobe] system prompt context registration failed', err)
289
- }
290
- }
291
- const registerIntegrations = (): void => {
292
- registerProvider()
293
- registerTool()
294
- registerModeContext()
295
- }
296
- registerIntegrations()
297
- // Ship the bundled skill through the registry: reuse the standard
298
- // filesystem provider over this package's own `skills/` directory, so
299
- // catalog discovery, frontmatter parsing, and SKILL.md loading behave
300
- // exactly like project/user skills while the plugin stays self-contained.
301
- // Registration lands in the global registry layer (this row mounts at the
302
- // profile root), so every agent preset sees the skill. `registerProvider`
303
- // returns the effect disposer; its teardown unregisters and invalidates.
304
- ctx.skills.registerProvider((control) => {
305
- return new FileSystemSkillProvider(ctx, control, {
306
- providerName: 'logicprobe',
307
- includeDefaultRoots: false,
308
- customSkillDirs: [SKILLS_DIR],
309
- })
310
- })
311
- if (!config.enabled) return
312
- // Inject the gate once per session on the FIRST model step that runs,
313
- // instead of at session-start: session-start injection lands in the agent's
314
- // inbox, which a blank-session preset switch (agentPreset.select ->
315
- // recompose) can clear before the first step - the gate would then be lost
316
- // for the whole session. The pre-step decision is the durable path a
317
- // first-step injection takes: the gate is appended to the first step's
318
- // decision and enters session history there, so every later step (and a
319
- // resume) skips it. Anchored/bootstrap presets that strip first-step
320
- // injected reminders (skill catalog, AGENTS.md, gate plugins) simply defer
321
- // this message to the first step after their promotion - the history guard
322
- // re-injects it there, so the gate still lands exactly once per session.
323
- ctx.on('agent/pre-step', async ({ agent }, next) => {
324
- const decision = await next()
325
- if (decision.kind === 'reject') return decision
326
- registerIntegrations()
327
- if (gateInHistory(agent.session)) return decision
328
- return {
329
- kind: 'enter',
330
- messages: [...decision.messages, gateMessage(config.gateContent)],
331
- }
332
- })
333
- }
334
-
335
- /**
336
- * Whether the gate already entered this session's durable history. The
337
- * pre-step listener re-appends the gate until it does; once a step committed
338
- * it, every later step (and a resume of a session that kept it) skips the
339
- * injection. A session whose gate was dropped before any step ran (e.g. an
340
- * inbox cleared by a blank-session preset switch) simply re-injects on the
341
- * first step that runs.
342
- */
343
- function gateInHistory(session: Session): boolean {
344
- return readSessionEvents(session).some((event) => {
345
- if (event.type !== 'user/message') return false
346
- const source = event.data?.source as { kind?: string; plugin?: string } | undefined
347
- return source?.kind === 'plugin' && source.plugin === GATE_PLUGIN_ID
348
- })
349
- }
1
+ /**
2
+ * logicprobe — DeepSeek Harness native plugin for the Logic Probe toolbox.
3
+ * Injects the session-start gate text (claim-verification doctrine, 1% Rule,
4
+ * Red Flags, proactive suggestion) into the first model step of every agent
5
+ * session, mirroring the SessionStart hook the Claude Code plugin installs.
6
+ * The skill ships in this package's `skills/` directory and is registered at
7
+ * apply time into dsh's `ctx.skills` registry through the standard filesystem
8
+ * provider, so it appears in every session catalog without a manual copy step.
9
+ *
10
+ * Injection listens on agent/pre-step and appends the gate to the FIRST
11
+ * model step that runs, once per session (guarded by the session's durable
12
+ * history). Session-start inbox injection was dropped: a blank-session preset
13
+ * switch (agentPreset.select -> recompose) can clear the inbox before the
14
+ * first step, losing the gate for the whole session. The pre-step decision is
15
+ * the durable path - anchored/bootstrap presets that strip first-step injected
16
+ * reminders (skill catalog, AGENTS.md, gate plugins) simply defer this message
17
+ * to the first step after their promotion, and the history guard re-injects it
18
+ * there. The default gate text is the dsh-native adaptation of
19
+ * `hooks/session-start-content.md`: behavior rules
20
+ * (1% Rule / Red Flags / proactive suggestion) stay in sync, while
21
+ * presentation is adapted to dsh's native skill catalog — the trigger list
22
+ * lives in the skill description, not duplicated in the gate. Deployments
23
+ * override via Config.
24
+ *
25
+ * @module logicprobe-dsh
26
+ */
27
+
28
+ import { fileURLToPath } from 'node:url'
29
+ import type { Context } from '@deepseek-ai/cordis'
30
+ import z from '@deepseek-ai/schemastery'
31
+ import { createUserMessage } from '@deepseek-ai/dsh-llm'
32
+ import type { ContextFormed } from '@deepseek-ai/dsh-llm'
33
+ import type { Session, UserMessage } from '@deepseek-ai/dsh-session'
34
+ import type { HostCordisInspectProviderRegistration } from '@deepseek-ai/dsh-cordis-host-runner'
35
+ import type { AssembleContext, PromptContext } from '@deepseek-ai/dsh-system-prompt'
36
+ import { FileSystemSkillProvider } from '@deepseek-ai/dsh-skill-filesystem'
37
+ import { logicProbeVerifyTool } from './tool.js'
38
+ import { logicProbeDataModelVerifyTool, DATA_ENGINE_SCHEMA_VERSION } from './data-tool.js'
39
+ import { logicProbeConcurrencyScanTool } from './concurrency-tool.js'
40
+ import { logicProbeComposeTool } from './compose-tool.js'
41
+ import { logicProbeExportTool } from './export-tool.js'
42
+ import { ENGINE_SCHEMA_VERSION } from './engine.js'
43
+
44
+ // DSH 0.1.7-alpha.1 (session format v4) retires the shared
45
+ // `{ kind: 'plugin', plugin }` wrapper: native admission rejects it in every
46
+ // declared durable message slot, and the official v3-to-v4 migration rewrites
47
+ // those historical rows to `plugin:<name>`. Declaring the producer-owned kind
48
+ // here keeps the write path and the history guard on one identity, and still
49
+ // compiles against the earlier releases that only declare `plugin`.
50
+ declare module '@deepseek-ai/dsh-llm' {
51
+ interface MessageSourceMap {
52
+ 'plugin:logicprobe': { kind: 'plugin:logicprobe' } & ContextFormed
53
+ }
54
+ }
55
+
56
+ export const name = 'logicprobe'
57
+
58
+ // Skills are contributed through the registry service, which dsh-base always
59
+ // mounts before bundle rows such as this one apply.
60
+ export const inject = ['skills']
61
+
62
+ // Absolute path of the package's shipped skills directory. `lib/index.js`
63
+ // lives one level below the package root, so `../skills` from the module URL
64
+ // lands on `<package>/skills` regardless of where the package was installed.
65
+ const SKILLS_DIR = fileURLToPath(new URL('../skills', import.meta.url))
66
+
67
+ const GATE_PLUGIN_ID = 'logicprobe'
68
+
69
+ /** Producer-owned message source kind declared in `MessageSourceMap` above. */
70
+ const GATE_SOURCE_KIND: 'plugin:logicprobe' = 'plugin:logicprobe'
71
+
72
+ export type InteractionMode = 'ask' | 'auto' | 'follow-approval'
73
+
74
+ const DEFAULT_GATE_CONTENT = `<EXTREMELY_IMPORTANT>
75
+ Plugin logicprobe is active. Documents are not truth — code is. Verify every verifiable claim before accepting or acting on any design.
76
+
77
+ **1% Rule**: If there is even a 1% chance the logicprobe skill applies — reviewing design documents, architecture specs, technical proposals, or refactoring plans that make claims about API names, file locations, enum values, mechanism feasibility, state machines, protocol logic, data models, schema migrations, data invariants, or behavioral guarantees ("always"/"never"/"guaranteed") — load it with the skill tool before responding. The cost of loading is trivial compared to the cost of a false claim.
78
+
79
+ **Red Flags** — if you think any of these, STOP. You are rationalizing:
80
+
81
+ | You think | Reality |
82
+ |-----------|---------|
83
+ | "This plan is too simple to verify" | The skill auto-classifies depth (LIGHTWEIGHT / STANDARD / ESCALATED). You don't decide. |
84
+ | "I already know the file paths are correct" | Organic verification leaves no audit trail. Run Phase 0, append the "## Plan Verification" block. |
85
+ | "I'll verify while implementing" | Verification happens before implementation, not during. |
86
+ | "I can check this with reasoning alone" | Behavioral claims are verified with code/models, not intuition. One counter-example refutes a universal claim. |
87
+
88
+ **Native verification path**: In dsh, prefer the \`logicprobe_verify\` tool for state-machine checks and \`logicprobe_datamodel_verify\` for data-model/schema migration checks. Both support before/after regression and common domain constraints (idempotency, monotonic, sequence, leads-to, atomicity). Python harnesses remain the fallback for non-dsh hosts.
89
+
90
+ **Proactive suggestion**: When a user asks code-level behavioral questions — "could this state machine deadlock", "is this retry limit safe", "check this timing sequence for bugs", "is this migration non-breaking", "does this copy cover all required fields" — suggest logicprobe as an optional verification pass (do not auto-escalate).
91
+ </EXTREMELY_IMPORTANT>`
92
+
93
+ export interface Config {
94
+ enabled: boolean
95
+ gateContent: string
96
+ interaction: InteractionMode
97
+ }
98
+
99
+ export const Config = z.object({
100
+ enabled: z.boolean().default(true),
101
+ gateContent: z.string().default(DEFAULT_GATE_CONTENT),
102
+ interaction: z.union(['ask', 'auto', 'follow-approval']).default('follow-approval'),
103
+ })
104
+
105
+ function gateMessage(text: string): UserMessage {
106
+ return createUserMessage({
107
+ content: [{ type: 'text', text }],
108
+ // `form` omitted — an undeclared context is the documented default.
109
+ source: { kind: GATE_SOURCE_KIND },
110
+ })
111
+ }
112
+
113
+ interface SessionEventLike {
114
+ type: string
115
+ data?: Record<string, unknown>
116
+ }
117
+
118
+ /**
119
+ * Minimum session-store surface this plugin reads. DSH 0.1.2-alpha.4 replaced
120
+ * the `Session.events` getter with on-demand reads (`seq`, `eventAt()`,
121
+ * `snapshotEvents()`); releases up to 0.1.2-alpha.3 expose `events` as the
122
+ * full log snapshot. Read through a structural union so one build serves every
123
+ * declared DSH release.
124
+ */
125
+ interface SessionEventSource {
126
+ events?: readonly SessionEventLike[]
127
+ snapshotEvents?: () => readonly SessionEventLike[]
128
+ }
129
+
130
+ function readSessionEvents(session: Session): readonly SessionEventLike[] {
131
+ const source = session as unknown as SessionEventSource
132
+ if (typeof source.snapshotEvents === 'function') {
133
+ const snapshot = source.snapshotEvents()
134
+ if (Array.isArray(snapshot)) return snapshot
135
+ }
136
+ if (Array.isArray(source.events)) return source.events
137
+ return []
138
+ }
139
+
140
+ function lastApprovalPolicy(session: Session): 'ask' | 'never' | undefined {
141
+ const events = readSessionEvents(session)
142
+ for (let index = events.length - 1; index >= 0; index -= 1) {
143
+ const event = events[index]
144
+ if (event.type === 'approval/policy') {
145
+ return event.data?.policy === 'never' ? 'never' : 'ask'
146
+ }
147
+ }
148
+ return undefined
149
+ }
150
+
151
+ function planModeActive(session: Session): boolean {
152
+ const events = readSessionEvents(session)
153
+ for (let index = events.length - 1; index >= 0; index -= 1) {
154
+ const event = events[index]
155
+ if (event.type === 'plan/mode') return event.data?.active === true
156
+ }
157
+ return false
158
+ }
159
+
160
+ function resolveInteraction(config: Config, session: Session): 'ask' | 'auto' {
161
+ if (config.interaction === 'ask' || config.interaction === 'auto') return config.interaction
162
+ return lastApprovalPolicy(session) === 'never' ? 'auto' : 'ask'
163
+ }
164
+
165
+ function modeContextText(config: Config, session: Session): string {
166
+ const interaction = resolveInteraction(config, session)
167
+ const lines = [
168
+ 'logicprobe: use `logicprobe_verify` for state machines and `logicprobe_datamodel_verify` for data models; both cover before/after regression and common domain constraints.',
169
+ interaction === 'auto'
170
+ ? 'logicprobe interaction=auto: do NOT call ask_user_question for model confirmation; run round-trip validation of the extracted transition table and mark the result UNCONFIRMED.'
171
+ : 'logicprobe interaction=ask: show the extracted transition table and get user confirmation before running verification.',
172
+ ]
173
+ if (planModeActive(session)) {
174
+ lines.push('Plan mode active: before exit_plan_mode, run logicprobe Phase 0 and append the "## Plan Verification" block to the plan file.')
175
+ }
176
+ return lines.join(' ')
177
+ }
178
+
179
+ interface ToolRegistryLike {
180
+ register(definition: unknown): () => void
181
+ }
182
+
183
+ interface SystemPromptLike {
184
+ context(contribution: PromptContext): () => void
185
+ }
186
+
187
+ /**
188
+ * Model-visible catalog entry (cordis_inspect_list / cordis_inspect_query):
189
+ * lets the model read this plugin's runtime status without guessing. Mirrors
190
+ * the registration pattern of the official dsh-tool-cordis host providers.
191
+ */
192
+ function inspectProvider(config: Config, isToolRegistered: () => boolean, isDataToolRegistered: () => boolean, isConcurrencyToolRegistered: () => boolean, isComposeToolRegistered: () => boolean, isExportToolRegistered: () => boolean): HostCordisInspectProviderRegistration {
193
+ return {
194
+ manifest: {
195
+ id: 'logicprobe',
196
+ description: 'Session-start gate injection and native verification tooling for the Logic Probe toolbox — folds the claim-verification doctrine (1% Rule / Red Flags / proactive suggestion) into the first model step of every agent session and registers the logicprobe_verify tool.',
197
+ methods: [
198
+ {
199
+ name: 'status',
200
+ description: 'Read gate injection status, interaction mode, tool registration state, and engine schema version.',
201
+ inputSchema: {
202
+ type: 'object',
203
+ properties: {},
204
+ additionalProperties: false,
205
+ },
206
+ outputSchema: {
207
+ type: 'object',
208
+ description: 'Gate-injection plugin status.',
209
+ properties: {
210
+ enabled: { type: 'boolean', description: 'Whether the gate folds into the first model step.' },
211
+ gateContentLength: { type: 'integer', description: 'Length in characters of the injected gate text.' },
212
+ interaction: { type: 'string', enum: ['ask', 'auto', 'follow-approval'], description: 'Configured interaction mode. follow-approval resolves per session from approval/policy.' },
213
+ toolRegistered: { type: 'boolean', description: 'Whether the logicprobe_verify tool is registered on ctx.tools.' },
214
+ dataToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_datamodel_verify tool is registered on ctx.tools.' },
215
+ concurrencyToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_concurrency_scan tool is registered on ctx.tools.' },
216
+ composeToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_compose_verify tool is registered on ctx.tools.' },
217
+ exportToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_export tool is registered on ctx.tools.' },
218
+ engineSchemaVersion: { type: 'integer', description: 'Model schema version the bundled state-machine verification engine accepts.' },
219
+ dataEngineSchemaVersion: { type: 'integer', description: 'Model schema version the bundled data-model verification engine accepts.' },
220
+ },
221
+ required: ['enabled', 'gateContentLength', 'interaction', 'toolRegistered', 'dataToolRegistered', 'concurrencyToolRegistered', 'composeToolRegistered', 'exportToolRegistered', 'engineSchemaVersion', 'dataEngineSchemaVersion'],
222
+ additionalProperties: false,
223
+ },
224
+ },
225
+ ],
226
+ },
227
+ query: async (method) => {
228
+ if (method === 'status') {
229
+ return {
230
+ enabled: config.enabled,
231
+ gateContentLength: config.gateContent.length,
232
+ interaction: config.interaction,
233
+ toolRegistered: isToolRegistered(),
234
+ dataToolRegistered: isDataToolRegistered(),
235
+ concurrencyToolRegistered: isConcurrencyToolRegistered(),
236
+ composeToolRegistered: isComposeToolRegistered(),
237
+ exportToolRegistered: isExportToolRegistered(),
238
+ engineSchemaVersion: ENGINE_SCHEMA_VERSION,
239
+ dataEngineSchemaVersion: DATA_ENGINE_SCHEMA_VERSION,
240
+ }
241
+ }
242
+ return null
243
+ },
244
+ }
245
+ }
246
+
247
+ export function apply(ctx: Context, config: Config): void {
248
+ // Optional services are registered opportunistically; base-bundle rows can
249
+ // mount after this row applies, so registration is retried on the first
250
+ // agent/pre-step — by then the app is fully booted.
251
+ let providerRegistered = false
252
+ let toolRegistered = false
253
+ let dataToolRegistered = false
254
+ let concurrencyToolRegistered = false
255
+ let composeToolRegistered = false
256
+ let exportToolRegistered = false
257
+ let modeContextRegistered = false
258
+ const registerProvider = (): void => {
259
+ if (providerRegistered) return
260
+ const inspect = ctx.get('cordisInspect')
261
+ if (inspect === undefined) return
262
+ try {
263
+ ctx.effect(() => inspect.register(inspectProvider(config, () => toolRegistered, () => dataToolRegistered, () => concurrencyToolRegistered, () => composeToolRegistered, () => exportToolRegistered)), 'logicprobe: inspect provider')
264
+ providerRegistered = true
265
+ } catch (err) {
266
+ console.warn('[logicprobe] inspect provider registration failed', err)
267
+ }
268
+ }
269
+ const registerTool = (): void => {
270
+ if (toolRegistered) return
271
+ const tools = ctx.get('tools') as ToolRegistryLike | undefined
272
+ if (tools === undefined) return
273
+ try {
274
+ ctx.effect(() => tools.register(logicProbeVerifyTool), 'logicprobe: verify tool')
275
+ ctx.effect(() => tools.register(logicProbeDataModelVerifyTool), 'logicprobe: data verify tool')
276
+ ctx.effect(() => tools.register(logicProbeConcurrencyScanTool), 'logicprobe: concurrency scan tool')
277
+ ctx.effect(() => tools.register(logicProbeComposeTool), 'logicprobe: compose tool')
278
+ ctx.effect(() => tools.register(logicProbeExportTool), 'logicprobe: export tool')
279
+ toolRegistered = true
280
+ dataToolRegistered = true
281
+ concurrencyToolRegistered = true
282
+ composeToolRegistered = true
283
+ exportToolRegistered = true
284
+ } catch (err) {
285
+ console.warn('[logicprobe] logicprobe_verify/logicprobe_datamodel_verify tool registration failed', err)
286
+ }
287
+ }
288
+ const registerModeContext = (): void => {
289
+ if (modeContextRegistered) return
290
+ const systemPrompt = ctx.get('systemPrompt') as SystemPromptLike | undefined
291
+ if (systemPrompt === undefined) return
292
+ try {
293
+ ctx.effect(() => systemPrompt.context({
294
+ name: 'logicprobe:mode',
295
+ order: 118,
296
+ text: (context: AssembleContext) => {
297
+ const agent = (context as AssembleContext & { agent?: { session: Session } }).agent
298
+ if (agent === undefined) return ''
299
+ return modeContextText(config, agent.session)
300
+ },
301
+ }), 'logicprobe: system prompt context')
302
+ modeContextRegistered = true
303
+ } catch (err) {
304
+ console.warn('[logicprobe] system prompt context registration failed', err)
305
+ }
306
+ }
307
+ const registerIntegrations = (): void => {
308
+ registerProvider()
309
+ registerTool()
310
+ registerModeContext()
311
+ }
312
+ registerIntegrations()
313
+ // Ship the bundled skill through the registry: reuse the standard
314
+ // filesystem provider over this package's own `skills/` directory, so
315
+ // catalog discovery, frontmatter parsing, and SKILL.md loading behave
316
+ // exactly like project/user skills while the plugin stays self-contained.
317
+ // Registration lands in the global registry layer (this row mounts at the
318
+ // profile root), so every agent preset sees the skill. `registerProvider`
319
+ // returns the effect disposer; its teardown unregisters and invalidates.
320
+ ctx.skills.registerProvider((control) => {
321
+ return new FileSystemSkillProvider(ctx, control, {
322
+ providerName: 'logicprobe',
323
+ includeDefaultRoots: false,
324
+ customSkillDirs: [SKILLS_DIR],
325
+ })
326
+ })
327
+ if (!config.enabled) return
328
+ // Inject the gate once per session on the FIRST model step that runs,
329
+ // instead of at session-start: session-start injection lands in the agent's
330
+ // inbox, which a blank-session preset switch (agentPreset.select ->
331
+ // recompose) can clear before the first step - the gate would then be lost
332
+ // for the whole session. The pre-step decision is the durable path a
333
+ // first-step injection takes: the gate is appended to the first step's
334
+ // decision and enters session history there, so every later step (and a
335
+ // resume) skips it. Anchored/bootstrap presets that strip first-step
336
+ // injected reminders (skill catalog, AGENTS.md, gate plugins) simply defer
337
+ // this message to the first step after their promotion - the history guard
338
+ // re-injects it there, so the gate still lands exactly once per session.
339
+ ctx.on('agent/pre-step', async ({ agent }, next) => {
340
+ const decision = await next()
341
+ if (decision.kind === 'reject') return decision
342
+ registerIntegrations()
343
+ if (gateInHistory(agent.session)) return decision
344
+ return {
345
+ kind: 'enter',
346
+ messages: [...decision.messages, gateMessage(config.gateContent)],
347
+ }
348
+ })
349
+ }
350
+
351
+ /**
352
+ * Whether the gate already entered this session's durable history. The
353
+ * pre-step listener re-appends the gate until it does; once a step committed
354
+ * it, every later step (and a resume of a session that kept it) skips the
355
+ * injection. A session whose gate was dropped before any step ran (e.g. an
356
+ * inbox cleared by a blank-session preset switch) simply re-injects on the
357
+ * first step that runs.
358
+ */
359
+ function gateInHistory(session: Session): boolean {
360
+ return readSessionEvents(session).some((event) => {
361
+ if (event.type !== 'user/message') return false
362
+ const source = event.data?.source as { kind?: string; plugin?: string } | undefined
363
+ if (source === undefined) return false
364
+ // The v4 producer-owned kind, plus the pre-v4 wrapper this bundle wrote
365
+ // before DSH 0.1.7-alpha.1 retired it.
366
+ return source.kind === GATE_SOURCE_KIND
367
+ || (source.kind === 'plugin' && source.plugin === GATE_PLUGIN_ID)
368
+ })
369
+ }