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/lib/index.js CHANGED
@@ -1,314 +1,321 @@
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
- import { fileURLToPath } from 'node:url';
28
- import z from '@deepseek-ai/schemastery';
29
- import { createUserMessage } from '@deepseek-ai/dsh-llm';
30
- import { FileSystemSkillProvider } from '@deepseek-ai/dsh-skill-filesystem';
31
- import { logicProbeVerifyTool } from './tool.js';
32
- import { logicProbeDataModelVerifyTool, DATA_ENGINE_SCHEMA_VERSION } from './data-tool.js';
33
- import { logicProbeConcurrencyScanTool } from './concurrency-tool.js';
34
- import { logicProbeComposeTool } from './compose-tool.js';
35
- import { logicProbeExportTool } from './export-tool.js';
36
- import { ENGINE_SCHEMA_VERSION } from './engine.js';
37
- export const name = 'logicprobe';
38
- // Skills are contributed through the registry service, which dsh-base always
39
- // mounts before bundle rows such as this one apply.
40
- export const inject = ['skills'];
41
- // Absolute path of the package's shipped skills directory. `lib/index.js`
42
- // lives one level below the package root, so `../skills` from the module URL
43
- // lands on `<package>/skills` regardless of where the package was installed.
44
- const SKILLS_DIR = fileURLToPath(new URL('../skills', import.meta.url));
45
- const GATE_PLUGIN_ID = 'logicprobe';
46
- const DEFAULT_GATE_CONTENT = `<EXTREMELY_IMPORTANT>
47
- Plugin logicprobe is active. Documents are not truth — code is. Verify every verifiable claim before accepting or acting on any design.
48
-
49
- **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.
50
-
51
- **Red Flags** — if you think any of these, STOP. You are rationalizing:
52
-
53
- | You think | Reality |
54
- |-----------|---------|
55
- | "This plan is too simple to verify" | The skill auto-classifies depth (LIGHTWEIGHT / STANDARD / ESCALATED). You don't decide. |
56
- | "I already know the file paths are correct" | Organic verification leaves no audit trail. Run Phase 0, append the "## Plan Verification" block. |
57
- | "I'll verify while implementing" | Verification happens before implementation, not during. |
58
- | "I can check this with reasoning alone" | Behavioral claims are verified with code/models, not intuition. One counter-example refutes a universal claim. |
59
-
60
- **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.
61
-
62
- **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).
63
- </EXTREMELY_IMPORTANT>`;
64
- export const Config = z.object({
65
- enabled: z.boolean().default(true),
66
- gateContent: z.string().default(DEFAULT_GATE_CONTENT),
67
- interaction: z.union(['ask', 'auto', 'follow-approval']).default('follow-approval'),
68
- });
69
- function gateMessage(text) {
70
- return createUserMessage({
71
- content: [{ type: 'text', text }],
72
- // `form` omitted — an undeclared context is the documented default.
73
- source: { kind: 'plugin', plugin: GATE_PLUGIN_ID },
74
- });
75
- }
76
- function readSessionEvents(session) {
77
- const source = session;
78
- if (typeof source.snapshotEvents === 'function') {
79
- const snapshot = source.snapshotEvents();
80
- if (Array.isArray(snapshot))
81
- return snapshot;
82
- }
83
- if (Array.isArray(source.events))
84
- return source.events;
85
- return [];
86
- }
87
- function lastApprovalPolicy(session) {
88
- const events = readSessionEvents(session);
89
- for (let index = events.length - 1; index >= 0; index -= 1) {
90
- const event = events[index];
91
- if (event.type === 'approval/policy') {
92
- return event.data?.policy === 'never' ? 'never' : 'ask';
93
- }
94
- }
95
- return undefined;
96
- }
97
- function planModeActive(session) {
98
- const events = readSessionEvents(session);
99
- for (let index = events.length - 1; index >= 0; index -= 1) {
100
- const event = events[index];
101
- if (event.type === 'plan/mode')
102
- return event.data?.active === true;
103
- }
104
- return false;
105
- }
106
- function resolveInteraction(config, session) {
107
- if (config.interaction === 'ask' || config.interaction === 'auto')
108
- return config.interaction;
109
- return lastApprovalPolicy(session) === 'never' ? 'auto' : 'ask';
110
- }
111
- function modeContextText(config, session) {
112
- const interaction = resolveInteraction(config, session);
113
- const lines = [
114
- 'logicprobe: use `logicprobe_verify` for state machines and `logicprobe_datamodel_verify` for data models; both cover before/after regression and common domain constraints.',
115
- interaction === 'auto'
116
- ? '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.'
117
- : 'logicprobe interaction=ask: show the extracted transition table and get user confirmation before running verification.',
118
- ];
119
- if (planModeActive(session)) {
120
- lines.push('Plan mode active: before exit_plan_mode, run logicprobe Phase 0 and append the "## Plan Verification" block to the plan file.');
121
- }
122
- return lines.join(' ');
123
- }
124
- /**
125
- * Model-visible catalog entry (cordis_inspect_list / cordis_inspect_query):
126
- * lets the model read this plugin's runtime status without guessing. Mirrors
127
- * the registration pattern of the official dsh-tool-cordis host providers.
128
- */
129
- function inspectProvider(config, isToolRegistered, isDataToolRegistered, isConcurrencyToolRegistered, isComposeToolRegistered, isExportToolRegistered) {
130
- return {
131
- manifest: {
132
- id: 'logicprobe',
133
- 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.',
134
- methods: [
135
- {
136
- name: 'status',
137
- description: 'Read gate injection status, interaction mode, tool registration state, and engine schema version.',
138
- inputSchema: {
139
- type: 'object',
140
- properties: {},
141
- additionalProperties: false,
142
- },
143
- outputSchema: {
144
- type: 'object',
145
- description: 'Gate-injection plugin status.',
146
- properties: {
147
- enabled: { type: 'boolean', description: 'Whether the gate folds into the first model step.' },
148
- gateContentLength: { type: 'integer', description: 'Length in characters of the injected gate text.' },
149
- interaction: { type: 'string', enum: ['ask', 'auto', 'follow-approval'], description: 'Configured interaction mode. follow-approval resolves per session from approval/policy.' },
150
- toolRegistered: { type: 'boolean', description: 'Whether the logicprobe_verify tool is registered on ctx.tools.' },
151
- dataToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_datamodel_verify tool is registered on ctx.tools.' },
152
- concurrencyToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_concurrency_scan tool is registered on ctx.tools.' },
153
- composeToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_compose_verify tool is registered on ctx.tools.' },
154
- exportToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_export tool is registered on ctx.tools.' },
155
- engineSchemaVersion: { type: 'integer', description: 'Model schema version the bundled state-machine verification engine accepts.' },
156
- dataEngineSchemaVersion: { type: 'integer', description: 'Model schema version the bundled data-model verification engine accepts.' },
157
- },
158
- required: ['enabled', 'gateContentLength', 'interaction', 'toolRegistered', 'dataToolRegistered', 'concurrencyToolRegistered', 'composeToolRegistered', 'exportToolRegistered', 'engineSchemaVersion', 'dataEngineSchemaVersion'],
159
- additionalProperties: false,
160
- },
161
- },
162
- ],
163
- },
164
- query: async (method) => {
165
- if (method === 'status') {
166
- return {
167
- enabled: config.enabled,
168
- gateContentLength: config.gateContent.length,
169
- interaction: config.interaction,
170
- toolRegistered: isToolRegistered(),
171
- dataToolRegistered: isDataToolRegistered(),
172
- concurrencyToolRegistered: isConcurrencyToolRegistered(),
173
- composeToolRegistered: isComposeToolRegistered(),
174
- exportToolRegistered: isExportToolRegistered(),
175
- engineSchemaVersion: ENGINE_SCHEMA_VERSION,
176
- dataEngineSchemaVersion: DATA_ENGINE_SCHEMA_VERSION,
177
- };
178
- }
179
- return null;
180
- },
181
- };
182
- }
183
- export function apply(ctx, config) {
184
- // Optional services are registered opportunistically; base-bundle rows can
185
- // mount after this row applies, so registration is retried on the first
186
- // agent/pre-step — by then the app is fully booted.
187
- let providerRegistered = false;
188
- let toolRegistered = false;
189
- let dataToolRegistered = false;
190
- let concurrencyToolRegistered = false;
191
- let composeToolRegistered = false;
192
- let exportToolRegistered = false;
193
- let modeContextRegistered = false;
194
- const registerProvider = () => {
195
- if (providerRegistered)
196
- return;
197
- const inspect = ctx.get('cordisInspect');
198
- if (inspect === undefined)
199
- return;
200
- try {
201
- ctx.effect(() => inspect.register(inspectProvider(config, () => toolRegistered, () => dataToolRegistered, () => concurrencyToolRegistered, () => composeToolRegistered, () => exportToolRegistered)), 'logicprobe: inspect provider');
202
- providerRegistered = true;
203
- }
204
- catch (err) {
205
- console.warn('[logicprobe] inspect provider registration failed', err);
206
- }
207
- };
208
- const registerTool = () => {
209
- if (toolRegistered)
210
- return;
211
- const tools = ctx.get('tools');
212
- if (tools === undefined)
213
- return;
214
- try {
215
- ctx.effect(() => tools.register(logicProbeVerifyTool), 'logicprobe: verify tool');
216
- ctx.effect(() => tools.register(logicProbeDataModelVerifyTool), 'logicprobe: data verify tool');
217
- ctx.effect(() => tools.register(logicProbeConcurrencyScanTool), 'logicprobe: concurrency scan tool');
218
- ctx.effect(() => tools.register(logicProbeComposeTool), 'logicprobe: compose tool');
219
- ctx.effect(() => tools.register(logicProbeExportTool), 'logicprobe: export tool');
220
- toolRegistered = true;
221
- dataToolRegistered = true;
222
- concurrencyToolRegistered = true;
223
- composeToolRegistered = true;
224
- exportToolRegistered = true;
225
- }
226
- catch (err) {
227
- console.warn('[logicprobe] logicprobe_verify/logicprobe_datamodel_verify tool registration failed', err);
228
- }
229
- };
230
- const registerModeContext = () => {
231
- if (modeContextRegistered)
232
- return;
233
- const systemPrompt = ctx.get('systemPrompt');
234
- if (systemPrompt === undefined)
235
- return;
236
- try {
237
- ctx.effect(() => systemPrompt.context({
238
- name: 'logicprobe:mode',
239
- order: 118,
240
- text: (context) => {
241
- const agent = context.agent;
242
- if (agent === undefined)
243
- return '';
244
- return modeContextText(config, agent.session);
245
- },
246
- }), 'logicprobe: system prompt context');
247
- modeContextRegistered = true;
248
- }
249
- catch (err) {
250
- console.warn('[logicprobe] system prompt context registration failed', err);
251
- }
252
- };
253
- const registerIntegrations = () => {
254
- registerProvider();
255
- registerTool();
256
- registerModeContext();
257
- };
258
- registerIntegrations();
259
- // Ship the bundled skill through the registry: reuse the standard
260
- // filesystem provider over this package's own `skills/` directory, so
261
- // catalog discovery, frontmatter parsing, and SKILL.md loading behave
262
- // exactly like project/user skills while the plugin stays self-contained.
263
- // Registration lands in the global registry layer (this row mounts at the
264
- // profile root), so every agent preset sees the skill. `registerProvider`
265
- // returns the effect disposer; its teardown unregisters and invalidates.
266
- ctx.skills.registerProvider((control) => {
267
- return new FileSystemSkillProvider(ctx, control, {
268
- providerName: 'logicprobe',
269
- includeDefaultRoots: false,
270
- customSkillDirs: [SKILLS_DIR],
271
- });
272
- });
273
- if (!config.enabled)
274
- return;
275
- // Inject the gate once per session on the FIRST model step that runs,
276
- // instead of at session-start: session-start injection lands in the agent's
277
- // inbox, which a blank-session preset switch (agentPreset.select ->
278
- // recompose) can clear before the first step - the gate would then be lost
279
- // for the whole session. The pre-step decision is the durable path a
280
- // first-step injection takes: the gate is appended to the first step's
281
- // decision and enters session history there, so every later step (and a
282
- // resume) skips it. Anchored/bootstrap presets that strip first-step
283
- // injected reminders (skill catalog, AGENTS.md, gate plugins) simply defer
284
- // this message to the first step after their promotion - the history guard
285
- // re-injects it there, so the gate still lands exactly once per session.
286
- ctx.on('agent/pre-step', async ({ agent }, next) => {
287
- const decision = await next();
288
- if (decision.kind === 'reject')
289
- return decision;
290
- registerIntegrations();
291
- if (gateInHistory(agent.session))
292
- return decision;
293
- return {
294
- kind: 'enter',
295
- messages: [...decision.messages, gateMessage(config.gateContent)],
296
- };
297
- });
298
- }
299
- /**
300
- * Whether the gate already entered this session's durable history. The
301
- * pre-step listener re-appends the gate until it does; once a step committed
302
- * it, every later step (and a resume of a session that kept it) skips the
303
- * injection. A session whose gate was dropped before any step ran (e.g. an
304
- * inbox cleared by a blank-session preset switch) simply re-injects on the
305
- * first step that runs.
306
- */
307
- function gateInHistory(session) {
308
- return readSessionEvents(session).some((event) => {
309
- if (event.type !== 'user/message')
310
- return false;
311
- const source = event.data?.source;
312
- return source?.kind === 'plugin' && source.plugin === GATE_PLUGIN_ID;
313
- });
314
- }
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
+ import { fileURLToPath } from 'node:url';
28
+ import z from '@deepseek-ai/schemastery';
29
+ import { createUserMessage } from '@deepseek-ai/dsh-llm';
30
+ import { FileSystemSkillProvider } from '@deepseek-ai/dsh-skill-filesystem';
31
+ import { logicProbeVerifyTool } from './tool.js';
32
+ import { logicProbeDataModelVerifyTool, DATA_ENGINE_SCHEMA_VERSION } from './data-tool.js';
33
+ import { logicProbeConcurrencyScanTool } from './concurrency-tool.js';
34
+ import { logicProbeComposeTool } from './compose-tool.js';
35
+ import { logicProbeExportTool } from './export-tool.js';
36
+ import { ENGINE_SCHEMA_VERSION } from './engine.js';
37
+ export const name = 'logicprobe';
38
+ // Skills are contributed through the registry service, which dsh-base always
39
+ // mounts before bundle rows such as this one apply.
40
+ export const inject = ['skills'];
41
+ // Absolute path of the package's shipped skills directory. `lib/index.js`
42
+ // lives one level below the package root, so `../skills` from the module URL
43
+ // lands on `<package>/skills` regardless of where the package was installed.
44
+ const SKILLS_DIR = fileURLToPath(new URL('../skills', import.meta.url));
45
+ const GATE_PLUGIN_ID = 'logicprobe';
46
+ /** Producer-owned message source kind declared in `MessageSourceMap` above. */
47
+ const GATE_SOURCE_KIND = 'plugin:logicprobe';
48
+ const DEFAULT_GATE_CONTENT = `<EXTREMELY_IMPORTANT>
49
+ Plugin logicprobe is active. Documents are not truth — code is. Verify every verifiable claim before accepting or acting on any design.
50
+
51
+ **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.
52
+
53
+ **Red Flags** — if you think any of these, STOP. You are rationalizing:
54
+
55
+ | You think | Reality |
56
+ |-----------|---------|
57
+ | "This plan is too simple to verify" | The skill auto-classifies depth (LIGHTWEIGHT / STANDARD / ESCALATED). You don't decide. |
58
+ | "I already know the file paths are correct" | Organic verification leaves no audit trail. Run Phase 0, append the "## Plan Verification" block. |
59
+ | "I'll verify while implementing" | Verification happens before implementation, not during. |
60
+ | "I can check this with reasoning alone" | Behavioral claims are verified with code/models, not intuition. One counter-example refutes a universal claim. |
61
+
62
+ **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.
63
+
64
+ **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).
65
+ </EXTREMELY_IMPORTANT>`;
66
+ export const Config = z.object({
67
+ enabled: z.boolean().default(true),
68
+ gateContent: z.string().default(DEFAULT_GATE_CONTENT),
69
+ interaction: z.union(['ask', 'auto', 'follow-approval']).default('follow-approval'),
70
+ });
71
+ function gateMessage(text) {
72
+ return createUserMessage({
73
+ content: [{ type: 'text', text }],
74
+ // `form` omitted — an undeclared context is the documented default.
75
+ source: { kind: GATE_SOURCE_KIND },
76
+ });
77
+ }
78
+ function readSessionEvents(session) {
79
+ const source = session;
80
+ if (typeof source.snapshotEvents === 'function') {
81
+ const snapshot = source.snapshotEvents();
82
+ if (Array.isArray(snapshot))
83
+ return snapshot;
84
+ }
85
+ if (Array.isArray(source.events))
86
+ return source.events;
87
+ return [];
88
+ }
89
+ function lastApprovalPolicy(session) {
90
+ const events = readSessionEvents(session);
91
+ for (let index = events.length - 1; index >= 0; index -= 1) {
92
+ const event = events[index];
93
+ if (event.type === 'approval/policy') {
94
+ return event.data?.policy === 'never' ? 'never' : 'ask';
95
+ }
96
+ }
97
+ return undefined;
98
+ }
99
+ function planModeActive(session) {
100
+ const events = readSessionEvents(session);
101
+ for (let index = events.length - 1; index >= 0; index -= 1) {
102
+ const event = events[index];
103
+ if (event.type === 'plan/mode')
104
+ return event.data?.active === true;
105
+ }
106
+ return false;
107
+ }
108
+ function resolveInteraction(config, session) {
109
+ if (config.interaction === 'ask' || config.interaction === 'auto')
110
+ return config.interaction;
111
+ return lastApprovalPolicy(session) === 'never' ? 'auto' : 'ask';
112
+ }
113
+ function modeContextText(config, session) {
114
+ const interaction = resolveInteraction(config, session);
115
+ const lines = [
116
+ 'logicprobe: use `logicprobe_verify` for state machines and `logicprobe_datamodel_verify` for data models; both cover before/after regression and common domain constraints.',
117
+ interaction === 'auto'
118
+ ? '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.'
119
+ : 'logicprobe interaction=ask: show the extracted transition table and get user confirmation before running verification.',
120
+ ];
121
+ if (planModeActive(session)) {
122
+ lines.push('Plan mode active: before exit_plan_mode, run logicprobe Phase 0 and append the "## Plan Verification" block to the plan file.');
123
+ }
124
+ return lines.join(' ');
125
+ }
126
+ /**
127
+ * Model-visible catalog entry (cordis_inspect_list / cordis_inspect_query):
128
+ * lets the model read this plugin's runtime status without guessing. Mirrors
129
+ * the registration pattern of the official dsh-tool-cordis host providers.
130
+ */
131
+ function inspectProvider(config, isToolRegistered, isDataToolRegistered, isConcurrencyToolRegistered, isComposeToolRegistered, isExportToolRegistered) {
132
+ return {
133
+ manifest: {
134
+ id: 'logicprobe',
135
+ 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.',
136
+ methods: [
137
+ {
138
+ name: 'status',
139
+ description: 'Read gate injection status, interaction mode, tool registration state, and engine schema version.',
140
+ inputSchema: {
141
+ type: 'object',
142
+ properties: {},
143
+ additionalProperties: false,
144
+ },
145
+ outputSchema: {
146
+ type: 'object',
147
+ description: 'Gate-injection plugin status.',
148
+ properties: {
149
+ enabled: { type: 'boolean', description: 'Whether the gate folds into the first model step.' },
150
+ gateContentLength: { type: 'integer', description: 'Length in characters of the injected gate text.' },
151
+ interaction: { type: 'string', enum: ['ask', 'auto', 'follow-approval'], description: 'Configured interaction mode. follow-approval resolves per session from approval/policy.' },
152
+ toolRegistered: { type: 'boolean', description: 'Whether the logicprobe_verify tool is registered on ctx.tools.' },
153
+ dataToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_datamodel_verify tool is registered on ctx.tools.' },
154
+ concurrencyToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_concurrency_scan tool is registered on ctx.tools.' },
155
+ composeToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_compose_verify tool is registered on ctx.tools.' },
156
+ exportToolRegistered: { type: 'boolean', description: 'Whether the logicprobe_export tool is registered on ctx.tools.' },
157
+ engineSchemaVersion: { type: 'integer', description: 'Model schema version the bundled state-machine verification engine accepts.' },
158
+ dataEngineSchemaVersion: { type: 'integer', description: 'Model schema version the bundled data-model verification engine accepts.' },
159
+ },
160
+ required: ['enabled', 'gateContentLength', 'interaction', 'toolRegistered', 'dataToolRegistered', 'concurrencyToolRegistered', 'composeToolRegistered', 'exportToolRegistered', 'engineSchemaVersion', 'dataEngineSchemaVersion'],
161
+ additionalProperties: false,
162
+ },
163
+ },
164
+ ],
165
+ },
166
+ query: async (method) => {
167
+ if (method === 'status') {
168
+ return {
169
+ enabled: config.enabled,
170
+ gateContentLength: config.gateContent.length,
171
+ interaction: config.interaction,
172
+ toolRegistered: isToolRegistered(),
173
+ dataToolRegistered: isDataToolRegistered(),
174
+ concurrencyToolRegistered: isConcurrencyToolRegistered(),
175
+ composeToolRegistered: isComposeToolRegistered(),
176
+ exportToolRegistered: isExportToolRegistered(),
177
+ engineSchemaVersion: ENGINE_SCHEMA_VERSION,
178
+ dataEngineSchemaVersion: DATA_ENGINE_SCHEMA_VERSION,
179
+ };
180
+ }
181
+ return null;
182
+ },
183
+ };
184
+ }
185
+ export function apply(ctx, config) {
186
+ // Optional services are registered opportunistically; base-bundle rows can
187
+ // mount after this row applies, so registration is retried on the first
188
+ // agent/pre-step — by then the app is fully booted.
189
+ let providerRegistered = false;
190
+ let toolRegistered = false;
191
+ let dataToolRegistered = false;
192
+ let concurrencyToolRegistered = false;
193
+ let composeToolRegistered = false;
194
+ let exportToolRegistered = false;
195
+ let modeContextRegistered = false;
196
+ const registerProvider = () => {
197
+ if (providerRegistered)
198
+ return;
199
+ const inspect = ctx.get('cordisInspect');
200
+ if (inspect === undefined)
201
+ return;
202
+ try {
203
+ ctx.effect(() => inspect.register(inspectProvider(config, () => toolRegistered, () => dataToolRegistered, () => concurrencyToolRegistered, () => composeToolRegistered, () => exportToolRegistered)), 'logicprobe: inspect provider');
204
+ providerRegistered = true;
205
+ }
206
+ catch (err) {
207
+ console.warn('[logicprobe] inspect provider registration failed', err);
208
+ }
209
+ };
210
+ const registerTool = () => {
211
+ if (toolRegistered)
212
+ return;
213
+ const tools = ctx.get('tools');
214
+ if (tools === undefined)
215
+ return;
216
+ try {
217
+ ctx.effect(() => tools.register(logicProbeVerifyTool), 'logicprobe: verify tool');
218
+ ctx.effect(() => tools.register(logicProbeDataModelVerifyTool), 'logicprobe: data verify tool');
219
+ ctx.effect(() => tools.register(logicProbeConcurrencyScanTool), 'logicprobe: concurrency scan tool');
220
+ ctx.effect(() => tools.register(logicProbeComposeTool), 'logicprobe: compose tool');
221
+ ctx.effect(() => tools.register(logicProbeExportTool), 'logicprobe: export tool');
222
+ toolRegistered = true;
223
+ dataToolRegistered = true;
224
+ concurrencyToolRegistered = true;
225
+ composeToolRegistered = true;
226
+ exportToolRegistered = true;
227
+ }
228
+ catch (err) {
229
+ console.warn('[logicprobe] logicprobe_verify/logicprobe_datamodel_verify tool registration failed', err);
230
+ }
231
+ };
232
+ const registerModeContext = () => {
233
+ if (modeContextRegistered)
234
+ return;
235
+ const systemPrompt = ctx.get('systemPrompt');
236
+ if (systemPrompt === undefined)
237
+ return;
238
+ try {
239
+ ctx.effect(() => systemPrompt.context({
240
+ name: 'logicprobe:mode',
241
+ order: 118,
242
+ text: (context) => {
243
+ const agent = context.agent;
244
+ if (agent === undefined)
245
+ return '';
246
+ return modeContextText(config, agent.session);
247
+ },
248
+ }), 'logicprobe: system prompt context');
249
+ modeContextRegistered = true;
250
+ }
251
+ catch (err) {
252
+ console.warn('[logicprobe] system prompt context registration failed', err);
253
+ }
254
+ };
255
+ const registerIntegrations = () => {
256
+ registerProvider();
257
+ registerTool();
258
+ registerModeContext();
259
+ };
260
+ registerIntegrations();
261
+ // Ship the bundled skill through the registry: reuse the standard
262
+ // filesystem provider over this package's own `skills/` directory, so
263
+ // catalog discovery, frontmatter parsing, and SKILL.md loading behave
264
+ // exactly like project/user skills while the plugin stays self-contained.
265
+ // Registration lands in the global registry layer (this row mounts at the
266
+ // profile root), so every agent preset sees the skill. `registerProvider`
267
+ // returns the effect disposer; its teardown unregisters and invalidates.
268
+ ctx.skills.registerProvider((control) => {
269
+ return new FileSystemSkillProvider(ctx, control, {
270
+ providerName: 'logicprobe',
271
+ includeDefaultRoots: false,
272
+ customSkillDirs: [SKILLS_DIR],
273
+ });
274
+ });
275
+ if (!config.enabled)
276
+ return;
277
+ // Inject the gate once per session on the FIRST model step that runs,
278
+ // instead of at session-start: session-start injection lands in the agent's
279
+ // inbox, which a blank-session preset switch (agentPreset.select ->
280
+ // recompose) can clear before the first step - the gate would then be lost
281
+ // for the whole session. The pre-step decision is the durable path a
282
+ // first-step injection takes: the gate is appended to the first step's
283
+ // decision and enters session history there, so every later step (and a
284
+ // resume) skips it. Anchored/bootstrap presets that strip first-step
285
+ // injected reminders (skill catalog, AGENTS.md, gate plugins) simply defer
286
+ // this message to the first step after their promotion - the history guard
287
+ // re-injects it there, so the gate still lands exactly once per session.
288
+ ctx.on('agent/pre-step', async ({ agent }, next) => {
289
+ const decision = await next();
290
+ if (decision.kind === 'reject')
291
+ return decision;
292
+ registerIntegrations();
293
+ if (gateInHistory(agent.session))
294
+ return decision;
295
+ return {
296
+ kind: 'enter',
297
+ messages: [...decision.messages, gateMessage(config.gateContent)],
298
+ };
299
+ });
300
+ }
301
+ /**
302
+ * Whether the gate already entered this session's durable history. The
303
+ * pre-step listener re-appends the gate until it does; once a step committed
304
+ * it, every later step (and a resume of a session that kept it) skips the
305
+ * injection. A session whose gate was dropped before any step ran (e.g. an
306
+ * inbox cleared by a blank-session preset switch) simply re-injects on the
307
+ * first step that runs.
308
+ */
309
+ function gateInHistory(session) {
310
+ return readSessionEvents(session).some((event) => {
311
+ if (event.type !== 'user/message')
312
+ return false;
313
+ const source = event.data?.source;
314
+ if (source === undefined)
315
+ return false;
316
+ // The v4 producer-owned kind, plus the pre-v4 wrapper this bundle wrote
317
+ // before DSH 0.1.7-alpha.1 retired it.
318
+ return source.kind === GATE_SOURCE_KIND
319
+ || (source.kind === 'plugin' && source.plugin === GATE_PLUGIN_ID);
320
+ });
321
+ }