@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,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WU-2241: Cleanup Lock Module
|
|
3
|
+
*
|
|
4
|
+
* Provides atomic locking mechanism for wu:done cleanup operations
|
|
5
|
+
* to prevent race conditions during concurrent worktree/branch cleanup.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - File-based lock with PID, timestamp, and worktree path
|
|
9
|
+
* - Stale lock detection and auto-cleanup (5-minute threshold)
|
|
10
|
+
* - Zombie lock detection (PID not running)
|
|
11
|
+
* - Idempotent re-acquisition for same WU
|
|
12
|
+
* - Guaranteed cleanup via withCleanupLock wrapper
|
|
13
|
+
*
|
|
14
|
+
* Lock ordering (WU-2241):
|
|
15
|
+
* Lane lock (phase-scoped) -> Merge lock -> Cleanup lock -> State store lock
|
|
16
|
+
*
|
|
17
|
+
* Pattern: Follows lane-lock.mjs and merge-lock.mjs internal patterns.
|
|
18
|
+
*
|
|
19
|
+
* @module cleanup-lock
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Default timeout for waiting to acquire lock (ms)
|
|
23
|
+
* After this time, acquisition fails if lock is held
|
|
24
|
+
*/
|
|
25
|
+
export declare const CLEANUP_LOCK_TIMEOUT_MS = 30000;
|
|
26
|
+
/**
|
|
27
|
+
* Time after which a cleanup lock is considered stale (ms)
|
|
28
|
+
* Should be greater than expected cleanup operation duration
|
|
29
|
+
* Cleanup is slower than merge, so longer timeout
|
|
30
|
+
*/
|
|
31
|
+
export declare const CLEANUP_LOCK_STALE_MS: number;
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {Object} CleanupLockInfo
|
|
34
|
+
* @property {string} wuId - WU ID that holds the lock
|
|
35
|
+
* @property {string} lockId - Unique lock identifier
|
|
36
|
+
* @property {string} createdAt - ISO timestamp when lock was created
|
|
37
|
+
* @property {number} pid - Process ID of lock holder
|
|
38
|
+
* @property {string} hostname - Hostname of lock holder
|
|
39
|
+
* @property {string} [worktreePath] - Path to worktree being cleaned up
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* @typedef {Object} CleanupAcquireResult
|
|
43
|
+
* @property {boolean} acquired - Whether lock was acquired
|
|
44
|
+
* @property {string} [lockId] - Lock ID if acquired
|
|
45
|
+
* @property {string} [heldBy] - WU ID holding the lock if not acquired
|
|
46
|
+
* @property {string} [heldSince] - ISO timestamp if not acquired
|
|
47
|
+
*/
|
|
48
|
+
/**
|
|
49
|
+
* Options for lock file operations
|
|
50
|
+
*/
|
|
51
|
+
interface BaseDirOptions {
|
|
52
|
+
/** Base directory (defaults to cwd) */
|
|
53
|
+
baseDir?: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Check if a lock is stale (older than CLEANUP_LOCK_STALE_MS)
|
|
57
|
+
*
|
|
58
|
+
* @param {CleanupLockInfo} lockInfo - Lock info to check
|
|
59
|
+
* @returns {boolean} True if lock is stale
|
|
60
|
+
*/
|
|
61
|
+
export declare function isCleanupLockStale(lockInfo: any): boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Check if a lock is a zombie (PID not running)
|
|
64
|
+
*
|
|
65
|
+
* @param {CleanupLockInfo} lockInfo - Lock info to check
|
|
66
|
+
* @returns {boolean} True if lock is a zombie (PID not running)
|
|
67
|
+
*/
|
|
68
|
+
export declare function isCleanupLockZombie(lockInfo: any): boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Check if cleanup lock is currently held
|
|
71
|
+
*
|
|
72
|
+
* @param {BaseDirOptions} [options]
|
|
73
|
+
* @returns {boolean} True if lock is held (and not stale)
|
|
74
|
+
*/
|
|
75
|
+
export declare function isCleanupLocked(options?: BaseDirOptions): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Get information about current cleanup lock
|
|
78
|
+
*
|
|
79
|
+
* @param {BaseDirOptions} [options]
|
|
80
|
+
* @returns {CleanupLockInfo|null} Lock info or null if unlocked
|
|
81
|
+
*/
|
|
82
|
+
export declare function getCleanupLockInfo(options?: BaseDirOptions): any;
|
|
83
|
+
/**
|
|
84
|
+
* Options for acquiring cleanup lock
|
|
85
|
+
*/
|
|
86
|
+
export interface AcquireCleanupLockOptions extends BaseDirOptions {
|
|
87
|
+
/** Max time to wait for lock (default: CLEANUP_LOCK_TIMEOUT_MS) */
|
|
88
|
+
waitMs?: number;
|
|
89
|
+
/** Path to worktree being cleaned up */
|
|
90
|
+
worktreePath?: string | null;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Attempt to acquire the cleanup lock
|
|
94
|
+
*
|
|
95
|
+
* Will wait up to waitMs for lock to become available.
|
|
96
|
+
* If the same WU already holds the lock, re-acquisition succeeds (idempotent).
|
|
97
|
+
* Stale and zombie locks are automatically cleaned up.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} wuId - WU ID requesting the lock
|
|
100
|
+
* @param {AcquireCleanupLockOptions} [options]
|
|
101
|
+
* @returns {Promise<CleanupAcquireResult>} Acquisition result
|
|
102
|
+
*/
|
|
103
|
+
export declare function acquireCleanupLock(wuId: any, options?: AcquireCleanupLockOptions): Promise<{
|
|
104
|
+
acquired: boolean;
|
|
105
|
+
lockId: any;
|
|
106
|
+
heldBy?: undefined;
|
|
107
|
+
heldSince?: undefined;
|
|
108
|
+
} | {
|
|
109
|
+
acquired: boolean;
|
|
110
|
+
heldBy: any;
|
|
111
|
+
heldSince: any;
|
|
112
|
+
lockId?: undefined;
|
|
113
|
+
}>;
|
|
114
|
+
/**
|
|
115
|
+
* Release the cleanup lock
|
|
116
|
+
*
|
|
117
|
+
* Only releases if the provided lockId matches the current lock.
|
|
118
|
+
* This prevents accidentally releasing another WU's lock.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} lockId - Lock ID to release
|
|
121
|
+
* @param {BaseDirOptions} [options]
|
|
122
|
+
* @returns {boolean} True if lock was released
|
|
123
|
+
*/
|
|
124
|
+
export declare function releaseCleanupLock(lockId: any, options?: BaseDirOptions): boolean;
|
|
125
|
+
/**
|
|
126
|
+
* Execute a function while holding the cleanup lock
|
|
127
|
+
*
|
|
128
|
+
* Guarantees the lock is released after function completes,
|
|
129
|
+
* even if the function throws an error.
|
|
130
|
+
*
|
|
131
|
+
* @template T
|
|
132
|
+
* @param {string} wuId - WU ID requesting the lock
|
|
133
|
+
* @param {function(): Promise<T>} fn - Async function to execute
|
|
134
|
+
* @param {AcquireCleanupLockOptions} [options]
|
|
135
|
+
* @returns {Promise<T>} Result of function execution
|
|
136
|
+
* @throws {Error} If lock cannot be acquired or function throws
|
|
137
|
+
*/
|
|
138
|
+
export declare function withCleanupLock<T>(wuId: string, fn: () => Promise<T>, options?: AcquireCleanupLockOptions): Promise<T>;
|
|
139
|
+
export {};
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WU-2241: Cleanup Lock Module
|
|
3
|
+
*
|
|
4
|
+
* Provides atomic locking mechanism for wu:done cleanup operations
|
|
5
|
+
* to prevent race conditions during concurrent worktree/branch cleanup.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - File-based lock with PID, timestamp, and worktree path
|
|
9
|
+
* - Stale lock detection and auto-cleanup (5-minute threshold)
|
|
10
|
+
* - Zombie lock detection (PID not running)
|
|
11
|
+
* - Idempotent re-acquisition for same WU
|
|
12
|
+
* - Guaranteed cleanup via withCleanupLock wrapper
|
|
13
|
+
*
|
|
14
|
+
* Lock ordering (WU-2241):
|
|
15
|
+
* Lane lock (phase-scoped) -> Merge lock -> Cleanup lock -> State store lock
|
|
16
|
+
*
|
|
17
|
+
* Pattern: Follows lane-lock.mjs and merge-lock.mjs internal patterns.
|
|
18
|
+
*
|
|
19
|
+
* @module cleanup-lock
|
|
20
|
+
*/
|
|
21
|
+
/* eslint-disable security/detect-non-literal-fs-filename -- Lock file paths are computed from trusted sources */
|
|
22
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync, openSync, closeSync, } from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import crypto from 'node:crypto';
|
|
25
|
+
import { LOG_PREFIX, EMOJI } from './wu-constants.js';
|
|
26
|
+
import { createError, ErrorCodes } from './error-handler.js';
|
|
27
|
+
/**
|
|
28
|
+
* Default timeout for waiting to acquire lock (ms)
|
|
29
|
+
* After this time, acquisition fails if lock is held
|
|
30
|
+
*/
|
|
31
|
+
export const CLEANUP_LOCK_TIMEOUT_MS = 30000; // 30 seconds
|
|
32
|
+
/**
|
|
33
|
+
* Time after which a cleanup lock is considered stale (ms)
|
|
34
|
+
* Should be greater than expected cleanup operation duration
|
|
35
|
+
* Cleanup is slower than merge, so longer timeout
|
|
36
|
+
*/
|
|
37
|
+
export const CLEANUP_LOCK_STALE_MS = 5 * 60 * 1000; // 5 minutes
|
|
38
|
+
/** Lock file name within .beacon directory */
|
|
39
|
+
const LOCK_FILE_NAME = 'cleanup.lock';
|
|
40
|
+
/**
|
|
41
|
+
* Polling interval for lock acquisition retries
|
|
42
|
+
*/
|
|
43
|
+
const LOCK_POLL_INTERVAL_MS = 500;
|
|
44
|
+
/**
|
|
45
|
+
* Get the path to the cleanup lock file
|
|
46
|
+
*
|
|
47
|
+
* @param {BaseDirOptions} [options]
|
|
48
|
+
* @returns {string} Path to lock file
|
|
49
|
+
*/
|
|
50
|
+
function getLockPath(options = {}) {
|
|
51
|
+
const baseDir = options.baseDir || process.cwd();
|
|
52
|
+
return path.join(baseDir, '.beacon', LOCK_FILE_NAME);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Read lock file contents
|
|
56
|
+
*
|
|
57
|
+
* @param {BaseDirOptions} [options]
|
|
58
|
+
* @returns {CleanupLockInfo|null} Lock info or null if no lock
|
|
59
|
+
*/
|
|
60
|
+
function readLockFile(options = {}) {
|
|
61
|
+
const lockPath = getLockPath(options);
|
|
62
|
+
if (!existsSync(lockPath)) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const content = readFileSync(lockPath, 'utf8');
|
|
67
|
+
return JSON.parse(content);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Corrupted lock file - treat as no lock
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Delete lock file
|
|
76
|
+
*
|
|
77
|
+
* @param {BaseDirOptions} [options]
|
|
78
|
+
*/
|
|
79
|
+
function deleteLockFile(options = {}) {
|
|
80
|
+
const lockPath = getLockPath(options);
|
|
81
|
+
if (existsSync(lockPath)) {
|
|
82
|
+
unlinkSync(lockPath);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Check if a lock is stale (older than CLEANUP_LOCK_STALE_MS)
|
|
87
|
+
*
|
|
88
|
+
* @param {CleanupLockInfo} lockInfo - Lock info to check
|
|
89
|
+
* @returns {boolean} True if lock is stale
|
|
90
|
+
*/
|
|
91
|
+
export function isCleanupLockStale(lockInfo) {
|
|
92
|
+
if (!lockInfo || !lockInfo.createdAt) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
const lockTime = new Date(lockInfo.createdAt).getTime();
|
|
96
|
+
const age = Date.now() - lockTime;
|
|
97
|
+
return age > CLEANUP_LOCK_STALE_MS;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Check if a lock is a zombie (PID not running)
|
|
101
|
+
*
|
|
102
|
+
* @param {CleanupLockInfo} lockInfo - Lock info to check
|
|
103
|
+
* @returns {boolean} True if lock is a zombie (PID not running)
|
|
104
|
+
*/
|
|
105
|
+
export function isCleanupLockZombie(lockInfo) {
|
|
106
|
+
if (!lockInfo || typeof lockInfo.pid !== 'number') {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
process.kill(lockInfo.pid, 0); // Signal 0 = check existence
|
|
111
|
+
return false; // Process exists
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return true; // Process doesn't exist
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Generate a unique lock ID
|
|
119
|
+
*
|
|
120
|
+
* @returns {string} Unique lock ID
|
|
121
|
+
*/
|
|
122
|
+
function generateLockId() {
|
|
123
|
+
return crypto.randomUUID();
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Sleep for specified milliseconds
|
|
127
|
+
*
|
|
128
|
+
* @param {number} ms - Milliseconds to sleep
|
|
129
|
+
* @returns {Promise<void>}
|
|
130
|
+
*/
|
|
131
|
+
function sleep(ms) {
|
|
132
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Check if cleanup lock is currently held
|
|
136
|
+
*
|
|
137
|
+
* @param {BaseDirOptions} [options]
|
|
138
|
+
* @returns {boolean} True if lock is held (and not stale)
|
|
139
|
+
*/
|
|
140
|
+
export function isCleanupLocked(options = {}) {
|
|
141
|
+
const lockInfo = readLockFile(options);
|
|
142
|
+
if (!lockInfo) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
// Stale or zombie locks are treated as unlocked
|
|
146
|
+
return !isCleanupLockStale(lockInfo) && !isCleanupLockZombie(lockInfo);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Get information about current cleanup lock
|
|
150
|
+
*
|
|
151
|
+
* @param {BaseDirOptions} [options]
|
|
152
|
+
* @returns {CleanupLockInfo|null} Lock info or null if unlocked
|
|
153
|
+
*/
|
|
154
|
+
export function getCleanupLockInfo(options = {}) {
|
|
155
|
+
const lockInfo = readLockFile(options);
|
|
156
|
+
if (!lockInfo || isCleanupLockStale(lockInfo) || isCleanupLockZombie(lockInfo)) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
return lockInfo;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Attempt atomic lock file creation
|
|
163
|
+
* Returns 'acquired' | 'race' | throws on other errors
|
|
164
|
+
*
|
|
165
|
+
* @param {CleanupLockInfo} lockInfo - Lock info to write
|
|
166
|
+
* @param {Object} options - Options with baseDir
|
|
167
|
+
* @returns {'acquired' | 'race'} Result of acquisition attempt
|
|
168
|
+
*/
|
|
169
|
+
function tryAtomicLockCreate(lockInfo, options) {
|
|
170
|
+
const lockPath = getLockPath(options);
|
|
171
|
+
const beaconDir = path.dirname(lockPath);
|
|
172
|
+
if (!existsSync(beaconDir)) {
|
|
173
|
+
mkdirSync(beaconDir, { recursive: true });
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
// Use 'wx' flag for atomic creation
|
|
177
|
+
const fd = openSync(lockPath, 'wx');
|
|
178
|
+
writeFileSync(lockPath, JSON.stringify(lockInfo, null, 2));
|
|
179
|
+
closeSync(fd);
|
|
180
|
+
return 'acquired';
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
if (err.code === 'EEXIST') {
|
|
184
|
+
return 'race';
|
|
185
|
+
}
|
|
186
|
+
throw err;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Handle stale or zombie lock cleanup
|
|
191
|
+
* Returns 'retry' if lock was cleaned, null if lock is valid
|
|
192
|
+
*
|
|
193
|
+
* @param {CleanupLockInfo} existingLock - Existing lock info
|
|
194
|
+
* @param {Object} options - Options with baseDir
|
|
195
|
+
* @returns {'retry' | null} Whether to retry acquisition
|
|
196
|
+
*/
|
|
197
|
+
function handleStaleLock(existingLock, options) {
|
|
198
|
+
if (isCleanupLockStale(existingLock)) {
|
|
199
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Cleaning up stale cleanup lock from ${existingLock.wuId}`);
|
|
200
|
+
deleteLockFile(options);
|
|
201
|
+
return 'retry';
|
|
202
|
+
}
|
|
203
|
+
if (isCleanupLockZombie(existingLock)) {
|
|
204
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Cleaning up zombie cleanup lock from ${existingLock.wuId} (PID ${existingLock.pid} not running)`);
|
|
205
|
+
deleteLockFile(options);
|
|
206
|
+
return 'retry';
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Attempt to acquire the cleanup lock
|
|
212
|
+
*
|
|
213
|
+
* Will wait up to waitMs for lock to become available.
|
|
214
|
+
* If the same WU already holds the lock, re-acquisition succeeds (idempotent).
|
|
215
|
+
* Stale and zombie locks are automatically cleaned up.
|
|
216
|
+
*
|
|
217
|
+
* @param {string} wuId - WU ID requesting the lock
|
|
218
|
+
* @param {AcquireCleanupLockOptions} [options]
|
|
219
|
+
* @returns {Promise<CleanupAcquireResult>} Acquisition result
|
|
220
|
+
*/
|
|
221
|
+
export async function acquireCleanupLock(wuId, options = {}) {
|
|
222
|
+
const { baseDir, waitMs = CLEANUP_LOCK_TIMEOUT_MS, worktreePath = null } = options;
|
|
223
|
+
const startTime = Date.now();
|
|
224
|
+
while (true) {
|
|
225
|
+
const existingLock = readLockFile({ baseDir });
|
|
226
|
+
// No lock exists - acquire it
|
|
227
|
+
if (!existingLock) {
|
|
228
|
+
const lockId = generateLockId();
|
|
229
|
+
const lockInfo = {
|
|
230
|
+
wuId,
|
|
231
|
+
lockId,
|
|
232
|
+
createdAt: new Date().toISOString(),
|
|
233
|
+
pid: process.pid,
|
|
234
|
+
hostname: process.env.HOSTNAME || 'unknown',
|
|
235
|
+
worktreePath,
|
|
236
|
+
};
|
|
237
|
+
const result = tryAtomicLockCreate(lockInfo, { baseDir });
|
|
238
|
+
if (result === 'race') {
|
|
239
|
+
continue; // Race condition - retry
|
|
240
|
+
}
|
|
241
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Acquired cleanup lock for ${wuId}`);
|
|
242
|
+
return { acquired: true, lockId };
|
|
243
|
+
}
|
|
244
|
+
// Check for stale/zombie locks
|
|
245
|
+
if (handleStaleLock(existingLock, { baseDir }) === 'retry') {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
// Same WU already holds lock - return existing lock ID (idempotent)
|
|
249
|
+
if (existingLock.wuId === wuId) {
|
|
250
|
+
return { acquired: true, lockId: existingLock.lockId };
|
|
251
|
+
}
|
|
252
|
+
// Different WU holds lock - check if we should wait
|
|
253
|
+
const elapsed = Date.now() - startTime;
|
|
254
|
+
if (elapsed >= waitMs) {
|
|
255
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Cleanup lock held by ${existingLock.wuId} since ${existingLock.createdAt}`);
|
|
256
|
+
return {
|
|
257
|
+
acquired: false,
|
|
258
|
+
heldBy: existingLock.wuId,
|
|
259
|
+
heldSince: existingLock.createdAt,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
// Wait and retry
|
|
263
|
+
await sleep(LOCK_POLL_INTERVAL_MS);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Release the cleanup lock
|
|
268
|
+
*
|
|
269
|
+
* Only releases if the provided lockId matches the current lock.
|
|
270
|
+
* This prevents accidentally releasing another WU's lock.
|
|
271
|
+
*
|
|
272
|
+
* @param {string} lockId - Lock ID to release
|
|
273
|
+
* @param {BaseDirOptions} [options]
|
|
274
|
+
* @returns {boolean} True if lock was released
|
|
275
|
+
*/
|
|
276
|
+
export function releaseCleanupLock(lockId, options = {}) {
|
|
277
|
+
const existingLock = readLockFile(options);
|
|
278
|
+
if (!existingLock) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
if (existingLock.lockId !== lockId) {
|
|
282
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Cannot release cleanup lock - lockId mismatch`);
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
deleteLockFile(options);
|
|
286
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Released cleanup lock for ${existingLock.wuId}`);
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Execute a function while holding the cleanup lock
|
|
291
|
+
*
|
|
292
|
+
* Guarantees the lock is released after function completes,
|
|
293
|
+
* even if the function throws an error.
|
|
294
|
+
*
|
|
295
|
+
* @template T
|
|
296
|
+
* @param {string} wuId - WU ID requesting the lock
|
|
297
|
+
* @param {function(): Promise<T>} fn - Async function to execute
|
|
298
|
+
* @param {AcquireCleanupLockOptions} [options]
|
|
299
|
+
* @returns {Promise<T>} Result of function execution
|
|
300
|
+
* @throws {Error} If lock cannot be acquired or function throws
|
|
301
|
+
*/
|
|
302
|
+
export async function withCleanupLock(wuId, fn, options = {}) {
|
|
303
|
+
const result = await acquireCleanupLock(wuId, options);
|
|
304
|
+
if (!result.acquired) {
|
|
305
|
+
throw createError(ErrorCodes.LOCK_ERROR, `Cannot acquire cleanup lock - held by ${result.heldBy} since ${result.heldSince}`, { wuId, heldBy: result.heldBy, heldSince: result.heldSince });
|
|
306
|
+
}
|
|
307
|
+
try {
|
|
308
|
+
return await fn();
|
|
309
|
+
}
|
|
310
|
+
finally {
|
|
311
|
+
releaseCleanupLock(result.lockId, options);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Unified Code Path Validator (WU-1825)
|
|
4
|
+
*
|
|
5
|
+
* Consolidates three separate code path validators into one module:
|
|
6
|
+
* - validateCodePathsExist (wu-done-validators.mjs) - file existence for wu:done
|
|
7
|
+
* - validateLaneCodePaths (lane-validator.mjs) - lane pattern matching
|
|
8
|
+
* - validateWUCodePaths (wu-validator.mjs) - code quality (TODOs, mocks)
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* import { validate } from './code-path-validator.js';
|
|
12
|
+
*
|
|
13
|
+
* // Mode: 'exist' - check file existence (wu:done workflow)
|
|
14
|
+
* const result = await validate(paths, { mode: 'exist', worktreePath, targetBranch });
|
|
15
|
+
*
|
|
16
|
+
* // Mode: 'lane' - check lane pattern matching (wu:claim workflow)
|
|
17
|
+
* const result = validate(paths, { mode: 'lane', lane: 'Operations: Tooling' });
|
|
18
|
+
*
|
|
19
|
+
* // Mode: 'quality' - check code quality (TODOs, mocks)
|
|
20
|
+
* const result = validate(paths, { mode: 'quality', worktreePath, allowTodos: false });
|
|
21
|
+
*
|
|
22
|
+
* Part of INIT-023: Workflow Integrity initiative.
|
|
23
|
+
*/
|
|
24
|
+
interface ExistValidationResult {
|
|
25
|
+
valid: boolean;
|
|
26
|
+
errors: string[];
|
|
27
|
+
missing: string[];
|
|
28
|
+
}
|
|
29
|
+
interface LaneValidationResult {
|
|
30
|
+
hasWarnings: boolean;
|
|
31
|
+
warnings: string[];
|
|
32
|
+
violations: string[];
|
|
33
|
+
skipped: boolean;
|
|
34
|
+
}
|
|
35
|
+
interface QualityValidationResult {
|
|
36
|
+
valid: boolean;
|
|
37
|
+
errors: string[];
|
|
38
|
+
warnings: string[];
|
|
39
|
+
}
|
|
40
|
+
interface QualityOptions {
|
|
41
|
+
allowTodos?: boolean;
|
|
42
|
+
worktreePath?: string | null;
|
|
43
|
+
}
|
|
44
|
+
interface ValidateOptions {
|
|
45
|
+
mode?: string;
|
|
46
|
+
worktreePath?: string;
|
|
47
|
+
targetBranch?: string;
|
|
48
|
+
lane?: string;
|
|
49
|
+
allowTodos?: boolean;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Validation modes for the unified validator
|
|
53
|
+
* @enum {string}
|
|
54
|
+
*/
|
|
55
|
+
export declare const VALIDATION_MODES: Readonly<{
|
|
56
|
+
/** Check file existence - used by wu:done */
|
|
57
|
+
EXIST: "exist";
|
|
58
|
+
/** Check lane pattern matching - used by wu:claim */
|
|
59
|
+
LANE: "lane";
|
|
60
|
+
/** Check code quality (TODOs, mocks) - used by wu:done */
|
|
61
|
+
QUALITY: "quality";
|
|
62
|
+
}>;
|
|
63
|
+
/**
|
|
64
|
+
* Check if a file path is a test file
|
|
65
|
+
* @param {string} filePath - Path to check
|
|
66
|
+
* @returns {boolean} True if file is a test file
|
|
67
|
+
*/
|
|
68
|
+
declare function isTestFile(filePath: string): boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Check if a file path is a markdown file
|
|
71
|
+
* @param {string} filePath - Path to check
|
|
72
|
+
* @returns {boolean} True if file is a markdown file
|
|
73
|
+
*/
|
|
74
|
+
declare function isMarkdownFile(filePath: string): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Scan a file for TODO/FIXME/HACK/XXX comments
|
|
77
|
+
* @param {string} filePath - Path to file to scan
|
|
78
|
+
* @returns {{found: boolean, matches: Array<{line: number, text: string, pattern: string}>}}
|
|
79
|
+
*/
|
|
80
|
+
declare function scanFileForTODOs(filePath: string): {
|
|
81
|
+
found: boolean;
|
|
82
|
+
matches: Array<{
|
|
83
|
+
line: number;
|
|
84
|
+
text: string;
|
|
85
|
+
pattern: string | null;
|
|
86
|
+
}>;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Scan a file for Mock/Stub/Fake class/function names
|
|
90
|
+
* @param {string} filePath - Path to file to scan
|
|
91
|
+
* @returns {{found: boolean, matches: Array<{line: number, text: string, type: string}>}}
|
|
92
|
+
*/
|
|
93
|
+
declare function scanFileForMocks(filePath: string): {
|
|
94
|
+
found: boolean;
|
|
95
|
+
matches: Array<{
|
|
96
|
+
line: number;
|
|
97
|
+
text: string;
|
|
98
|
+
type: string;
|
|
99
|
+
}>;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Unified code path validation function
|
|
103
|
+
*
|
|
104
|
+
* @param {string[]} codePaths - Array of file paths to validate
|
|
105
|
+
* @param {object} options - Validation options
|
|
106
|
+
* @param {string} options.mode - Validation mode: 'exist', 'lane', or 'quality'
|
|
107
|
+
* @param {string} [options.worktreePath] - Worktree path (for 'exist' and 'quality' modes)
|
|
108
|
+
* @param {string} [options.targetBranch] - Target branch (for 'exist' mode, branch-only)
|
|
109
|
+
* @param {string} [options.lane] - Lane name (for 'lane' mode)
|
|
110
|
+
* @param {boolean} [options.allowTodos] - Allow TODO comments (for 'quality' mode)
|
|
111
|
+
* @returns {Promise<ExistValidationResult|LaneValidationResult|QualityValidationResult>}
|
|
112
|
+
*/
|
|
113
|
+
export declare function validate(codePaths: string[], options?: ValidateOptions): Promise<ExistValidationResult | LaneValidationResult | QualityValidationResult>;
|
|
114
|
+
interface WUDoc {
|
|
115
|
+
code_paths?: string[];
|
|
116
|
+
}
|
|
117
|
+
interface ExistOptions {
|
|
118
|
+
targetBranch?: string;
|
|
119
|
+
worktreePath?: string | null;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* @deprecated Use validate(paths, { mode: 'exist' }) instead
|
|
123
|
+
* Backward-compatible wrapper for validateCodePathsExist
|
|
124
|
+
*/
|
|
125
|
+
export declare function validateCodePathsExist(doc: WUDoc, _id: string, options?: ExistOptions): Promise<ExistValidationResult>;
|
|
126
|
+
/**
|
|
127
|
+
* @deprecated Use validate(paths, { mode: 'lane', lane }) instead
|
|
128
|
+
* Backward-compatible wrapper for validateLaneCodePaths
|
|
129
|
+
* NOTE: This must remain SYNCHRONOUS for backward compatibility
|
|
130
|
+
*/
|
|
131
|
+
export declare function validateLaneCodePaths(doc: WUDoc, lane: string): LaneValidationResult;
|
|
132
|
+
/**
|
|
133
|
+
* @deprecated Use validate(paths, { mode: 'quality' }) instead
|
|
134
|
+
* Backward-compatible wrapper for validateWUCodePaths
|
|
135
|
+
* NOTE: This must remain SYNCHRONOUS for backward compatibility
|
|
136
|
+
*/
|
|
137
|
+
export declare function validateWUCodePaths(codePaths: string[], options?: QualityOptions): QualityValidationResult;
|
|
138
|
+
/**
|
|
139
|
+
* Log lane validation warnings to console.
|
|
140
|
+
* Helper function to format and display warnings consistently.
|
|
141
|
+
*
|
|
142
|
+
* @param {LaneValidationResult} result - Result from validateLaneCodePaths
|
|
143
|
+
* @param {string} logPrefix - Log prefix (e.g., "[wu-claim]")
|
|
144
|
+
*/
|
|
145
|
+
export declare function logLaneValidationWarnings(result: LaneValidationResult, logPrefix?: string): void;
|
|
146
|
+
export { isTestFile, isMarkdownFile, scanFileForTODOs, scanFileForMocks };
|