@ran-sh/dsh-crew 0.3.0 → 0.3.2

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.
Files changed (78) hide show
  1. package/.claude-plugin/marketplace.json +17 -17
  2. package/.claude-plugin/plugin.json +8 -8
  3. package/.mcp.json +8 -8
  4. package/LICENSE +21 -21
  5. package/README.de.md +359 -359
  6. package/README.es.md +359 -359
  7. package/README.fr.md +359 -359
  8. package/README.hi.md +359 -359
  9. package/README.id.md +359 -359
  10. package/README.ja.md +359 -359
  11. package/README.ko.md +359 -359
  12. package/README.md +140 -360
  13. package/README.pt.md +359 -359
  14. package/README.ru.md +359 -359
  15. package/README.th.md +359 -359
  16. package/README.tr.md +359 -359
  17. package/README.vi.md +359 -359
  18. package/README.zh-TW.md +359 -359
  19. package/README.zh.md +140 -305
  20. package/agents/ds-flash.md +26 -26
  21. package/agents/ds-pro.md +32 -32
  22. package/agents/ds-reviewer.md +23 -23
  23. package/agents/ds-worker.md +22 -22
  24. package/codex/agents/ds-flash.toml +30 -30
  25. package/codex/agents/ds-pro.toml +31 -31
  26. package/codex/agents/ds-reviewer.toml +28 -28
  27. package/codex/agents/ds-worker.toml +28 -28
  28. package/codex/prompts/dsh-config.md +3 -3
  29. package/codex/prompts/dsh-status.md +1 -1
  30. package/commands/config.md +11 -11
  31. package/commands/off.md +5 -5
  32. package/commands/on.md +5 -5
  33. package/commands/status.md +5 -5
  34. package/cordis.patch.yml +4 -4
  35. package/lib/client.js +2765 -2765
  36. package/package.json +127 -125
  37. package/scripts/build-client.mjs +28 -28
  38. package/scripts/live-crew-smoke.mjs +39 -39
  39. package/scripts/live-policy-matrix.mjs +177 -177
  40. package/scripts/policy-probe.mjs +101 -101
  41. package/scripts/setup.mjs +295 -294
  42. package/scripts/smoke-real.mjs +110 -110
  43. package/scripts/smoke.mjs +78 -78
  44. package/scripts/verify-installer-fix.mjs +26 -26
  45. package/scripts/verify-npm-install.mjs +297 -0
  46. package/src/adaptive-routing.mjs +260 -260
  47. package/src/client/activation-summary.tsx +64 -64
  48. package/src/client/entry.tsx +236 -236
  49. package/src/client/index.tsx +1120 -1120
  50. package/src/config-readiness.mjs +59 -59
  51. package/src/delivery.mjs +205 -205
  52. package/src/dsh-cli-runtime.mjs +435 -251
  53. package/src/failure-classification.mjs +172 -172
  54. package/src/hub/entry.mjs +98 -98
  55. package/src/hub-client.mjs +132 -132
  56. package/src/hub-compatibility.mjs +49 -49
  57. package/src/i18n.mjs +19 -19
  58. package/src/install/cli.mjs +28 -28
  59. package/src/install/install-legacy.mjs +460 -460
  60. package/src/install/install.mjs +451 -451
  61. package/src/mcp-runtime.mjs +257 -257
  62. package/src/model-catalog.mjs +173 -173
  63. package/src/model-routing.mjs +391 -391
  64. package/src/policy.mjs +197 -197
  65. package/src/readiness-matrix.mjs +169 -169
  66. package/src/runtime-controls.mjs +90 -90
  67. package/src/runtime-identity.mjs +108 -108
  68. package/src/server.mjs +477 -477
  69. package/src/status-shard.mjs +52 -52
  70. package/src/structured-error-code.mjs +38 -38
  71. package/src/vision-route.mjs +138 -138
  72. package/src/workflow-runtime.mjs +573 -567
  73. package/src/workflow.mjs +160 -160
  74. package/src/workspace-audit.mjs +231 -231
  75. package/src/workspace-isolation.mjs +365 -306
  76. package/statusline/statusline.sh +14 -14
  77. package/statusline/worker-segment.sh +35 -35
  78. package/worker.cordis.yml +77 -77
