@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.
@@ -1,31 +1,71 @@
1
1
  import { buildReadinessMatrix, READINESS_REASON_CODES } from './readiness-matrix.mjs';
2
2
 
3
- function warningCodes(catalogBody) {
3
+ function warningCodes(catalogBody) {
4
4
  const hints = Array.isArray(catalogBody?.health?.hints) ? catalogBody.health.hints : [];
5
5
  return [...new Set(hints
6
6
  .filter((hint) => hint?.level === 'warning' && typeof hint?.code === 'string' && hint.code.trim())
7
7
  .map((hint) => hint.code.trim()))];
8
- }
8
+ }
9
+
10
+ function verifiedHubJob(job) {
11
+ return job?.status === 'done'
12
+ && job?.task_status === 'success'
13
+ && job?.delivery_complete === true
14
+ && job?.workspace_evidence_ok === true;
15
+ }
16
+
17
+ export function buildHubExecutionRows(hubJobs = []) {
18
+ const jobs = Array.isArray(hubJobs) ? hubJobs : [];
19
+ const workerPassed = jobs.some((job) => job?.role === 'worker' && verifiedHubJob(job));
20
+ const reviewerPassed = jobs.some((job) => job?.role === 'reviewer'
21
+ && verifiedHubJob(job)
22
+ && job?.review_verdict === 'approve');
23
+ return [
24
+ workerPassed
25
+ ? { id: 'model_execution', status: 'PASS', reason_code: 'REAL_EXECUTION_PASSED', evidence_source: 'hub-jobs' }
26
+ : { id: 'model_execution', status: 'NOT_RUN', reason_code: 'NO_EXECUTION_EVIDENCE', evidence_source: 'none' },
27
+ reviewerPassed
28
+ ? { id: 'reviewer_pipeline', status: 'PASS', reason_code: 'REAL_REVIEW_PASSED', evidence_source: 'hub-jobs' }
29
+ : { id: 'reviewer_pipeline', status: 'NOT_RUN', reason_code: 'NO_EXECUTION_EVIDENCE', evidence_source: 'none' },
30
+ ];
31
+ }
9
32
 
10
33
  /**
11
34
  * Enrich the conservative runtime matrix with evidence the config report has
12
35
  * already collected. This function performs no I/O and never reads provider
13
36
  * configuration, credentials, quotas, pricing, or hidden catalog expectations.
14
37
  */
