@ran-sh/dsh-crew 1.1.0 → 1.2.0-rc.4

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 (85) hide show
  1. package/.claude-plugin/marketplace.json +16 -17
  2. package/.claude-plugin/plugin.json +7 -1
  3. package/README.md +44 -15
  4. package/README.zh.md +37 -12
  5. package/agents/ds-flash.md +16 -18
  6. package/agents/ds-pro.md +16 -24
  7. package/agents/ds-reviewer.md +15 -15
  8. package/agents/ds-worker.md +16 -16
  9. package/bin/dsh-crew.mjs +16 -12
  10. package/codex/AGENTS.md +37 -85
  11. package/codex/agents/ds-flash.toml +18 -14
  12. package/codex/agents/ds-pro.toml +18 -15
  13. package/codex/agents/ds-reviewer.toml +17 -13
  14. package/codex/agents/ds-worker.toml +17 -13
  15. package/codex/prompts/dsh-config.md +17 -1
  16. package/codex/prompts/dsh-status.md +5 -1
  17. package/commands/config.md +16 -4
  18. package/commands/status.md +6 -2
  19. package/docs/installation.md +54 -9
  20. package/docs/readiness-matrix.md +14 -6
  21. package/docs/ui-surfaces.md +32 -17
  22. package/lib/client.js +5434 -4844
  23. package/official-web-bridge/lib/client.js +567 -479
  24. package/official-web-bridge/overlay-entry.mjs +59 -0
  25. package/package.json +6 -3
  26. package/scripts/build-client.mjs +2 -0
  27. package/scripts/setup.mjs +8 -7
  28. package/scripts/verify-crew-ui-polish.mjs +145 -0
  29. package/scripts/verify-history-ui.mjs +97 -0
  30. package/scripts/verify-npm-install.mjs +1 -1
  31. package/src/client/history-panel.tsx +108 -0
  32. package/src/client/host-readiness.mjs +15 -6
  33. package/src/client/index.tsx +86 -64
  34. package/src/client/model-callability-view.mjs +17 -0
  35. package/src/client/panel-chrome.tsx +40 -0
  36. package/src/client/quick-panel.tsx +88 -73
  37. package/src/client/readiness-envelope.mjs +34 -0
  38. package/src/config-readiness.mjs +181 -115
  39. package/src/extension-contract.mjs +44 -26
  40. package/src/history/admission-gate.mjs +51 -0
  41. package/src/history/archive-store.mjs +261 -0
  42. package/src/history/cleanup-plan.mjs +64 -0
  43. package/src/history/http.mjs +32 -0
  44. package/src/history/operation.mjs +86 -0
  45. package/src/history/runner.mjs +51 -0
  46. package/src/history/runtime.mjs +28 -0
  47. package/src/history/service.mjs +137 -0
  48. package/src/history/state.mjs +27 -0
  49. package/src/hub/index.mjs +141 -24
  50. package/src/hub-client.mjs +17 -15
  51. package/src/image-output.mjs +71 -0
  52. package/src/install/install-legacy.mjs +126 -23
  53. package/src/install/npx-lifecycle.mjs +373 -64
  54. package/src/install/official-frontend-assets.mjs +78 -0
  55. package/src/install/payload-content.mjs +88 -0
  56. package/src/install/windows-startup.mjs +107 -19
  57. package/src/install/windows-supervisor-adapter.mjs +443 -0
  58. package/src/install/windows-supervisor-lifecycle.mjs +782 -0
  59. package/src/install/zcode.mjs +53 -14
  60. package/src/job-contracts.mjs +37 -31
  61. package/src/jobs.mjs +2 -2
  62. package/src/mcp-runtime.mjs +74 -9
  63. package/src/model-callability-contract.mjs +79 -0
  64. package/src/multimodal.mjs +0 -0
  65. package/src/official-web-bridge.mjs +0 -2
  66. package/src/provider-health.mjs +130 -124
  67. package/src/provider-profile-store.mjs +12 -0
  68. package/src/provider-settings-store.mjs +347 -5
  69. package/src/readiness-matrix.mjs +20 -15
  70. package/src/runtime-identity-contract.mjs +34 -0
  71. package/src/runtime-identity.mjs +14 -4
  72. package/src/runtime-readiness-snapshot.mjs +285 -87
  73. package/src/server.mjs +86 -76
  74. package/src/standalone-sdk.mjs +23 -0
  75. package/src/supervisor/restart-request.mjs +64 -10
  76. package/src/workflow-runtime.mjs +28 -13
  77. package/windows/start-dsh-crew.cmd +57 -57
  78. package/windows/start-dsh-crew.ps1 +327 -55
  79. package/windows/supervisor-control.ps1 +467 -0
  80. package/zcode/AGENTS.md +4 -1
  81. package/zcode/agents/ds-reviewer.md +31 -36
  82. package/zcode/agents/ds-worker.md +31 -39
  83. package/zcode/commands/dsh-config.md +17 -6
  84. package/zcode/commands/dsh-status.md +5 -4
  85. package/.mcp.json +0 -8
