@ran-sh/dsh-crew 2.1.8 → 2.2.1
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/.claude-plugin/plugin.json +1 -1
- package/docs/job-contracts.md +14 -1
- package/docs/readiness-matrix.md +20 -1
- package/lib/client.js +9 -8
- package/package.json +1 -1
- package/scripts/setup.mjs +6 -27
- package/scripts/verify-npm-install.mjs +2 -2
- package/skills/dsh-crew/SKILL.md +24 -15
- package/src/adaptive-routing.mjs +2 -2
- package/src/ci-evidence.mjs +1 -1
- package/src/client/history-panel.tsx +6 -5
- package/src/client/quick-panel.tsx +0 -1
- package/src/credential-lifecycle.mjs +0 -3
- package/src/credential-purge-state.mjs +1 -5
- package/src/credential-reference.mjs +0 -8
- package/src/crew-workspaces.mjs +136 -0
- package/src/delivery.mjs +2 -2
- package/src/dsh-cli-runtime.mjs +18 -22
- package/src/extension-contract.mjs +1 -1
- package/src/failure-classification.mjs +0 -10
- package/src/history/cleanup-plan.mjs +15 -5
- package/src/hub/entry.mjs +1 -1
- package/src/hub/index.mjs +45 -19
- package/src/i18n.mjs +0 -2
- package/src/install/crew-skill.mjs +1 -1
- package/src/install/install-legacy.mjs +3 -3
- package/src/install/npx-lifecycle.mjs +60 -112
- package/src/install/official-web.mjs +1 -1
- package/src/install/windows-startup.mjs +5 -5
- package/src/install/windows-supervisor-adapter.mjs +3 -2
- package/src/install/windows-supervisor-lifecycle.mjs +2 -2
- package/src/install/zcode.mjs +0 -2
- package/src/job-contracts.mjs +1 -1
- package/src/job-identity.mjs +13 -6
- package/src/jobs.mjs +3 -4
- package/src/mcp-runtime.mjs +17 -12
- package/src/model-routing.mjs +3 -4
- package/src/model-schedule.mjs +0 -1
- package/src/policy-legacy.mjs +10 -12
- package/src/provider-inventory.mjs +1 -1
- package/src/provider-layer-migration.mjs +0 -2
- package/src/readiness-matrix.mjs +1 -1
- package/src/release-in-use.mjs +1 -1
- package/src/role-profiles.mjs +2 -2
- package/src/runtime-controls.mjs +1 -1
- package/src/runtime-identity.mjs +1 -1
- package/src/status-shard.mjs +1 -1
- package/src/supervisor/restart-request.mjs +4 -4
- package/src/workflow-runtime.mjs +1 -2
- package/src/workspace-context.mjs +2 -2
- package/src/workspace-isolation.mjs +71 -4
- package/src/workspace-lock.mjs +134 -0
- package/scripts/policy-probe.mjs +0 -101
- package/scripts/verify-crew-ui-polish.mjs +0 -145
- package/scripts/verify-history-ui.mjs +0 -98
- package/scripts/verify-installer-fix.mjs +0 -26
|
@@ -5,6 +5,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
5
5
|
import { join, win32 } from 'node:path';
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
7
|
|
|
8
|
+
import { RELEASE_COHORT_FILENAME } from '../dsh-cohort.mjs';
|
|
8
9
|
import {
|
|
9
10
|
readMaintenanceSession,
|
|
10
11
|
readSupervisorHeartbeat,
|
|
@@ -47,7 +48,7 @@ export function hashSupervisorHelper(file) {
|
|
|
47
48
|
try { return createHash('sha256').update(readFileSync(file)).digest('hex'); } catch { return null; }
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
function canonicalWindowsPowerShellPath({
|
|
51
52
|
environment = process.env,
|
|
52
53
|
exists = existsSync,
|
|
53
54
|
} = {}) {
|
|
@@ -344,7 +345,7 @@ function prepareWindowsSupervisorConvergence({ home, root, platform }) {
|
|
|
344
345
|
let expectedDshVersion = text(manifest?.dependencies?.['@deepseek-ai/dsh'] ?? manifest?.peerDependencies?.['@deepseek-ai/dsh']);
|
|
345
346
|
if (!EXACT_VERSION_RE.test(expectedDshVersion ?? '')) {
|
|
346
347
|
try {
|
|
347
|
-
const cohort = JSON.parse(readFileSync(join(root,
|
|
348
|
+
const cohort = JSON.parse(readFileSync(join(root, RELEASE_COHORT_FILENAME), 'utf8'));
|
|
348
349
|
expectedDshVersion = text(cohort?.dsh_version);
|
|
349
350
|
} catch { expectedDshVersion = null; }
|
|
350
351
|
}
|
|
@@ -10,8 +10,8 @@ import {
|
|
|
10
10
|
import { dirname, join } from 'node:path';
|
|
11
11
|
import { compareProcessToken, processStartToken } from '../process-identity.mjs';
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
const WINDOWS_SUPERVISOR_HANDOFF_SCHEMA = 1;
|
|
14
|
+
const WINDOWS_SUPERVISOR_HANDOFF_PHASES = Object.freeze([
|
|
15
15
|
'planned',
|
|
16
16
|
'backend-stopped',
|
|
17
17
|
'old-watcher-stopped',
|
package/src/install/zcode.mjs
CHANGED
package/src/job-contracts.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// internal details remain available only through an explicit detail=full
|
|
6
6
|
// request at the MCP boundary.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
const JOB_CONTRACT_SCHEMA_VERSION = 1;
|
|
9
9
|
|
|
10
10
|
export const JOB_EVENT_TYPES = Object.freeze([
|
|
11
11
|
'job.created',
|
package/src/job-identity.mjs
CHANGED
|
@@ -7,13 +7,20 @@
|
|
|
7
7
|
// of the first message the agent receives — so they only agree if they are built
|
|
8
8
|
// from the same string. This module is that string.
|
|
9
9
|
//
|
|
10
|
-
// The shape is `Crew_<YYYYMMDD>_<HHMMSS>_<purpose>`, matching the
|
|
11
|
-
// rule; `src/workspace-isolation.mjs` allocates
|
|
12
|
-
// test asserts that every name produced here satisfies its ownership
|
|
13
|
-
// the two cannot drift apart silently.
|
|
10
|
+
// The shape is `Crew_<YYYYMMDD>_<HHMMSS>_<purpose>`, matching the disposable
|
|
11
|
+
// worktree naming rule; `src/workspace-isolation.mjs` allocates those in the same
|
|
12
|
+
// shape, and a test asserts that every name produced here satisfies its ownership
|
|
13
|
+
// grammar, so the two cannot drift apart silently. A job that runs in a
|
|
14
|
+
// caller-supplied Crew worktree is still named after that directory.
|
|
15
|
+
//
|
|
16
|
+
// A job running in one of the stable per-role workspaces is the exception, and
|
|
17
|
+
// the agreement above deliberately does not apply to it: `dsh-crew-worker` is
|
|
18
|
+
// shared by every job that uses it, so naming jobs after their workspace would
|
|
19
|
+
// give them all one title. There the workspace groups the sessions and this name
|
|
20
|
+
// tells them apart.
|
|
14
21
|
|
|
15
|
-
|
|
16
|
-
|
|
22
|
+
const JOB_NAME_PREFIX = 'Crew_';
|
|
23
|
+
const JOB_PURPOSE_MAX = 32;
|
|
17
24
|
|
|
18
25
|
export const JOB_NAME_RE = /^Crew_\d{8}_\d{6}_[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*(?:-\d+)?$/;
|
|
19
26
|
|
package/src/jobs.mjs
CHANGED
|
@@ -14,8 +14,8 @@ import { createShardWriter } from './status-shard.mjs';
|
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
import { dirname, join, resolve, basename } from 'node:path';
|
|
16
16
|
import { homedir } from 'node:os';
|
|
17
|
-
import { appendDeliveryInstructions, prependJobIdentity, parseDeliveryReport, formatDeliveryMetadata } from './delivery.mjs';
|
|
18
|
-
import { jobDisplayName } from './job-identity.mjs';
|
|
17
|
+
import { appendDeliveryInstructions, prependJobIdentity, parseDeliveryReport, formatDeliveryMetadata } from './delivery.mjs';
|
|
18
|
+
import { jobDisplayName } from './job-identity.mjs';
|
|
19
19
|
import { isCrewWorktreeName } from './workspace-isolation.mjs';
|
|
20
20
|
import { captureWorkspaceBaseline, captureWorkspaceDiff, NOT_A_GIT_REPOSITORY } from './workspace-audit.mjs';
|
|
21
21
|
import { buildOutcome, JOB_PHASES } from './workflow.mjs';
|
|
@@ -28,7 +28,6 @@ import { readGlobalConfig } from './install/install.mjs';
|
|
|
28
28
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
29
29
|
const CONFIG_DIR = join(homedir(), '.config', 'dsh-crew');
|
|
30
30
|
const CREW_DSH_HOME = join(homedir(), '.config', 'dsh-crew', 'harness');
|
|
31
|
-
const STATUS_FILE = join(CONFIG_DIR, 'status.json');
|
|
32
31
|
const CORDIS = join(ROOT, 'worker.cordis.yml');
|
|
33
32
|
|
|
34
33
|
// Read the operator's schedule once per dispatch. An unreadable config must not
|
|
@@ -38,7 +37,7 @@ function readModelSchedule() {
|
|
|
38
37
|
try { return normalizeModelSchedule(readGlobalConfig().model_schedule); } catch { return normalizeModelSchedule(undefined); }
|
|
39
38
|
}
|
|
40
39
|
|
|
41
|
-
|
|
40
|
+
const TIERS = {
|
|
42
41
|
flash: { model: 'deepseek-v4-flash', label: 'V4 Flash' },
|
|
43
42
|
pro: { model: 'deepseek-v4-pro', label: 'V4 Pro' },
|
|
44
43
|
};
|
package/src/mcp-runtime.mjs
CHANGED
|
@@ -11,7 +11,6 @@ import { createWorkflowRuntime } from './workflow-runtime.mjs';
|
|
|
11
11
|
import { CONFIG_SCHEMA_VERSION, normalizeGlobalConfig } from './policy.mjs';
|
|
12
12
|
import { buildDirectSelectionTrace, enrichSelectionTrace } from './model-routing.mjs';
|
|
13
13
|
import {
|
|
14
|
-
createIsolatedWorkspace,
|
|
15
14
|
cleanupIsolatedWorkspace,
|
|
16
15
|
inspectRepository,
|
|
17
16
|
captureCandidate as captureIsolationCandidate,
|
|
@@ -19,6 +18,7 @@ import {
|
|
|
19
18
|
import { startJob, waitJob, jobView, cancelJob } from './jobs.mjs';
|
|
20
19
|
import { hub } from './hub-client.mjs';
|
|
21
20
|
import { crewHarnessSessionsDir } from './install/crew-paths.mjs';
|
|
21
|
+
import { ensureCrewWorkspace, releaseCrewWorkspace } from './crew-workspaces.mjs';
|
|
22
22
|
|
|
23
23
|
const SESSION_CONFIG_KEYS = [
|
|
24
24
|
'default_tier', 'default_effort', 'mode', 'default_timeout_seconds',
|
|
@@ -301,25 +301,27 @@ export function buildMcpWorkflowRuntime(deps) {
|
|
|
301
301
|
if (!repo.ok) {
|
|
302
302
|
return { ok: false, reason: repo.reason ?? 'ISOLATION_UNAVAILABLE', error: `${job.role ?? 'worker'} needs an isolated git worktree: ${repo.error ?? repo.reason}` };
|
|
303
303
|
}
|
|
304
|
-
// The
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
|
|
304
|
+
// The job runs in this project's stable worker/reviewer workspace rather
|
|
305
|
+
// than a worktree of its own, so the sessions the Harness groups by cwd
|
|
306
|
+
// accumulate in two entries instead of one per job. The workspace is reset
|
|
307
|
+
// to the job's base revision on the way in, so the candidate diff is still
|
|
308
|
+
// this job's work alone, and it is held for the job's duration so two jobs
|
|
309
|
+
// cannot share it.
|
|
310
|
+
const workspace = await ensureCrewWorkspace({
|
|
308
311
|
cwd: job.requested_cwd,
|
|
309
|
-
|
|
310
|
-
purpose: job.role ?? 'job',
|
|
312
|
+
role: job.role ?? 'worker',
|
|
311
313
|
baseRevision: job.workspace_branch ?? repo.baseRevision,
|
|
312
314
|
});
|
|
313
|
-
if (!
|
|
314
|
-
return { ok: false, reason:
|
|
315
|
+
if (!workspace.ok) {
|
|
316
|
+
return { ok: false, reason: workspace.reason ?? 'WORKTREE_CREATE_FAILED', error: `workspace unavailable: ${workspace.error ?? ''}` };
|
|
315
317
|
}
|
|
316
318
|
return {
|
|
317
319
|
ok: true,
|
|
318
|
-
execution_cwd:
|
|
319
|
-
base_revision:
|
|
320
|
+
execution_cwd: workspace.worktreePath,
|
|
321
|
+
base_revision: workspace.baseRevision,
|
|
320
322
|
isolation: 'worktree',
|
|
321
323
|
primary_workspace_dirty: repo.dirty === true,
|
|
322
|
-
handle: { worktreePath:
|
|
324
|
+
handle: { worktreePath: workspace.worktreePath, repoRoot: workspace.repoRoot, stable: true, release: workspace.release },
|
|
323
325
|
};
|
|
324
326
|
};
|
|
325
327
|
|
|
@@ -327,6 +329,9 @@ export function buildMcpWorkflowRuntime(deps) {
|
|
|
327
329
|
|
|
328
330
|
const releaseWorkspace = async (handle) => {
|
|
329
331
|
if (!handle) return { ok: true };
|
|
332
|
+
// A stable workspace is not disposed of — it is the directory the next job
|
|
333
|
+
// and every session it produced belong to. Only the lock is dropped.
|
|
334
|
+
if (handle.stable === true) return { ok: releaseCrewWorkspace(handle) };
|
|
330
335
|
try {
|
|
331
336
|
const r = await cleanupIsolatedWorkspace({ worktreePath: handle.worktreePath, repoRoot: handle.repoRoot });
|
|
332
337
|
return { ok: r.ok, error: r.ok ? undefined : r.error };
|
package/src/model-routing.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { rankAdaptiveCandidates } from './adaptive-routing.mjs';
|
|
8
8
|
import { scheduleAdmission } from './model-schedule.mjs';
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
const DEFAULT_TIER_MODEL_PREFERENCES = Object.freeze({
|
|
11
11
|
flash: 'deepseek-v4-flash',
|
|
12
12
|
pro: 'deepseek-v4-pro',
|
|
13
13
|
});
|
|
@@ -20,9 +20,8 @@ export const DEFAULT_ROLE_MODEL_PREFERENCES = Object.freeze({
|
|
|
20
20
|
reviewer: 'deepseek-v4-pro',
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
export const MODEL_FALLBACKS = ['harness-default'];
|
|
24
23
|
export const NO_WORKER_MODEL_AVAILABLE = 'NO_WORKER_MODEL_AVAILABLE';
|
|
25
|
-
|
|
24
|
+
const MODEL_SELECTION_TRACE_VERSION = 1;
|
|
26
25
|
export const MODEL_SELECTION_REASON_CODES = Object.freeze({
|
|
27
26
|
PROVIDER_UNAVAILABLE: 'PROVIDER_UNAVAILABLE',
|
|
28
27
|
PREFERRED_MODEL_UNAVAILABLE: 'PREFERRED_MODEL_UNAVAILABLE',
|
|
@@ -61,7 +60,7 @@ const BLOCKED_MODEL_CODES = Object.freeze({
|
|
|
61
60
|
[MODEL_SELECTION_REASON_CODES.PEAK_RESTRICTED]: 'MODEL_BLOCKED_PEAK',
|
|
62
61
|
});
|
|
63
62
|
|
|
64
|
-
|
|
63
|
+
function normalizeModelRef(raw) {
|
|
65
64
|
if (!raw || typeof raw !== 'object') return null;
|
|
66
65
|
const provider = typeof raw.provider === 'string' ? raw.provider.trim() : '';
|
|
67
66
|
const model = typeof raw.model === 'string' ? raw.model.trim() : '';
|
package/src/model-schedule.mjs
CHANGED
|
@@ -27,7 +27,6 @@ export const DEFAULT_PEAK_WINDOWS = Object.freeze([
|
|
|
27
27
|
|
|
28
28
|
export const DEFAULT_TIMEZONE_OFFSET_MINUTES = 8 * 60;
|
|
29
29
|
export const DEFAULT_PEAK_WEEKDAYS = Object.freeze([1, 2, 3, 4, 5]);
|
|
30
|
-
export const MINUTES_PER_DAY = 24 * 60;
|
|
31
30
|
// A fixed offset is bounded by the real-world range of UTC offsets; anything
|
|
32
31
|
// wider is a typo, not a timezone.
|
|
33
32
|
const MAX_OFFSET_MINUTES = 14 * 60;
|
package/src/policy-legacy.mjs
CHANGED
|
@@ -21,8 +21,8 @@ import { normalizeModelPriority } from './model-routing.mjs';
|
|
|
21
21
|
|
|
22
22
|
export const TIER_STATES = ['disabled', 'manual', 'auto'];
|
|
23
23
|
export const COLLABORATION_MODES = ['flash-only', 'pro-only', 'balanced', 'review-pipeline', 'custom'];
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
const MAIN_AGENT_MODES = ['direct-allowed', 'coordinator-first', 'dispatcher-only'];
|
|
25
|
+
const ROLE_IDS = [
|
|
26
26
|
'implementation',
|
|
27
27
|
'simple_fix',
|
|
28
28
|
'tests',
|
|
@@ -52,7 +52,7 @@ export const POLICY_ERROR_CODES = {
|
|
|
52
52
|
ROLE_TIER_CONFLICT: 'ROLE_TIER_CONFLICT',
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
const POLICY_ERROR_MESSAGES = {
|
|
56
56
|
[POLICY_ERROR_CODES.SUBAGENTS_DISABLED]:
|
|
57
57
|
'DSH Crew worker dispatch is disabled.',
|
|
58
58
|
[POLICY_ERROR_CODES.TIER_DISABLED]:
|
|
@@ -89,18 +89,16 @@ export const POLICY_ERROR_MESSAGES = {
|
|
|
89
89
|
// Everything new here reads a canonical config shape produced by
|
|
90
90
|
// migrateLegacyConfig; the legacy collab/tier functions stay for compatibility.
|
|
91
91
|
|
|
92
|
-
export const DISPATCH_ROLES = ['worker', 'reviewer'];
|
|
93
|
-
export const ROLE_STATES = TIER_STATES; // disabled | manual | auto
|
|
94
92
|
export const ROLE_MODEL_STRATEGIES = ['economy', 'balanced', 'quality', 'strong'];
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
const DEFAULT_WORKER_STRATEGY = 'balanced';
|
|
94
|
+
const DEFAULT_REVIEWER_STRATEGY = 'strong';
|
|
97
95
|
export const DEFAULT_MAX_PARALLEL = 3;
|
|
98
96
|
|
|
99
|
-
|
|
97
|
+
function normalizeRoleState(raw) {
|
|
100
98
|
return normalizeState(raw);
|
|
101
99
|
}
|
|
102
100
|
|
|
103
|
-
|
|
101
|
+
function normalizeIsolation(raw) {
|
|
104
102
|
return raw === 'worktree' || raw === 'shared' ? raw : 'worktree';
|
|
105
103
|
}
|
|
106
104
|
|
|
@@ -117,7 +115,7 @@ function normalizeMaxParallel(raw) {
|
|
|
117
115
|
* escalation_priority_configured, fallback, escalation: { enabled, max_attempts } }.
|
|
118
116
|
* Pure; unknown fields drop, empty priority falls back to the role default.
|
|
119
117
|
*/
|
|
120
|
-
|
|
118
|
+
function normalizeModelPolicy(raw = {}) {
|
|
121
119
|
return {
|
|
122
120
|
role: raw.role === 'reviewer' ? 'reviewer' : 'worker',
|
|
123
121
|
strategy: ROLE_MODEL_STRATEGIES.includes(raw.strategy)
|
|
@@ -444,7 +442,7 @@ export function resolveHubWorkerProvider({ worker_provider_mode, getCurrentSelec
|
|
|
444
442
|
}
|
|
445
443
|
|
|
446
444
|
/** Structured policy error; `code` is machine-readable, `message` user-facing. */
|
|
447
|
-
|
|
445
|
+
function policyError(code, extra = {}) {
|
|
448
446
|
const base = POLICY_ERROR_MESSAGES[code] ?? code;
|
|
449
447
|
return Object.assign(new Error(base.replace('{tier}', extra.tier ?? 'pro')), { policyCode: code });
|
|
450
448
|
}
|
|
@@ -677,7 +675,7 @@ export function shouldRunProReview(config, session = {}) {
|
|
|
677
675
|
}
|
|
678
676
|
|
|
679
677
|
/** Roles for a tier (host guidance for who does what, not a hard classifier). */
|
|
680
|
-
|
|
678
|
+
function getTierRoles(config, tier) {
|
|
681
679
|
const roles = tier === 'flash' ? config.flash_roles : config.pro_roles;
|
|
682
680
|
return normalizeRoles(roles, tier === 'flash' ? DEFAULT_FLASH_ROLES : DEFAULT_PRO_ROLES);
|
|
683
681
|
}
|
|
@@ -9,7 +9,7 @@ import { classifyCredentialReference } from './credential-reference.mjs';
|
|
|
9
9
|
const LIFECYCLE_ORIGINS = new Set(['builtin', 'profile-managed', 'dynamic', 'unknown']);
|
|
10
10
|
const OWNERSHIPS = new Set(['harness', 'crew-managed-profile', 'user-managed-profile', 'dynamic-user', 'unknown']);
|
|
11
11
|
const MUTABLE_AUTHORITY_KINDS = new Set(['crew-profile', 'harness-settings']);
|
|
12
|
-
|
|
12
|
+
const IMMUTABLE_HARNESS_PROVIDER_IDS = Object.freeze(['deepseek-official']);
|
|
13
13
|
|
|
14
14
|
function text(value) {
|
|
15
15
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
@@ -120,8 +120,6 @@ export function hasProviderLayerMigration(plan) {
|
|
|
120
120
|
return Array.isArray(plan?.providers) && plan.providers.length > 0;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
const MIGRATION_STATES = Object.freeze(['PLANNED', 'APPLIED', 'RESTART_PENDING', 'VERIFYING', 'VERIFIED', 'FAILED']);
|
|
124
|
-
|
|
125
123
|
function migrationCode(error, fallback = 'PROVIDER_MIGRATION_FAILED') {
|
|
126
124
|
const code = text(error?.code);
|
|
127
125
|
return code && /^[A-Z][A-Z0-9_]{1,63}$/u.test(code) ? code : fallback;
|
package/src/readiness-matrix.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// (Hub handshake / catalog read) are intentionally separate from execution
|
|
6
6
|
// verification (real worker, reviewer, cancellation, timeout, etc.).
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
const READINESS_STATUSES = Object.freeze(['PASS', 'FAIL', 'BLOCKED', 'SKIP', 'NOT_RUN']);
|
|
9
9
|
|
|
10
10
|
export const READINESS_REASON_CODES = Object.freeze({
|
|
11
11
|
LIVE_CHECK_PASSED: 'LIVE_CHECK_PASSED',
|
package/src/release-in-use.mjs
CHANGED
|
@@ -22,7 +22,7 @@ import { homedir } from 'node:os';
|
|
|
22
22
|
import { dirname, join, resolve } from 'node:path';
|
|
23
23
|
import { fileURLToPath } from 'node:url';
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
const IN_USE_DIRNAME = 'in-use';
|
|
26
26
|
|
|
27
27
|
// A claim file is `<pid>.json` or `<pid>-<nonce>.json`. The PID alone is not
|
|
28
28
|
// enough to identify a claim: two mounts inside one process would publish to the
|
package/src/role-profiles.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync
|
|
|
6
6
|
import { dirname, join } from 'node:path';
|
|
7
7
|
import { homedir } from 'node:os';
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
const ROLE_PROFILE_SCHEMA_VERSION = 1;
|
|
10
10
|
export const DEFAULT_ROLE_PROFILES = Object.freeze({
|
|
11
11
|
'worker-default': Object.freeze({
|
|
12
12
|
role: 'worker', routing: 'auto', isolation: 'worktree', fallback: true,
|
|
@@ -45,7 +45,7 @@ function normalizeProfile(id, raw) {
|
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
function roleProfilesFile({ home = homedir() } = {}) {
|
|
49
49
|
return join(home, '.config', 'dsh-crew', 'profiles.json');
|
|
50
50
|
}
|
|
51
51
|
|
package/src/runtime-controls.mjs
CHANGED
|
@@ -22,7 +22,7 @@ const R = ACTIVATION_BOUNDARY.RESTART_REQUIRED;
|
|
|
22
22
|
* running MCP process. `session: null` means the setting is not a session
|
|
23
23
|
* override surface.
|
|
24
24
|
*/
|
|
25
|
-
|
|
25
|
+
const RUNTIME_SETTING_ACTIVATION = Object.freeze({
|
|
26
26
|
max_parallel: Object.freeze({ global: L, session: null, note: 'Admission limit refreshes before new workflow starts and when slots release; lowering never cancels active work.' }),
|
|
27
27
|
|
|
28
28
|
subagents_enabled: Object.freeze({ global: N, session: N }),
|
package/src/runtime-identity.mjs
CHANGED
|
@@ -36,7 +36,7 @@ export {
|
|
|
36
36
|
// included in the identity contract.
|
|
37
37
|
const RUNTIME_ID = randomUUID();
|
|
38
38
|
|
|
39
|
-
export const RUNTIME_VERSION = '2.1
|
|
39
|
+
export const RUNTIME_VERSION = '2.2.1';
|
|
40
40
|
export const HUB_PROTOCOL_VERSION = 1;
|
|
41
41
|
|
|
42
42
|
export const HUB_CAPABILITIES = Object.freeze([
|
package/src/status-shard.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { homedir } from 'node:os';
|
|
|
9
9
|
|
|
10
10
|
const CONFIG_DIR = join(homedir(), '.config', 'dsh-crew');
|
|
11
11
|
const SHARD_DIR = join(CONFIG_DIR, 'status.d');
|
|
12
|
-
|
|
12
|
+
const SHARD_FRESH_MS = 30 * 60 * 1000;
|
|
13
13
|
|
|
14
14
|
export function createShardWriter(kind) {
|
|
15
15
|
const writer = `${kind}-${process.pid}`;
|
|
@@ -18,18 +18,18 @@ import { randomUUID } from 'node:crypto';
|
|
|
18
18
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
19
19
|
import { dirname, join } from 'node:path';
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const RESTART_REQUEST_SCHEMA = 1;
|
|
22
|
+
const RESTART_REQUEST_TTL_MS = 60_000;
|
|
23
23
|
|
|
24
24
|
export function supervisorStateRoot(appRoot) {
|
|
25
25
|
return join(appRoot, 'supervisor');
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
function restartRequestsDir(appRoot) {
|
|
29
29
|
return join(supervisorStateRoot(appRoot), 'restart-requests');
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
function restartResultsDir(appRoot) {
|
|
33
33
|
return join(supervisorStateRoot(appRoot), 'restart-results');
|
|
34
34
|
}
|
|
35
35
|
|
package/src/workflow-runtime.mjs
CHANGED
|
@@ -31,7 +31,6 @@ export const WORKFLOW_ERROR_CODES = {
|
|
|
31
31
|
REVIEWER_MUTATED_CANDIDATE: 'REVIEWER_MUTATED_CANDIDATE',
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
-
const TERMINAL = new Set([JOB_PHASES.COMPLETED, JOB_PHASES.FAILED, JOB_PHASES.CANCELLED, JOB_PHASES.INTERRUPTED]);
|
|
35
34
|
|
|
36
35
|
function lines(value) {
|
|
37
36
|
if (typeof value !== 'string' || value.trim() === '') return [];
|
|
@@ -817,4 +816,4 @@ export function createWorkflowRuntime(adapters, {
|
|
|
817
816
|
return { start, wait, get, list, cancel, runtimeState, setMaxParallel, refreshRuntimeControls };
|
|
818
817
|
}
|
|
819
818
|
|
|
820
|
-
export { JOB_PHASES
|
|
819
|
+
export { JOB_PHASES };
|
|
@@ -6,7 +6,7 @@ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync
|
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
7
|
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
const WORKSPACE_CONTEXT_SCHEMA_VERSION = 1;
|
|
10
10
|
const ID = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
11
11
|
|
|
12
12
|
export function isSafeBranchName(value) {
|
|
@@ -51,7 +51,7 @@ function normalizeContext(id, raw) {
|
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
function workspaceContextsFile({ home = homedir() } = {}) {
|
|
55
55
|
return join(home, '.config', 'dsh-crew', 'workspaces.json');
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -28,9 +28,9 @@ export const GIT_ERROR = 'GIT_ERROR';
|
|
|
28
28
|
/** A valid repository whose HEAD does not resolve because nothing is committed. */
|
|
29
29
|
export const REPOSITORY_HAS_NO_COMMITS = 'REPOSITORY_HAS_NO_COMMITS';
|
|
30
30
|
export const WORKTREE_LOCKED = 'WORKTREE_LOCKED';
|
|
31
|
-
|
|
31
|
+
const WORKTREE_RESERVE_FAILED = 'WORKTREE_RESERVE_FAILED';
|
|
32
32
|
export const CANDIDATE_CAPTURE_FAILED = 'CANDIDATE_CAPTURE_FAILED';
|
|
33
|
-
|
|
33
|
+
const MAX_PARALLEL_CAP = 16;
|
|
34
34
|
export const DEFAULT_MAX_PARALLEL = 3;
|
|
35
35
|
// Worktree names read `Crew_YYYYMMDD_HHMMSS_<purpose>` so an operator can tell
|
|
36
36
|
// from the directory alone when a job ran and what it was for. Worktrees from
|
|
@@ -642,8 +642,75 @@ async function ownershipDrift(run, worktreePath, expect) {
|
|
|
642
642
|
* the registration is gone and no directory remains — success is never claimed
|
|
643
643
|
* while a worktree stays registered or on disk.
|
|
644
644
|
*/
|
|
645
|
-
|
|
646
|
-
|
|
645
|
+
const WORKTREE_CLEANUP_RETRIES = 3;
|
|
646
|
+
const WORKTREE_CLEANUP_BACKOFF_MS = 150;
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Is `worktreePath` a worktree Crew created, for this repository?
|
|
650
|
+
*
|
|
651
|
+
* The same identity check the automatic prune uses, exposed for the stable
|
|
652
|
+
* per-role workspaces: they are created once and then adopted by every later
|
|
653
|
+
* job, and "adopt" has to mean the same thing it means to the pruner — shape,
|
|
654
|
+
* name, path, repository and incarnation all matching a record Crew wrote.
|
|
655
|
+
* Anything less would let a directory that merely has the right name be reused
|
|
656
|
+
* as if Crew had made it.
|
|
657
|
+
*/
|
|
658
|
+
export async function crewOwnsWorktree({ worktreePath, repoRoot, git } = {}) {
|
|
659
|
+
const run = git ?? defaultRunner;
|
|
660
|
+
const repo = await commonDirOf(run, repoRoot);
|
|
661
|
+
if (!repo) return false;
|
|
662
|
+
const incarnation = await incarnationOf(run, worktreePath);
|
|
663
|
+
return Boolean(readOwnership({ worktreePath, repo, incarnation }));
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Create a worktree of `repoRoot` at an exact path, and record ownership.
|
|
668
|
+
*
|
|
669
|
+
* `createIsolatedWorkspace` reserves a name it invents; the stable per-role
|
|
670
|
+
* workspaces already have their path, so this is the same creation without the
|
|
671
|
+
* reservation step. Ownership is recorded only after git succeeded, so a failed
|
|
672
|
+
* creation never leaves a claim on a tree that does not exist.
|
|
673
|
+
*/
|
|
674
|
+
export async function createWorktreeAt({ dir, repoRoot, revision, purpose, at, git } = {}) {
|
|
675
|
+
const run = git ?? defaultRunner;
|
|
676
|
+
const res = await runGit(run, ['worktree', 'add', '--detach', dir, revision], { cwd: repoRoot });
|
|
677
|
+
if (!res.ok) {
|
|
678
|
+
const released = await cleanupIsolatedWorkspace({ worktreePath: dir, repoRoot, git: run, reservation: true });
|
|
679
|
+
return {
|
|
680
|
+
ok: false,
|
|
681
|
+
reason: res.reason,
|
|
682
|
+
error: res.error,
|
|
683
|
+
...(released.ok ? {} : { cleanupBlocked: true, cleanupError: released.error }),
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
const headRes = await runGit(run, ['rev-parse', 'HEAD'], { cwd: dir });
|
|
687
|
+
const created = headRes.ok ? headRes.stdout.trim() : '';
|
|
688
|
+
const commonDir = await commonDirOf(run, repoRoot);
|
|
689
|
+
const incarnation = commonDir ? await claimIncarnation(run, dir) : null;
|
|
690
|
+
const owned = Boolean(commonDir && incarnation && created)
|
|
691
|
+
&& recordOwnership({ worktreePath: dir, repo: commonDir, incarnation, head: created, purpose, at });
|
|
692
|
+
return { ok: true, worktreePath: dir, repoRoot, owned };
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Return a stable workspace to a known revision before a job starts.
|
|
697
|
+
*
|
|
698
|
+
* The workspace outlives the job, so without this a job would inherit whatever
|
|
699
|
+
* the previous one left — and the candidate diff, which is taken against a base
|
|
700
|
+
* revision, would then contain someone else's work. `reset --hard` discards
|
|
701
|
+
* tracked modifications; `clean -fd` removes untracked files but deliberately
|
|
702
|
+
* not ignored ones, so a workspace's `node_modules` survives. A reset that fails
|
|
703
|
+
* is reported rather than ignored: starting a job on an unknown tree is what the
|
|
704
|
+
* reset exists to prevent.
|
|
705
|
+
*/
|
|
706
|
+
export async function resetWorktreeTo({ worktreePath, revision, git } = {}) {
|
|
707
|
+
const run = git ?? defaultRunner;
|
|
708
|
+
const reset = await runGit(run, ['reset', '--hard', revision], { cwd: worktreePath });
|
|
709
|
+
if (!reset.ok) return { ok: false, reason: reset.reason, error: reset.error };
|
|
710
|
+
const clean = await runGit(run, ['clean', '-fd'], { cwd: worktreePath });
|
|
711
|
+
if (!clean.ok) return { ok: false, reason: clean.reason, error: clean.error };
|
|
712
|
+
return { ok: true };
|
|
713
|
+
}
|
|
647
714
|
|
|
648
715
|
export async function cleanupIsolatedWorkspace({
|
|
649
716
|
worktreePath,
|