@principles/workbuddy-adapter 0.2.2 → 0.2.3

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.
@@ -0,0 +1,3 @@
1
+ import type { HostEvent } from '@principles/core/host';
2
+ /** Only this native current-user hook can assert Owner authorship. No transcript is read. */
3
+ export declare function recordWorkbuddyOwnerStatement(event: HostEvent, workspaceDir: string): Promise<string | undefined>;
@@ -0,0 +1,52 @@
1
+ import { admitGovernanceSignals, ensureGovernanceDiagnosticianTask, parseOwnerStatementPrefix } from '@principles/host-runtime';
2
+ /** Only this native current-user hook can assert Owner authorship. No transcript is read. */
3
+ export async function recordWorkbuddyOwnerStatement(event, workspaceDir) {
4
+ if (event.kind !== 'before_prompt_build')
5
+ return undefined;
6
+ const parsed = parseOwnerStatementPrefix(event.context.promptContent);
7
+ if (!parsed.matched)
8
+ return undefined;
9
+ const rejected = (reason, nextAction) => `[PD] host=workbuddy status=rejected reason=owner_statement_${reason} nextAction=${nextAction}`;
10
+ if (parsed.text === null)
11
+ return rejected(parsed.reason ?? 'statement_empty', 'Resubmit PD记录痛苦:<Owner statement of 1–500 characters>; nothing was recorded.');
12
+ const raw = event.rawPayload;
13
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)
14
+ || Object.getOwnPropertyDescriptor(raw, 'is_internal_continuation')?.value !== false) {
15
+ return rejected('owner_source_unverified', 'Submit a new current-user message; native is_internal_continuation=false is required. Nothing was recorded.');
16
+ }
17
+ const { sessionId, turnId } = event.context;
18
+ if (sessionId.trim().length === 0 || turnId === undefined || turnId.trim().length === 0) {
19
+ return rejected('lineage_invalid', 'Native session_id and generation_id are required; nothing was recorded. Submit a new message.');
20
+ }
21
+ // A tuple avoids delimiter collisions; the occurrence namespace prevents
22
+ // collisions with Codex without changing its established canonical IDs.
23
+ const nativeIdentity = JSON.stringify(['workbuddy', sessionId, turnId]);
24
+ const logicalObservationKey = `${nativeIdentity}|user`;
25
+ try {
26
+ const admitted = admitGovernanceSignals({ workspaceDir, candidates: [{
27
+ kind: 'user_correction', hostKind: 'workbuddy', logicalObservationKey,
28
+ rolloutIdentity: JSON.stringify(['workbuddy', sessionId]), rootSessionId: sessionId,
29
+ hostTurnId: nativeIdentity, text: parsed.text, observedAt: new Date().toISOString(), explicitOwnerStatement: true,
30
+ }] });
31
+ if (!admitted.ok)
32
+ return rejected('continuation_failed', `${admitted.reason}: ${admitted.nextAction}`);
33
+ const [outcome] = admitted.outcomes;
34
+ if (outcome === undefined)
35
+ return rejected('continuation_failed', 'Admission returned no outcome; inspect PD status and retry the same event.');
36
+ // Admission outcomes are a trusted discriminated union from the shared runtime.
37
+ // eslint-disable-next-line no-restricted-syntax
38
+ if (!('disposition' in outcome))
39
+ return rejected('continuation_failed', `${outcome.reason}: ${outcome.nextAction}`);
40
+ if (outcome.disposition === 'rate_limited')
41
+ return rejected('rate_limited', 'The existing per-session quota is exhausted; resubmit later. Nothing was recorded.');
42
+ if (outcome.disposition !== 'admitted' && outcome.disposition !== 'already_admitted')
43
+ return rejected('continuation_failed', 'The signal was not admitted; inspect PD status.');
44
+ const ensured = await ensureGovernanceDiagnosticianTask({ workspaceDir, logicalObservationKey, canonicalPainId: outcome.canonicalPainId });
45
+ if (!ensured.ok)
46
+ return rejected('continuation_failed', `painId=${outcome.canonicalPainId}; ${ensured.reason}: ${ensured.nextAction}. The pain may already be durable; retry the same event.`);
47
+ return `[PD] host=workbuddy status=received painId=${outcome.canonicalPainId} taskId=${ensured.taskId} disposition=${outcome.disposition}. Owner statement recorded as unverified owner_statement; diagnosis, candidate admission, approval and activation are not complete.`;
48
+ }
49
+ catch {
50
+ return rejected('continuation_failed', 'Inspect PD workspace storage and retry the same event; a durable pain may exist. No completed receipt is available.');
51
+ }
52
+ }
package/dist/pd-hook.js CHANGED
@@ -14,9 +14,9 @@
14
14
  * encode/serialization fails
