@ran-sh/dsh-crew 0.5.1 → 0.5.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.
- package/README.md +93 -112
- package/README.zh.md +93 -111
- package/agents/ds-worker.md +5 -4
- package/codex/AGENTS.md +92 -0
- package/codex/agents/ds-worker.toml +1 -1
- package/docs/installation.md +63 -0
- package/docs/job-contracts.md +12 -4
- package/docs/readiness-matrix.md +6 -2
- package/docs/ui-surfaces.md +1 -2
- package/lib/client.js +41 -4
- package/official-web-bridge/lib/client.js +41 -4
- package/package.json +6 -1
- package/scripts/setup.mjs +127 -42
- package/src/client/host-readiness.mjs +2 -1
- package/src/client/index.tsx +29 -15
- package/src/config-readiness.mjs +50 -10
- package/src/hub/index.mjs +42 -15
- package/src/install/install-legacy.mjs +65 -11
- package/src/install/install.mjs +3 -1
- package/src/install/npx-lifecycle.mjs +55 -19
- package/src/install/windows-startup.mjs +115 -0
- package/src/install/zcode.mjs +316 -0
- package/src/orchestrator.mjs +53 -0
- package/src/readiness-matrix.mjs +4 -3
- package/src/runtime-identity.mjs +1 -1
- package/src/server.mjs +38 -34
- package/src/workflow-runtime.mjs +27 -27
- package/src/workflow.mjs +63 -5
- package/windows/start-dsh-crew.cmd +57 -0
- package/windows/start-dsh-crew.ps1 +326 -0
- package/windows/start-dsh-crew.vbs +9 -0
- package/zcode/AGENTS.md +26 -0
- package/zcode/agents/ds-reviewer.md +36 -0
- package/zcode/agents/ds-worker.md +39 -0
- package/zcode/commands/dsh-config.md +6 -0
- package/zcode/commands/dsh-status.md +4 -0
package/src/workflow.mjs
CHANGED
|
@@ -58,10 +58,68 @@ export function canTransition(from, to) {
|
|
|
58
58
|
return allowed.includes(to);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
function splitSection(value) {
|
|
62
|
-
if (typeof value !== 'string' || value.trim() === '') return [];
|
|
63
|
-
return value.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
64
|
-
}
|
|
61
|
+
function splitSection(value) {
|
|
62
|
+
if (typeof value !== 'string' || value.trim() === '') return [];
|
|
63
|
+
return value.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const NO_CHANGE_SENTINELS = new Set([
|
|
67
|
+
'no files changed',
|
|
68
|
+
'no file changed',
|
|
69
|
+
'no changes',
|
|
70
|
+
'无文件变更',
|
|
71
|
+
'没有文件变更',
|
|
72
|
+
'未更改任何文件',
|
|
73
|
+
'无变更',
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
function deliveryClaimsChanges(outcome) {
|
|
77
|
+
return Array.isArray(outcome?.changes) && outcome.changes.length > 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function applyWorkspaceEvidence(outcome, {
|
|
81
|
+
evidenceAvailable = false,
|
|
82
|
+
hasChanges = false,
|
|
83
|
+
allowNoChanges = false,
|
|
84
|
+
requireNoChangeAuthorization = true,
|
|
85
|
+
} = {}) {
|
|
86
|
+
const next = { ...outcome };
|
|
87
|
+
const claimsChanges = deliveryClaimsChanges(next);
|
|
88
|
+
if (evidenceAvailable && next.execution_status === 'completed') {
|
|
89
|
+
next.workspace_evidence_ok = claimsChanges === hasChanges;
|
|
90
|
+
}
|
|
91
|
+
const tests = Array.isArray(next.tests) ? next.tests : [];
|
|
92
|
+
const verifiedNoChange = requireNoChangeAuthorization === true
|
|
93
|
+
&& evidenceAvailable === true
|
|
94
|
+
&& allowNoChanges === true
|
|
95
|
+
&& hasChanges === false
|
|
96
|
+
&& claimsChanges === false
|
|
97
|
+
&& next.execution_status === 'completed'
|
|
98
|
+
&& next.workspace_evidence_ok === true
|
|
99
|
+
&& next.delivery?.complete === true
|
|
100
|
+
&& tests.some((test) => test.status === 'PASS')
|
|
101
|
+
&& !tests.some((test) => test.status === 'FAIL');
|
|
102
|
+
if (verifiedNoChange) {
|
|
103
|
+
next.task_status = 'success';
|
|
104
|
+
next.no_change_verified = true;
|
|
105
|
+
} else if (requireNoChangeAuthorization === true && claimsChanges === false && next.task_status === 'success') {
|
|
106
|
+
next.task_status = 'partial';
|
|
107
|
+
delete next.no_change_verified;
|
|
108
|
+
}
|
|
109
|
+
return next;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function parseChanges(section) {
|
|
113
|
+
return splitSection(section).filter((line) => {
|
|
114
|
+
const normalized = line
|
|
115
|
+
.replace(/^(?:[-*+]\s+)+/, '')
|
|
116
|
+
.replace(/[`"'“”‘’]/g, '')
|
|
117
|
+
.replace(/[.!。!]+$/g, '')
|
|
118
|
+
.trim()
|
|
119
|
+
.toLowerCase();
|
|
120
|
+
return !NO_CHANGE_SENTINELS.has(normalized);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
65
123
|
|
|
66
124
|
function parseTests(section) {
|
|
67
125
|
return splitSection(section).map((line) => {
|
|
@@ -106,7 +164,7 @@ export function buildOutcome({ result = '', deliveryMeta, executionStatus, stopR
|
|
|
106
164
|
}),
|
|
107
165
|
confidence: null,
|
|
108
166
|
needs_escalation: false,
|
|
109
|
-
changes:
|
|
167
|
+
changes: parseChanges(parsed.sections.Diff),
|
|
110
168
|
tests,
|
|
111
169
|
tests_status: testsStatus ?? null,
|
|
112
170
|
risks: splitSection(parsed.sections.Risks),
|
|
@@ -0,0 +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 both services and open http://127.0.0.1:3080/.
|
|
55
|
+
echo --background Start both services silently without opening a browser.
|
|
56
|
+
echo --watch Keep both services healthy and restart them after an exit.
|
|
57
|
+
exit /b 0
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
# DSH Crew managed Windows launcher
|
|
2
|
+
[CmdletBinding()]
|
|
3
|
+
param(
|
|
4
|
+
[ValidateSet('background', 'open', 'watch')]
|
|
5
|
+
[string] $Mode = 'open'
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
Set-StrictMode -Version Latest
|
|
9
|
+
$ErrorActionPreference = 'Stop'
|
|
10
|
+
|
|
11
|
+
$crewHome = Join-Path $env:USERPROFILE '.config\dsh-crew\harness'
|
|
12
|
+
$officialHome = Join-Path $env:USERPROFILE '.dsh'
|
|
13
|
+
$dshCli = Join-Path $crewHome 'runtime\node_modules\.bin\dsh.cmd'
|
|
14
|
+
$logRoot = if ($env:TEMP) { $env:TEMP } else { [System.IO.Path]::GetTempPath() }
|
|
15
|
+
$launcherLog = Join-Path $logRoot 'dsh-crew-launcher.log'
|
|
16
|
+
$startedAt = Get-Date
|
|
17
|
+
$services = @(
|
|
18
|
+
[pscustomobject]@{ Name = 'Crew backend'; Profile = 'dsh-crew'; Home = $crewHome; Port = 3210; Url = 'http://127.0.0.1:3210'; State = 'pending'; Process = $null; RootPid = $null; RootStartedAtUtcTicks = $null; ListenerPid = $null; ListenerStartedAtUtcTicks = $null; ConsecutiveFailures = 0; LastError = $null },
|
|
19
|
+
[pscustomobject]@{ Name = 'Official UI'; Profile = 'web'; Home = $officialHome; Port = 3080; Url = 'http://127.0.0.1:3080'; State = 'pending'; Process = $null; RootPid = $null; RootStartedAtUtcTicks = $null; ListenerPid = $null; ListenerStartedAtUtcTicks = $null; ConsecutiveFailures = 0; LastError = $null }
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
function Write-LaunchLog {
|
|
23
|
+
param([string] $Message, [ValidateSet('INFO', 'WARN', 'ERROR')] [string] $Level = 'INFO')
|
|
24
|
+
$line = '[{0}] [{1}] {2}' -f (Get-Date -Format 'yyyy-MM-ddTHH:mm:ss.fffK'), $Level, $Message
|
|
25
|
+
Add-Content -LiteralPath $launcherLog -Value $line -Encoding UTF8
|
|
26
|
+
if ($Mode -eq 'open') {
|
|
27
|
+
if ($Level -eq 'ERROR') { Write-Host $line -ForegroundColor Red }
|
|
28
|
+
elseif ($Level -eq 'WARN') { Write-Host $line -ForegroundColor Yellow }
|
|
29
|
+
else { Write-Host $line }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function Get-HealthState {
|
|
34
|
+
param([pscustomobject] $Service)
|
|
35
|
+
try {
|
|
36
|
+
$response = Invoke-RestMethod -Uri ($Service.Url + '/_dsh/dsh-crew/extension') -TimeoutSec 2
|
|
37
|
+
$version = $response.extension.runtime.runtime_version
|
|
38
|
+
if ($response.ok -eq $true -and $version) {
|
|
39
|
+
return [pscustomobject]@{ Ready = $true; Version = [string] $version; Error = $null }
|
|
40
|
+
}
|
|
41
|
+
return [pscustomobject]@{ Ready = $false; Version = $null; Error = 'Response did not contain a ready runtime contract.' }
|
|
42
|
+
} catch {
|
|
43
|
+
return [pscustomobject]@{ Ready = $false; Version = $null; Error = $_.Exception.Message }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function Get-PortState {
|
|
48
|
+
param([int] $Port)
|
|
49
|
+
try {
|
|
50
|
+
$listeners = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties().GetActiveTcpListeners()
|
|
51
|
+
$occupied = @($listeners | Where-Object Port -eq $Port).Count -gt 0
|
|
52
|
+
if (-not $occupied) {
|
|
53
|
+
return [pscustomobject]@{ State = 'free'; Error = $null; Pid = $null }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
$ownerPid = $null
|
|
57
|
+
try {
|
|
58
|
+
$ownerPid = (Get-NetTCPConnection -State Listen -LocalPort $Port -ErrorAction Stop | Select-Object -First 1).OwningProcess
|
|
59
|
+
} catch { }
|
|
60
|
+
return [pscustomobject]@{ State = 'occupied'; Error = $null; Pid = $ownerPid }
|
|
61
|
+
} catch {
|
|
62
|
+
return [pscustomobject]@{ State = 'unknown'; Error = ('Listener enumeration failed: {0}' -f $_.Exception.Message); Pid = $null }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function Test-TrackedProcessIdentity {
|
|
67
|
+
param([int] $ProcessId, [long] $ExpectedStartTicks, [object[]] $ProcessTable)
|
|
68
|
+
if ($ProcessId -lt 1 -or $ExpectedStartTicks -lt 1) { return $false }
|
|
69
|
+
$record = @($ProcessTable | Where-Object { [int] $_.ProcessId -eq $ProcessId } | Select-Object -First 1)
|
|
70
|
+
if ($record.Count -eq 0) { return $false }
|
|
71
|
+
$startTicks = $null
|
|
72
|
+
if ($record[0].PSObject.Properties.Name -contains 'StartTicks') {
|
|
73
|
+
$startTicks = [long] $record[0].StartTicks
|
|
74
|
+
} else {
|
|
75
|
+
try { $startTicks = (Get-Process -Id $ProcessId -ErrorAction Stop).StartTime.ToUniversalTime().Ticks } catch { return $false }
|
|
76
|
+
}
|
|
77
|
+
return $startTicks -eq $ExpectedStartTicks
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function Get-TrackedProcessTree {
|
|
81
|
+
param([pscustomobject] $Service, [object[]] $ProcessTable = $null)
|
|
82
|
+
$processes = if ($null -ne $ProcessTable) { @($ProcessTable) } else { @(Get-CimInstance Win32_Process -ErrorAction Stop) }
|
|
83
|
+
$owned = [System.Collections.Generic.HashSet[int]]::new()
|
|
84
|
+
$rootMatches = Test-TrackedProcessIdentity -ProcessId $Service.RootPid -ExpectedStartTicks $Service.RootStartedAtUtcTicks -ProcessTable $processes
|
|
85
|
+
$hasTrackedListener = $Service.ListenerPid -and $Service.ListenerStartedAtUtcTicks
|
|
86
|
+
if ($hasTrackedListener) {
|
|
87
|
+
$listenerMatches = Test-TrackedProcessIdentity -ProcessId $Service.ListenerPid -ExpectedStartTicks $Service.ListenerStartedAtUtcTicks -ProcessTable $processes
|
|
88
|
+
if (-not $listenerMatches) { return @() }
|
|
89
|
+
[void] $owned.Add([int] $Service.ListenerPid)
|
|
90
|
+
} elseif (-not $rootMatches) {
|
|
91
|
+
return @()
|
|
92
|
+
}
|
|
93
|
+
if ($rootMatches) { [void] $owned.Add([int] $Service.RootPid) }
|
|
94
|
+
do {
|
|
95
|
+
$added = $false
|
|
96
|
+
foreach ($candidate in $processes) {
|
|
97
|
+
$candidateId = [int] $candidate.ProcessId
|
|
98
|
+
$parentId = [int] $candidate.ParentProcessId
|
|
99
|
+
if ($owned.Contains($parentId) -and $owned.Add($candidateId)) { $added = $true }
|
|
100
|
+
}
|
|
101
|
+
} while ($added)
|
|
102
|
+
return @($owned | ForEach-Object { [int] $_ })
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function Set-TrackedListenerIdentity {
|
|
106
|
+
param([pscustomobject] $Service)
|
|
107
|
+
$port = Get-PortState $Service.Port
|
|
108
|
+
if ($port.State -ne 'occupied' -or -not $port.Pid) { return $false }
|
|
109
|
+
$tree = @(Get-TrackedProcessTree -Service $Service)
|
|
110
|
+
if ($port.Pid -notin $tree) { return $false }
|
|
111
|
+
try {
|
|
112
|
+
$listener = Get-Process -Id $port.Pid -ErrorAction Stop
|
|
113
|
+
$Service.ListenerPid = [int] $port.Pid
|
|
114
|
+
$Service.ListenerStartedAtUtcTicks = $listener.StartTime.ToUniversalTime().Ticks
|
|
115
|
+
return $true
|
|
116
|
+
} catch {
|
|
117
|
+
return $false
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function Stop-OwnedListener {
|
|
122
|
+
param([pscustomobject] $Service, [int] $ListenerPid)
|
|
123
|
+
if ($Mode -ne 'watch' -or -not $Service.ListenerPid -or $ListenerPid -ne $Service.ListenerPid) {
|
|
124
|
+
return $false
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
$ownedProcessIds = @(Get-TrackedProcessTree -Service $Service)
|
|
128
|
+
if ($ListenerPid -notin $ownedProcessIds) { return $false }
|
|
129
|
+
Write-LaunchLog ('Supervisor confirmed owned listener PID={0} under tracked root PID={1}; stopping that process tree after {2} consecutive failed health checks.' -f $ListenerPid, $Service.RootPid, $Service.ConsecutiveFailures) 'WARN'
|
|
130
|
+
foreach ($processId in $ownedProcessIds) {
|
|
131
|
+
Stop-Process -Id $processId -Force -ErrorAction SilentlyContinue
|
|
132
|
+
}
|
|
133
|
+
$deadline = (Get-Date).AddSeconds(5)
|
|
134
|
+
do {
|
|
135
|
+
$port = Get-PortState $Service.Port
|
|
136
|
+
if ($port.State -eq 'free') { return $true }
|
|
137
|
+
Start-Sleep -Milliseconds 250
|
|
138
|
+
} while ((Get-Date) -lt $deadline)
|
|
139
|
+
} catch {
|
|
140
|
+
Write-LaunchLog ('Supervisor could not stop its owned listener safely: {0}' -f $_.Exception.Message) 'WARN'
|
|
141
|
+
}
|
|
142
|
+
return $false
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function Start-CrewService {
|
|
146
|
+
param([pscustomobject] $Service)
|
|
147
|
+
$serviceRunStamp = (Get-Date).ToString('yyyyMMdd-HHmmssfff')
|
|
148
|
+
$stdout = Join-Path $logRoot ('dsh-crew-{0}-{1}-{2}.out.log' -f $Service.Profile, $Service.Port, $serviceRunStamp)
|
|
149
|
+
$stderr = Join-Path $logRoot ('dsh-crew-{0}-{1}-{2}.err.log' -f $Service.Profile, $Service.Port, $serviceRunStamp)
|
|
150
|
+
$previousHome = $env:DSH_HOME
|
|
151
|
+
try {
|
|
152
|
+
$env:DSH_HOME = $Service.Home
|
|
153
|
+
$arguments = @('--profile', $Service.Profile, '--host', '127.0.0.1', '--port', [string] $Service.Port, '--no-open')
|
|
154
|
+
$process = Start-Process -FilePath $dshCli -ArgumentList $arguments -WindowStyle Hidden -PassThru `
|
|
155
|
+
-RedirectStandardOutput $stdout -RedirectStandardError $stderr
|
|
156
|
+
$Service.Process = $process
|
|
157
|
+
$Service.RootPid = $process.Id
|
|
158
|
+
$Service.RootStartedAtUtcTicks = $process.StartTime.ToUniversalTime().Ticks
|
|
159
|
+
$Service.ListenerPid = $null
|
|
160
|
+
$Service.ListenerStartedAtUtcTicks = $null
|
|
161
|
+
$Service.ConsecutiveFailures = 0
|
|
162
|
+
$Service.State = 'starting'
|
|
163
|
+
Write-LaunchLog ('Started {0} on port {1}; PID={2}; stdout={3}; stderr={4}' -f $Service.Profile, $Service.Port, $process.Id, $stdout, $stderr)
|
|
164
|
+
} finally {
|
|
165
|
+
$env:DSH_HOME = $previousHome
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function Wait-CrewServices {
|
|
170
|
+
$deadline = (Get-Date).AddSeconds(90)
|
|
171
|
+
while (@($services | Where-Object State -eq 'starting').Count -gt 0 -and (Get-Date) -lt $deadline) {
|
|
172
|
+
foreach ($service in ($services | Where-Object State -eq 'starting')) {
|
|
173
|
+
$health = Get-HealthState $service
|
|
174
|
+
$service.LastError = $health.Error
|
|
175
|
+
if ($health.Ready) {
|
|
176
|
+
$service.State = 'ready'
|
|
177
|
+
$service.ConsecutiveFailures = 0
|
|
178
|
+
[void] (Set-TrackedListenerIdentity -Service $service)
|
|
179
|
+
Write-LaunchLog ('{0} ready on {1}; runtime={2}' -f $service.Name, $service.Port, $health.Version)
|
|
180
|
+
} elseif ($service.Process -and $service.Process.HasExited) {
|
|
181
|
+
throw ('{0} exited before becoming ready; PID={1}; exit={2}; last health error: {3}' -f $service.Name, $service.Process.Id, $service.Process.ExitCode, $health.Error)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (@($services | Where-Object State -eq 'starting').Count -gt 0) { Start-Sleep -Milliseconds 500 }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
$notReady = @($services | Where-Object State -ne 'ready')
|
|
188
|
+
if ($notReady.Count -gt 0) {
|
|
189
|
+
$details = ($notReady | ForEach-Object { '{0}:{1} ({2})' -f $_.Profile, $_.Port, $_.LastError }) -join '; '
|
|
190
|
+
throw "Startup health deadline exceeded: $details"
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function Ensure-CrewServices {
|
|
195
|
+
param([switch] $QuietHealthy)
|
|
196
|
+
|
|
197
|
+
foreach ($service in $services) {
|
|
198
|
+
$health = Get-HealthState $service
|
|
199
|
+
if ($health.Ready) {
|
|
200
|
+
$wasReady = $service.State -eq 'ready'
|
|
201
|
+
$service.State = 'ready'
|
|
202
|
+
$service.ConsecutiveFailures = 0
|
|
203
|
+
$service.LastError = $null
|
|
204
|
+
if (-not $service.ListenerPid) { [void] (Set-TrackedListenerIdentity -Service $service) }
|
|
205
|
+
if (-not $QuietHealthy -or -not $wasReady) {
|
|
206
|
+
Write-LaunchLog ('{0} already ready on {1}; runtime={2}' -f $service.Name, $service.Port, $health.Version)
|
|
207
|
+
}
|
|
208
|
+
continue
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
$service.LastError = $health.Error
|
|
212
|
+
$service.ConsecutiveFailures += 1
|
|
213
|
+
if ($service.Process -and $service.Process.HasExited) {
|
|
214
|
+
Write-LaunchLog ('{0} process exited after startup; PID={1}; exit={2}' -f $service.Name, $service.Process.Id, $service.Process.ExitCode) 'WARN'
|
|
215
|
+
$service.Process = $null
|
|
216
|
+
}
|
|
217
|
+
$service.State = 'pending'
|
|
218
|
+
|
|
219
|
+
$port = Get-PortState $service.Port
|
|
220
|
+
if ($port.State -eq 'occupied') {
|
|
221
|
+
if ($service.ConsecutiveFailures -lt 3) {
|
|
222
|
+
throw ('Health check {0}/3 failed for {1}; owned process remains untouched until failure is confirmed. Health error: {2}' -f $service.ConsecutiveFailures, $service.Name, $health.Error)
|
|
223
|
+
}
|
|
224
|
+
if (Stop-OwnedListener -Service $service -ListenerPid $port.Pid) {
|
|
225
|
+
$service.Process = $null
|
|
226
|
+
$service.RootPid = $null
|
|
227
|
+
$service.RootStartedAtUtcTicks = $null
|
|
228
|
+
$service.ListenerPid = $null
|
|
229
|
+
$service.ListenerStartedAtUtcTicks = $null
|
|
230
|
+
$service.ConsecutiveFailures = 0
|
|
231
|
+
$port = Get-PortState $service.Port
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if ($port.State -eq 'occupied') {
|
|
235
|
+
$owner = if ($port.Pid) { "; listener PID=$($port.Pid)" } else { '' }
|
|
236
|
+
throw ('Port {0} is occupied, but {1} failed its health contract{2}. Health error: {3}' -f $service.Port, $service.Name, $owner, $health.Error)
|
|
237
|
+
}
|
|
238
|
+
if ($port.State -ne 'free') {
|
|
239
|
+
throw ('Could not determine whether port {0} is available: {1}' -f $service.Port, $port.Error)
|
|
240
|
+
}
|
|
241
|
+
if ($Mode -eq 'watch') {
|
|
242
|
+
Write-LaunchLog ('Supervisor detected {0} unavailable on {1}; restarting it.' -f $service.Name, $service.Port) 'WARN'
|
|
243
|
+
}
|
|
244
|
+
Start-CrewService $service
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
Wait-CrewServices
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function Start-ServiceSupervisor {
|
|
251
|
+
$mutex = [System.Threading.Mutex]::new($false, 'Local\DSHCrewServiceSupervisor')
|
|
252
|
+
$ownsMutex = $false
|
|
253
|
+
try {
|
|
254
|
+
try {
|
|
255
|
+
$ownsMutex = $mutex.WaitOne(0)
|
|
256
|
+
} catch [System.Threading.AbandonedMutexException] {
|
|
257
|
+
$ownsMutex = $true
|
|
258
|
+
}
|
|
259
|
+
if (-not $ownsMutex) {
|
|
260
|
+
Write-LaunchLog 'Supervisor already active; duplicate watcher exiting.'
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
Write-LaunchLog 'Supervisor active; monitoring 3080 and 3210 every 10 seconds.'
|
|
265
|
+
$lastRecoveryError = $null
|
|
266
|
+
while ($true) {
|
|
267
|
+
try {
|
|
268
|
+
Ensure-CrewServices -QuietHealthy
|
|
269
|
+
if ($lastRecoveryError) {
|
|
270
|
+
Write-LaunchLog 'Supervisor recovery succeeded; both services are healthy.'
|
|
271
|
+
$lastRecoveryError = $null
|
|
272
|
+
}
|
|
273
|
+
} catch {
|
|
274
|
+
$recoveryError = $_.Exception.Message
|
|
275
|
+
if ($recoveryError -ne $lastRecoveryError) {
|
|
276
|
+
Write-LaunchLog ('Supervisor recovery failed; will retry: {0}' -f $recoveryError) 'WARN'
|
|
277
|
+
$lastRecoveryError = $recoveryError
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
Start-Sleep -Seconds 10
|
|
281
|
+
}
|
|
282
|
+
} finally {
|
|
283
|
+
if ($ownsMutex) { $mutex.ReleaseMutex() }
|
|
284
|
+
$mutex.Dispose()
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if ($env:DSH_CREW_LAUNCHER_TEST_IMPORT -eq '1') { return }
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
New-Item -ItemType Directory -Path $logRoot -Force | Out-Null
|
|
292
|
+
Write-LaunchLog ('Launcher started; mode={0}; user={1}' -f $Mode, $env:USERNAME)
|
|
293
|
+
|
|
294
|
+
if (-not (Test-Path -LiteralPath $dshCli -PathType Leaf)) {
|
|
295
|
+
throw "DSH CLI was not found at $dshCli. Run: npm install -g @ran-sh/dsh-crew@latest; dsh-crew update"
|
|
296
|
+
}
|
|
297
|
+
if (-not (Test-Path -LiteralPath (Join-Path $crewHome 'profiles\dsh-crew\package.json') -PathType Leaf)) {
|
|
298
|
+
throw "The isolated dsh-crew profile is missing under $crewHome. Run: dsh-crew update"
|
|
299
|
+
}
|
|
300
|
+
if (-not (Test-Path -LiteralPath (Join-Path $officialHome 'profiles\web\package.json') -PathType Leaf)) {
|
|
301
|
+
throw "The official web profile is missing under $officialHome. Run: dsh-crew integrate"
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if ($Mode -eq 'watch') {
|
|
305
|
+
Start-ServiceSupervisor
|
|
306
|
+
Write-LaunchLog 'Supervisor stopped.' 'WARN'
|
|
307
|
+
exit 0
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
Ensure-CrewServices
|
|
311
|
+
|
|
312
|
+
if ($Mode -eq 'open') {
|
|
313
|
+
Start-Process 'http://127.0.0.1:3080/' | Out-Null
|
|
314
|
+
Write-LaunchLog 'Opened daily console at http://127.0.0.1:3080/'
|
|
315
|
+
}
|
|
316
|
+
Write-LaunchLog ('Launcher completed successfully in {0:n1}s.' -f ((Get-Date) - $startedAt).TotalSeconds)
|
|
317
|
+
exit 0
|
|
318
|
+
} catch {
|
|
319
|
+
Write-LaunchLog $_.Exception.Message 'ERROR'
|
|
320
|
+
if ($Mode -eq 'open') {
|
|
321
|
+
Write-Host ''
|
|
322
|
+
Write-Host "Diagnostic log: $launcherLog" -ForegroundColor Yellow
|
|
323
|
+
Write-Host 'Startup failed. Review the diagnostic log and the service stdout/stderr paths recorded above.' -ForegroundColor Yellow
|
|
324
|
+
}
|
|
325
|
+
exit 1
|
|
326
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Option Explicit
|
|
2
|
+
|
|
3
|
+
Dim shell, launcher, command
|
|
4
|
+
Set shell = CreateObject("WScript.Shell")
|
|
5
|
+
launcher = "__LAUNCHER__"
|
|
6
|
+
command = shell.ExpandEnvironmentStrings("%COMSPEC%") & " /d /c " & _
|
|
7
|
+
Chr(34) & Chr(34) & launcher & Chr(34) & " --watch" & Chr(34)
|
|
8
|
+
|
|
9
|
+
shell.Run command, 0, False
|
package/zcode/AGENTS.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Global capability-aware delegation policy for ZCode
|
|
2
|
+
|
|
3
|
+
ZCode is a host adapter for DSH Crew. Use the `dsh-crew` MCP server for Crew
|
|
4
|
+
work only after its live capability and readiness surfaces have been checked.
|
|
5
|
+
|
|
6
|
+
- Discover the current Crew configuration, capabilities, activation state and
|
|
7
|
+
readiness before delegating substantial work. Installed, configured, enabled
|
|
8
|
+
and callable are different states; do not infer one from another.
|
|
9
|
+
- Match a bounded work unit to an available Crew role/model and preserve the
|
|
10
|
+
repository/worktree and Result Contract boundaries. Keep planning, ambiguous
|
|
11
|
+
requirements, integration, external side effects and final communication in
|
|
12
|
+
the host agent.
|
|
13
|
+
- Dispatch selected work asynchronously with `dsh_spawn_worker`, save its
|
|
14
|
+
workflow ID, and poll that same workflow with `dsh_worker_result` using
|
|
15
|
+
`wait_seconds: 10`. A bounded wait that returns a nonterminal state is not a
|
|
16
|
+
failure when `dsh_worker_status` confirms that workflow is still running;
|
|
17
|
+
continue polling it and never start a duplicate workflow.
|
|
18
|
+
- If Crew is selected and any required capability is unavailable, non-callable,
|
|
19
|
+
or returns an unknown/runtime/configuration/credential/routing/timeout error,
|
|
20
|
+
pause. Report the evidence and wait for the operator to choose repair Crew or
|
|
21
|
+
continue locally; never silently fall back or retry blindly.
|
|
22
|
+
- Validate returned evidence, changed scope, tests and completion state before
|
|
23
|
+
accepting delegated work. Do not expose credentials or raw provider payloads.
|
|
24
|
+
|
|
25
|
+
This file is installed as a managed block in `~/.zcode/AGENTS.md`; user-authored
|
|
26
|
+
instructions outside the block are preserved.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ds-reviewer
|
|
3
|
+
description: Independent DSH Crew reviewer dispatcher. Inspect completed changes read-only and return a structured verdict.
|
|
4
|
+
mcpServers:
|
|
5
|
+
- dsh-crew
|
|
6
|
+
tools:
|
|
7
|
+
- mcp__dsh-crew__dsh_run_worker
|
|
8
|
+
- mcp__dsh-crew__dsh_spawn_worker
|
|
9
|
+
- mcp__dsh-crew__dsh_worker_status
|
|
10
|
+
- mcp__dsh-crew__dsh_worker_result
|
|
11
|
+
- mcp__dsh-crew__dsh_worker_cancel
|
|
12
|
+
- mcp__dsh-crew__dsh_worker_config
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
You are a thin, read-only dispatcher. Never edit files or implement fixes.
|
|
16
|
+
|
|
17
|
+
Check `dsh_worker_config` before dispatch. Derive one bounded, read-only review
|
|
18
|
+
task that contains only the code, acceptance criteria and validation evidence
|
|
19
|
+
the Reviewer must inspect. Keep harness-reporting requirements in this
|
|
20
|
+
dispatcher, then call `dsh_spawn_worker` with role `reviewer` and the current
|
|
21
|
+
workspace as `cwd`; omit effort unless explicitly requested. Save the returned
|
|
22
|
+
workflow ID.
|
|
23
|
+
|
|
24
|
+
Poll that same workflow through `dsh_worker_result` with `wait_seconds: 10`.
|
|
25
|
+
If a bounded result wait expires or returns a nonterminal state, check
|
|
26
|
+
`dsh_worker_status` once. When it confirms the workflow is still running,
|
|
27
|
+
continue polling the same workflow; never start a duplicate. A genuine
|
|
28
|
+
transport, runtime, configuration, credential or routing error is a hard stop
|
|
29
|
+
and must be reported to the operator.
|
|
30
|
+
|
|
31
|
+
Treat the workflow ID as host structured-result metadata; never ask the
|
|
32
|
+
Reviewer to discover or report its workflow ID, provider, model or other
|
|
33
|
+
harness metadata. Do not forward workflow ID, provider, model or status-reporting
|
|
34
|
+
fields as Reviewer task requirements. Combine the host-owned metadata with
|
|
35
|
+
Review Findings, Evidence, Risks and Verdict. Treat failing tests or incomplete
|
|
36
|
+
evidence as not approved.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ds-worker
|
|
3
|
+
description: DSH Crew worker dispatcher for implementation, fixes, tests, search and analysis. Never implement locally; return the auditable Crew result.
|
|
4
|
+
mcpServers:
|
|
5
|
+
- dsh-crew
|
|
6
|
+
tools:
|
|
7
|
+
- mcp__dsh-crew__dsh_run_worker
|
|
8
|
+
- mcp__dsh-crew__dsh_spawn_worker
|
|
9
|
+
- mcp__dsh-crew__dsh_worker_status
|
|
10
|
+
- mcp__dsh-crew__dsh_worker_result
|
|
11
|
+
- mcp__dsh-crew__dsh_worker_cancel
|
|
12
|
+
- mcp__dsh-crew__dsh_worker_config
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
You are a thin dispatcher. You never do the task yourself.
|
|
16
|
+
|
|
17
|
+
Check `dsh_worker_config` before dispatch. Derive one bounded, domain-only task
|
|
18
|
+
for the Worker, preserving the requested implementation and validation scope.
|
|
19
|
+
Keep harness-reporting requirements in this dispatcher, then call
|
|
20
|
+
`dsh_spawn_worker` with role `worker` and the current workspace as `cwd`; omit
|
|
21
|
+
effort unless the task explicitly requests it. Save the returned workflow ID.
|
|
22
|
+
When the operator explicitly requests read-only search or analysis that should
|
|
23
|
+
make zero file changes, pass `constraints: { allow_no_changes: true }`.
|
|
24
|
+
Otherwise omit that constraint so implementation work still fails closed when
|
|
25
|
+
the reported diff and isolated workspace disagree.
|
|
26
|
+
|
|
27
|
+
Poll that same workflow through `dsh_worker_result` with `wait_seconds: 10`.
|
|
28
|
+
If a bounded result wait expires or returns a nonterminal state, check
|
|
29
|
+
`dsh_worker_status` once. When it confirms the workflow is still running,
|
|
30
|
+
continue polling the same workflow; never start a duplicate. A genuine
|
|
31
|
+
transport, runtime, configuration, credential or routing error is a hard stop
|
|
32
|
+
and must be reported to the operator.
|
|
33
|
+
|
|
34
|
+
Treat the workflow ID as host structured-result metadata; never ask the Worker
|
|
35
|
+
to discover or report its workflow ID, provider, model or other harness
|
|
36
|
+
metadata. Do not forward workflow ID, provider, model or status-reporting fields
|
|
37
|
+
as Worker task requirements. If the workflow is `done`, combine the host-owned
|
|
38
|
+
metadata with its compact result and evidence footer. Otherwise report the
|
|
39
|
+
terminal error and stop reason clearly.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
Parse arguments after this command as key=value pairs and call the
|
|
2
|
+
`dsh_worker_config` tool. Supported keys include enabled, tier, effort, mode,
|
|
3
|
+
timeout, policy, escalate, collab, main, flash, pro, review and reset. With no
|
|
4
|
+
arguments read the current configuration. Show one compact table including
|
|
5
|
+
hub_reachable, effective flash/pro state, effective policy and routing guidance.
|
|
6
|
+
Name changed fields. Reply in the user's language and do nothing else.
|