@@ -1,172 +1,172 @@
1
- // Pure, secret-free failure classification for workflow and MCP results.
2
- //
3
- // Classification consumes structured status/codes/outcomes only. It never
4
- // parses raw exception text, provider responses, credentials, quotas or logs.
5
-
6
- export const FAILURE_CATEGORIES = Object.freeze([
7
- 'none',
8
- 'policy',
9
- 'compatibility',
10
- 'provider',
11
- 'runtime',
12
- 'verification',
13
- 'cancelled',
14
- ]);
15
-
16
- export const FAILURE_REASON_CODES = Object.freeze({
17
- NONE: 'NONE',
18
- CANCELLED: 'CANCELLED',
19
- ATTEMPT_TIMEOUT: 'ATTEMPT_TIMEOUT',
20
- RUNTIME_FAILURE: 'RUNTIME_FAILURE',
21
- EXECUTION_FAILED: 'EXECUTION_FAILED',
22
- TESTS_FAILED: 'TESTS_FAILED',
23
- TESTS_NOT_RUN: 'TESTS_NOT_RUN',
24
- DELIVERY_INCOMPLETE: 'DELIVERY_INCOMPLETE',
25
- WORKSPACE_MISMATCH: 'WORKSPACE_MISMATCH',
26
- TASK_BLOCKED: 'TASK_BLOCKED',
27
- TASK_PARTIAL: 'TASK_PARTIAL',
28
- REVIEW_CHANGES_REQUESTED: 'REVIEW_CHANGES_REQUESTED',
29
- REVIEW_INCONCLUSIVE: 'REVIEW_INCONCLUSIVE',
30
- POLICY_REJECTED: 'POLICY_REJECTED',
31
- HUB_INCOMPATIBLE: 'HUB_INCOMPATIBLE',
32
- PROVIDER_UNAVAILABLE: 'PROVIDER_UNAVAILABLE',
33
- });
34
-
35
- const PROVIDER_CODES = new Set([
36
- 'NO_DSH_PROVIDER_SELECTED',
37
- 'NO_WORKER_MODEL_AVAILABLE',
38
- 'MODEL_CATALOG_UNAVAILABLE',
39
- 'PROVIDER_CATALOG_UNAVAILABLE',
40
- 'PROVIDER_CATALOG_HEALTH_WARNING',
41
- ]);
42
-
43
- const POLICY_CODES = new Set([
44
- 'SUBAGENTS_DISABLED',
45
- 'TIER_DISABLED',
46
- 'NO_AUTO_TIER',
47
- 'NO_WORKER_TIER',
48
- 'PRO_NOT_AUTO',
49
- 'VISION_DISABLED',
50
- 'ROLE_DISABLED',
51
- 'ROLE_NOT_AUTO',
52
- 'ROLE_TIER_CONFLICT',
53
- ]);
54
-
55
- const COMPATIBILITY_CODES = new Set([
56
- 'HUB_UNREACHABLE',
57
- 'HUB_HTTP_ERROR',
58
- 'HUB_SERVICE_MISMATCH',
59
- 'HUB_PROTOCOL_MISSING',
60
- 'HUB_PROTOCOL_MISMATCH',
61
- 'HUB_CAPABILITY_MISSING',
62
- ]);
63
-
64
- const RUNTIME_CODES = new Set([
65
- 'ISOLATION_UNAVAILABLE',
66
- 'NOT_GIT_REPOSITORY',
67
- 'GIT_NOT_FOUND',
68
- 'GIT_TIMEOUT',
69
- 'GIT_ERROR',
70
- 'WORKTREE_LOCKED',
71
- 'WORKTREE_CREATE_FAILED',
72
- 'CANDIDATE_CAPTURE_FAILED',
73
- 'ATTEMPT_INFRA_FAILURE',
74
- 'HUB_REQUEST_FAILED',
75
- ]);
76
-
77
- function normalizedCode(value) {
78
- return typeof value === 'string' && value.trim() ? value.trim() : null;
79
- }
80
-
81
- function result(category, reasonCode, { sourceCode = null, terminalReason = null } = {}) {
82
- return {
83
- schema_version: 1,
84
- category,
85
- reason_code: reasonCode,
86
- ...(sourceCode ? { source_code: sourceCode } : {}),
87
- ...(terminalReason ? { terminal_reason: terminalReason } : {}),
88
- };
89
- }
90
-
91
- function verificationRoot({ outcome, review } = {}) {
92
- if (review?.verdict === 'request_changes') return FAILURE_REASON_CODES.REVIEW_CHANGES_REQUESTED;
93
- if (review?.verdict === 'inconclusive' && review?.status === 'failed') return FAILURE_REASON_CODES.REVIEW_INCONCLUSIVE;
94
- if (outcome?.tests_status === 'FAIL') return FAILURE_REASON_CODES.TESTS_FAILED;
95
- if (outcome?.delivery?.complete === false) return FAILURE_REASON_CODES.DELIVERY_INCOMPLETE;
96
- if (outcome?.workspace_evidence_ok === false) return FAILURE_REASON_CODES.WORKSPACE_MISMATCH;
97
- if (outcome?.task_status === 'blocked') return FAILURE_REASON_CODES.TASK_BLOCKED;
98
- if (outcome?.tests_status === 'NOT RUN') return FAILURE_REASON_CODES.TESTS_NOT_RUN;
99
- if (outcome?.task_status === 'partial') return FAILURE_REASON_CODES.TASK_PARTIAL;
100
- return null;
101
- }
102
-
103
- /**
104
- * Classify one workflow/result snapshot.
105
- *
106
- * Precedence is intentional:
107
- * 1) cancellation is explicit user/runtime intent;
108
- * 2) stable compatibility/provider/policy codes identify the failing boundary;
109
- * 3) timeout/runtime execution failures outrank verification;
110
- * 4) verification uses the business evidence that caused escalation/failure;
111
- * 5) otherwise the result is `none`.
112
- *
113
- * `decision.reason` is retained only as a bounded terminal reason (for example
114
- * max_attempts_reached); it never replaces the underlying verification cause.
115
- */
116
- export function classifyFailure({
117
- phase = null,
118
- status = null,
119
- errorCode = null,
120
- outcome = null,
121
- decision = null,
122
- review = null,
123
- childAttempts = [],
124
- } = {}) {
125
- const code = normalizedCode(errorCode);
126
- const terminalReason = normalizedCode(decision?.reason);
127
-
128
- if (phase === 'cancelled' || status === 'cancelled' || code === 'WORKFLOW_CANCELLED') {
129
- return result('cancelled', FAILURE_REASON_CODES.CANCELLED, {
130
- sourceCode: code === 'WORKFLOW_CANCELLED' ? code : null,
131
- terminalReason,
132
- });
133
- }
134
-
135
- if (code && COMPATIBILITY_CODES.has(code)) {
136
- return result('compatibility', code, { sourceCode: code, terminalReason });
137
- }
138
- if (code && PROVIDER_CODES.has(code)) {
139
- return result('provider', FAILURE_REASON_CODES.PROVIDER_UNAVAILABLE, { sourceCode: code, terminalReason });
140
- }
141
- if (code && POLICY_CODES.has(code)) {
142
- return result('policy', FAILURE_REASON_CODES.POLICY_REJECTED, { sourceCode: code, terminalReason });
143
- }
144
-
145
- const attempts = Array.isArray(childAttempts) ? childAttempts : [];
146
- if (attempts.some((attempt) => attempt?.timed_out === true || attempt?.stopReason === 'timeout')) {
147
- return result('runtime', FAILURE_REASON_CODES.ATTEMPT_TIMEOUT, { terminalReason });
148
- }
149
- if (code && RUNTIME_CODES.has(code)) {
150
- return result('runtime', code, { sourceCode: code, terminalReason });
151
- }
152
- if (outcome?.execution_status === 'failed') {
153
- return result('runtime', FAILURE_REASON_CODES.EXECUTION_FAILED, { terminalReason });
154
- }
155
-
156
- const verification = verificationRoot({ outcome, review });
157
- if (verification) return result('verification', verification, { terminalReason });
158
-
159
- if (phase === 'failed' || status === 'failed') {
160
- return result('runtime', FAILURE_REASON_CODES.RUNTIME_FAILURE, {
161
- sourceCode: code,
162
- terminalReason,
163
- });
164
- }
165
-
166
- return result('none', FAILURE_REASON_CODES.NONE);
167
- }
168
-
169
- /** Classify an immediate MCP rejection before a workflow exists. */
170
- export function classifyFailureCode(code) {
171
- return classifyFailure({ status: 'failed', errorCode: code });
172
- }
1
+ // Pure, secret-free failure classification for workflow and MCP results.
2
+ //
3
+ // Classification consumes structured status/codes/outcomes only. It never
4
+ // parses raw exception text, provider responses, credentials, quotas or logs.
5
+
6
+ export const FAILURE_CATEGORIES = Object.freeze([
7
+ 'none',
8
+ 'policy',
9
+ 'compatibility',
10
+ 'provider',
11
+ 'runtime',
12
+ 'verification',
13
+ 'cancelled',
14
+ ]);
15
+
16
+ export const FAILURE_REASON_CODES = Object.freeze({
17
+ NONE: 'NONE',
18
+ CANCELLED: 'CANCELLED',
19
+ ATTEMPT_TIMEOUT: 'ATTEMPT_TIMEOUT',
20
+ RUNTIME_FAILURE: 'RUNTIME_FAILURE',
21
+ EXECUTION_FAILED: 'EXECUTION_FAILED',
22
+ TESTS_FAILED: 'TESTS_FAILED',
23
+ TESTS_NOT_RUN: 'TESTS_NOT_RUN',
24
+ DELIVERY_INCOMPLETE: 'DELIVERY_INCOMPLETE',
25
+ WORKSPACE_MISMATCH: 'WORKSPACE_MISMATCH',
26
+ TASK_BLOCKED: 'TASK_BLOCKED',
27
+ TASK_PARTIAL: 'TASK_PARTIAL',
28
+ REVIEW_CHANGES_REQUESTED: 'REVIEW_CHANGES_REQUESTED',
29
+ REVIEW_INCONCLUSIVE: 'REVIEW_INCONCLUSIVE',
30
+ POLICY_REJECTED: 'POLICY_REJECTED',
31
+ HUB_INCOMPATIBLE: 'HUB_INCOMPATIBLE',
32
+ PROVIDER_UNAVAILABLE: 'PROVIDER_UNAVAILABLE',
33
+ });
34
+
35
+ const PROVIDER_CODES = new Set([
36
+ 'NO_DSH_PROVIDER_SELECTED',
37
+ 'NO_WORKER_MODEL_AVAILABLE',
38
+ 'MODEL_CATALOG_UNAVAILABLE',
39
+ 'PROVIDER_CATALOG_UNAVAILABLE',
40
+ 'PROVIDER_CATALOG_HEALTH_WARNING',
41
+ ]);
42
+
43
+ const POLICY_CODES = new Set([
44
+ 'SUBAGENTS_DISABLED',
45
+ 'TIER_DISABLED',
46
+ 'NO_AUTO_TIER',
47
+ 'NO_WORKER_TIER',
48
+ 'PRO_NOT_AUTO',
49
+ 'VISION_DISABLED',
50
+ 'ROLE_DISABLED',
51
+ 'ROLE_NOT_AUTO',
52
+ 'ROLE_TIER_CONFLICT',
53
+ ]);
54
+
55
+ const COMPATIBILITY_CODES = new Set([
56
+ 'HUB_UNREACHABLE',
57
+ 'HUB_HTTP_ERROR',
58
+ 'HUB_SERVICE_MISMATCH',
59
+ 'HUB_PROTOCOL_MISSING',
60
+ 'HUB_PROTOCOL_MISMATCH',
61
+ 'HUB_CAPABILITY_MISSING',
62
+ ]);
63
+
64
+ const RUNTIME_CODES = new Set([
65
+ 'ISOLATION_UNAVAILABLE',
66
+ 'NOT_GIT_REPOSITORY',
67
+ 'GIT_NOT_FOUND',
68
+ 'GIT_TIMEOUT',
69
+ 'GIT_ERROR',
70
+ 'WORKTREE_LOCKED',
71
+ 'WORKTREE_CREATE_FAILED',
72
+ 'CANDIDATE_CAPTURE_FAILED',
73
+ 'ATTEMPT_INFRA_FAILURE',
74
+ 'HUB_REQUEST_FAILED',
75
+ ]);
76
+
77
+ function normalizedCode(value) {
78
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
79
+ }
80
+
81
+ function result(category, reasonCode, { sourceCode = null, terminalReason = null } = {}) {
82
+ return {
83
+ schema_version: 1,
84
+ category,
85
+ reason_code: reasonCode,
86
+ ...(sourceCode ? { source_code: sourceCode } : {}),
87
+ ...(terminalReason ? { terminal_reason: terminalReason } : {}),
88
+ };
89
+ }
90
+
91
+ function verificationRoot({ outcome, review } = {}) {
92
+ if (review?.verdict === 'request_changes') return FAILURE_REASON_CODES.REVIEW_CHANGES_REQUESTED;
93
+ if (review?.verdict === 'inconclusive' && review?.status === 'failed') return FAILURE_REASON_CODES.REVIEW_INCONCLUSIVE;
94
+ if (outcome?.tests_status === 'FAIL') return FAILURE_REASON_CODES.TESTS_FAILED;
95
+ if (outcome?.delivery?.complete === false) return FAILURE_REASON_CODES.DELIVERY_INCOMPLETE;
96
+ if (outcome?.workspace_evidence_ok === false) return FAILURE_REASON_CODES.WORKSPACE_MISMATCH;
97
+ if (outcome?.task_status === 'blocked') return FAILURE_REASON_CODES.TASK_BLOCKED;
98
+ if (outcome?.tests_status === 'NOT RUN') return FAILURE_REASON_CODES.TESTS_NOT_RUN;
99
+ if (outcome?.task_status === 'partial') return FAILURE_REASON_CODES.TASK_PARTIAL;
100
+ return null;
101
+ }
102
+
103
+ /**
104
+ * Classify one workflow/result snapshot.
105
+ *
106
+ * Precedence is intentional:
107
+ * 1) cancellation is explicit user/runtime intent;
108
+ * 2) stable compatibility/provider/policy codes identify the failing boundary;
109
+ * 3) timeout/runtime execution failures outrank verification;
110
+ * 4) verification uses the business evidence that caused escalation/failure;
111
+ * 5) otherwise the result is `none`.
112
+ *
113
+ * `decision.reason` is retained only as a bounded terminal reason (for example
114
+ * max_attempts_reached); it never replaces the underlying verification cause.
115
+ */
116
+ export function classifyFailure({
117
+ phase = null,
118
+ status = null,
119
+ errorCode = null,
120
+ outcome = null,
121
+ decision = null,
122
+ review = null,
123
+ childAttempts = [],
124
+ } = {}) {
125
+ const code = normalizedCode(errorCode);
126
+ const terminalReason = normalizedCode(decision?.reason);
127
+
128
+ if (phase === 'cancelled' || status === 'cancelled' || code === 'WORKFLOW_CANCELLED') {
129
+ return result('cancelled', FAILURE_REASON_CODES.CANCELLED, {
130
+ sourceCode: code === 'WORKFLOW_CANCELLED' ? code : null,
131
+ terminalReason,
132
+ });
133
+ }
134
+
135
+ if (code && COMPATIBILITY_CODES.has(code)) {
136
+ return result('compatibility', code, { sourceCode: code, terminalReason });
137
+ }
138
+ if (code && PROVIDER_CODES.has(code)) {
139
+ return result('provider', FAILURE_REASON_CODES.PROVIDER_UNAVAILABLE, { sourceCode: code, terminalReason });
140
+ }
141
+ if (code && POLICY_CODES.has(code)) {
142
+ return result('policy', FAILURE_REASON_CODES.POLICY_REJECTED, { sourceCode: code, terminalReason });
143
+ }
144
+
145
+ const attempts = Array.isArray(childAttempts) ? childAttempts : [];
146
+ if (attempts.some((attempt) => attempt?.timed_out === true || attempt?.stopReason === 'timeout')) {
147
+ return result('runtime', FAILURE_REASON_CODES.ATTEMPT_TIMEOUT, { terminalReason });
148
+ }
149
+ if (code && RUNTIME_CODES.has(code)) {
150
+ return result('runtime', code, { sourceCode: code, terminalReason });
151
+ }
152
+ if (outcome?.execution_status === 'failed') {
153
+ return result('runtime', FAILURE_REASON_CODES.EXECUTION_FAILED, { terminalReason });
154
+ }
155
+
156
+ const verification = verificationRoot({ outcome, review });
157
+ if (verification) return result('verification', verification, { terminalReason });
158
+
159
+ if (phase === 'failed' || status === 'failed') {
160
+ return result('runtime', FAILURE_REASON_CODES.RUNTIME_FAILURE, {
161
+ sourceCode: code,
162
+ terminalReason,
163
+ });
164
+ }
165
+
166
+ return result('none', FAILURE_REASON_CODES.NONE);
167
+ }
168
+
169
+ /** Classify an immediate MCP rejection before a workflow exists. */
170
+ export function classifyFailureCode(code) {
171
+ return classifyFailure({ status: 'failed', errorCode: code });
172
+ }
package/src/hub/entry.mjs CHANGED
@@ -1,98 +1,98 @@
1
- // Thin DSH plugin entry wrapper for v0.3 runtime compatibility discovery.
2
- //
3
- // The legacy /ping endpoint stays reachability-only inside index.mjs. This
4
- // wrapper adds a separate /runtime endpoint carrying the explicit Hub/MCP
5
- // compatibility contract, then delegates all existing Hub behavior unchanged.
6
- // It also owns the v0.3 adaptive-routing observer so the large legacy Hub body
7
- // stays untouched: only jobs that explicitly opted into adaptive routing feed
8
- // bounded process-local health for later selections.
9
-
10
- import { apply as applyHub, inject, name, WorkerRegistry } from './index.mjs';
11
- import { getHubRuntimeIdentity } from '../runtime-identity.mjs';
12
- import { getProcessAdaptiveHealthStore } from '../adaptive-routing.mjs';
13
-
14
- const RUNTIME_PATH = '/_dsh/dsh-crew/runtime';
15
- const ADAPTIVE_OBSERVER_INSTALLED = Symbol.for('@ran-sh/dsh-crew/adaptive-observer-installed');
16
- const ADAPTIVE_JOB_OBSERVED = Symbol.for('@ran-sh/dsh-crew/adaptive-job-observed');
17
-
18
- export { inject, name };
19
-
20
- function sendJson(res, status, value) {
21
- const body = JSON.stringify(value);
22
- res.writeHead(status, {
23
- 'content-type': 'application/json; charset=utf-8',
24
- 'content-length': Buffer.byteLength(body),
25
- 'cache-control': 'no-store',
26
- 'x-content-type-options': 'nosniff',
27
- 'cross-origin-resource-policy': 'same-origin',
28
- });
29
- res.end(body);
30
- }
31
-
32
- export function registerRuntimeEndpoint(ctx) {
33
- return ctx.inject(['webServer'], (webCtx) => webCtx.webServer.register({
34
- kind: 'exact',
35
- path: RUNTIME_PATH,
36
- handler: (_req, res) => sendJson(res, 200, { ok: true, ...getHubRuntimeIdentity() }),
37
- }));
38
- }
39
-
40
- /**
41
- * Record only an opt-in adaptive Hub attempt. The store accepts bounded
42
- * provider/model identifiers plus outcome/latency; arbitrary job errors,
43
- * credentials, quota and provider payloads never cross this boundary.
44
- */
45
- export function recordAdaptiveJobOutcome(job, store = getProcessAdaptiveHealthStore()) {
46
- if (job?.selection_trace?.adaptive?.enabled !== true) return false;
47
- const started = Date.parse(job.startedAt ?? '');
48
- const ended = Date.parse(job.endedAt ?? '');
49
- const latencyMs = Number.isFinite(started) && Number.isFinite(ended)
50
- ? Math.max(0, ended - started)
51
- : undefined;
52
- return store.record(
53
- { provider: job.provider, model: job.model },
54
- {
55
- role: job.role,
56
- status: job.status,
57
- stopReason: job.stopReason,
58
- latencyMs,
59
- },
60
- );
61
- }
62
-
63
- /**
64
- * Wrap WorkerRegistry.spawn once per process. Selection itself still lives in
65
- * the existing Hub implementation; rankAdaptiveCandidates reads the same
66
- * process-local store through adaptive-routing.mjs. Replacing job.promise with
67
- * a chained promise preserves all legacy completion/waiter behavior and runs
68
- * this observer only after the Hub's own finalizer populated endedAt/status.
69
- */
70
- export function installAdaptiveHealthObserver() {
71
- if (WorkerRegistry.prototype[ADAPTIVE_OBSERVER_INSTALLED] === true) return false;
72
- const originalSpawn = WorkerRegistry.prototype.spawn;
73
- Object.defineProperty(WorkerRegistry.prototype, ADAPTIVE_OBSERVER_INSTALLED, {
74
- value: true,
75
- enumerable: false,
76
- configurable: false,
77
- });
78
- WorkerRegistry.prototype.spawn = async function observedAdaptiveSpawn(...args) {
79
- const job = await originalSpawn.apply(this, args);
80
- if (!job || job[ADAPTIVE_JOB_OBSERVED] === true || typeof job.promise?.finally !== 'function') return job;
81
- Object.defineProperty(job, ADAPTIVE_JOB_OBSERVED, {
82
- value: true,
83
- enumerable: false,
84
- configurable: false,
85
- });
86
- job.promise = job.promise.finally(() => {
87
- recordAdaptiveJobOutcome(job);
88
- });
89
- return job;
90
- };
91
- return true;
92
- }
93
-
94
- export async function apply(ctx) {
95
- registerRuntimeEndpoint(ctx);
96
- installAdaptiveHealthObserver();
97
- return applyHub(ctx);
98
- }
1
+ // Thin DSH plugin entry wrapper for v0.3 runtime compatibility discovery.
2
+ //
3
+ // The legacy /ping endpoint stays reachability-only inside index.mjs. This
4
+ // wrapper adds a separate /runtime endpoint carrying the explicit Hub/MCP
5
+ // compatibility contract, then delegates all existing Hub behavior unchanged.
6
+ // It also owns the v0.3 adaptive-routing observer so the large legacy Hub body
7
+ // stays untouched: only jobs that explicitly opted into adaptive routing feed
8
+ // bounded process-local health for later selections.
9
+
10
+ import { apply as applyHub, inject, name, WorkerRegistry } from './index.mjs';
11
+ import { getHubRuntimeIdentity } from '../runtime-identity.mjs';
12
+ import { getProcessAdaptiveHealthStore } from '../adaptive-routing.mjs';
13
+
14
+ const RUNTIME_PATH = '/_dsh/dsh-crew/runtime';
15
+ const ADAPTIVE_OBSERVER_INSTALLED = Symbol.for('@ran-sh/dsh-crew/adaptive-observer-installed');
16
+ const ADAPTIVE_JOB_OBSERVED = Symbol.for('@ran-sh/dsh-crew/adaptive-job-observed');
17
+
18
+ export { inject, name };
19
+
20
+ function sendJson(res, status, value) {
21
+ const body = JSON.stringify(value);
22
+ res.writeHead(status, {
23
+ 'content-type': 'application/json; charset=utf-8',
24
+ 'content-length': Buffer.byteLength(body),
25
+ 'cache-control': 'no-store',
26
+ 'x-content-type-options': 'nosniff',
27
+ 'cross-origin-resource-policy': 'same-origin',
28
+ });
29
+ res.end(body);
30
+ }
31
+
32
+ export function registerRuntimeEndpoint(ctx) {
33
+ return ctx.inject(['webServer'], (webCtx) => webCtx.webServer.register({
34
+ kind: 'exact',
35
+ path: RUNTIME_PATH,
36
+ handler: (_req, res) => sendJson(res, 200, { ok: true, ...getHubRuntimeIdentity() }),
37
+ }));
38
+ }
39
+
40
+ /**
41
+ * Record only an opt-in adaptive Hub attempt. The store accepts bounded
42
+ * provider/model identifiers plus outcome/latency; arbitrary job errors,
43
+ * credentials, quota and provider payloads never cross this boundary.
44
+ */
45
+ export function recordAdaptiveJobOutcome(job, store = getProcessAdaptiveHealthStore()) {
46
+ if (job?.selection_trace?.adaptive?.enabled !== true) return false;
47
+ const started = Date.parse(job.startedAt ?? '');
48
+ const ended = Date.parse(job.endedAt ?? '');
49
+ const latencyMs = Number.isFinite(started) && Number.isFinite(ended)
50
+ ? Math.max(0, ended - started)
51
+ : undefined;
52
+ return store.record(
53
+ { provider: job.provider, model: job.model },
54
+ {
55
+ role: job.role,
56
+ status: job.status,
57
+ stopReason: job.stopReason,
58
+ latencyMs,
59
+ },
60
+ );
61
+ }
62
+
63
+ /**
64
+ * Wrap WorkerRegistry.spawn once per process. Selection itself still lives in
65
+ * the existing Hub implementation; rankAdaptiveCandidates reads the same
66
+ * process-local store through adaptive-routing.mjs. Replacing job.promise with
67
+ * a chained promise preserves all legacy completion/waiter behavior and runs
68
+ * this observer only after the Hub's own finalizer populated endedAt/status.
69
+ */
70
+ export function installAdaptiveHealthObserver() {
71
+ if (WorkerRegistry.prototype[ADAPTIVE_OBSERVER_INSTALLED] === true) return false;
72
+ const originalSpawn = WorkerRegistry.prototype.spawn;
73
+ Object.defineProperty(WorkerRegistry.prototype, ADAPTIVE_OBSERVER_INSTALLED, {
74
+ value: true,
75
+ enumerable: false,
76
+ configurable: false,
77
+ });
78
+ WorkerRegistry.prototype.spawn = async function observedAdaptiveSpawn(...args) {
79
+ const job = await originalSpawn.apply(this, args);
80
+ if (!job || job[ADAPTIVE_JOB_OBSERVED] === true || typeof job.promise?.finally !== 'function') return job;
81
+ Object.defineProperty(job, ADAPTIVE_JOB_OBSERVED, {
82
+ value: true,
83
+ enumerable: false,
84
+ configurable: false,
85
+ });
86
+ job.promise = job.promise.finally(() => {
87
+ recordAdaptiveJobOutcome(job);
88
+ });
89
+ return job;
90
+ };
91
+ return true;
92
+ }
93
+
94
+ export async function apply(ctx) {
95
+ registerRuntimeEndpoint(ctx);
96
+ installAdaptiveHealthObserver();
97
+ return applyHub(ctx);
98
+ }