15
15
  * → exit 2 + bounded stderr + EMPTY stdout (P02 verified exit 2 blocks
16
16
  * the tool even when stdout is closed);
17
- * UserPromptSubmit ANY failure
18
- * → EMPTY stdout + exit 0 + degraded stderr (P02: exit 2 would CLEAR the
19
- * user's prompt — protecting the prompt outranks reporting);
17
+ * UserPromptSubmit failure
18
+ * → exit 0 + degraded stderr; explicit Owner submissions may also carry
19
+ * a bounded rejection context (exit 2 would CLEAR the user's prompt);
20
20
  * PostToolUse ANY failure
21
21
  * → EMPTY stdout + exit 0 + evidence-degradation stderr (the tool already
22
22
  * ran; nothing can be blocked, so blocking language is never used);
@@ -37,12 +37,13 @@ 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 } from '@principles/host-runtime';
40
+ import { createProductionHostRuntime, loadPdConfigForPlugin, parseOwnerStatementPrefix } 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';
44
44
  import { resolveWorkbuddyWorkspace } from './workspace-resolution.js';
45
45
  import { WorkBuddyDecoderError, WorkBuddyEncoderError, isWorkBuddyHookOutput, WORKBUDDY_EVENT_PRE_TOOL_USE } from './codec/index.js';
46
+ import { recordWorkbuddyOwnerStatement } from './explicit-owner-statement.js';
46
47
  import { recordWorkbuddyPromptDeliveryEvidence, recordWorkbuddyDenyEvidence } from './workbuddy-evidence-recorder.js';
47
48
  const MAX_DIAGNOSTIC = 500;
48
49
  function bound(text) {
@@ -206,7 +207,24 @@ export async function processWorkbuddyHookInvocation(rawStdin, channelEventName,
206
207
  catch (error) {
207
208
  const reason = error instanceof WorkBuddyDecoderError ? error.reason : `decode_threw:${errorMessage(error)}`;
208
209
  const nextAction = error instanceof WorkBuddyDecoderError ? error.nextAction : 'Inspect the WorkBuddy hook payload against the Phase 0 host contract.';
209
- return channelFailure(channelEventName, reason, nextAction);
210
+ const failure = channelFailure(channelEventName, reason, nextAction);
211
+ // Rejection communicates failure, never grants Owner authority or bypasses
212
+ // decoding. Keep malformed native identity visible to the requesting Owner.
213
+ if (channelEventName === 'UserPromptSubmit' && typeof parsed === 'object' && parsed !== null
214
+ && Object.getOwnPropertyDescriptor(parsed, 'hook_event_name')?.value === 'UserPromptSubmit'
215
+ && parseOwnerStatementPrefix(Object.getOwnPropertyDescriptor(parsed, 'prompt')?.value).matched) {
216
+ const workspace = resolveWorkbuddyWorkspace(os.homedir());
217
+ if (workspace.status === 'ok') {
218
+ const config = loadPdConfigForPlugin(workspace.workspaceDir);
219
+ if (config.ok && computeFeatureFlagsFromConfig(config.effective).flags['host.workbuddy']?.enabled === true) {
220
+ failure.stdout = { hookSpecificOutput: {
221
+ hookEventName: 'UserPromptSubmit',
222
+ additionalContext: '[PD] host=workbuddy status=rejected reason=owner_statement_decode_invalid nextAction=Native hook input is invalid; nothing was recorded. Check PD status and submit a new current-user message.',
223
+ } };
224
+ }
225
+ }
226
+ }
227
+ return failure;
210
228
  }
211
229
  // OD-07/P06 governance workspace: explicit, user-level, shared across
212
230
  // projects; never auto-created here and never falling back to another host.
@@ -234,6 +252,12 @@ export async function processWorkbuddyHookInvocation(rawStdin, channelEventName,
234
252
  // it before replacing with the governance workspace.
235
253
  const projectDir = event.context.workspaceDir.length > 0 ? event.context.workspaceDir : undefined;
236
254
  event = { ...event, context: { ...event.context, workspaceDir: resolution.workspaceDir } };
255
+ const ownerReceipt = channelEventName === 'UserPromptSubmit'
256
+ ? await recordWorkbuddyOwnerStatement(event, resolution.workspaceDir)
257
+ : undefined;
258
+ const receiptOutput = (output) => ownerReceipt === undefined ? output : {
259
+ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: [ownerReceipt, output?.hookSpecificOutput.additionalContext].filter(Boolean).join('\n\n') },
260
+ };
237
261
  try {
238
262
  if (event.kind === 'session_start') {
239
263
  // SPEC §4.2: health check only — first version injects NO body, and the
@@ -337,7 +361,7 @@ export async function processWorkbuddyHookInvocation(rawStdin, channelEventName,
337
361
  ...evaluationDiagnostics,
338
362
  ...evidenceDiagnostics,
339
363
  ];
340
- return { stdout: encoded, exitCode: 0, stderr };
364
+ return { stdout: receiptOutput(encoded), exitCode: 0, stderr: [...stderr, ...(ownerReceipt === undefined ? [] : [ownerReceipt])] };
341
365
  }
342
366
  catch (error) {
343
367
  const reason = error instanceof WorkBuddyEncoderError ? error.reason : `runtime_failed:${errorMessage(error)}`;
@@ -349,7 +373,7 @@ export async function processWorkbuddyHookInvocation(rawStdin, channelEventName,
349
373
  }
350
374
  // UserPromptSubmit: protect the prompt (exit 2 clears it — P02).
351
375
  // after_tool_call: the tool already ran — report evidence degradation only.
352
- return neutralWithDiagnostics([diagnostic('degraded', reason, nextAction)]);
376
+ return { stdout: receiptOutput(null), exitCode: 0, stderr: [diagnostic('degraded', reason, nextAction), ...(ownerReceipt === undefined ? [] : [ownerReceipt])] };
353
377
  }
354
378
  }
355
379
  // ─── 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.2",
3
+ "version": "0.2.3",
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.0",
46
- "@principles/host-runtime": "^0.10.0",
45
+ "@principles/core": "^1.293.4",
46
+ "@principles/host-runtime": "^0.10.2",
47
47
  "@principles/install-layout": "^0.4.0"
48
48
  },
49
49
  "devDependencies": {
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { existsSync } = require('node:fs');
5
+ const { homedir } = require('node:os');
6
+ const path = require('node:path');
7
+ const { pathToFileURL } = require('node:url');
8
+
9
+ async function main() {
10
+ if (process.argv.length !== 3 || process.argv[2] !== 'disable') {
11
+ throw new Error('Usage: node pd-control.cjs disable');
12
+ }
13
+ const home = homedir();
14
+ const workspace = path.join(home, '.workbuddy', 'pd-workspace');
15
+ const entry = path.join(home, '.pd', 'bootstrap', 'executor', 'dist', 'installers', 'workbuddy-host-installer.js');
16
+ if (!existsSync(entry)) throw new Error(`official_installer_api_missing: ${entry}`);
17
+ if (!existsSync(path.join(workspace, '.pd', 'config.yaml'))) {
18
+ throw new Error(`workbuddy_governance_config_missing: ${workspace}`);
19
+ }
20
+ const api = await import(pathToFileURL(entry).href);
21
+ if (typeof api.setWorkbuddyGovernanceFlag !== 'function'
22
+ || typeof api.readWorkbuddyGovernanceFlagOverride !== 'function') {
23
+ throw new Error('official_installer_api_unavailable');
24
+ }
25
+ const result = api.setWorkbuddyGovernanceFlag(workspace, false);
26
+ if (!result.ok) {
27
+ process.stdout.write(JSON.stringify({ ...result, host: 'workbuddy', workspace }) + '\n');
28
+ process.exitCode = 1;
29
+ return;
30
+ }
31
+ if (api.readWorkbuddyGovernanceFlagOverride(workspace) !== false) {
32
+ throw new Error('workbuddy_disable_readback_failed');
33
+ }
34
+ process.stdout.write(JSON.stringify({ ok: true, host: 'workbuddy', workspace, enabled: false }) + '\n');
35
+ }
36
+
37
+ main().catch((error) => {
38
+ process.stdout.write(JSON.stringify({
39
+ ok: false,
40
+ host: 'workbuddy',
41
+ reason: error instanceof Error ? error.message : String(error),
42
+ nextAction: 'Use the official installer: npx create-principles-disciple repair-update-chain; if the WorkBuddy workspace is missing, run npx create-principles-disciple --host workbuddy.',
43
+ }) + '\n');
44
+ process.exitCode = 1;
45
+ });
@@ -0,0 +1,18 @@
1
+ ---
2
+ name: pd-disable
3
+ description: 当 Owner 要求停止或关闭 WorkBuddy 的 Principles Disciple 行为时,关闭此宿主治理开关并保留数据。
4
+ ---
5
+
6
+ # 关闭 WorkBuddy PD
7
+
8
+ Owner 的关闭请求已经授权这次操作。根据本技能文件的路径定位同一插件根目录(`skills/pd-disable/SKILL.md` 的上三级目录),运行随插件交付的控制脚本:
9
+
10
+ ```text
11
+ node "<插件根目录>/scripts/pd-control.cjs" disable
12
+ ```
13
+
14
+ 脚本调用正式安装器的 `setWorkbuddyGovernanceFlag` API,在用户固定工作区 `~/.workbuddy/pd-workspace` 关闭 `host.workbuddy` 并读回验证。它保留治理数据和其他宿主配置,不手工修改数据库或已安装运行时代码。
15
+
16
+ 只有退出码为 0 且 JSON 回执 `ok=true`、`enabled=false`、`host=workbuddy` 时才确认开关已关闭。报告回执中的工作区和状态;新 Hook 调用将读取该配置。已运行中的任务是否停止须另查证。
17
+
18
+ 如果正式安装器 API 或工作区缺失,脚本明确失败;按回执使用官方安装/修复流程恢复能力。不要搜索 npm 缓存、拼装第二套写开关逻辑或用删除文件替代关闭。
@@ -0,0 +1,21 @@
1
+ ---
2
+ name: pd-record-pain
3
+ description: 记录 WorkBuddy 行为失误或痛苦反馈。当 Owner 要求登记痛苦、记录错误或留下行为改进证据时使用。
4
+ ---
5
+
6
+ # WorkBuddy 痛苦登记
7
+
8
+ 入口是 Owner 在当前 WorkBuddy 会话提交的一条消息:
9
+
10
+ ```text
11
+ PD记录痛苦:<Owner 原文>
12
+ ```
13
+
14
+ 1. 仅使用当前 Owner 明确提供的陈述,正文须为 1–500 个字符。保留原文,不补写技术根因、评分或模型自述;超长时请 Owner 提供较短原文,不静默截断。
15
+ 2. 若当前消息已以 `PD记录痛苦:` 开头,检查本次 UserPromptSubmit Hook 的真实回执。Hook 使用宿主的会话标识登记到 `~/.workbuddy/pd-workspace`。
16
+ 3. 若 Owner 仅要求登记而消息没有该前缀,给出带前缀和 Owner 原文的完整消息,请 Owner 在当前会话提交。代理输出、引用和工具输出不构成新的 Owner 提交。不要代发或模拟宿主事件。
17
+ 4. 只有回执包含真实 `painId`、`taskId`、`host=workbuddy` 和状态时才报告登记成功;原样呈现这些字段。缺失回执或返回拒绝时,说明尚未确认登记并报告真实原因,使用 `pd-status` 检查运行状态。
18
+
19
+ Owner 陈述是未经独立验证的 `owner_statement`。登记成功只证明痛苦和任务已登记;诊断、准入、原则审批和生效须分别查证。Owner 审批沿用 `pd-review`,不得自动批准或激活。
20
+
21
+ 此入口不要求模型猜测 session/turn 标识或扫描最新会话。不要改用 OpenClaw `/pd-pain` 或 CLI `pd pain record --host workbuddy`,也不要通过省略宿主参数绕过限制。
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: pd-review
3
+ description: 打开 WorkBuddy 的 PD Owner 审阅控制台,查看痛苦、诊断、原则候选或进行 Owner 审批。
4
+ ---
5
+
6
+ # WorkBuddy Owner 审阅
7
+
8
+ 将用户主目录解析为绝对路径,然后运行正式 CLI:
9
+
10
+ ```text
11
+ pd console open --workspace "<用户主目录>/.workbuddy/pd-workspace"
12
+ ```
13
+
14
+ 只报告命令实际返回的控制台地址和状态。失败时保留错误原因,不声称已打开。此工作区与 WorkBuddy Hook、worker 使用的治理库相同;当前项目目录不能替代它。
15
+
16
+ Owner 在控制台审阅证据并决定批准、拒绝或关闭。展示 Owner 原话、模型诊断和已核实证据之间的区别。打开控制台不构成审批或激活,代理不得自动代替 Owner 决策。
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: pd-status
3
+ description: 检查 WorkBuddy 的 Principles Disciple 运行状态、治理开关或诊断 worker 状态。
4
+ ---
5
+
6
+ # WorkBuddy PD 状态
7
+
8
+ 使用当前用户固定治理工作区 `~/.workbuddy/pd-workspace`,将 `~` 解析为操作系统用户主目录的绝对路径,再运行:
9
+
10
+ ```text
11
+ pd runtime features --workspace "<用户主目录>/.workbuddy/pd-workspace" --json
12
+ pd workbuddy worker --status --workspace "<用户主目录>/.workbuddy/pd-workspace" --json
13
+ ```
14
+
15
+ 报告实际命令结果、`host.workbuddy` 有效开关和 worker 模式;命令失败或安装缺失时如实报告错误,建议通过正式安装器修复。不要初始化项目目录或切换到其他宿主的治理库。
16
+
17
+ worker 状态不证明当前痛苦已完成诊断,插件存在也不证明技能已加载或原则已生效。核对具体任务应使用真实登记回执中的 ID,在 `pd-review` 指向的同一工作区审阅。