@lumenflow/core 1.0.0
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/LICENSE +190 -0
- package/README.md +119 -0
- package/dist/active-wu-detector.d.ts +33 -0
- package/dist/active-wu-detector.js +106 -0
- package/dist/adapters/filesystem-metrics.adapter.d.ts +108 -0
- package/dist/adapters/filesystem-metrics.adapter.js +519 -0
- package/dist/adapters/terminal-renderer.adapter.d.ts +106 -0
- package/dist/adapters/terminal-renderer.adapter.js +337 -0
- package/dist/arg-parser.d.ts +63 -0
- package/dist/arg-parser.js +560 -0
- package/dist/backlog-editor.d.ts +98 -0
- package/dist/backlog-editor.js +179 -0
- package/dist/backlog-generator.d.ts +111 -0
- package/dist/backlog-generator.js +381 -0
- package/dist/backlog-parser.d.ts +45 -0
- package/dist/backlog-parser.js +102 -0
- package/dist/backlog-sync-validator.d.ts +78 -0
- package/dist/backlog-sync-validator.js +294 -0
- package/dist/branch-drift.d.ts +34 -0
- package/dist/branch-drift.js +51 -0
- package/dist/cleanup-install-config.d.ts +33 -0
- package/dist/cleanup-install-config.js +37 -0
- package/dist/cleanup-lock.d.ts +139 -0
- package/dist/cleanup-lock.js +313 -0
- package/dist/code-path-validator.d.ts +146 -0
- package/dist/code-path-validator.js +537 -0
- package/dist/code-paths-overlap.d.ts +55 -0
- package/dist/code-paths-overlap.js +245 -0
- package/dist/commands-logger.d.ts +77 -0
- package/dist/commands-logger.js +254 -0
- package/dist/commit-message-utils.d.ts +25 -0
- package/dist/commit-message-utils.js +41 -0
- package/dist/compliance-parser.d.ts +150 -0
- package/dist/compliance-parser.js +507 -0
- package/dist/constants/backlog-patterns.d.ts +20 -0
- package/dist/constants/backlog-patterns.js +23 -0
- package/dist/constants/dora-constants.d.ts +49 -0
- package/dist/constants/dora-constants.js +53 -0
- package/dist/constants/gate-constants.d.ts +15 -0
- package/dist/constants/gate-constants.js +15 -0
- package/dist/constants/linter-constants.d.ts +16 -0
- package/dist/constants/linter-constants.js +16 -0
- package/dist/constants/tokenizer-constants.d.ts +15 -0
- package/dist/constants/tokenizer-constants.js +15 -0
- package/dist/core/scope-checker.d.ts +97 -0
- package/dist/core/scope-checker.js +163 -0
- package/dist/core/tool-runner.d.ts +161 -0
- package/dist/core/tool-runner.js +393 -0
- package/dist/core/tool.constants.d.ts +105 -0
- package/dist/core/tool.constants.js +101 -0
- package/dist/core/tool.schemas.d.ts +226 -0
- package/dist/core/tool.schemas.js +226 -0
- package/dist/core/worktree-guard.d.ts +130 -0
- package/dist/core/worktree-guard.js +242 -0
- package/dist/coverage-gate.d.ts +108 -0
- package/dist/coverage-gate.js +196 -0
- package/dist/date-utils.d.ts +75 -0
- package/dist/date-utils.js +140 -0
- package/dist/dependency-graph.d.ts +142 -0
- package/dist/dependency-graph.js +550 -0
- package/dist/dependency-guard.d.ts +54 -0
- package/dist/dependency-guard.js +142 -0
- package/dist/dependency-validator.d.ts +105 -0
- package/dist/dependency-validator.js +154 -0
- package/dist/docs-path-validator.d.ts +36 -0
- package/dist/docs-path-validator.js +95 -0
- package/dist/domain/orchestration.constants.d.ts +99 -0
- package/dist/domain/orchestration.constants.js +97 -0
- package/dist/domain/orchestration.schemas.d.ts +280 -0
- package/dist/domain/orchestration.schemas.js +211 -0
- package/dist/domain/orchestration.types.d.ts +133 -0
- package/dist/domain/orchestration.types.js +12 -0
- package/dist/error-handler.d.ts +116 -0
- package/dist/error-handler.js +136 -0
- package/dist/file-classifiers.d.ts +62 -0
- package/dist/file-classifiers.js +108 -0
- package/dist/gates-agent-mode.d.ts +81 -0
- package/dist/gates-agent-mode.js +94 -0
- package/dist/generate-traceability.d.ts +107 -0
- package/dist/generate-traceability.js +411 -0
- package/dist/git-adapter.d.ts +395 -0
- package/dist/git-adapter.js +649 -0
- package/dist/git-staged-validator.d.ts +32 -0
- package/dist/git-staged-validator.js +48 -0
- package/dist/hardcoded-strings.d.ts +61 -0
- package/dist/hardcoded-strings.js +270 -0
- package/dist/incremental-lint.d.ts +78 -0
- package/dist/incremental-lint.js +129 -0
- package/dist/incremental-test.d.ts +39 -0
- package/dist/incremental-test.js +61 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +61 -0
- package/dist/invariants/check-automated-tests.d.ts +50 -0
- package/dist/invariants/check-automated-tests.js +166 -0
- package/dist/invariants-runner.d.ts +103 -0
- package/dist/invariants-runner.js +527 -0
- package/dist/lane-checker.d.ts +50 -0
- package/dist/lane-checker.js +319 -0
- package/dist/lane-inference.d.ts +39 -0
- package/dist/lane-inference.js +195 -0
- package/dist/lane-lock.d.ts +211 -0
- package/dist/lane-lock.js +474 -0
- package/dist/lane-validator.d.ts +48 -0
- package/dist/lane-validator.js +114 -0
- package/dist/logs-lib.d.ts +104 -0
- package/dist/logs-lib.js +207 -0
- package/dist/lumenflow-config-schema.d.ts +272 -0
- package/dist/lumenflow-config-schema.js +207 -0
- package/dist/lumenflow-config.d.ts +95 -0
- package/dist/lumenflow-config.js +236 -0
- package/dist/manual-test-validator.d.ts +80 -0
- package/dist/manual-test-validator.js +200 -0
- package/dist/merge-lock.d.ts +115 -0
- package/dist/merge-lock.js +251 -0
- package/dist/micro-worktree.d.ts +159 -0
- package/dist/micro-worktree.js +427 -0
- package/dist/migration-deployer.d.ts +69 -0
- package/dist/migration-deployer.js +151 -0
- package/dist/orchestration-advisory-loader.d.ts +28 -0
- package/dist/orchestration-advisory-loader.js +87 -0
- package/dist/orchestration-advisory.d.ts +58 -0
- package/dist/orchestration-advisory.js +94 -0
- package/dist/orchestration-di.d.ts +48 -0
- package/dist/orchestration-di.js +57 -0
- package/dist/orchestration-rules.d.ts +57 -0
- package/dist/orchestration-rules.js +201 -0
- package/dist/orphan-detector.d.ts +131 -0
- package/dist/orphan-detector.js +226 -0
- package/dist/path-classifiers.d.ts +57 -0
- package/dist/path-classifiers.js +93 -0
- package/dist/piped-command-detector.d.ts +34 -0
- package/dist/piped-command-detector.js +64 -0
- package/dist/ports/dashboard-renderer.port.d.ts +112 -0
- package/dist/ports/dashboard-renderer.port.js +25 -0
- package/dist/ports/metrics-collector.port.d.ts +132 -0
- package/dist/ports/metrics-collector.port.js +26 -0
- package/dist/process-detector.d.ts +84 -0
- package/dist/process-detector.js +172 -0
- package/dist/prompt-linter.d.ts +72 -0
- package/dist/prompt-linter.js +312 -0
- package/dist/prompt-monitor.d.ts +15 -0
- package/dist/prompt-monitor.js +205 -0
- package/dist/rebase-artifact-cleanup.d.ts +145 -0
- package/dist/rebase-artifact-cleanup.js +433 -0
- package/dist/retry-strategy.d.ts +189 -0
- package/dist/retry-strategy.js +283 -0
- package/dist/risk-detector.d.ts +108 -0
- package/dist/risk-detector.js +252 -0
- package/dist/rollback-utils.d.ts +76 -0
- package/dist/rollback-utils.js +104 -0
- package/dist/section-headings.d.ts +43 -0
- package/dist/section-headings.js +49 -0
- package/dist/spawn-escalation.d.ts +90 -0
- package/dist/spawn-escalation.js +253 -0
- package/dist/spawn-monitor.d.ts +229 -0
- package/dist/spawn-monitor.js +672 -0
- package/dist/spawn-recovery.d.ts +82 -0
- package/dist/spawn-recovery.js +298 -0
- package/dist/spawn-registry-schema.d.ts +98 -0
- package/dist/spawn-registry-schema.js +108 -0
- package/dist/spawn-registry-store.d.ts +146 -0
- package/dist/spawn-registry-store.js +273 -0
- package/dist/spawn-tree.d.ts +121 -0
- package/dist/spawn-tree.js +285 -0
- package/dist/stamp-status-validator.d.ts +84 -0
- package/dist/stamp-status-validator.js +134 -0
- package/dist/stamp-utils.d.ts +100 -0
- package/dist/stamp-utils.js +229 -0
- package/dist/state-machine.d.ts +26 -0
- package/dist/state-machine.js +83 -0
- package/dist/system-map-validator.d.ts +80 -0
- package/dist/system-map-validator.js +272 -0
- package/dist/telemetry.d.ts +80 -0
- package/dist/telemetry.js +213 -0
- package/dist/token-counter.d.ts +51 -0
- package/dist/token-counter.js +145 -0
- package/dist/usecases/get-dashboard-data.usecase.d.ts +52 -0
- package/dist/usecases/get-dashboard-data.usecase.js +61 -0
- package/dist/usecases/get-suggestions.usecase.d.ts +100 -0
- package/dist/usecases/get-suggestions.usecase.js +153 -0
- package/dist/user-normalizer.d.ts +41 -0
- package/dist/user-normalizer.js +141 -0
- package/dist/validators/phi-constants.d.ts +97 -0
- package/dist/validators/phi-constants.js +152 -0
- package/dist/validators/phi-scanner.d.ts +58 -0
- package/dist/validators/phi-scanner.js +215 -0
- package/dist/worktree-ownership.d.ts +50 -0
- package/dist/worktree-ownership.js +74 -0
- package/dist/worktree-scanner.d.ts +103 -0
- package/dist/worktree-scanner.js +168 -0
- package/dist/worktree-symlink.d.ts +99 -0
- package/dist/worktree-symlink.js +359 -0
- package/dist/wu-backlog-updater.d.ts +17 -0
- package/dist/wu-backlog-updater.js +37 -0
- package/dist/wu-checkpoint.d.ts +124 -0
- package/dist/wu-checkpoint.js +233 -0
- package/dist/wu-claim-helpers.d.ts +26 -0
- package/dist/wu-claim-helpers.js +63 -0
- package/dist/wu-claim-resume.d.ts +106 -0
- package/dist/wu-claim-resume.js +276 -0
- package/dist/wu-consistency-checker.d.ts +95 -0
- package/dist/wu-consistency-checker.js +567 -0
- package/dist/wu-constants.d.ts +1275 -0
- package/dist/wu-constants.js +1382 -0
- package/dist/wu-create-validators.d.ts +42 -0
- package/dist/wu-create-validators.js +93 -0
- package/dist/wu-done-branch-only.d.ts +63 -0
- package/dist/wu-done-branch-only.js +191 -0
- package/dist/wu-done-messages.d.ts +119 -0
- package/dist/wu-done-messages.js +185 -0
- package/dist/wu-done-pr.d.ts +72 -0
- package/dist/wu-done-pr.js +174 -0
- package/dist/wu-done-retry-helpers.d.ts +85 -0
- package/dist/wu-done-retry-helpers.js +172 -0
- package/dist/wu-done-ui.d.ts +37 -0
- package/dist/wu-done-ui.js +69 -0
- package/dist/wu-done-validators.d.ts +411 -0
- package/dist/wu-done-validators.js +1229 -0
- package/dist/wu-done-worktree.d.ts +182 -0
- package/dist/wu-done-worktree.js +1097 -0
- package/dist/wu-helpers.d.ts +128 -0
- package/dist/wu-helpers.js +248 -0
- package/dist/wu-lint.d.ts +70 -0
- package/dist/wu-lint.js +234 -0
- package/dist/wu-paths.d.ts +171 -0
- package/dist/wu-paths.js +178 -0
- package/dist/wu-preflight-validators.d.ts +86 -0
- package/dist/wu-preflight-validators.js +251 -0
- package/dist/wu-recovery.d.ts +138 -0
- package/dist/wu-recovery.js +341 -0
- package/dist/wu-repair-core.d.ts +131 -0
- package/dist/wu-repair-core.js +669 -0
- package/dist/wu-schema-normalization.d.ts +17 -0
- package/dist/wu-schema-normalization.js +82 -0
- package/dist/wu-schema.d.ts +793 -0
- package/dist/wu-schema.js +881 -0
- package/dist/wu-spawn-helpers.d.ts +121 -0
- package/dist/wu-spawn-helpers.js +271 -0
- package/dist/wu-spawn.d.ts +158 -0
- package/dist/wu-spawn.js +1306 -0
- package/dist/wu-state-schema.d.ts +213 -0
- package/dist/wu-state-schema.js +156 -0
- package/dist/wu-state-store.d.ts +264 -0
- package/dist/wu-state-store.js +691 -0
- package/dist/wu-status-transition.d.ts +63 -0
- package/dist/wu-status-transition.js +382 -0
- package/dist/wu-status-updater.d.ts +25 -0
- package/dist/wu-status-updater.js +116 -0
- package/dist/wu-transaction-collectors.d.ts +116 -0
- package/dist/wu-transaction-collectors.js +272 -0
- package/dist/wu-transaction.d.ts +170 -0
- package/dist/wu-transaction.js +273 -0
- package/dist/wu-validation-constants.d.ts +60 -0
- package/dist/wu-validation-constants.js +66 -0
- package/dist/wu-validation.d.ts +118 -0
- package/dist/wu-validation.js +243 -0
- package/dist/wu-validator.d.ts +62 -0
- package/dist/wu-validator.js +325 -0
- package/dist/wu-yaml-fixer.d.ts +97 -0
- package/dist/wu-yaml-fixer.js +264 -0
- package/dist/wu-yaml.d.ts +86 -0
- package/dist/wu-yaml.js +222 -0
- package/package.json +114 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawn Recovery Module (WU-1951)
|
|
3
|
+
*
|
|
4
|
+
* Auto-recovery heuristics for stuck spawns and zombie locks.
|
|
5
|
+
* Used by orchestrate:monitor for automatic spawn health management.
|
|
6
|
+
*
|
|
7
|
+
* Recovery Heuristics:
|
|
8
|
+
* 1. Zombie lock (PID not running) -> auto-release, mark spawn crashed
|
|
9
|
+
* 2. Stale lock (>2h) -> auto-release, mark spawn timeout
|
|
10
|
+
* 3. Active lock + no checkpoint in 1h -> mark stuck, escalate
|
|
11
|
+
*
|
|
12
|
+
* All recovery actions are logged to .beacon/recovery/ for audit.
|
|
13
|
+
*
|
|
14
|
+
* Library-First Note: This is project-specific spawn recovery code for
|
|
15
|
+
* PatientPath's custom spawn-registry.jsonl, lane-lock, and memory-store.
|
|
16
|
+
* No external library exists for this domain-specific agent lifecycle management.
|
|
17
|
+
*
|
|
18
|
+
* @see {@link tools/lib/__tests__/spawn-recovery.test.mjs} - Tests
|
|
19
|
+
* @see {@link tools/lib/spawn-monitor.mjs} - Monitoring logic
|
|
20
|
+
* @see {@link tools/lib/spawn-registry-store.mjs} - Spawn state
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Recovery action constants
|
|
24
|
+
*/
|
|
25
|
+
export declare const RecoveryAction: Readonly<{
|
|
26
|
+
/** No recovery needed */
|
|
27
|
+
NONE: "none";
|
|
28
|
+
/** Zombie lock released (PID not running) */
|
|
29
|
+
RELEASED_ZOMBIE: "released_zombie";
|
|
30
|
+
/** Stale lock released (>2h old) */
|
|
31
|
+
RELEASED_STALE: "released_stale";
|
|
32
|
+
/** Stuck spawn escalated (active but no checkpoint in 1h) */
|
|
33
|
+
ESCALATED_STUCK: "escalated_stuck";
|
|
34
|
+
}>;
|
|
35
|
+
/**
|
|
36
|
+
* Recovery directory name
|
|
37
|
+
*/
|
|
38
|
+
export declare const RECOVERY_DIR_NAME = "recovery";
|
|
39
|
+
/**
|
|
40
|
+
* Threshold for "no checkpoint" detection (1 hour in milliseconds)
|
|
41
|
+
*/
|
|
42
|
+
export declare const NO_CHECKPOINT_THRESHOLD_MS: number;
|
|
43
|
+
/**
|
|
44
|
+
* Recovers a stuck spawn by applying appropriate heuristics.
|
|
45
|
+
*
|
|
46
|
+
* Recovery order (first match wins):
|
|
47
|
+
* 1. Zombie lock (PID not running) -> release lock, mark crashed
|
|
48
|
+
* 2. Stale lock (>2h) -> release lock, mark timeout
|
|
49
|
+
* 3. Active lock + no checkpoint in 1h -> escalate (no auto-release)
|
|
50
|
+
* 4. Healthy spawn -> no action
|
|
51
|
+
*
|
|
52
|
+
* @param {string} spawnId - ID of the spawn to recover
|
|
53
|
+
* @param {RecoverStuckSpawnOptions} options - Options
|
|
54
|
+
* @returns {Promise<RecoveryResult>} Recovery result
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* const result = await recoverStuckSpawn('spawn-1234', { baseDir: '/path/to/project' });
|
|
58
|
+
* if (result.recovered) {
|
|
59
|
+
* console.log(`Recovered: ${result.action} - ${result.reason}`);
|
|
60
|
+
* }
|
|
61
|
+
*/
|
|
62
|
+
export interface RecoverStuckSpawnOptions {
|
|
63
|
+
/** Base directory for .beacon/ */
|
|
64
|
+
baseDir?: string;
|
|
65
|
+
}
|
|
66
|
+
export declare function recoverStuckSpawn(spawnId: any, options?: RecoverStuckSpawnOptions): Promise<{
|
|
67
|
+
recovered: boolean;
|
|
68
|
+
action: "none";
|
|
69
|
+
reason: string;
|
|
70
|
+
} | {
|
|
71
|
+
recovered: boolean;
|
|
72
|
+
action: "released_zombie";
|
|
73
|
+
reason: string;
|
|
74
|
+
} | {
|
|
75
|
+
recovered: boolean;
|
|
76
|
+
action: "released_stale";
|
|
77
|
+
reason: string;
|
|
78
|
+
} | {
|
|
79
|
+
recovered: boolean;
|
|
80
|
+
action: "escalated_stuck";
|
|
81
|
+
reason: string;
|
|
82
|
+
}>;
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawn Recovery Module (WU-1951)
|
|
3
|
+
*
|
|
4
|
+
* Auto-recovery heuristics for stuck spawns and zombie locks.
|
|
5
|
+
* Used by orchestrate:monitor for automatic spawn health management.
|
|
6
|
+
*
|
|
7
|
+
* Recovery Heuristics:
|
|
8
|
+
* 1. Zombie lock (PID not running) -> auto-release, mark spawn crashed
|
|
9
|
+
* 2. Stale lock (>2h) -> auto-release, mark spawn timeout
|
|
10
|
+
* 3. Active lock + no checkpoint in 1h -> mark stuck, escalate
|
|
11
|
+
*
|
|
12
|
+
* All recovery actions are logged to .beacon/recovery/ for audit.
|
|
13
|
+
*
|
|
14
|
+
* Library-First Note: This is project-specific spawn recovery code for
|
|
15
|
+
* PatientPath's custom spawn-registry.jsonl, lane-lock, and memory-store.
|
|
16
|
+
* No external library exists for this domain-specific agent lifecycle management.
|
|
17
|
+
*
|
|
18
|
+
* @see {@link tools/lib/__tests__/spawn-recovery.test.mjs} - Tests
|
|
19
|
+
* @see {@link tools/lib/spawn-monitor.mjs} - Monitoring logic
|
|
20
|
+
* @see {@link tools/lib/spawn-registry-store.mjs} - Spawn state
|
|
21
|
+
*/
|
|
22
|
+
import fs from 'node:fs/promises';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { SpawnRegistryStore } from './spawn-registry-store.js';
|
|
25
|
+
import { SpawnStatus } from './spawn-registry-schema.js';
|
|
26
|
+
import { isZombieLock, isLockStale, readLockMetadata, getLockFilePath, releaseLaneLock, } from './lane-lock.js';
|
|
27
|
+
import { toKebab } from './wu-constants.js';
|
|
28
|
+
// Optional import from @lumenflow/memory
|
|
29
|
+
let loadMemory = null;
|
|
30
|
+
try {
|
|
31
|
+
const mod = await import('@lumenflow/memory/store');
|
|
32
|
+
loadMemory = mod.loadMemory;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// @lumenflow/memory not available - memory features disabled
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Recovery action constants
|
|
39
|
+
*/
|
|
40
|
+
export const RecoveryAction = Object.freeze({
|
|
41
|
+
/** No recovery needed */
|
|
42
|
+
NONE: 'none',
|
|
43
|
+
/** Zombie lock released (PID not running) */
|
|
44
|
+
RELEASED_ZOMBIE: 'released_zombie',
|
|
45
|
+
/** Stale lock released (>2h old) */
|
|
46
|
+
RELEASED_STALE: 'released_stale',
|
|
47
|
+
/** Stuck spawn escalated (active but no checkpoint in 1h) */
|
|
48
|
+
ESCALATED_STUCK: 'escalated_stuck',
|
|
49
|
+
});
|
|
50
|
+
/**
|
|
51
|
+
* Recovery directory name
|
|
52
|
+
*/
|
|
53
|
+
export const RECOVERY_DIR_NAME = 'recovery';
|
|
54
|
+
/**
|
|
55
|
+
* Threshold for "no checkpoint" detection (1 hour in milliseconds)
|
|
56
|
+
*/
|
|
57
|
+
export const NO_CHECKPOINT_THRESHOLD_MS = 60 * 60 * 1000;
|
|
58
|
+
/**
|
|
59
|
+
* Log prefix for spawn-recovery messages
|
|
60
|
+
*/
|
|
61
|
+
const LOG_PREFIX = '[spawn-recovery]';
|
|
62
|
+
/**
|
|
63
|
+
* @typedef {Object} RecoveryResult
|
|
64
|
+
* @property {boolean} recovered - Whether a recovery action was taken
|
|
65
|
+
* @property {string} action - The recovery action taken (from RecoveryAction)
|
|
66
|
+
* @property {string} reason - Human-readable explanation of the result
|
|
67
|
+
*/
|
|
68
|
+
/**
|
|
69
|
+
* @typedef {Object} AuditLogEntry
|
|
70
|
+
* @property {string} timestamp - ISO timestamp of recovery action
|
|
71
|
+
* @property {string} spawnId - ID of the spawn being recovered
|
|
72
|
+
* @property {string} action - Recovery action taken
|
|
73
|
+
* @property {string} reason - Explanation of why action was taken
|
|
74
|
+
* @property {Object} context - Additional context
|
|
75
|
+
* @property {string} context.targetWuId - Target WU ID
|
|
76
|
+
* @property {string} context.lane - Lane name
|
|
77
|
+
* @property {Object|null} context.lockMetadata - Lock metadata if present
|
|
78
|
+
* @property {string|null} context.lastCheckpoint - Last checkpoint timestamp
|
|
79
|
+
*/
|
|
80
|
+
/**
|
|
81
|
+
* Converts lane name to lock file path (kebab-case)
|
|
82
|
+
*
|
|
83
|
+
* @param {string} lane - Lane name (e.g., "Operations: Tooling")
|
|
84
|
+
* @returns {string} Kebab-case lane name (e.g., "operations-tooling")
|
|
85
|
+
*/
|
|
86
|
+
function laneToKebab(lane) {
|
|
87
|
+
return toKebab(lane);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Gets the recovery directory path
|
|
91
|
+
*
|
|
92
|
+
* @param {string} baseDir - Base directory
|
|
93
|
+
* @returns {string} Path to .beacon/recovery/
|
|
94
|
+
*/
|
|
95
|
+
function getRecoveryDir(baseDir) {
|
|
96
|
+
return path.join(baseDir, '.beacon', RECOVERY_DIR_NAME);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Creates an audit log entry
|
|
100
|
+
*
|
|
101
|
+
* @param {string} baseDir - Base directory
|
|
102
|
+
* @param {AuditLogEntry} entry - Audit log entry
|
|
103
|
+
* @returns {Promise<void>}
|
|
104
|
+
*/
|
|
105
|
+
async function createAuditLog(baseDir, entry) {
|
|
106
|
+
const recoveryDir = getRecoveryDir(baseDir);
|
|
107
|
+
await fs.mkdir(recoveryDir, { recursive: true });
|
|
108
|
+
const timestamp = entry.timestamp.replace(/[:.]/g, '-');
|
|
109
|
+
const fileName = `${entry.spawnId}-${timestamp}.json`;
|
|
110
|
+
const filePath = path.join(recoveryDir, fileName);
|
|
111
|
+
await fs.writeFile(filePath, JSON.stringify(entry, null, 2), 'utf-8');
|
|
112
|
+
console.log(`${LOG_PREFIX} Audit log created: ${fileName}`);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Gets the most recent checkpoint for a WU from the memory store
|
|
116
|
+
*
|
|
117
|
+
* @param {string} baseDir - Base directory
|
|
118
|
+
* @param {string} wuId - WU ID to find checkpoints for
|
|
119
|
+
* @returns {Promise<{timestamp: string, content: string}|null>} Most recent checkpoint or null
|
|
120
|
+
*/
|
|
121
|
+
async function getLastCheckpoint(baseDir, wuId) {
|
|
122
|
+
// If memory module not available, return null
|
|
123
|
+
if (!loadMemory) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
const memoryDir = path.join(baseDir, '.beacon', 'state');
|
|
127
|
+
try {
|
|
128
|
+
const memory = await loadMemory(memoryDir, wuId);
|
|
129
|
+
if (!memory) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
const checkpoints = memory.checkpoints ?? [];
|
|
133
|
+
if (checkpoints.length === 0) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
// Sort by timestamp descending, get most recent
|
|
137
|
+
checkpoints.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
138
|
+
const latest = checkpoints[0];
|
|
139
|
+
return {
|
|
140
|
+
timestamp: latest.timestamp,
|
|
141
|
+
content: '',
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Memory store doesn't exist or is invalid
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Checks if a checkpoint is recent enough (within 1 hour)
|
|
151
|
+
*
|
|
152
|
+
* @param {string|null} checkpointTimestamp - ISO timestamp of last checkpoint
|
|
153
|
+
* @returns {boolean} True if checkpoint is recent (within 1 hour)
|
|
154
|
+
*/
|
|
155
|
+
function isCheckpointRecent(checkpointTimestamp) {
|
|
156
|
+
if (!checkpointTimestamp) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
const checkpointTime = new Date(checkpointTimestamp).getTime();
|
|
160
|
+
const now = Date.now();
|
|
161
|
+
return now - checkpointTime <= NO_CHECKPOINT_THRESHOLD_MS;
|
|
162
|
+
}
|
|
163
|
+
export async function recoverStuckSpawn(spawnId, options = {}) {
|
|
164
|
+
const { baseDir = process.cwd() } = options;
|
|
165
|
+
const registryDir = path.join(baseDir, '.beacon', 'state');
|
|
166
|
+
// Load spawn registry
|
|
167
|
+
const store = new SpawnRegistryStore(registryDir);
|
|
168
|
+
try {
|
|
169
|
+
await store.load();
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// Registry doesn't exist or is invalid
|
|
173
|
+
return {
|
|
174
|
+
recovered: false,
|
|
175
|
+
action: RecoveryAction.NONE,
|
|
176
|
+
reason: `Spawn ${spawnId} not found: registry unavailable`,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
// Find the spawn
|
|
180
|
+
const spawn = store.getById(spawnId);
|
|
181
|
+
if (!spawn) {
|
|
182
|
+
return {
|
|
183
|
+
recovered: false,
|
|
184
|
+
action: RecoveryAction.NONE,
|
|
185
|
+
reason: `Spawn ${spawnId} not found in registry`,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
// Check if already completed
|
|
189
|
+
if (spawn.status !== SpawnStatus.PENDING) {
|
|
190
|
+
return {
|
|
191
|
+
recovered: false,
|
|
192
|
+
action: RecoveryAction.NONE,
|
|
193
|
+
reason: `Spawn ${spawnId} already ${spawn.status}`,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
// Get lock for this spawn's lane
|
|
197
|
+
const laneKebab = laneToKebab(spawn.lane);
|
|
198
|
+
const lockPath = getLockFilePath(spawn.lane, baseDir);
|
|
199
|
+
const lockMetadata = readLockMetadata(lockPath);
|
|
200
|
+
// If no lock, nothing to recover
|
|
201
|
+
if (!lockMetadata) {
|
|
202
|
+
return {
|
|
203
|
+
recovered: false,
|
|
204
|
+
action: RecoveryAction.NONE,
|
|
205
|
+
reason: `No lock found for spawn ${spawnId} (lane: ${spawn.lane})`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
// Check if lock belongs to this WU
|
|
209
|
+
if (lockMetadata.wuId !== spawn.targetWuId) {
|
|
210
|
+
return {
|
|
211
|
+
recovered: false,
|
|
212
|
+
action: RecoveryAction.NONE,
|
|
213
|
+
reason: `Lock belongs to ${lockMetadata.wuId}, not spawn target ${spawn.targetWuId}`,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
// Get last checkpoint for context
|
|
217
|
+
const lastCheckpoint = await getLastCheckpoint(baseDir, spawn.targetWuId);
|
|
218
|
+
const lastCheckpointTs = lastCheckpoint?.timestamp ?? null;
|
|
219
|
+
// Build common audit context
|
|
220
|
+
const auditContext = {
|
|
221
|
+
targetWuId: spawn.targetWuId,
|
|
222
|
+
parentWuId: spawn.parentWuId,
|
|
223
|
+
lane: spawn.lane,
|
|
224
|
+
spawnedAt: spawn.spawnedAt,
|
|
225
|
+
lockMetadata,
|
|
226
|
+
lastCheckpoint: lastCheckpointTs,
|
|
227
|
+
};
|
|
228
|
+
// Heuristic 1: Zombie lock (PID not running)
|
|
229
|
+
if (isZombieLock(lockMetadata)) {
|
|
230
|
+
console.log(`${LOG_PREFIX} Detected zombie lock for ${spawnId} (PID ${lockMetadata.pid} not running)`);
|
|
231
|
+
// Release the lock
|
|
232
|
+
releaseLaneLock(spawn.lane, { baseDir, force: true });
|
|
233
|
+
// Mark spawn as crashed
|
|
234
|
+
await store.updateStatus(spawnId, SpawnStatus.CRASHED);
|
|
235
|
+
const reason = `Zombie lock detected: PID ${lockMetadata.pid} not running`;
|
|
236
|
+
// Create audit log
|
|
237
|
+
await createAuditLog(baseDir, {
|
|
238
|
+
timestamp: new Date().toISOString(),
|
|
239
|
+
spawnId,
|
|
240
|
+
action: RecoveryAction.RELEASED_ZOMBIE,
|
|
241
|
+
reason,
|
|
242
|
+
context: auditContext,
|
|
243
|
+
});
|
|
244
|
+
return {
|
|
245
|
+
recovered: true,
|
|
246
|
+
action: RecoveryAction.RELEASED_ZOMBIE,
|
|
247
|
+
reason,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
// Heuristic 2: Stale lock (>2h old)
|
|
251
|
+
if (isLockStale(lockMetadata)) {
|
|
252
|
+
console.log(`${LOG_PREFIX} Detected stale lock for ${spawnId} (acquired ${lockMetadata.timestamp})`);
|
|
253
|
+
// Release the lock
|
|
254
|
+
releaseLaneLock(spawn.lane, { baseDir, force: true });
|
|
255
|
+
// Mark spawn as timeout
|
|
256
|
+
await store.updateStatus(spawnId, SpawnStatus.TIMEOUT);
|
|
257
|
+
const reason = `Stale lock detected: acquired ${lockMetadata.timestamp} (>2h threshold)`;
|
|
258
|
+
// Create audit log
|
|
259
|
+
await createAuditLog(baseDir, {
|
|
260
|
+
timestamp: new Date().toISOString(),
|
|
261
|
+
spawnId,
|
|
262
|
+
action: RecoveryAction.RELEASED_STALE,
|
|
263
|
+
reason,
|
|
264
|
+
context: auditContext,
|
|
265
|
+
});
|
|
266
|
+
return {
|
|
267
|
+
recovered: true,
|
|
268
|
+
action: RecoveryAction.RELEASED_STALE,
|
|
269
|
+
reason,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
// Heuristic 3: Active lock + no recent checkpoint -> escalate
|
|
273
|
+
if (!isCheckpointRecent(lastCheckpointTs)) {
|
|
274
|
+
const reason = lastCheckpointTs
|
|
275
|
+
? `No checkpoint in last hour (last: ${lastCheckpointTs})`
|
|
276
|
+
: 'No checkpoints recorded for this spawn';
|
|
277
|
+
console.log(`${LOG_PREFIX} Escalating stuck spawn ${spawnId}: ${reason}`);
|
|
278
|
+
// Create audit log (escalation, not recovery)
|
|
279
|
+
await createAuditLog(baseDir, {
|
|
280
|
+
timestamp: new Date().toISOString(),
|
|
281
|
+
spawnId,
|
|
282
|
+
action: RecoveryAction.ESCALATED_STUCK,
|
|
283
|
+
reason,
|
|
284
|
+
context: auditContext,
|
|
285
|
+
});
|
|
286
|
+
return {
|
|
287
|
+
recovered: false, // No auto-recovery, just escalation
|
|
288
|
+
action: RecoveryAction.ESCALATED_STUCK,
|
|
289
|
+
reason: `Stuck spawn: ${reason}`,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
// Healthy spawn with recent checkpoint
|
|
293
|
+
return {
|
|
294
|
+
recovered: false,
|
|
295
|
+
action: RecoveryAction.NONE,
|
|
296
|
+
reason: `Spawn ${spawnId} healthy (recent checkpoint at ${lastCheckpointTs})`,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawn Registry Schema (WU-1944)
|
|
3
|
+
*
|
|
4
|
+
* Zod schemas for spawn event validation.
|
|
5
|
+
* Defines schema for tracking sub-agent spawns by orchestrators.
|
|
6
|
+
*
|
|
7
|
+
* @see {@link tools/lib/__tests__/spawn-registry-store.test.mjs} - Tests
|
|
8
|
+
* @see {@link tools/lib/spawn-registry-store.mjs} - Store implementation
|
|
9
|
+
*/
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
/**
|
|
12
|
+
* Spawn status values
|
|
13
|
+
*/
|
|
14
|
+
export declare const SpawnStatus: {
|
|
15
|
+
readonly PENDING: "pending";
|
|
16
|
+
readonly COMPLETED: "completed";
|
|
17
|
+
readonly TIMEOUT: "timeout";
|
|
18
|
+
readonly CRASHED: "crashed";
|
|
19
|
+
/** WU-1967: Spawn escalated to orchestrator (signal sent, prevents duplicates) */
|
|
20
|
+
readonly ESCALATED: "escalated";
|
|
21
|
+
};
|
|
22
|
+
/** Type for spawn status values */
|
|
23
|
+
export type SpawnStatusValue = (typeof SpawnStatus)[keyof typeof SpawnStatus];
|
|
24
|
+
/**
|
|
25
|
+
* Array of valid spawn statuses
|
|
26
|
+
*/
|
|
27
|
+
export declare const SPAWN_STATUSES: readonly ["pending", "completed", "timeout", "crashed", "escalated"];
|
|
28
|
+
/**
|
|
29
|
+
* Regex patterns for spawn validation
|
|
30
|
+
*/
|
|
31
|
+
export declare const SPAWN_PATTERNS: {
|
|
32
|
+
/** Spawn ID format: spawn-{4 hex chars} */
|
|
33
|
+
SPAWN_ID: RegExp;
|
|
34
|
+
/** WU ID format: WU-{digits} */
|
|
35
|
+
WU_ID: RegExp;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Spawn Event Schema
|
|
39
|
+
*
|
|
40
|
+
* Defines the structure for spawn registry events.
|
|
41
|
+
* Uses append-only JSONL storage with event replay for state reconstruction.
|
|
42
|
+
*/
|
|
43
|
+
export declare const SpawnEventSchema: z.ZodObject<{
|
|
44
|
+
id: z.ZodString;
|
|
45
|
+
parentWuId: z.ZodString;
|
|
46
|
+
targetWuId: z.ZodString;
|
|
47
|
+
lane: z.ZodString;
|
|
48
|
+
spawnedAt: z.ZodString;
|
|
49
|
+
status: z.ZodEnum<{
|
|
50
|
+
completed: "completed";
|
|
51
|
+
timeout: "timeout";
|
|
52
|
+
pending: "pending";
|
|
53
|
+
crashed: "crashed";
|
|
54
|
+
escalated: "escalated";
|
|
55
|
+
}>;
|
|
56
|
+
completedAt: z.ZodNullable<z.ZodString>;
|
|
57
|
+
}, z.core.$strip>;
|
|
58
|
+
/**
|
|
59
|
+
* TypeScript type inferred from schema
|
|
60
|
+
*/
|
|
61
|
+
export type SpawnEvent = z.infer<typeof SpawnEventSchema>;
|
|
62
|
+
/**
|
|
63
|
+
* Validates spawn event data against schema
|
|
64
|
+
*
|
|
65
|
+
* @param {unknown} data - Data to validate
|
|
66
|
+
* @returns Validation result
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* const result = validateSpawnEvent(eventData);
|
|
70
|
+
* if (!result.success) {
|
|
71
|
+
* result.error.issues.forEach(issue => {
|
|
72
|
+
* console.error(`${issue.path.join('.')}: ${issue.message}`);
|
|
73
|
+
* });
|
|
74
|
+
* }
|
|
75
|
+
*/
|
|
76
|
+
export declare function validateSpawnEvent(data: unknown): z.ZodSafeParseResult<{
|
|
77
|
+
id: string;
|
|
78
|
+
parentWuId: string;
|
|
79
|
+
targetWuId: string;
|
|
80
|
+
lane: string;
|
|
81
|
+
spawnedAt: string;
|
|
82
|
+
status: "completed" | "timeout" | "pending" | "crashed" | "escalated";
|
|
83
|
+
completedAt?: string;
|
|
84
|
+
}>;
|
|
85
|
+
/**
|
|
86
|
+
* Generates a unique spawn ID from parent WU, target WU, and timestamp
|
|
87
|
+
*
|
|
88
|
+
* Format: spawn-XXXX (4 hex characters from SHA-256 hash)
|
|
89
|
+
*
|
|
90
|
+
* @param {string} parentWuId - Parent WU ID
|
|
91
|
+
* @param {string} targetWuId - Target WU ID
|
|
92
|
+
* @returns {string} Spawn ID in format spawn-XXXX
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* const id = generateSpawnId('WU-1000', 'WU-1001');
|
|
96
|
+
* // Returns: 'spawn-a1b2'
|
|
97
|
+
*/
|
|
98
|
+
export declare function generateSpawnId(parentWuId: string, targetWuId: string): string;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawn Registry Schema (WU-1944)
|
|
3
|
+
*
|
|
4
|
+
* Zod schemas for spawn event validation.
|
|
5
|
+
* Defines schema for tracking sub-agent spawns by orchestrators.
|
|
6
|
+
*
|
|
7
|
+
* @see {@link tools/lib/__tests__/spawn-registry-store.test.mjs} - Tests
|
|
8
|
+
* @see {@link tools/lib/spawn-registry-store.mjs} - Store implementation
|
|
9
|
+
*/
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import crypto from 'node:crypto';
|
|
12
|
+
/**
|
|
13
|
+
* Spawn status values
|
|
14
|
+
*/
|
|
15
|
+
export const SpawnStatus = {
|
|
16
|
+
PENDING: 'pending',
|
|
17
|
+
COMPLETED: 'completed',
|
|
18
|
+
TIMEOUT: 'timeout',
|
|
19
|
+
CRASHED: 'crashed',
|
|
20
|
+
/** WU-1967: Spawn escalated to orchestrator (signal sent, prevents duplicates) */
|
|
21
|
+
ESCALATED: 'escalated',
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Array of valid spawn statuses
|
|
25
|
+
*/
|
|
26
|
+
export const SPAWN_STATUSES = ['pending', 'completed', 'timeout', 'crashed', 'escalated'];
|
|
27
|
+
/**
|
|
28
|
+
* Regex patterns for spawn validation
|
|
29
|
+
*/
|
|
30
|
+
export const SPAWN_PATTERNS = {
|
|
31
|
+
/** Spawn ID format: spawn-{4 hex chars} */
|
|
32
|
+
SPAWN_ID: /^spawn-[0-9a-f]{4}$/,
|
|
33
|
+
/** WU ID format: WU-{digits} */
|
|
34
|
+
WU_ID: /^WU-\d+$/,
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Error messages for schema validation
|
|
38
|
+
*/
|
|
39
|
+
const ERROR_MESSAGES = {
|
|
40
|
+
SPAWN_ID: 'Spawn ID must match pattern spawn-XXXX (e.g., spawn-a1b2)',
|
|
41
|
+
WU_ID: 'WU ID must match pattern WU-XXX (e.g., WU-1000)',
|
|
42
|
+
LANE_REQUIRED: 'Lane is required',
|
|
43
|
+
STATUS: `Status must be one of: ${SPAWN_STATUSES.join(', ')}`,
|
|
44
|
+
TIMESTAMP_REQUIRED: 'Timestamp is required',
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Spawn Event Schema
|
|
48
|
+
*
|
|
49
|
+
* Defines the structure for spawn registry events.
|
|
50
|
+
* Uses append-only JSONL storage with event replay for state reconstruction.
|
|
51
|
+
*/
|
|
52
|
+
export const SpawnEventSchema = z.object({
|
|
53
|
+
/** Unique spawn ID in format spawn-XXXX (4 hex chars from SHA hash) */
|
|
54
|
+
id: z.string().regex(SPAWN_PATTERNS.SPAWN_ID, { message: ERROR_MESSAGES.SPAWN_ID }),
|
|
55
|
+
/** Parent WU ID (the orchestrator that spawned this agent) */
|
|
56
|
+
parentWuId: z.string().regex(SPAWN_PATTERNS.WU_ID, { message: ERROR_MESSAGES.WU_ID }),
|
|
57
|
+
/** Target WU ID (the WU being executed by the spawned agent) */
|
|
58
|
+
targetWuId: z.string().regex(SPAWN_PATTERNS.WU_ID, { message: ERROR_MESSAGES.WU_ID }),
|
|
59
|
+
/** Lane for the spawned work */
|
|
60
|
+
lane: z.string().min(1, { message: ERROR_MESSAGES.LANE_REQUIRED }),
|
|
61
|
+
/** ISO 8601 timestamp when spawn was recorded */
|
|
62
|
+
spawnedAt: z.string().datetime({ message: ERROR_MESSAGES.TIMESTAMP_REQUIRED }),
|
|
63
|
+
/** Current status of the spawned agent */
|
|
64
|
+
status: z.enum(SPAWN_STATUSES, {
|
|
65
|
+
error: ERROR_MESSAGES.STATUS,
|
|
66
|
+
}),
|
|
67
|
+
/** ISO 8601 timestamp when spawn completed (null if pending) */
|
|
68
|
+
completedAt: z.string().datetime().nullable(),
|
|
69
|
+
});
|
|
70
|
+
/**
|
|
71
|
+
* Validates spawn event data against schema
|
|
72
|
+
*
|
|
73
|
+
* @param {unknown} data - Data to validate
|
|
74
|
+
* @returns Validation result
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* const result = validateSpawnEvent(eventData);
|
|
78
|
+
* if (!result.success) {
|
|
79
|
+
* result.error.issues.forEach(issue => {
|
|
80
|
+
* console.error(`${issue.path.join('.')}: ${issue.message}`);
|
|
81
|
+
* });
|
|
82
|
+
* }
|
|
83
|
+
*/
|
|
84
|
+
export function validateSpawnEvent(data) {
|
|
85
|
+
return SpawnEventSchema.safeParse(data);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Generates a unique spawn ID from parent WU, target WU, and timestamp
|
|
89
|
+
*
|
|
90
|
+
* Format: spawn-XXXX (4 hex characters from SHA-256 hash)
|
|
91
|
+
*
|
|
92
|
+
* @param {string} parentWuId - Parent WU ID
|
|
93
|
+
* @param {string} targetWuId - Target WU ID
|
|
94
|
+
* @returns {string} Spawn ID in format spawn-XXXX
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* const id = generateSpawnId('WU-1000', 'WU-1001');
|
|
98
|
+
* // Returns: 'spawn-a1b2'
|
|
99
|
+
*/
|
|
100
|
+
export function generateSpawnId(parentWuId, targetWuId) {
|
|
101
|
+
// Include timestamp and random bytes for uniqueness
|
|
102
|
+
const timestamp = Date.now().toString();
|
|
103
|
+
const randomBytes = crypto.randomBytes(4).toString('hex');
|
|
104
|
+
const input = `${parentWuId}:${targetWuId}:${timestamp}:${randomBytes}`;
|
|
105
|
+
const hash = crypto.createHash('sha256').update(input).digest('hex');
|
|
106
|
+
// Take first 4 hex chars
|
|
107
|
+
return `spawn-${hash.slice(0, 4)}`;
|
|
108
|
+
}
|