@principles/workbuddy-adapter 0.2.13 → 0.2.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principles/workbuddy-adapter",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
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.297.3",
46
- "@principles/host-runtime": "^0.14.0",
45
+ "@principles/core": "^1.297.4",
46
+ "@principles/host-runtime": "^0.14.1",
47
47
  "@principles/install-layout": "^0.4.0"
48
48
  },
49
49
  "devDependencies": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "principles-disciple",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Principles Disciple — Owner-governed behavior internalization for AI agents (WorkBuddy host adapter, ADR-0020 third host). Requires explicit setup and host.workbuddy feature enablement; inactive until then.",
5
5
  "defaultEnabled": false
6
6
  }
@@ -42,7 +42,7 @@
42
42
  {
43
43
  "type": "command",
44
44
  "command": "node \"${CODEBUDDY_PLUGIN_ROOT}/scripts/wrapper.cjs\" UserPromptSubmit",
45
- "timeout": 30
45
+ "timeout": 15
46
46
  }
47
47
  ]
48
48
  }
@@ -23,6 +23,25 @@
23
23
  * 1 with a bounded stderr diagnostic: probe P02 established exit 1 is
24
24
  * non-blocking on WorkBuddy, and the SPEC §4.4 startup-failure row forbids
25
25
  * claiming self-blocking the wrapper cannot guarantee.
26
+ *
27
+ * SELF-DEADLINE (SPEC §4.4 timeout row): the adapter entry's own fail-open
28
+ * exits can never run when the HOST kills this wrapper on the hooks.json
29
+ * timeout — the process dies mid-flight, and the host's timeout semantics for
30
+ * a killed hook are undefined. So the wrapper arms a deadline STRICTLY BELOW
31
+ * the host budget and kills the child itself, degrading non-blocking
32
+ * (exit 1 + bounded stderr) instead of leaving the worst failure mode — a
33
+ * lost UserPromptSubmit prompt — to the host. The budget pairs are locked
34
+ * below the hooks.json timeouts by tests/plugin-skeleton.test.ts:
35
+ * UserPromptSubmit 12000ms < 15s (prompt loss is the costliest channel);
36
+ * every other channel 25000ms < 30s.
37
+ * PD_WORKBUDDY_HOOK_SELF_TIMEOUT_MS overrides the table (integer ms > 0, all
38
+ * events) for tests and field diagnostics; invalid values fall back to the
39
+ * table, and a valid one is CLAMPED strictly below the host budget so the
40
+ * override can never re-create the bug this deadline exists to fix.
41
+ *
42
+ * A killed child gets no chance to clean up (SIGTERM is TerminateProcess on
43
+ * Windows), so the deadline path deliberately does NOT promise adapter-side
44
+ * flush of evidence / SQLite locks — see SPEC §4.4.
26
45
  */
27
46
  const { spawnSync } = require('node:child_process');
28
47
  const fs = require('node:fs');
@@ -30,13 +49,42 @@ const path = require('node:path');
30
49
 
31
50
  const eventName = process.argv[2];
32
51
 