@@ -124,6 +124,31 @@ export function maintenanceResultsDir(appRoot) {
124
124
  return join(supervisorStateRoot(appRoot), 'maintenance-results');
125
125
  }
126
126
 
127
+ export function maintenanceSessionFile(appRoot) {
128
+ return join(supervisorStateRoot(appRoot), 'maintenance-session.json');
129
+ }
130
+
131
+ export function readMaintenanceSession(appRoot) {
132
+ const file = maintenanceSessionFile(appRoot);
133
+ if (!existsSync(file)) return { ok: true, state: 'absent', session: null };
134
+ try {
135
+ const session = JSON.parse(readFileSync(file, 'utf8'));
136
+ const valid = session?.schema_version === 1
137
+ && session.state === 'STOPPED'
138
+ && typeof session.lease === 'string'
139
+ && session.lease.length > 0
140
+ && typeof session.runtime_id === 'string'
141
+ && session.runtime_id.length > 0
142
+ && typeof session.request_id === 'string'
143
+ && session.request_id.length > 0;
144
+ return valid
145
+ ? { ok: true, state: 'present', session }
146
+ : { ok: false, state: 'malformed', code: 'MAINTENANCE_SESSION_MALFORMED', session: null };
147
+ } catch (error) {
148
+ return { ok: false, state: 'malformed', code: 'MAINTENANCE_SESSION_MALFORMED', error: error?.message, session: null };
149
+ }
150
+ }
151
+
127
152
  export function readRestartRequest(appRoot, requestId) {
128
153
  try {
129
154
  const parsed = JSON.parse(readFileSync(join(restartRequestsDir(appRoot), `${requestId}.json`), 'utf8'));
@@ -170,24 +195,47 @@ export function readRestartResult(appRoot, requestId) {
170
195
  return null;
171
196
  }
172
197
 
173
- /**
174
- * Read the supervisor heartbeat. Returns null when the supervisor is not
175
- * actively heartbeating (absent, stale beyond tolerance, or malformed).
176
- */
177
- export function readSupervisorHeartbeat(appRoot, { now = Date.now(), staleAfterMs = 15_000 } = {}) {
198
+ /** Observe a fresh heartbeat without granting it restart authority. */
199
+ export function readSupervisorHeartbeatRecord(appRoot, { now = Date.now(), staleAfterMs = 15_000 } = {}) {
178
200
  try {
179
201
  const parsed = JSON.parse(readFileSync(heartbeatFile(appRoot), 'utf8'));
180
202
  if (parsed?.schema_version !== 1 || !Number.isInteger(parsed.pid) || parsed.pid < 1) return null;
181
- if (typeof parsed.last_seen !== 'number' || now - parsed.last_seen > staleAfterMs) return null;
182
- return parsed;
203
+ if (typeof parsed.last_seen !== 'number' || now - parsed.last_seen > staleAfterMs || parsed.last_seen - now > 5_000) return null;
204
+ const hasOwnershipField = Object.hasOwn(parsed, 'ownership_ready');
205
+ const identityReady = typeof parsed.process_started_at_utc_ticks === 'string'
206
+ && /^\d+$/u.test(parsed.process_started_at_utc_ticks)
207
+ && parsed.process_started_at_utc_ticks !== '0'
208
+ && typeof parsed.helper_hash === 'string'
209
+ && /^[a-f0-9]{64}$/u.test(parsed.helper_hash)
210
+ && typeof parsed.supervisor_instance_id === 'string'
211
+ && parsed.supervisor_instance_id.trim().length > 0;
212
+ const state = !hasOwnershipField ? 'legacy-v1' : parsed.ownership_ready === true && identityReady ? 'ready' : 'starting';
213
+ return { state, record: { ...parsed, ownership_source: state } };
183
214
  } catch { return null; }
184
215
  }
185
216
 
186
- export function writeSupervisorHeartbeat({ appRoot, pid, now = Date.now() }) {
217
+ /** Return only an ownership-proven supervisor heartbeat. */
218
+ export function readSupervisorHeartbeat(appRoot, options = {}) {
219
+ const observed = readSupervisorHeartbeatRecord(appRoot, options);
220
+ return observed?.state === 'ready' ? observed.record : null;
221
+ }
222
+
223
+ export function writeSupervisorHeartbeat({
224
+ appRoot,
225
+ pid,
226
+ now = Date.now(),
227
+ ownershipReady = true,
228
+ processStartedAtUtcTicks = null,
229
+ helperHash = null,
230
+ supervisorInstanceId = randomUUID(),
231
+ }) {
187
232
  const record = {
188
233
  schema_version: 1,
189
- supervisor_instance_id: randomUUID(),
234
+ supervisor_instance_id: supervisorInstanceId,
190
235
  pid,
236
+ process_started_at_utc_ticks: typeof processStartedAtUtcTicks === 'string' ? processStartedAtUtcTicks : null,
237
+ helper_hash: helperHash,
238
+ ownership_ready: ownershipReady === true,
191
239
  last_seen: now,
192
240
  protocol_version: 1,
193
241
  };
@@ -195,7 +243,13 @@ export function writeSupervisorHeartbeat({ appRoot, pid, now = Date.now() }) {
195
243
  // run (the watcher rewrites this file every few seconds).
196
244
  try {
197
245
  const prev = JSON.parse(readFileSync(heartbeatFile(appRoot), 'utf8'));
198
- if (prev?.schema_version === 1) record.supervisor_instance_id = prev.supervisor_instance_id;
246
+ const sameWatcher = prev?.schema_version === 1
247
+ && prev.pid === record.pid
248
+ && prev.process_started_at_utc_ticks === record.process_started_at_utc_ticks
249
+ && prev.helper_hash === record.helper_hash
250
+ && typeof prev.supervisor_instance_id === 'string'
251
+ && prev.supervisor_instance_id.length > 0;
252
+ if (sameWatcher) record.supervisor_instance_id = prev.supervisor_instance_id;
199
253
  } catch { /* first beat */ }
200
254
  writeFileAtomic(heartbeatFile(appRoot), JSON.stringify(record, null, 2) + '\n');
201
255
  return record;
@@ -299,8 +299,8 @@ export function createWorkflowRuntime(adapters, {
299
299
  let config = {};
300
300
 
301
301
  try {
302
- config = adapters.getConfig?.() ?? {};
303
- job.review_gate = ['required', 'optional', 'off'].includes(config?.review?.gate) ? config.review.gate : 'required';
302
+ config = adapters.getConfig?.() ?? {};
303
+ job.review_gate = ['required', 'optional', 'off'].includes(config?.review?.gate) ? config.review.gate : 'required';
304
304
  const alloc = await adapters.allocateWorkspace?.(job);
305
305
  if (alloc && alloc.ok === false) {
306
306
  failJob(job, Object.assign(new Error(alloc.error ?? alloc.reason), { code: alloc.reason }));
@@ -322,7 +322,11 @@ export function createWorkflowRuntime(adapters, {
322
322
  if (isReviewJob) {
323
323
  transition(job, JOB_PHASES.REVIEWING, 'explicit reviewer');
324
324
  const before = alloc?.ok && alloc.isolation === 'worktree' ? await safeCapture(adapters, job.execution_cwd, job.base_revision) : null;
325
- const reviewTask = adapters.buildReviewTask(job.original_task, null, { strictness: job.review_strictness ?? 'standard' });
325
+ // Explicit reviewers inspect the caller's stated task/workspace
326
+ // directly. The bounded worker-candidate capsule belongs only to the
327
+ // automatic post-worker review path; constructing it with no worker
328
+ // view falsely tells an explicit reviewer that nothing was delivered.
329
+ const reviewTask = job.original_task;
326
330
  const review = await runReviewerAttempt(job, reviewTask, config, before);
327
331
  if (job.cancelling) { await cancelWorkflow(job); return; }
328
332
  job.review = review;
@@ -476,7 +480,7 @@ export function createWorkflowRuntime(adapters, {
476
480
  const reviewApproved = review.status === 'done'
477
481
  && review.delivery_complete === true
478
482
  && review.verdict === 'approve';
479
- if (!reviewApproved && job.review_gate === 'required') {
483
+ if (!reviewApproved && job.review_gate === 'required') {
480
484
  const code = review.verdict === 'request_changes'
481
485
  ? 'REVIEW_CHANGES_REQUESTED'
482
486
  : 'REVIEW_INCONCLUSIVE';
@@ -512,6 +516,16 @@ export function createWorkflowRuntime(adapters, {
512
516
  }
513
517
 
514
518
  async function runReviewerAttempt(job, task, config, beforeCandidate, cwd = job.execution_cwd, baseRevision = job.base_revision) {
519
+ const requireFingerprint = (candidate) => {
520
+ if (job.isolation !== 'worktree') return;
521
+ if (candidate?.ok === true && typeof candidate.fingerprint === 'string' && candidate.fingerprint.trim()) return;
522
+ job.candidate_capture_failed = true;
523
+ job.retain_workspace = true;
524
+ throw Object.assign(new Error('review workspace evidence unavailable; worktree retained'), {
525
+ code: 'REVIEW_EVIDENCE_UNAVAILABLE',
526
+ });
527
+ };
528
+ requireFingerprint(beforeCandidate);
515
529
  const policy = resolveModelPolicy(config, 'reviewer', { attempt: 0 });
516
530
  const attemptId = attemptIdFor(adapters, job.id, 'review');
517
531
  job.current_attempt_id = attemptId;
@@ -544,6 +558,7 @@ export function createWorkflowRuntime(adapters, {
544
558
  }, 0);
545
559
  if (ar.status === 'failed' && attemptView.error_code) job.error_code = attemptView.error_code;
546
560
  const afterCandidate = job.isolation === 'worktree' ? await safeCapture(adapters, cwd, baseRevision) : null;
561
+ requireFingerprint(afterCandidate);
547
562
  const review = normalizeReview({ attemptResult: ar, beforeCandidate, afterCandidate });
548
563
  recordCanonical(job, 'review.completed', {
549
564
  attempt_id: attemptView.id,
@@ -568,9 +583,9 @@ export function createWorkflowRuntime(adapters, {
568
583
  id: ar.id,
569
584
  role: ar.role ?? 'worker',
570
585
  attempt,
571
- provider: ar.provider ?? null,
572
- model: ar.model ?? null,
573
- execution_context: ar.execution_context ?? null,
586
+ provider: ar.provider ?? null,
587
+ model: ar.model ?? null,
588
+ execution_context: ar.execution_context ?? null,
574
589
  selection_source: ar.selection_source ?? null,
575
590
  selection_trace: ar.selection_trace ?? null,
576
591
  status: ar.status ?? 'failed',
@@ -628,8 +643,8 @@ export function createWorkflowRuntime(adapters, {
628
643
  phase: job.phase,
629
644
  status: job.status,
630
645
  attempt: job.attempts.length,
631
- current_model: job.attempts[job.attempts.length - 1]?.model ?? null,
632
- execution_context: job.attempts[job.attempts.length - 1]?.execution_context ?? null,
646
+ current_model: job.attempts[job.attempts.length - 1]?.model ?? null,
647
+ execution_context: job.attempts[job.attempts.length - 1]?.execution_context ?? null,
633
648
  model_class_hint: job.model_class_hint,
634
649
  source: job.source,
635
650
  profile_id: job.profile_id,
@@ -654,14 +669,14 @@ export function createWorkflowRuntime(adapters, {
654
669
  workspace_retained: job.workspace_retained === true,
655
670
  decision: job.decision,
656
671
  candidate_available: !!job.candidate,
657
- review_status: job.review ? job.review.status ?? null : null,
658
- review_gate: job.review_gate ?? 'required',
672
+ review_status: job.review ? job.review.status ?? null : null,
673
+ review_gate: job.review_gate ?? 'required',
659
674
  event_cursor: job.canonical_events.at(-1)?.sequence ?? 0,
660
675
  };
661
676
  if (withResult) {
662
677
  v.child_attempts = job.attempts.map((a) => ({
663
- id: a.id, role: a.role, attempt: a.attempt, provider: a.provider, model: a.model,
664
- execution_context: a.execution_context ?? null,
678
+ id: a.id, role: a.role, attempt: a.attempt, provider: a.provider, model: a.model,
679
+ execution_context: a.execution_context ?? null,
665
680
  selection_source: a.selection_source, selection_trace: a.selection_trace ?? null,
666
681
  status: a.status, stopReason: a.stopReason,
667
682
  outcome_task: a.outcome?.task_status ?? null,
@@ -1,57 +1,57 @@
1
- @echo off
2
- setlocal EnableExtensions
3
- title DSH Crew Launcher
4
-
5
- set "LAUNCH_REQUEST=%*"
6
- set "LAUNCH_MODE=open"
7
- set "LAUNCH_DIR=%~dp0"
8
-
9
- if "%~1"=="" goto :run
10
- if /i "%~1"=="--background" (
11
- set "LAUNCH_MODE=background"
12
- shift
13
- goto :validate
14
- )
15
- if /i "%~1"=="--open" (
16
- set "LAUNCH_MODE=open"
17
- shift
18
- goto :validate
19
- )
20
- if /i "%~1"=="--watch" (
21
- set "LAUNCH_MODE=watch"
22
- shift
23
- goto :validate
24
- )
25
- if /i "%~1"=="--help" goto :help
26
- goto :invalid_argument
27
-
28
- :validate
29
- if not "%~1"=="" goto :invalid_argument
30
-
31
- :run
32
- set "LAUNCH_HELPER=%LAUNCH_DIR%start-dsh-crew.ps1"
33
- set "LAUNCH_LOG=%TEMP%\dsh-crew-launcher.log"
34
- if not exist "%LAUNCH_HELPER%" (
35
- >>"%LAUNCH_LOG%" echo [%date% %time%] ERROR Managed launcher helper is missing: %LAUNCH_HELPER%
36
- echo ERROR: DSH Crew launcher helper is missing.
37
- echo Repair it with: dsh-crew update
38
- if /i "%LAUNCH_MODE%"=="open" pause
39
- exit /b 1
40
- )
41
-
42
- powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%LAUNCH_HELPER%" -Mode "%LAUNCH_MODE%"
43
- set "LAUNCH_EXIT=%ERRORLEVEL%"
44
- if not "%LAUNCH_EXIT%"=="0" if /i "%LAUNCH_MODE%"=="open" pause
45
- exit /b %LAUNCH_EXIT%
46
-
47
- :invalid_argument
48
- echo ERROR: Unsupported launcher arguments: %LAUNCH_REQUEST%
49
- echo Use --open, --background, or --watch.
50
- exit /b 64
51
-
52
- :help
53
- echo Usage: %~nx0 [--open ^| --background ^| --watch]
54
- echo --open Start the 3080 console and its supervised 3210 backend, then open the console.
55
- echo --background Start the 3080 console and its supervised 3210 backend silently.
56
- echo --watch Keep the 3080 console and supervised 3210 backend healthy.
57
- exit /b 0
1
+ @echo off
2
+ setlocal EnableExtensions
3
+ title DSH Crew Launcher
4
+
5
+ set "LAUNCH_REQUEST=%*"
6
+ set "LAUNCH_MODE=open"
7
+ set "LAUNCH_DIR=%~dp0"
8
+
9
+ if "%~1"=="" goto :run
10
+ if /i "%~1"=="--background" (
11
+ set "LAUNCH_MODE=background"
12
+ shift
13
+ goto :validate
14
+ )
15
+ if /i "%~1"=="--open" (
16
+ set "LAUNCH_MODE=open"
17
+ shift
18
+ goto :validate
19
+ )
20
+ if /i "%~1"=="--watch" (
21
+ set "LAUNCH_MODE=watch"
22
+ shift
23
+ goto :validate
24
+ )
25
+ if /i "%~1"=="--help" goto :help
26
+ goto :invalid_argument
27
+
28
+ :validate
29
+ if not "%~1"=="" goto :invalid_argument
30
+
31
+ :run
32
+ set "LAUNCH_HELPER=%LAUNCH_DIR%start-dsh-crew.ps1"
33
+ set "LAUNCH_LOG=%TEMP%\dsh-crew-launcher.log"
34
+ if not exist "%LAUNCH_HELPER%" (
35
+ >>"%LAUNCH_LOG%" echo [%date% %time%] ERROR Managed launcher helper is missing: %LAUNCH_HELPER%
36
+ echo ERROR: DSH Crew launcher helper is missing.
37
+ echo Repair it with: dsh-crew update
38
+ if /i "%LAUNCH_MODE%"=="open" pause
39
+ exit /b 1
40
+ )
41
+
42
+ powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%LAUNCH_HELPER%" -Mode "%LAUNCH_MODE%"
43
+ set "LAUNCH_EXIT=%ERRORLEVEL%"
44
+ if not "%LAUNCH_EXIT%"=="0" if /i "%LAUNCH_MODE%"=="open" pause
45
+ exit /b %LAUNCH_EXIT%
46
+
47
+ :invalid_argument
48
+ echo ERROR: Unsupported launcher arguments: %LAUNCH_REQUEST%
49
+ echo Use --open, --background, or --watch.
50
+ exit /b 64
51
+
52
+ :help
53
+ echo Usage: %~nx0 [--open ^| --background ^| --watch]
54
+ echo --open Open official Harness on 3080, then start Crew silently on 3210.
55
+ echo --background Start the Crew-owned 3210 service silently.
56
+ echo --watch Keep the Crew-owned 3210 service healthy.
57
+ exit /b 0