15
- export function buildConfigReadinessMatrix({
16
- platform = process.platform,
17
- hubCompatibility = null,
18
- workerProviderMode = null,
19
- providerCatalogChecked = false,
20
- providerCatalogBody = null,
21
- } = {}) {
38
+ export function buildConfigReadinessMatrix({
39
+ platform = process.platform,
40
+ hubCompatibility = null,
41
+ workerProviderMode = null,
42
+ providerCatalogChecked = false,
43
+ providerCatalogBody = null,
44
+ hubJobsChecked = false,
45
+ hubJobsBody = null,
46
+ } = {}) {
22
47
  const warnings = warningCodes(providerCatalogBody);
23
48
  const catalogResponseOk = !!providerCatalogBody
24
49
  && typeof providerCatalogBody === 'object'
25
50
  && providerCatalogBody.ok !== false;
26
51
  const catalogOk = providerCatalogChecked && catalogResponseOk && warnings.length === 0;
27
52
 
28
- const evidence = {};
53
+ const evidence = {};
54
+ const hubJobs = hubCompatibility?.compatible === true
55
+ && hubJobsChecked
56
+ && hubJobsBody?.ok !== false
57
+ && Array.isArray(hubJobsBody?.jobs)
58
+ ? hubJobsBody.jobs
59
+ : [];
60
+ for (const row of buildHubExecutionRows(hubJobs)) {
61
+ if (row.status === 'PASS') {
62
+ evidence[row.id] = {
63
+ status: row.status,
64
+ reason_code: row.reason_code,
65
+ evidence_source: row.evidence_source,
66
+ };
67
+ }
68
+ }
29
69
  if (
30
70
  workerProviderMode !== 'deepseek-official'
31
71
  && hubCompatibility?.compatible === true
package/src/hub/index.mjs CHANGED
@@ -20,7 +20,7 @@ import { buildDirectSelectionTrace, resolveWorkerModel, resolveModel } from '../
20
20
  import { readHarnessModelCatalog } from '../model-catalog.mjs';
21
21
  import { appendDeliveryInstructions, parseDeliveryReport, formatDeliveryMetadata } from '../delivery.mjs';
22
22
  import { captureWorkspaceBaseline, captureWorkspaceDiff, NOT_A_GIT_REPOSITORY } from '../workspace-audit.mjs';
23
- import { buildOutcome, JOB_PHASES } from '../workflow.mjs';
23
+ import { applyWorkspaceEvidence, buildOutcome, JOB_PHASES } from '../workflow.mjs';
24
24
  import { boundedMachineCodeFromError } from '../structured-error-code.mjs';
25
25
  import { createCanonicalJobEvent, projectWorkflowView } from '../job-contracts.mjs';
26
26
  import { getHubRuntimeIdentity } from '../runtime-identity.mjs';
@@ -29,6 +29,7 @@ import { addContextReferences, buildWorkspaceTask, isSafeBranchName, loadWorkspa
29
29
  import { buildExtensionContract } from '../extension-contract.mjs';
30
30
  import { cleanupIsolatedWorkspace, createIsolatedWorkspace } from '../workspace-isolation.mjs';
31
31
  import { assessWorkspaceReadiness } from '../workspace-readiness.mjs';
32
+ import { buildHubExecutionRows } from '../config-readiness.mjs';
32
33
 
33
34
  // policy.mjs is pure (no @deepseek-ai imports, no ctx access), so importing it
34
35
  // here is safe for the profile-realm discipline: it never pulls in package
@@ -119,7 +120,20 @@ export function hubCanonicalEvents(job = {}) {
119
120
  }));
120
121
  }
121
122
 
122
- // ---------- job registry ----------
123
+ // ---------- job registry ----------
124
+
125
+ export function applyHubWorkspaceEvidence({ outcome, workspaceDiff, allowNoChanges = false, isolation = 'shared', role = 'worker' } = {}) {
126
+ const changes = workspaceDiff?.changes ?? {};
127
+ const hasChanges = ['modified', 'deleted', 'renamed', 'untracked']
128
+ .some((key) => Array.isArray(changes[key]) && changes[key].length > 0);
129
+ const evidenceAvailable = workspaceDiff?.kind === 'git' && workspaceDiff.dirtyBaseline !== true;
130
+ return applyWorkspaceEvidence(outcome, {
131
+ evidenceAvailable,
132
+ hasChanges,
133
+ allowNoChanges: allowNoChanges === true && isolation === 'worktree',
134
+ requireNoChangeAuthorization: role === 'worker',
135
+ });
136
+ }
123
137
 
124
138
  // Exported for the unit tests (test/hub-windows.test.mjs); instantiation
125
139
  // needs only a duck-typed ctx, so spawn()'s path guard is testable without a
@@ -145,6 +159,10 @@ export class WorkerRegistry { constructor(ctx) {
145
159
  startedAt: job.startedAt, endedAt: job.endedAt,
146
160
  isolation: job.isolation ?? 'shared', workspace_branch: job.workspace_branch ?? null,
147
161
  delivery_complete: !!job.delivery_complete,
162
+ allow_no_changes: job.allow_no_changes === true,
163
+ task_status: job.outcome?.task_status ?? null,
164
+ workspace_evidence_ok: job.outcome?.workspace_evidence_ok ?? null,
165
+ review_verdict: job.review?.verdict ?? null,
148
166
  workspace_diff_available: !!job.workspaceDiff && job.workspaceDiff.kind === 'git',
149
167
  workspace_retained: job.workspace_retained === true,
150
168
  cleanup_warning: job.cleanup_warning ?? null,
@@ -182,7 +200,7 @@ export class WorkerRegistry { constructor(ctx) {
182
200
  * `role` (worker | reviewer) records who does the work; `tier` remains the
183
201
  * legacy model-class slot. Reviewer-role jobs always use the pro slot.
184
202
  */
185
- async spawn({ task, tier = 'flash', role, attempt = 0, effort = 'max', cwd, source = 'api', preset, delivery = 'coding', client_job_id, requested_isolation, workspace_branch, timeout_seconds, profile_id, workspace_context }) {
203
+ async spawn({ task, tier = 'flash', role, attempt = 0, effort = 'max', cwd, source = 'api', preset, delivery = 'coding', client_job_id, requested_isolation, workspace_branch, timeout_seconds, profile_id, workspace_context, allow_no_changes }) {
186
204
  // role is only honored when the caller explicitly names it; a legacy
187
205
  // tier-only spawn (role === undefined) keeps the exact v0.1 resolution.
188
206
  const hasRole = role === 'worker' || role === 'reviewer';
@@ -291,6 +309,7 @@ export class WorkerRegistry { constructor(ctx) {
291
309
  effort, reasoning_effort: selection.reasoningEffort,
292
310
  task, source, cwd: executionCwd, requested_cwd: cwd,
293
311
  isolation: isolatedWorkspace ? 'worktree' : 'shared',
312
+ allow_no_changes: allow_no_changes === true,
294
313
  workspace_branch: workspace_branch ?? null, isolatedWorkspace,
295
314
  profile_id: profile_id ?? null, workspace_context: workspace_context ?? null,
296
315
  prompt: workerPrompt, delivery: delivery === 'review' ? 'review' : 'coding',
@@ -451,6 +470,13 @@ export class WorkerRegistry { constructor(ctx) {
451
470
  job.workspaceDiff = job.baseline.kind === 'git'
452
471
  ? await captureWorkspaceDiff({ cwd: executionCwd, baseline: job.baseline }).catch(() => ({ kind: 'no-git', reason: NOT_A_GIT_REPOSITORY, error: 'workspace diff failed' }))
453
472
  : job.baseline;
473
+ job.outcome = applyHubWorkspaceEvidence({
474
+ outcome: job.outcome,
475
+ workspaceDiff: job.workspaceDiff,
476
+ allowNoChanges: job.allow_no_changes,
477
+ isolation: job.isolation,
478
+ role: job.role,
479
+ });
454
480
  if (job.role === 'reviewer' && job.review && job.workspaceDiff?.kind === 'git') {
455
481
  const changes = job.workspaceDiff.changes ?? {};
456
482
  const mutated = ['modified', 'deleted', 'renamed', 'untracked'].some((key) => Array.isArray(changes[key]) && changes[key].length > 0);
@@ -609,6 +635,9 @@ export function resolveHubSpawnPayload(payload, getConfig = () => ({}), dependen
609
635
  if (raw.constraints?.allow_fallback !== undefined && typeof raw.constraints.allow_fallback !== 'boolean') {
610
636
  return { ok: false, code: 'JOB_CONSTRAINTS_INVALID', error: 'allow_fallback must be boolean' };
611
637
  }
638
+ if (raw.constraints?.allow_no_changes !== undefined && typeof raw.constraints.allow_no_changes !== 'boolean') {
639
+ return { ok: false, code: 'JOB_CONSTRAINTS_INVALID', error: 'allow_no_changes must be boolean' };
640
+ }
612
641
  const profileRegistry = dependencies.profileRegistry ?? loadRoleProfiles();
613
642
  const workspaceRegistry = dependencies.workspaceRegistry ?? loadWorkspaceContexts();
614
643
  if (!profileRegistry.ok) return { ok: false, code: 'PROFILE_FILE_INVALID', error: 'role profile registry is invalid' };
@@ -647,6 +676,7 @@ export function resolveHubSpawnPayload(payload, getConfig = () => ({}), dependen
647
676
  workspace_branch: raw.workspace?.branch ?? withRefs.context?.default_branch ?? null,
648
677
  timeout_seconds: raw.constraints?.timeout_seconds ?? profile.timeout_seconds,
649
678
  allow_fallback: raw.constraints?.allow_fallback ?? profile.fallback,
679
+ allow_no_changes: raw.constraints?.allow_no_changes === true,
650
680
  routing: profile.routing,
651
681
  review_strictness: profile.review_strictness,
652
682
  profile_id: resolvedProfile.profile_id,
@@ -852,12 +882,7 @@ export async function apply(ctx) {
852
882
  ? { status: 'UNAVAILABLE', reason_code: 'WORKSPACE_CONTEXT_NOT_FOUND' }
853
883
  : await assessWorkspaceReadiness({ cwd: requestedContext?.repo_root ?? null });
854
884
  const liveJobs = typeof hub.list === 'function' ? hub.list() : [];
855
- const modelExecution = liveJobs.some((job) => job?.role === 'worker' && job?.status === 'done')
856
- ? { id: 'model_execution', status: 'PASS', reason_code: 'REAL_EXECUTION_PASSED' }
857
- : { id: 'model_execution', status: 'NOT_RUN', reason_code: 'NO_EXECUTION_EVIDENCE' };
858
- const reviewerExecution = liveJobs.some((job) => job?.role === 'reviewer' && job?.status === 'done')
859
- ? { id: 'reviewer_pipeline', status: 'PASS', reason_code: 'REAL_REVIEW_PASSED' }
860
- : { id: 'reviewer_pipeline', status: 'NOT_RUN', reason_code: 'NO_EXECUTION_EVIDENCE' };
885
+ const [modelExecution, reviewerExecution] = buildHubExecutionRows(liveJobs);
861
886
  const contract = buildExtensionContract({
862
887
  config,
863
888
  readinessMatrix: { rows: [
@@ -1033,8 +1058,8 @@ export async function apply(ctx) {
1033
1058
  // Cache-busted import: the installer must always run the code
1034
1059
  // currently on disk, not whatever this process first loaded —
1035
1060
  // a stale cached copy once re-broke user settings after a fix.
1036
- const { installClaudeCode, installCodex, installHudSegment, uninstallClaudeCode, uninstallCodex } =
1037
- await import(`../install/install.mjs?t=${Date.now()}`);
1061
+ const { installClaudeCode, installCodex, installHudSegment, uninstallClaudeCode, uninstallCodex, installZCode, uninstallZCode } =
1062
+ await import(`../install/install.mjs?t=${Date.now()}`);
1038
1063
  if (target === 'claude') {
1039
1064
  const base = await installClaudeCode({ statusline: !!statusline });
1040
1065
  const hud = installHudSegment({});
@@ -1043,10 +1068,12 @@ export async function apply(ctx) {
1043
1068
  actions: [...base.actions, ...hud.actions.map((a) => `hud: ${a}`)],
1044
1069
  });
1045
1070
  }
1046
- if (target === 'codex') return sendJson(res, 200, installCodex({}));
1047
- if (target === 'claude-uninstall') return sendJson(res, 200, uninstallClaudeCode({}));
1048
- if (target === 'codex-uninstall') return sendJson(res, 200, uninstallCodex({}));
1049
- return sendJson(res, 400, { ok: false, error: 'target must be claude | codex | claude-uninstall | codex-uninstall' });
1071
+ if (target === 'codex') return sendJson(res, 200, installCodex({}));
1072
+ if (target === 'zcode') return sendJson(res, 200, installZCode({}));
1073
+ if (target === 'claude-uninstall') return sendJson(res, 200, uninstallClaudeCode({}));
1074
+ if (target === 'codex-uninstall') return sendJson(res, 200, uninstallCodex({}));
1075
+ if (target === 'zcode-uninstall') return sendJson(res, 200, uninstallZCode({}));
1076
+ return sendJson(res, 400, { ok: false, error: 'target must be claude | codex | zcode | claude-uninstall | codex-uninstall | zcode-uninstall' });
1050
1077
  } catch (err) {
1051
1078
  return sendJson(res, 500, { ok: false, error: err?.message ?? String(err) });
1052
1079
  }
@@ -6,11 +6,14 @@ import { readFileSync, writeFileSync, mkdirSync, copyFileSync, existsSync, readd
6
6
  import { dirname, join, resolve, relative } from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import { homedir } from 'node:os';
9
- import { normalizeModelPriority } from '../model-routing.mjs';
9
+ import { normalizeModelPriority } from '../model-routing.mjs';
10
+ import { zcodeStatus } from './zcode.mjs';
10
11
 
11
12
  const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
12
13
  const MARKETPLACE_NAME = 'dsh-crew';
13
- const PLUGIN_KEY = `dsh-crew@${MARKETPLACE_NAME}`;
14
+ const PLUGIN_KEY = `dsh-crew@${MARKETPLACE_NAME}`;
15
+ const POLICY_START = '<!-- DSH CREW MANAGED POLICY:START -->';
16
+ const POLICY_END = '<!-- DSH CREW MANAGED POLICY:END -->';
14
17
  // dsh_worker_config is included so the session commands (/dsh-crew:config,
15
18
  // /dsh-config) and any orchestrator policy lookup run without an extra
16
19
  // authorization prompt.
@@ -33,6 +36,50 @@ function readText(file) {
33
36
  try { return readFileSync(file, 'utf8'); } catch { return null; }
34
37
  }
35
38
 
39
+ function managedPolicyBlock(root) {
40
+ const policy = readText(join(root, 'codex', 'AGENTS.md'))?.trim();
41
+ if (!policy) return null;
42
+ return `${POLICY_START}\n${policy}\n${POLICY_END}`;
43
+ }
44
+
45
+ function installGlobalCodexPolicy({ home, root }) {
46
+ const file = join(home, '.codex', 'AGENTS.md');
47
+ const block = managedPolicyBlock(root);
48
+ if (!block) return { ok: false, action: 'global policy template missing' };
49
+ mkdirSync(dirname(file), { recursive: true });
50
+ const current = readText(file) ?? '';
51
+ const managed = new RegExp(`${POLICY_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${POLICY_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, 'm');
52
+ const template = readText(join(root, 'codex', 'AGENTS.md'))?.trim() ?? '';
53
+ let next;
54
+ if (managed.test(current)) next = current.replace(managed, block);
55
+ else if (current.trim() === template) next = `${block}\n`;
56
+ else next = `${current.trimEnd()}${current.trim() ? '\n\n' : ''}${block}\n`;
57
+ if (next !== current) {
58
+ backup(file);
59
+ writeFileSync(file, next);
60
+ }
61
+ return { ok: true, action: `global policy: ${file}` };
62
+ }
63
+
64
+ function globalCodexPolicyReady({ home, root = ROOT }) {
65
+ const block = managedPolicyBlock(root);
66
+ const text = readText(join(home, '.codex', 'AGENTS.md'));
67
+ return !!block && typeof text === 'string' && text.includes(block);
68
+ }
69
+
70
+ function uninstallGlobalCodexPolicy({ home }) {
71
+ const file = join(home, '.codex', 'AGENTS.md');
72
+ const current = readText(file);
73
+ if (typeof current !== 'string') return null;
74
+ const managed = new RegExp(`(?:\\r?\\n){0,2}${POLICY_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${POLICY_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:\\r?\\n)?`, 'm');
75
+ if (!managed.test(current)) return null;
76
+ const next = current.replace(managed, '').trimEnd();
77
+ backup(file);
78
+ if (next.trim()) writeFileSync(file, `${next}\n`);
79
+ else rmSync(file);
80
+ return `codex global policy: removed managed block`;
81
+ }
82
+
36
83
  function tomlSection(text, name) {
37
84
  if (typeof text !== 'string') return null;
38
85
  const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -256,7 +303,7 @@ export function writeGlobalConfig(patch) {
256
303
  }
257
304
 
258
305
  /** What is currently installed where — drives the settings-page buttons. */
259
- export function installStatus({ home = homedir() } = {}) {
306
+ export function installStatus({ home = homedir(), root } = {}) {
260
307
  const settings = readJson(join(home, '.claude', 'settings.json'), {});
261
308
  const enabled = settings.enabledPlugins;
262
309
  const claudeInstalled = !!(enabled && !Array.isArray(enabled) && enabled[PLUGIN_KEY]);
@@ -282,6 +329,7 @@ export function installStatus({ home = homedir() } = {}) {
282
329
  status_prompt: !!readText(join(codexRoot, 'prompts', 'dsh-status.md'))?.trim(),
283
330
  mcp: !!mcpTarget,
284
331
  target_alignment: !!workerTarget && workerTarget === reviewerTarget && workerTarget === mcpTarget,
332
+ global_policy: globalCodexPolicyReady({ home }),
285
333
  };
286
334
  const codexInstalled = Object.values(components).some(Boolean)
287
335
  || existsSync(join(codexRoot, 'agents', 'ds-flash.toml'))
@@ -296,10 +344,11 @@ export function installStatus({ home = homedir() } = {}) {
296
344
  missing: claudeMissing,
297
345
  },
298
346
  codex: { installed: codexInstalled, ready: missing.length === 0, components, missing },
347
+ zcode: zcodeStatus({ home, ...(root ? { root } : {}) }),
299
348
  };
300
349
  }
301
350
 
302
- export function uninstallCodex({ home = homedir() } = {}) {
351
+ export function uninstallCodex({ home = homedir() } = {}) {
303
352
  const actions = [];
304
353
  // Both the v0.2 roles (ds-worker / ds-reviewer) and the deprecated v0.1
305
354
  // aliases (ds-flash / ds-pro) are dsh-crew managed; uninstall removes only
@@ -308,10 +357,12 @@ export function uninstallCodex({ home = homedir() } = {}) {
308
357
  const p = join(home, '.codex', 'agents', f);
309
358
  if (existsSync(p)) { backup(p); rmSync(p); actions.push(`removed: ${p} (backup kept)`); }
310
359
  }
311
- for (const f of ['dsh-config.md', 'dsh-status.md']) {
360
+ for (const f of ['dsh-config.md', 'dsh-status.md']) {
312
361
  const p = join(home, '.codex', 'prompts', f);
313
362
  if (existsSync(p)) { rmSync(p); actions.push(`removed: ${p}`); }
314
- }
363
+ }
364
+ const policyAction = uninstallGlobalCodexPolicy({ home });
365
+ if (policyAction) actions.push(policyAction);
315
366
  // Remove only the dsh-crew entry from [mcp_servers], keeping any other
316
367
  // MCP servers the user configured.
317
368
  const configFile = join(home, '.codex', 'config.toml');
@@ -428,7 +479,7 @@ export async function installClaudeCode({ home = homedir(), statusline = false,
428
479
  return { ok: true, actions };
429
480
  }
430
481
 
431
- export function installCodex({ home = homedir(), scope, root = ROOT } = {}) {
482
+ export function installCodex({ home = homedir(), scope, root = ROOT } = {}) {
432
483
  const actions = [];
433
484
  const agentsDir = scope === 'project' ? join(process.cwd(), '.codex', 'agents') : join(home, '.codex', 'agents');
434
485
  mkdirSync(agentsDir, { recursive: true });
@@ -455,10 +506,13 @@ export function installCodex({ home = homedir(), scope, root = ROOT } = {}) {
455
506
  writeFileSync(join(promptsDir, f), readFileSync(join(promptsSrc, f), 'utf8'));
456
507
  actions.push(`prompt: ${join(promptsDir, f)}`);
457
508
  }
458
- if (scope !== 'project') {
459
- const act = writeGlobalCodexMcpServer(home, renderedPath);
460
- actions.push(...act);
461
- }
509
+ if (scope !== 'project') {
510
+ const act = writeGlobalCodexMcpServer(home, renderedPath);
511
+ actions.push(...act);
512
+ const policy = installGlobalCodexPolicy({ home, root });
513
+ if (!policy.ok) return { ok: false, actions: [...actions, policy.action] };
514
+ actions.push(policy.action);
515
+ }
462
516
  return { ok: true, actions };
463
517
  }
464
518
 
@@ -4,7 +4,9 @@
4
4
  // This module keeps all installer exports while replacing only global config
5
5
  // read/write semantics with schema-v3 canonical authority.
6
6
 
7
- export * from './install-legacy.mjs';
7
+ export * from './install-legacy.mjs';
8
+ export * from './windows-startup.mjs';
9
+ export * from './zcode.mjs';
8
10
 
9
11
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
10
12
  import { dirname, join } from 'node:path';
@@ -549,14 +549,30 @@ async function activateRelease({ home, releaseDir, manifest, log, installer }) {
549
549
  }
550
550
  log(`✓ Harness plugin registered (dedicated dsh-crew profile → ${releaseDir})`);
551
551
 
552
- const codex = installer.installCodex({ home, root: releaseDir });
552
+ const codex = installer.installCodex({ home, root: releaseDir });
553
553
  if (codex.ok === false) {
554
554
  log(`✗ Codex Desktop integration failed: ${(codex.actions ?? []).join('; ')}`);
555
555
  return false;
556
556
  }
557
- log('✓ Codex Desktop integration');
558
-
559
- const claude = await installer.installClaudeCode({ home, root: releaseDir });
557
+ log('✓ Codex Desktop integration');
558
+
559
+ if (installer.installZCode) {
560
+ const zcode = installer.installZCode({ home, root: releaseDir });
561
+ if (zcode.ok === false) {
562
+ log(`✗ ZCode integration failed (${zcode.code ?? 'unknown'})`);
563
+ return false;
564
+ }
565
+ log('✓ ZCode integration');
566
+ }
567
+
568
+ const startup = installer.installWindowsStartup?.({ home, root: releaseDir });
569
+ if (startup?.ok === false) {
570
+ log(`✗ Windows login startup failed (${startup.code ?? 'unknown'})`);
571
+ return false;
572
+ }
573
+ if (startup?.supported) log('✓ Windows login startup');
574
+
575
+ const claude = await installer.installClaudeCode({ home, root: releaseDir });
560
576
  if (claude.ok === false) {
561
577
  log(`✗ Claude Code integration failed`);
562
578
  return false;
@@ -944,11 +960,17 @@ export function npxStatus({
944
960
  } catch { dshPlugin = 'unknown'; }
945
961
  }
946
962
 
947
- const st = installer.installStatus ? installer.installStatus({ home }) : realInstaller.installStatus({ home });
948
- const codex = st?.codex?.installed ? 'installed' : 'not installed';
949
- const claude = st?.claude?.installed ? 'installed' : 'not installed';
963
+ const st = installer.installStatus
964
+ ? installer.installStatus({ home, root: pointer?.path ?? runningPackageRoot() })
965
+ : realInstaller.installStatus({ home, root: pointer?.path ?? runningPackageRoot() });
966
+ const codex = st?.codex?.installed ? 'installed' : 'not installed';
967
+ const zcode = st?.zcode?.installed ? 'installed' : 'not installed';
968
+ const claude = st?.claude?.installed ? 'installed' : 'not installed';
950
969
  const official = officialWebIntegrationStatus({ home, releaseDir: pointer?.path });
951
- const officialWeb = !official.enabled ? 'disabled' : official.healthy ? 'installed' : 'needs repair';
970
+ const officialWeb = !official.enabled ? 'disabled' : official.healthy ? 'installed' : 'needs repair';
971
+ const startupState = installer.windowsStartupStatus?.({ home });
972
+ const windowsStartup = !startupState?.supported ? 'not supported'
973
+ : startupState.ready ? 'installed' : startupState.installed ? 'needs repair' : 'not installed';
952
974
 
953
975
  log(`DSH Crew launcher/candidate: ${candidateVersion ?? 'unknown'}`);
954
976
  log(`Installed DSH Crew payload: ${installedLine}`);
@@ -964,8 +986,10 @@ export function npxStatus({
964
986
  }
965
987
  log(`DSH plugin: ${dshPlugin} (dedicated dsh-crew profile on 3210)`);
966
988
  log(`Official 3080 UI bridge: ${officialWeb}`);
967
- log(`Codex Desktop integration: ${codex}`);
968
- log(`Claude Code integration: ${claude}`);
989
+ log(`Codex Desktop integration: ${codex}`);
990
+ log(`ZCode integration: ${zcode}`);
991
+ log(`Claude Code integration: ${claude}`);
992
+ log(`Windows login startup: ${windowsStartup}`);
969
993
 
970
994
  return {
971
995
  ok: true,
@@ -974,9 +998,11 @@ export function npxStatus({
974
998
  installedPath: pointer?.path ?? null,
975
999
  dshPlugin,
976
1000
  officialWeb,
977
- codex,
978
- claude,
979
- };
1001
+ codex,
1002
+ zcode,
1003
+ claude,
1004
+ windowsStartup,
1005
+ };
980
1006
  }
981
1007
 
982
1008
  export async function npxUninstall({
@@ -992,13 +1018,23 @@ export async function npxUninstall({
992
1018
  const pointer = readCurrentPointer({ home });
993
1019
  const name = pointer?.name ?? readManifest(runningPackageRoot())?.name;
994
1020
 
995
- const cx = installer.uninstallCodex({ home });
1021
+ const cx = installer.uninstallCodex({ home });
996
1022
  if (cx.ok !== false) log('✓ Codex Desktop integration removed');
997
- else fail('Codex Desktop integration removal failed');
998
-
999
- const cl = installer.uninstallClaudeCode ? await installer.uninstallClaudeCode({ home }) : realInstaller.uninstallClaudeCode({ home });
1000
- if (cl.ok !== false) log('✓ Claude Code integration removed');
1001
- else fail('Claude Code integration removal failed');
1023
+ else fail('Codex Desktop integration removal failed');
1024
+
1025
+ if (installer.uninstallZCode) {
1026
+ const zc = installer.uninstallZCode({ home });
1027
+ if (zc.ok !== false) log(' ZCode integration removed');
1028
+ else fail('ZCode integration removal failed');
1029
+ }
1030
+
1031
+ const cl = installer.uninstallClaudeCode ? await installer.uninstallClaudeCode({ home }) : realInstaller.uninstallClaudeCode({ home });
1032
+ if (cl.ok !== false) log('✓ Claude Code integration removed');
1033
+ else fail('Claude Code integration removal failed');
1034
+
1035
+ const startup = installer.uninstallWindowsStartup?.({ home });
1036
+ if (startup?.ok === false) fail('Windows login startup removal failed');
1037
+ else if (startup?.supported) log('✓ Windows login startup removed');
1002
1038
 
1003
1039
  const official = removeOfficialWebIntegration({ home, preserveIntent: !purge, remember: !purge });
1004
1040
  if (!official.ok) fail(`official 3080 bridge removal failed (${official.code ?? 'unknown'})`);
@@ -0,0 +1,115 @@
1
+ import {
2
+ copyFileSync,
3
+ existsSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ rmSync,
7
+ writeFileSync,
8
+ } from 'node:fs';
9
+ import { dirname, join } from 'node:path';
10
+ import { homedir } from 'node:os';
11
+
12
+ export const WINDOWS_STARTUP_FILENAME = 'DSH Crew.vbs';
13
+ export const WINDOWS_LAUNCHER_FILENAME = 'start-dsh-crew.cmd';
14
+ export const WINDOWS_HELPER_FILENAME = 'start-dsh-crew.ps1';
15
+
16
+ function defaultStartupDir({ home, env }) {
17
+ if (home === homedir() && env.APPDATA) {
18
+ return join(env.APPDATA, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
19
+ }
20
+ return join(home, 'AppData', 'Roaming', 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
21
+ }
22
+
23
+ function paths({ home, startupDir, env }) {
24
+ const launcherFile = join(home, '.config', 'dsh-crew', 'launchers', WINDOWS_LAUNCHER_FILENAME);
25
+ const helperFile = join(home, '.config', 'dsh-crew', 'launchers', WINDOWS_HELPER_FILENAME);
26
+ const resolvedStartupDir = startupDir ?? defaultStartupDir({ home, env });
27
+ return {
28
+ launcherFile,
29
+ helperFile,
30
+ startupFile: join(resolvedStartupDir, WINDOWS_STARTUP_FILENAME),
31
+ };
32
+ }
33
+
34
+ function renderVbs(template, launcherFile) {
35
+ const escaped = launcherFile.replace(/"/g, '""');
36
+ return template.replace('__LAUNCHER__', escaped);
37
+ }
38
+
39
+ export function windowsStartupStatus({
40
+ home = homedir(),
41
+ startupDir,
42
+ platform = process.platform,
43
+ env = process.env,
44
+ } = {}) {
45
+ if (platform !== 'win32') return { supported: false, installed: false, ready: false };
46
+ const resolved = paths({ home, startupDir, env });
47
+ const installed = existsSync(resolved.startupFile)
48
+ || existsSync(resolved.launcherFile)
49
+ || existsSync(resolved.helperFile);
50
+ let ready = existsSync(resolved.startupFile)
51
+ && existsSync(resolved.launcherFile)
52
+ && existsSync(resolved.helperFile);
53
+ if (ready) {
54
+ try {
55
+ const startup = readFileSync(resolved.startupFile, 'utf16le').replace(/^\uFEFF/, '');
56
+ const launcher = readFileSync(resolved.launcherFile, 'utf8');
57
+ const helper = readFileSync(resolved.helperFile, 'utf8');
58
+ ready = startup.includes(resolved.launcherFile)
59
+ && startup.includes('--watch')
60
+ && launcher.includes(WINDOWS_HELPER_FILENAME)
61
+ && helper.includes('DSH Crew managed Windows launcher')
62
+ && helper.includes('DSHCrewServiceSupervisor');
63
+ } catch { ready = false; }
64
+ }
65
+ return { supported: true, installed, ready, ...resolved };
66
+ }
67
+
68
+ export function installWindowsStartup({
69
+ home = homedir(),
70
+ root,
71
+ startupDir,
72
+ platform = process.platform,
73
+ env = process.env,
74
+ } = {}) {
75
+ if (platform !== 'win32') return { ok: true, supported: false, changed: false };
76
+ if (!root) return { ok: false, supported: true, code: 'STARTUP_SOURCE_REQUIRED' };
77
+ const sourceLauncher = join(root, 'windows', WINDOWS_LAUNCHER_FILENAME);
78
+ const sourceHelper = join(root, 'windows', WINDOWS_HELPER_FILENAME);
79
+ const sourceVbs = join(root, 'windows', 'start-dsh-crew.vbs');
80
+ if (!existsSync(sourceLauncher) || !existsSync(sourceHelper) || !existsSync(sourceVbs)) {
81
+ return { ok: false, supported: true, code: 'STARTUP_ASSET_MISSING' };
82
+ }
83
+ const resolved = paths({ home, startupDir, env });
84
+ mkdirSync(dirname(resolved.launcherFile), { recursive: true });
85
+ mkdirSync(dirname(resolved.startupFile), { recursive: true });
86
+ const beforeLauncher = existsSync(resolved.launcherFile) ? readFileSync(resolved.launcherFile) : null;
87
+ const beforeHelper = existsSync(resolved.helperFile) ? readFileSync(resolved.helperFile) : null;
88
+ const beforeStartup = existsSync(resolved.startupFile) ? readFileSync(resolved.startupFile) : null;
89
+ copyFileSync(sourceLauncher, resolved.launcherFile);
90
+ copyFileSync(sourceHelper, resolved.helperFile);
91
+ const rendered = renderVbs(readFileSync(sourceVbs, 'utf8'), resolved.launcherFile);
92
+ writeFileSync(resolved.startupFile, `\uFEFF${rendered}`, 'utf16le');
93
+ const changed = !beforeLauncher?.equals(readFileSync(resolved.launcherFile))
94
+ || !beforeHelper?.equals(readFileSync(resolved.helperFile))
95
+ || !beforeStartup?.equals(readFileSync(resolved.startupFile));
96
+ return { ok: true, supported: true, changed, ...resolved };
97
+ }
98
+
99
+ export function uninstallWindowsStartup({
100
+ home = homedir(),
101
+ startupDir,
102
+ platform = process.platform,
103
+ env = process.env,
104
+ } = {}) {
105
+ if (platform !== 'win32') return { ok: true, supported: false, removed: false };
106
+ const resolved = paths({ home, startupDir, env });
107
+ let removed = false;
108
+ for (const file of [resolved.startupFile, resolved.launcherFile, resolved.helperFile]) {
109
+ if (existsSync(file)) {
110
+ rmSync(file, { force: true });
111
+ removed = true;
112
+ }
113
+ }
114
+ return { ok: true, supported: true, removed, ...resolved };
115
+ }