33
- function fail(message) {
52
+ // hooks.json host budgets in ms. Kept here ONLY so the override below can be
53
+ // clamped and so the guard test can prove the two tables agree with
54
+ // hooks.json; the host remains the authority for actually killing us.
55
+ const HOST_TIMEOUT_MS = { UserPromptSubmit: 15000, default: 30000 };
56
+ // Margin for this process's own startup plus the degradation write, so our
57
+ // timer always fires before the host's.
58
+ const SELF_DEADLINE_MARGIN_MS = 3000;
59
+ // Must stay strictly below the matching hooks.json timeout (seconds) — the
60
+ // guard test in tests/plugin-skeleton.test.ts locks every pair.
61
+ const SELF_TIMEOUT_MS = { UserPromptSubmit: 12000, default: 25000 };
62
+
63
+ function hostBudgetMs(event) {
64
+ return HOST_TIMEOUT_MS[event] ?? HOST_TIMEOUT_MS.default;
65
+ }
66
+
67
+ function resolveSelfDeadlineMs(event) {
68
+ const budget = SELF_TIMEOUT_MS[event] ?? SELF_TIMEOUT_MS.default;
69
+ const override = Number.parseInt(process.env.PD_WORKBUDDY_HOOK_SELF_TIMEOUT_MS ?? '', 10);
70
+ if (!Number.isInteger(override) || override <= 0) return budget;
71
+ // Clamp, never trust: a diagnostic setting of 60000 would otherwise leave
72
+ // the outcome to the host's undefined timeout semantics again.
73
+ return Math.max(1, Math.min(override, hostBudgetMs(event) - SELF_DEADLINE_MARGIN_MS));
74
+ }
75
+
76
+ function fail(message, nextAction = 'Reinstall PD (npx create-principles-disciple) or set PD_WORKBUDDY_ADAPTER_ENTRY to the built adapter entry.') {
34
77
  try {
35
- process.stderr.write(`[PD] status=degraded reason=workbuddy_wrapper:${message} nextAction=Reinstall PD (npx create-principles-disciple) or set PD_WORKBUDDY_ADAPTER_ENTRY to the built adapter entry.\n`);
78
+ process.stderr.write(`[PD] status=degraded reason=workbuddy_wrapper:${message} nextAction=${nextAction}\n`);
36
79
  } catch (_) { /* no channel left */ }
37
80
  process.exit(1);
38
81
  }
39
82
 
83
+ // The adapter child was killed at OUR deadline (before the host timeout):
84
+ // the hook result is a non-blocking degradation, so the user's prompt/tool
85
+ // call survives — the recovery action is diagnosis, not reinstall.
86
+ const DEADLINE_NEXT_ACTION = 'The hook degraded at its self-deadline before the host timeout (non-blocking: the prompt/tool call survived). If this repeats, inspect PD stderr and the governance workspace, and time one manual pd-hook run.';
87
+
40
88
  function resolveEntry() {
41
89
  const override = process.env.PD_WORKBUDDY_ADAPTER_ENTRY;
42
90
  if (override && override.trim().length > 0) {
@@ -62,18 +110,34 @@ try {
62
110
  } catch (_) {
63
111
  fail('entry_not_found:' + entry.slice(0, 120));
64
112
  }
113
+ const deadlineMs = resolveSelfDeadlineMs(eventName);
114
+ const startedAt = Date.now();
65
115
  const child = spawnSync(process.execPath, [entry, eventName], {
66
116
  stdio: ['inherit', 'inherit', 'inherit'],
67
117
  windowsHide: true,
118
+ timeout: deadlineMs,
119
+ killSignal: 'SIGTERM',
68
120
  });
69
121
 
70
122
  if (child.error) {
123
+ // spawnSync surfaces its OWN timeout kill as error.code === 'ETIMEDOUT'
124
+ // (status null, no child exit code) — that is the self-deadline firing,
125
+ // not a spawn failure. Degrade non-blocking with the observable budget.
126
+ if (child.error.code === 'ETIMEDOUT') {
127
+ fail(`entry_deadline_exceeded:${Date.now() - startedAt}ms_budget_${deadlineMs}ms`, DEADLINE_NEXT_ACTION);
128
+ }
71
129
  const code = child.error.code === 'ENOENT' ? 'entry_not_found' : 'spawn_failed';
72
130
  fail(`${code}:${child.error.message.slice(0, 120)}`);
73
131
  }
74
132
  if (child.status === null) {
75
- // Killed by signal (e.g. host timeout): mirror as a non-blocking exit 1
76
- // with the observable reason; P02 timeout semantics are host-managed.
133
+ // Killed by signal. When our own deadline armed first, this is the
134
+ // self-timeout degradation (the host kills THIS wrapper, not the child);
135
+ // report it distinctly so field logs separate PD-side degradation from an
136
+ // externally killed adapter.
137
+ const elapsedMs = Date.now() - startedAt;
138
+ if (child.signal === 'SIGTERM' && elapsedMs >= deadlineMs - 500) {
139
+ fail(`entry_deadline_exceeded:${elapsedMs}ms_budget_${deadlineMs}ms`, DEADLINE_NEXT_ACTION);
140
+ }
77
141
  fail(`entry_killed_signal:${String(child.signal).slice(0, 30)}`);
78
142
  }
79
143
  process.exit(child.status);