@principles/workbuddy-adapter 0.2.3 → 0.2.5

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.
@@ -180,6 +180,18 @@ export function decodeWorkbuddyInput(raw) {
180
180
  // is ever fabricated when missing (rc-6).
181
181
  const turnId = consumedOptionalString(raw, 'generation_id');
182
182
  const base = { sessionId, ...(turnId !== undefined ? { turnId } : {}) };
183
+ // SPEC-PD-MODEL-AWARE-PROMPT-BUDGET-001 §5.3: `model` is an OBSERVED
184
+ // OPTIONAL field of the frozen host contract — the only model identity
185
+ // this host exposes (no capacity field exists). Consumed SOFTLY (absent or
186
+ // malformed → undefined, fixed-budget fallback) rather than
187
+ // consumedOptionalString's fail-loud: SPEC §8.6 forbids metadata failure
188
+ // from blocking the host conversation, and a bad model string degrades to
189
+ // the legacy budget instead of erroring the hook. Dynamic support is
190
+ // therefore catalog-unambiguous-match-only — never a guessed capacity.
191
+ const rawModel = own(raw, 'model');
192
+ const modelId = typeof rawModel === 'string' && rawModel.trim().length > 0 ? rawModel : undefined;
193
+ const modelContext = modelId !== undefined ? { modelId } : undefined;
194
+ const baseWithContext = modelContext !== undefined ? { ...base, modelContext } : base;
183
195
  let context;
184
196
  let rawPayload = raw;
185
197
  if (kind === 'session_start') {
@@ -190,12 +202,12 @@ export function decodeWorkbuddyInput(raw) {
190
202
  requiredString(raw, 'permission_mode');
191
203
  requiredString(raw, 'client');
192
204
  requiredString(raw, 'version');
193
- context = { ...base, workspaceDir: '', source };
205
+ context = { ...baseWithContext, workspaceDir: '', source };
194
206
  }
195
207
  else if (kind === 'before_prompt_build') {
196
208
  const cwd = requiredAbsoluteCwd(raw);
197
209
  validateOptionalStringType(raw, 'permission_mode');
198
- context = { ...base, workspaceDir: cwd, promptContent: requiredString(raw, 'prompt') };
210
+ context = { ...baseWithContext, workspaceDir: cwd, promptContent: requiredString(raw, 'prompt') };
199
211
  }
200
212
  else {
201
213
  const cwd = requiredAbsoluteCwd(raw);
@@ -218,10 +230,10 @@ export function decodeWorkbuddyInput(raw) {
218
230
  const thrownError = requiredString(raw, 'error');
219
231
  toolOutput = { result: raw, error: thrownError };
220
232
  }
221
- context = { ...base, workspaceDir: cwd, toolName, ...(callId !== undefined ? { toolCallId: callId } : {}), toolInput, toolOutput };
233
+ context = { ...baseWithContext, workspaceDir: cwd, toolName, ...(callId !== undefined ? { toolCallId: callId } : {}), toolInput, toolOutput };
222
234
  }
223
235
  else {
224
- context = { ...base, workspaceDir: cwd, toolName, ...(callId !== undefined ? { toolCallId: callId } : {}), toolInput };
236
+ context = { ...baseWithContext, workspaceDir: cwd, toolName, ...(callId !== undefined ? { toolCallId: callId } : {}), toolInput };
225
237
  }
226
238
  // The shared production gate reads its tool tuple from rawPayload.toolInput
227
239
  // (production-rulehost-gate readToolInput contract).
package/dist/pd-hook.js CHANGED
@@ -37,7 +37,7 @@ import path from 'node:path';
37
37
  import process from 'node:process';
38
38
  import { pathToFileURL, fileURLToPath } from 'node:url';
39
39
  import { appendEventLogLine, redactTelemetryString, isRuleHostEvaluatedEventData } from '@principles/core/runtime-v2';
40
- import { createProductionHostRuntime, loadPdConfigForPlugin, parseOwnerStatementPrefix } from '@principles/host-runtime';
40
+ import { createProductionHostRuntime, loadPdConfigForPlugin, parseOwnerStatementPrefix, assessWorkbuddyLiveKeyword, admitTrustedWorkbuddyLiveCorrection, ensureGovernanceDiagnosticianTask } from '@principles/host-runtime';
41
41
  import { computeFeatureFlagsFromConfig } from '@principles/core/runtime-v2';
42
42
  import { WorkBuddyHooksHostAdapter } from './host-adapter.js';
43
43
  import { WORKBUDDY_TOOL_SEMANTICS } from './tool-semantics.js';
@@ -255,6 +255,76 @@ export async function processWorkbuddyHookInvocation(rawStdin, channelEventName,
255
255
  const ownerReceipt = channelEventName === 'UserPromptSubmit'
256
256
  ? await recordWorkbuddyOwnerStatement(event, resolution.workspaceDir)
257
257
  : undefined;
258
+ // SPEC-PD-CROSS-HOST-KEYWORD-PAIN-001 P0: only a REAL current-user
259
+ // UserPromptSubmit may enter this quiet, default-OFF live keyword path.
260
+ // It does not inspect transcripts or alter normal hook stdout / exit codes.
261
+ const autoDiagnostics = [];
262
+ if (channelEventName === 'UserPromptSubmit' &&
263
+ event.kind === 'before_prompt_build' &&
264
+ flags.workbuddy_live_keyword_pain?.enabled === true) {
265
+ try {
266
+ const raw = parsed;
267
+ const prompt = event.context.promptContent;
268
+ const fromOwner = Object.getOwnPropertyDescriptor(raw, 'is_internal_continuation')?.value === false;
269
+ const nativeId = event.context.turnId;
270
+ const explicitReserved = typeof prompt === 'string' && prompt.trimStart().startsWith('PD记录痛苦');
271
+ if (fromOwner && nativeId !== undefined && nativeId.trim().length > 0 &&
272
+ typeof prompt === 'string' && !explicitReserved) {
273
+ const matched = assessWorkbuddyLiveKeyword(prompt, event.context.sessionId);
274
+ if (matched.matched) {
275
+ const admitted = admitTrustedWorkbuddyLiveCorrection({
276
+ workspaceDir: resolution.workspaceDir,
277
+ rawEvent: parsed,
278
+ });
279
+ if (!admitted.ok) {
280
+ autoDiagnostics.push(diagnostic('degraded', 'workbuddy_keyword_admission_' + admitted.reason, admitted.nextAction));
281
+ }
282
+ else {
283
+ for (const outcome of admitted.outcomes) {
284
+ if (!('disposition' in outcome)) {
285
+ autoDiagnostics.push(diagnostic('degraded', 'workbuddy_keyword_admission_rejected', outcome.reason));
286
+ continue;
287
+ }
288
+ if (outcome.disposition === 'admitted' || outcome.disposition === 'already_admitted') {
289
+ const ensured = await ensureGovernanceDiagnosticianTask({
290
+ workspaceDir: resolution.workspaceDir,
291
+ logicalObservationKey: outcome.logicalObservationKey,
292
+ canonicalPainId: outcome.canonicalPainId,
293
+ });
294
+ if (!ensured.ok) {
295
+ autoDiagnostics.push(diagnostic('degraded', 'workbuddy_keyword_task_' + ensured.reason, ensured.nextAction));
296
+ }
297
+ else {
298
+ // Metadata only: never log the raw prompt or fabricate a
299
+ // completed diagnosis, approval or activation.
300
+ try {
301
+ appendEventLogLine(path.join(resolution.workspaceDir, '.state'), {
302
+ ts: new Date().toISOString(),
303
+ type: 'workbuddy_keyword_pain_admitted',
304
+ category: 'detected',
305
+ sessionId: event.context.sessionId,
306
+ data: { hostKind: 'workbuddy', outcome: outcome.disposition,
307
+ painId: outcome.canonicalPainId, taskId: ensured.taskId },
308
+ });
309
+ }
310
+ catch { /* Evidence-log failure must not block the prompt. */ }
311
+ }
312
+ }
313
+ else if (outcome.disposition === 'rate_limited') {
314
+ autoDiagnostics.push(diagnostic('degraded', 'workbuddy_keyword_rate_limited', 'Per-session strong signal limit reached; no new Pain was created.'));
315
+ }
316
+ }
317
+ }
318
+ }
319
+ else if (matched.reason === 'prompt_too_large') {
320
+ autoDiagnostics.push(diagnostic('degraded', 'workbuddy_keyword_prompt_too_large', 'Oversized prompt was not scanned or persisted as a keyword signal.'));
321
+ }
322
+ }
323
+ }
324
+ catch (error) {
325
+ autoDiagnostics.push(diagnostic('degraded', 'workbuddy_keyword_unexpected:' + errorMessage(error), 'The current prompt was preserved; inspect the governance workspace.'));
326
+ }
327
+ }
258
328
  const receiptOutput = (output) => ownerReceipt === undefined ? output : {
259
329
  hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: [ownerReceipt, output?.hookSpecificOutput.additionalContext].filter(Boolean).join('\n\n') },
260
330
  };
@@ -360,6 +430,7 @@ export async function processWorkbuddyHookInvocation(rawStdin, channelEventName,
360
430
  ...annotateContextWarnings(result.warnings ?? []).slice(0, 16).map((warning) => diagnostic('degraded', warning, 'Inspect the PD governance workspace state and retry; the hook stayed permissive (allow+warnings is degraded health, not fail-closed).')),
361
431
  ...evaluationDiagnostics,
362
432
  ...evidenceDiagnostics,
433
+ ...autoDiagnostics,
363
434
  ];
364
435
  return { stdout: receiptOutput(encoded), exitCode: 0, stderr: [...stderr, ...(ownerReceipt === undefined ? [] : [ownerReceipt])] };
365
436
  }
@@ -373,7 +444,7 @@ export async function processWorkbuddyHookInvocation(rawStdin, channelEventName,
373
444
  }
374
445
  // UserPromptSubmit: protect the prompt (exit 2 clears it — P02).
375
446
  // after_tool_call: the tool already ran — report evidence degradation only.
376
- return { stdout: receiptOutput(null), exitCode: 0, stderr: [diagnostic('degraded', reason, nextAction), ...(ownerReceipt === undefined ? [] : [ownerReceipt])] };
447
+ return { stdout: receiptOutput(null), exitCode: 0, stderr: [diagnostic('degraded', reason, nextAction), ...autoDiagnostics, ...(ownerReceipt === undefined ? [] : [ownerReceipt])] };
377
448
  }
378
449
  }
379
450
  // ─── Process-boundary belt (PRI-943 pattern, WorkBuddy exit-code semantics) ──
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principles/workbuddy-adapter",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "WorkBuddy host adapter for Principles Disciple — implements the HostAdapter interface for WorkBuddy's Git Bash command-hook model (ADR-0020 third host, SPEC workbuddy-host-adapter v0.3.0).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -42,8 +42,8 @@
42
42
  "lint": "eslint \"src/**/*.ts\""
43
43
  },
44
44
  "dependencies": {
45
- "@principles/core": "^1.293.4",
46
- "@principles/host-runtime": "^0.10.2",
45
+ "@principles/core": "^1.295.0",
46
+ "@principles/host-runtime": "^0.11.0",
47
47
  "@principles/install-layout": "^0.4.0"
48
48
  },
49
49
  "devDependencies": {