scene-capability-engine 3.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/CHANGELOG.md +2513 -0
- package/LICENSE +21 -0
- package/README.md +765 -0
- package/README.zh.md +630 -0
- package/bin/kiro-spec-engine.js +796 -0
- package/bin/kse.js +3 -0
- package/bin/sce.js +3 -0
- package/bin/sco.js +3 -0
- package/docs/331-poc-adaptation-roadmap.md +156 -0
- package/docs/331-poc-dual-track-integration-guide.md +120 -0
- package/docs/331-poc-weekly-delivery-checklist.md +52 -0
- package/docs/OFFLINE_INSTALL.md +96 -0
- package/docs/README.md +279 -0
- package/docs/adopt-migration-guide.md +599 -0
- package/docs/adoption-guide.md +616 -0
- package/docs/agent-hooks-analysis.md +815 -0
- package/docs/architecture.md +733 -0
- package/docs/articles/ai-driven-development-philosophy-and-practice-review.md +208 -0
- package/docs/articles/ai-driven-development-philosophy-and-practice.en.md +459 -0
- package/docs/articles/ai-driven-development-philosophy-and-practice.md +492 -0
- package/docs/autonomous-control-guide.md +851 -0
- package/docs/command-reference.md +1368 -0
- package/docs/community.md +115 -0
- package/docs/cross-tool-guide.md +555 -0
- package/docs/developer-guide.md +619 -0
- package/docs/document-governance.md +865 -0
- package/docs/environment-management-guide.md +526 -0
- package/docs/examples/add-export-command/design.md +194 -0
- package/docs/examples/add-export-command/requirements.md +110 -0
- package/docs/examples/add-export-command/tasks.md +88 -0
- package/docs/examples/add-rest-api/design.md +855 -0
- package/docs/examples/add-rest-api/requirements.md +323 -0
- package/docs/examples/add-rest-api/tasks.md +355 -0
- package/docs/examples/add-user-dashboard/design.md +192 -0
- package/docs/examples/add-user-dashboard/requirements.md +143 -0
- package/docs/examples/add-user-dashboard/tasks.md +91 -0
- package/docs/faq.md +697 -0
- package/docs/handoffs/evidence/ontology/moqui-template-baseline-2026-02-17-232922.json +156 -0
- package/docs/handoffs/evidence/ontology/moqui-template-baseline-2026-02-17-232922.md +24 -0
- package/docs/images/wechat-qr.png +0 -0
- package/docs/integration-modes.md +529 -0
- package/docs/integration-philosophy.md +313 -0
- package/docs/knowledge-management-guide.md +263 -0
- package/docs/manual-workflows-guide.md +418 -0
- package/docs/moqui-capability-matrix.md +73 -0
- package/docs/moqui-template-core-library-playbook.md +109 -0
- package/docs/multi-agent-coordination-guide.md +553 -0
- package/docs/multi-repo-management-guide.md +1344 -0
- package/docs/quick-start-with-ai-tools.md +375 -0
- package/docs/quick-start.md +146 -0
- package/docs/release-checklist.md +121 -0
- package/docs/releases/README.md +13 -0
- package/docs/releases/v1.46.2-validation.md +45 -0
- package/docs/releases/v1.46.2.md +50 -0
- package/docs/scene-runtime-guide.md +347 -0
- package/docs/spec-collaboration-guide.md +369 -0
- package/docs/spec-locking-guide.md +225 -0
- package/docs/spec-numbering-guide.md +348 -0
- package/docs/spec-workflow.md +519 -0
- package/docs/steering-strategy-guide.md +196 -0
- package/docs/team-collaboration-guide.md +465 -0
- package/docs/testing-strategy.md +272 -0
- package/docs/tools/claude-guide.md +654 -0
- package/docs/tools/cursor-guide.md +706 -0
- package/docs/tools/generic-guide.md +446 -0
- package/docs/tools/kiro-guide.md +308 -0
- package/docs/tools/vscode-guide.md +445 -0
- package/docs/tools/windsurf-guide.md +391 -0
- package/docs/troubleshooting.md +1135 -0
- package/docs/upgrade-guide.md +639 -0
- package/docs/value-observability-guide.md +127 -0
- package/docs/zh/README.md +341 -0
- package/docs/zh/quick-start.md +764 -0
- package/docs/zh/release-checklist.md +121 -0
- package/docs/zh/releases/README.md +13 -0
- package/docs/zh/releases/v1.46.2-validation.md +45 -0
- package/docs/zh/releases/v1.46.2.md +50 -0
- package/docs/zh/spec-numbering-guide.md +348 -0
- package/docs/zh/tools/claude-guide.md +349 -0
- package/docs/zh/tools/cursor-guide.md +281 -0
- package/docs/zh/tools/generic-guide.md +499 -0
- package/docs/zh/tools/kiro-guide.md +342 -0
- package/docs/zh/tools/vscode-guide.md +449 -0
- package/docs/zh/tools/windsurf-guide.md +378 -0
- package/docs/zh/value-observability-guide.md +127 -0
- package/docs//344/272/244/344/273/230/346/270/205/345/215/225.md +75 -0
- package/lib/adoption/adoption-logger.js +487 -0
- package/lib/adoption/adoption-strategy.js +538 -0
- package/lib/adoption/backup-manager.js +420 -0
- package/lib/adoption/conflict-resolver.js +410 -0
- package/lib/adoption/detection-engine.js +275 -0
- package/lib/adoption/diff-viewer.js +226 -0
- package/lib/adoption/error-formatter.js +509 -0
- package/lib/adoption/file-classifier.js +385 -0
- package/lib/adoption/progress-reporter.js +534 -0
- package/lib/adoption/smart-orchestrator.js +470 -0
- package/lib/adoption/strategy-selector.js +218 -0
- package/lib/adoption/summary-generator.js +493 -0
- package/lib/adoption/template-sync.js +605 -0
- package/lib/auto/autonomous-engine.js +485 -0
- package/lib/auto/checkpoint-manager.js +300 -0
- package/lib/auto/close-loop-runner.js +2476 -0
- package/lib/auto/config-schema.js +176 -0
- package/lib/auto/decision-engine.js +344 -0
- package/lib/auto/error-recovery-manager.js +580 -0
- package/lib/auto/goal-decomposer.js +278 -0
- package/lib/auto/progress-tracker.js +502 -0
- package/lib/auto/safety-manager.js +186 -0
- package/lib/auto/semantic-decomposer.js +137 -0
- package/lib/auto/state-manager.js +126 -0
- package/lib/auto/task-queue-manager.js +340 -0
- package/lib/backup/backup-system.js +372 -0
- package/lib/backup/selective-backup.js +207 -0
- package/lib/collab/agent-registry.js +240 -0
- package/lib/collab/collab-manager.js +285 -0
- package/lib/collab/contract-manager.js +320 -0
- package/lib/collab/coordinator.js +370 -0
- package/lib/collab/dependency-manager.js +280 -0
- package/lib/collab/index.js +20 -0
- package/lib/collab/integration-manager.js +202 -0
- package/lib/collab/merge-coordinator.js +252 -0
- package/lib/collab/metadata-manager.js +233 -0
- package/lib/collab/multi-agent-config.js +120 -0
- package/lib/collab/spec-lifecycle-manager.js +304 -0
- package/lib/collab/sync-barrier.js +88 -0
- package/lib/collab/visualizer.js +208 -0
- package/lib/commands/adopt.js +749 -0
- package/lib/commands/auto.js +19559 -0
- package/lib/commands/collab.js +275 -0
- package/lib/commands/context.js +99 -0
- package/lib/commands/docs.js +808 -0
- package/lib/commands/doctor.js +273 -0
- package/lib/commands/env.js +420 -0
- package/lib/commands/knowledge.js +309 -0
- package/lib/commands/lock.js +235 -0
- package/lib/commands/ops.js +409 -0
- package/lib/commands/orchestrate.js +446 -0
- package/lib/commands/prompt.js +105 -0
- package/lib/commands/repo.js +118 -0
- package/lib/commands/rollback.js +219 -0
- package/lib/commands/scene.js +15549 -0
- package/lib/commands/spec-bootstrap.js +147 -0
- package/lib/commands/spec-gate.js +157 -0
- package/lib/commands/spec-pipeline.js +205 -0
- package/lib/commands/status.js +321 -0
- package/lib/commands/task.js +199 -0
- package/lib/commands/templates.js +654 -0
- package/lib/commands/upgrade.js +231 -0
- package/lib/commands/value.js +569 -0
- package/lib/commands/watch.js +684 -0
- package/lib/commands/workflows.js +240 -0
- package/lib/commands/workspace-multi.js +325 -0
- package/lib/commands/workspace.js +189 -0
- package/lib/context/context-exporter.js +378 -0
- package/lib/context/prompt-generator.js +482 -0
- package/lib/data/moqui-capability-lexicon.json +45 -0
- package/lib/environment/backup-system.js +189 -0
- package/lib/environment/environment-manager.js +379 -0
- package/lib/environment/environment-registry.js +168 -0
- package/lib/gitignore/gitignore-backup.js +229 -0
- package/lib/gitignore/gitignore-detector.js +239 -0
- package/lib/gitignore/gitignore-integration.js +267 -0
- package/lib/gitignore/gitignore-transformer.js +193 -0
- package/lib/gitignore/layered-rules-template.js +42 -0
- package/lib/governance/archive-tool.js +284 -0
- package/lib/governance/cleanup-tool.js +237 -0
- package/lib/governance/config-manager.js +186 -0
- package/lib/governance/diagnostic-engine.js +271 -0
- package/lib/governance/doc-reference-checker.js +200 -0
- package/lib/governance/execution-logger.js +243 -0
- package/lib/governance/file-scanner.js +285 -0
- package/lib/governance/hooks-manager.js +333 -0
- package/lib/governance/reporter.js +337 -0
- package/lib/governance/validation-engine.js +181 -0
- package/lib/i18n.js +79 -0
- package/lib/knowledge/entry-manager.js +208 -0
- package/lib/knowledge/index-manager.js +261 -0
- package/lib/knowledge/knowledge-manager.js +273 -0
- package/lib/knowledge/template-manager.js +191 -0
- package/lib/lock/index.js +21 -0
- package/lib/lock/lock-file.js +192 -0
- package/lib/lock/lock-manager.js +321 -0
- package/lib/lock/machine-identifier.js +135 -0
- package/lib/lock/steering-file-lock.js +207 -0
- package/lib/lock/task-lock-manager.js +345 -0
- package/lib/operations/audit-logger.js +293 -0
- package/lib/operations/feedback-manager.js +1147 -0
- package/lib/operations/index.js +23 -0
- package/lib/operations/models/index.js +170 -0
- package/lib/operations/operations-manager.js +151 -0
- package/lib/operations/operations-validator.js +280 -0
- package/lib/operations/permission-manager.js +354 -0
- package/lib/operations/template-loader.js +143 -0
- package/lib/orchestrator/agent-spawner.js +629 -0
- package/lib/orchestrator/bootstrap-prompt-builder.js +236 -0
- package/lib/orchestrator/index.js +19 -0
- package/lib/orchestrator/orchestration-engine.js +1270 -0
- package/lib/orchestrator/orchestrator-config.js +173 -0
- package/lib/orchestrator/status-monitor.js +591 -0
- package/lib/python-checker.js +209 -0
- package/lib/repo/config-manager.js +580 -0
- package/lib/repo/errors/config-error.js +13 -0
- package/lib/repo/errors/git-error.js +15 -0
- package/lib/repo/errors/repo-error.js +14 -0
- package/lib/repo/git-operations.js +181 -0
- package/lib/repo/handlers/.gitkeep +1 -0
- package/lib/repo/handlers/exec-handler.js +155 -0
- package/lib/repo/handlers/health-handler.js +169 -0
- package/lib/repo/handlers/init-handler.js +197 -0
- package/lib/repo/handlers/status-handler.js +176 -0
- package/lib/repo/output-formatter.js +184 -0
- package/lib/repo/path-resolver.js +178 -0
- package/lib/repo/repo-manager.js +514 -0
- package/lib/scene-runtime/audit-emitter.js +59 -0
- package/lib/scene-runtime/binding-plugin-loader.js +351 -0
- package/lib/scene-runtime/binding-registry.js +349 -0
- package/lib/scene-runtime/eval-bridge.js +44 -0
- package/lib/scene-runtime/index.js +19 -0
- package/lib/scene-runtime/moqui-adapter.js +620 -0
- package/lib/scene-runtime/moqui-client.js +606 -0
- package/lib/scene-runtime/moqui-extractor.js +2029 -0
- package/lib/scene-runtime/plan-compiler.js +208 -0
- package/lib/scene-runtime/policy-gate.js +58 -0
- package/lib/scene-runtime/runtime-executor.js +358 -0
- package/lib/scene-runtime/scene-loader.js +96 -0
- package/lib/scene-runtime/scene-ontology.js +959 -0
- package/lib/scene-runtime/scene-template-linter.js +852 -0
- package/lib/scene-runtime/templates/scene-template-erp-query-v0.1.yaml +28 -0
- package/lib/scene-runtime/templates/scene-template-hybrid-shadow-v0.1.yaml +34 -0
- package/lib/spec/bootstrap/context-collector.js +48 -0
- package/lib/spec/bootstrap/draft-generator.js +158 -0
- package/lib/spec/bootstrap/questionnaire-engine.js +70 -0
- package/lib/spec/bootstrap/trace-emitter.js +59 -0
- package/lib/spec/multi-spec-orchestrate.js +93 -0
- package/lib/spec/pipeline/constants.js +6 -0
- package/lib/spec/pipeline/stage-adapters.js +118 -0
- package/lib/spec/pipeline/stage-runner.js +146 -0
- package/lib/spec/pipeline/state-store.js +119 -0
- package/lib/spec-gate/engine/gate-engine.js +165 -0
- package/lib/spec-gate/policy/default-policy.js +22 -0
- package/lib/spec-gate/policy/policy-loader.js +103 -0
- package/lib/spec-gate/result-emitter.js +81 -0
- package/lib/spec-gate/rules/default-rules.js +156 -0
- package/lib/spec-gate/rules/rule-registry.js +51 -0
- package/lib/steering/adoption-config.js +164 -0
- package/lib/steering/compliance-auto-fixer.js +204 -0
- package/lib/steering/compliance-cache.js +99 -0
- package/lib/steering/compliance-error-reporter.js +70 -0
- package/lib/steering/context-sync-manager.js +273 -0
- package/lib/steering/index.js +92 -0
- package/lib/steering/spec-steering.js +230 -0
- package/lib/steering/steering-compliance-checker.js +73 -0
- package/lib/steering/steering-loader.js +144 -0
- package/lib/steering/steering-manager.js +289 -0
- package/lib/task/index.js +12 -0
- package/lib/task/task-claimer.js +489 -0
- package/lib/task/task-status-store.js +418 -0
- package/lib/templates/cache-manager.js +440 -0
- package/lib/templates/content-generalizer.js +247 -0
- package/lib/templates/frontmatter-generator.js +128 -0
- package/lib/templates/git-handler.js +471 -0
- package/lib/templates/metadata-collector.js +328 -0
- package/lib/templates/path-utils.js +144 -0
- package/lib/templates/registry-parser.js +505 -0
- package/lib/templates/spec-reader.js +216 -0
- package/lib/templates/template-applicator.js +249 -0
- package/lib/templates/template-creator.js +256 -0
- package/lib/templates/template-error.js +143 -0
- package/lib/templates/template-exporter.js +502 -0
- package/lib/templates/template-manager.js +782 -0
- package/lib/templates/template-validator.js +361 -0
- package/lib/upgrade/migration-engine.js +382 -0
- package/lib/upgrade/migrations/.gitkeep +52 -0
- package/lib/upgrade/migrations/1.0.0-to-1.1.0.js +78 -0
- package/lib/utils/file-diff.js +177 -0
- package/lib/utils/fs-utils.js +274 -0
- package/lib/utils/tool-detector.js +383 -0
- package/lib/utils/validation.js +324 -0
- package/lib/value/gate-summary-emitter.js +99 -0
- package/lib/value/metric-contract-loader.js +210 -0
- package/lib/value/risk-evaluator.js +117 -0
- package/lib/value/weekly-snapshot-builder.js +61 -0
- package/lib/version/version-checker.js +156 -0
- package/lib/version/version-manager.js +327 -0
- package/lib/watch/action-executor.js +458 -0
- package/lib/watch/event-debouncer.js +323 -0
- package/lib/watch/execution-logger.js +550 -0
- package/lib/watch/file-watcher.js +499 -0
- package/lib/watch/presets.js +266 -0
- package/lib/watch/watch-manager.js +533 -0
- package/lib/workspace/multi/global-config.js +150 -0
- package/lib/workspace/multi/index.js +22 -0
- package/lib/workspace/multi/path-utils.js +173 -0
- package/lib/workspace/multi/workspace-context-resolver.js +244 -0
- package/lib/workspace/multi/workspace-registry.js +196 -0
- package/lib/workspace/multi/workspace-state-manager.js +537 -0
- package/lib/workspace/multi/workspace.js +90 -0
- package/lib/workspace/workspace-manager.js +370 -0
- package/lib/workspace/workspace-sync.js +356 -0
- package/locales/en.json +114 -0
- package/locales/zh.json +114 -0
- package/package.json +102 -0
- package/template/.kiro/README.md +247 -0
- package/template/.kiro/hooks/check-spec-on-create.kiro.hook +17 -0
- package/template/.kiro/hooks/run-tests-on-save.kiro.hook +13 -0
- package/template/.kiro/hooks/sync-tasks-on-edit.kiro.hook +16 -0
- package/template/.kiro/specs/SPEC_WORKFLOW_GUIDE.md +134 -0
- package/template/.kiro/steering/CORE_PRINCIPLES.md +133 -0
- package/template/.kiro/steering/CURRENT_CONTEXT.md +30 -0
- package/template/.kiro/steering/ENVIRONMENT.md +35 -0
- package/template/.kiro/steering/RULES_GUIDE.md +46 -0
- package/template/.kiro/templates/operations/default/change-impact.md +112 -0
- package/template/.kiro/templates/operations/default/deployment.md +91 -0
- package/template/.kiro/templates/operations/default/feedback-response.md +269 -0
- package/template/.kiro/templates/operations/default/migration-plan.md +172 -0
- package/template/.kiro/templates/operations/default/monitoring.md +135 -0
- package/template/.kiro/templates/operations/default/operations.md +135 -0
- package/template/.kiro/templates/operations/default/rollback.md +143 -0
- package/template/.kiro/templates/operations/default/tools.yaml +364 -0
- package/template/.kiro/templates/operations/default/troubleshooting.md +123 -0
- package/template/.kiro/tools/backup_manager.py +295 -0
- package/template/.kiro/tools/configuration_manager.py +218 -0
- package/template/.kiro/tools/document_evaluator.py +550 -0
- package/template/.kiro/tools/enhancement_logger.py +168 -0
- package/template/.kiro/tools/error_handler.py +335 -0
- package/template/.kiro/tools/improvement_identifier.py +444 -0
- package/template/.kiro/tools/modification_applicator.py +737 -0
- package/template/.kiro/tools/quality_gate_enforcer.py +207 -0
- package/template/.kiro/tools/quality_scorer.py +305 -0
- package/template/.kiro/tools/report_generator.py +154 -0
- package/template/.kiro/tools/ultrawork_enhancer.py +676 -0
- package/template/.kiro/tools/ultrawork_enhancer_refactored.py +0 -0
- package/template/.kiro/tools/ultrawork_enhancer_v2.py +463 -0
- package/template/.kiro/tools/ultrawork_enhancer_v3.py +606 -0
- package/template/.kiro/tools/workflow_quality_gate.py +100 -0
- package/template/README.md +111 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LockManager - Manages Spec locks for multi-user collaboration
|
|
3
|
+
* @module lib/lock/lock-manager
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const { LockFile } = require('./lock-file');
|
|
9
|
+
const { MachineIdentifier } = require('./machine-identifier');
|
|
10
|
+
|
|
11
|
+
const DEFAULT_TIMEOUT_HOURS = 24;
|
|
12
|
+
const MAX_RETRY_COUNT = 3;
|
|
13
|
+
const RETRY_DELAY_MS = 100;
|
|
14
|
+
|
|
15
|
+
class LockManager {
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} workspaceRoot - Root directory of the workspace
|
|
18
|
+
* @param {MachineIdentifier} [machineIdentifier] - Machine ID provider
|
|
19
|
+
*/
|
|
20
|
+
constructor(workspaceRoot, machineIdentifier = null) {
|
|
21
|
+
this.workspaceRoot = workspaceRoot;
|
|
22
|
+
this.specsDir = path.join(workspaceRoot, '.kiro', 'specs');
|
|
23
|
+
this.configDir = path.join(workspaceRoot, '.kiro', 'config');
|
|
24
|
+
this.lockFile = new LockFile(this.specsDir);
|
|
25
|
+
this.machineIdentifier = machineIdentifier || new MachineIdentifier(this.configDir);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Acquire a lock on a Spec
|
|
30
|
+
* @param {string} specName - Name of the Spec to lock
|
|
31
|
+
* @param {Object} options - Lock options
|
|
32
|
+
* @param {string} [options.reason] - Reason for acquiring the lock
|
|
33
|
+
* @param {number} [options.timeout] - Lock timeout in hours (default: 24)
|
|
34
|
+
* @returns {Promise<LockResult>}
|
|
35
|
+
*/
|
|
36
|
+
async acquireLock(specName, options = {}) {
|
|
37
|
+
const { reason = null, timeout = DEFAULT_TIMEOUT_HOURS } = options;
|
|
38
|
+
|
|
39
|
+
// Check if spec directory exists
|
|
40
|
+
const specDir = path.join(this.specsDir, specName);
|
|
41
|
+
try {
|
|
42
|
+
const fs = require('fs').promises;
|
|
43
|
+
await fs.access(specDir);
|
|
44
|
+
} catch {
|
|
45
|
+
return { success: false, error: 'Spec not found' };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check existing lock with retry for concurrent access
|
|
49
|
+
for (let attempt = 0; attempt < MAX_RETRY_COUNT; attempt++) {
|
|
50
|
+
const existingLock = await this.lockFile.read(specName);
|
|
51
|
+
|
|
52
|
+
if (existingLock) {
|
|
53
|
+
const machineId = await this.machineIdentifier.getMachineId();
|
|
54
|
+
if (existingLock.machineId === machineId.id) {
|
|
55
|
+
// Already locked by this machine - update the lock
|
|
56
|
+
const lock = await this._createLockMetadata(reason, timeout);
|
|
57
|
+
await this.lockFile.write(specName, lock);
|
|
58
|
+
return { success: true, lock };
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
success: false,
|
|
62
|
+
error: 'Spec is already locked',
|
|
63
|
+
existingLock
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Try to acquire lock
|
|
68
|
+
try {
|
|
69
|
+
const lock = await this._createLockMetadata(reason, timeout);
|
|
70
|
+
await this.lockFile.write(specName, lock);
|
|
71
|
+
|
|
72
|
+
// Verify we got the lock (handle race condition)
|
|
73
|
+
const verifyLock = await this.lockFile.read(specName);
|
|
74
|
+
const machineId = await this.machineIdentifier.getMachineId();
|
|
75
|
+
|
|
76
|
+
if (verifyLock && verifyLock.machineId === machineId.id) {
|
|
77
|
+
return { success: true, lock };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Someone else got the lock
|
|
81
|
+
return {
|
|
82
|
+
success: false,
|
|
83
|
+
error: 'Spec is already locked',
|
|
84
|
+
existingLock: verifyLock
|
|
85
|
+
};
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (attempt < MAX_RETRY_COUNT - 1) {
|
|
88
|
+
await this._delay(RETRY_DELAY_MS * Math.pow(2, attempt));
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { success: false, error: 'Failed to acquire lock after retries' };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Release a lock on a Spec
|
|
101
|
+
* @param {string} specName - Name of the Spec to unlock
|
|
102
|
+
* @param {Object} options - Unlock options
|
|
103
|
+
* @param {boolean} [options.force] - Force unlock regardless of ownership
|
|
104
|
+
* @returns {Promise<UnlockResult>}
|
|
105
|
+
*/
|
|
106
|
+
async releaseLock(specName, options = {}) {
|
|
107
|
+
const { force = false } = options;
|
|
108
|
+
|
|
109
|
+
const existingLock = await this.lockFile.read(specName);
|
|
110
|
+
|
|
111
|
+
if (!existingLock) {
|
|
112
|
+
return { success: true, message: 'No lock exists on this Spec' };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const machineId = await this.machineIdentifier.getMachineId();
|
|
116
|
+
|
|
117
|
+
if (existingLock.machineId !== machineId.id && !force) {
|
|
118
|
+
return {
|
|
119
|
+
success: false,
|
|
120
|
+
error: 'Lock owned by different machine',
|
|
121
|
+
existingLock
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const deleted = await this.lockFile.delete(specName);
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
success: true,
|
|
129
|
+
forced: force && existingLock.machineId !== machineId.id,
|
|
130
|
+
previousLock: existingLock
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get lock status for a specific Spec or all Specs
|
|
136
|
+
* @param {string} [specName] - Optional Spec name, if omitted returns all locks
|
|
137
|
+
* @returns {Promise<LockStatus|LockStatus[]>}
|
|
138
|
+
*/
|
|
139
|
+
async getLockStatus(specName) {
|
|
140
|
+
if (specName) {
|
|
141
|
+
return this._getSingleLockStatus(specName);
|
|
142
|
+
}
|
|
143
|
+
return this._getAllLockStatus();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get lock status for a single spec
|
|
148
|
+
* @param {string} specName
|
|
149
|
+
* @returns {Promise<LockStatus>}
|
|
150
|
+
* @private
|
|
151
|
+
*/
|
|
152
|
+
async _getSingleLockStatus(specName) {
|
|
153
|
+
const lock = await this.lockFile.read(specName);
|
|
154
|
+
const machineId = await this.machineIdentifier.getMachineId();
|
|
155
|
+
|
|
156
|
+
if (!lock) {
|
|
157
|
+
return { specName, locked: false };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
specName,
|
|
162
|
+
locked: true,
|
|
163
|
+
lock,
|
|
164
|
+
isStale: this._isStale(lock),
|
|
165
|
+
isOwnedByMe: lock.machineId === machineId.id,
|
|
166
|
+
duration: this._formatDuration(lock.timestamp)
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get lock status for all specs
|
|
172
|
+
* @returns {Promise<LockStatus[]>}
|
|
173
|
+
* @private
|
|
174
|
+
*/
|
|
175
|
+
async _getAllLockStatus() {
|
|
176
|
+
const lockedSpecs = await this.lockFile.listLockedSpecs();
|
|
177
|
+
const statuses = [];
|
|
178
|
+
|
|
179
|
+
for (const specName of lockedSpecs) {
|
|
180
|
+
const status = await this._getSingleLockStatus(specName);
|
|
181
|
+
statuses.push(status);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return statuses;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Clean up stale locks
|
|
189
|
+
* @returns {Promise<CleanupResult>}
|
|
190
|
+
*/
|
|
191
|
+
async cleanupStaleLocks() {
|
|
192
|
+
const result = { cleaned: 0, cleanedLocks: [], errors: [] };
|
|
193
|
+
const lockedSpecs = await this.lockFile.listLockedSpecs();
|
|
194
|
+
|
|
195
|
+
for (const specName of lockedSpecs) {
|
|
196
|
+
try {
|
|
197
|
+
const lock = await this.lockFile.read(specName);
|
|
198
|
+
if (lock && this._isStale(lock)) {
|
|
199
|
+
await this.lockFile.delete(specName);
|
|
200
|
+
result.cleaned++;
|
|
201
|
+
result.cleanedLocks.push({ specName, lock });
|
|
202
|
+
}
|
|
203
|
+
} catch (error) {
|
|
204
|
+
result.errors.push({ specName, error: error.message });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Check if a Spec is locked
|
|
213
|
+
* @param {string} specName - Name of the Spec
|
|
214
|
+
* @returns {Promise<boolean>}
|
|
215
|
+
*/
|
|
216
|
+
async isLocked(specName) {
|
|
217
|
+
return this.lockFile.exists(specName);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Check if current machine owns the lock
|
|
222
|
+
* @param {string} specName - Name of the Spec
|
|
223
|
+
* @returns {Promise<boolean>}
|
|
224
|
+
*/
|
|
225
|
+
async isLockedByMe(specName) {
|
|
226
|
+
const lock = await this.lockFile.read(specName);
|
|
227
|
+
if (!lock) return false;
|
|
228
|
+
|
|
229
|
+
const machineId = await this.machineIdentifier.getMachineId();
|
|
230
|
+
return lock.machineId === machineId.id;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Create lock metadata
|
|
236
|
+
* @param {string|null} reason
|
|
237
|
+
* @param {number} timeout
|
|
238
|
+
* @returns {Promise<LockMetadata>}
|
|
239
|
+
* @private
|
|
240
|
+
*/
|
|
241
|
+
async _createLockMetadata(reason, timeout) {
|
|
242
|
+
const machineId = await this.machineIdentifier.getMachineId();
|
|
243
|
+
const owner = this._getOwnerName();
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
owner,
|
|
247
|
+
machineId: machineId.id,
|
|
248
|
+
hostname: machineId.hostname,
|
|
249
|
+
timestamp: new Date().toISOString(),
|
|
250
|
+
reason: reason || null,
|
|
251
|
+
timeout,
|
|
252
|
+
version: '1.0.0'
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Get owner name from git config or environment
|
|
258
|
+
* @returns {string}
|
|
259
|
+
* @private
|
|
260
|
+
*/
|
|
261
|
+
_getOwnerName() {
|
|
262
|
+
try {
|
|
263
|
+
const { execSync } = require('child_process');
|
|
264
|
+
const gitUser = execSync('git config user.name', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
|
|
265
|
+
if (gitUser) return gitUser;
|
|
266
|
+
} catch {
|
|
267
|
+
// Git not available or not configured
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
return os.userInfo().username || process.env.USER || process.env.USERNAME || 'unknown';
|
|
272
|
+
} catch {
|
|
273
|
+
return process.env.USER || process.env.USERNAME || 'unknown';
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Check if a lock is stale
|
|
279
|
+
* @param {LockMetadata} lock
|
|
280
|
+
* @returns {boolean}
|
|
281
|
+
* @private
|
|
282
|
+
*/
|
|
283
|
+
_isStale(lock) {
|
|
284
|
+
const lockTime = new Date(lock.timestamp).getTime();
|
|
285
|
+
const now = Date.now();
|
|
286
|
+
const timeoutMs = lock.timeout * 60 * 60 * 1000;
|
|
287
|
+
return (now - lockTime) > timeoutMs;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Format duration since lock was acquired
|
|
292
|
+
* @param {string} timestamp
|
|
293
|
+
* @returns {string}
|
|
294
|
+
* @private
|
|
295
|
+
*/
|
|
296
|
+
_formatDuration(timestamp) {
|
|
297
|
+
const lockTime = new Date(timestamp).getTime();
|
|
298
|
+
const now = Date.now();
|
|
299
|
+
const diffMs = now - lockTime;
|
|
300
|
+
|
|
301
|
+
const hours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
302
|
+
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
|
303
|
+
|
|
304
|
+
if (hours > 0) {
|
|
305
|
+
return `${hours}h ${minutes}m`;
|
|
306
|
+
}
|
|
307
|
+
return `${minutes}m`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Delay helper for retry logic
|
|
312
|
+
* @param {number} ms
|
|
313
|
+
* @returns {Promise<void>}
|
|
314
|
+
* @private
|
|
315
|
+
*/
|
|
316
|
+
_delay(ms) {
|
|
317
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
module.exports = { LockManager, DEFAULT_TIMEOUT_HOURS };
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MachineIdentifier - Provides unique machine identification for lock ownership
|
|
3
|
+
* @module lib/lock/machine-identifier
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs').promises;
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const crypto = require('crypto');
|
|
10
|
+
|
|
11
|
+
class MachineIdentifier {
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} configDir - Directory for storing machine ID
|
|
14
|
+
*/
|
|
15
|
+
constructor(configDir) {
|
|
16
|
+
this.configDir = configDir;
|
|
17
|
+
this.configFile = path.join(configDir, 'machine-id.json');
|
|
18
|
+
this._cachedId = null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get the current machine's identifier
|
|
23
|
+
* @returns {Promise<MachineId>}
|
|
24
|
+
*/
|
|
25
|
+
async getMachineId() {
|
|
26
|
+
if (this._cachedId) {
|
|
27
|
+
return this._cachedId;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const data = await fs.readFile(this.configFile, 'utf8');
|
|
32
|
+
const machineId = JSON.parse(data);
|
|
33
|
+
if (this._isValidMachineId(machineId)) {
|
|
34
|
+
this._cachedId = machineId;
|
|
35
|
+
return machineId;
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
// File doesn't exist or is corrupted, generate new ID
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const newId = this.generateMachineId();
|
|
42
|
+
await this._persistMachineId(newId);
|
|
43
|
+
this._cachedId = newId;
|
|
44
|
+
return newId;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Generate a new machine ID
|
|
49
|
+
* @returns {MachineId}
|
|
50
|
+
*/
|
|
51
|
+
generateMachineId() {
|
|
52
|
+
const hostname = this._getHostname();
|
|
53
|
+
const uuid = crypto.randomUUID();
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
id: `${hostname}-${uuid}`,
|
|
57
|
+
hostname: hostname,
|
|
58
|
+
createdAt: new Date().toISOString()
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get human-readable machine info
|
|
64
|
+
* @returns {Promise<MachineInfo>}
|
|
65
|
+
*/
|
|
66
|
+
async getMachineInfo() {
|
|
67
|
+
const machineId = await this.getMachineId();
|
|
68
|
+
return {
|
|
69
|
+
id: machineId.id,
|
|
70
|
+
hostname: machineId.hostname,
|
|
71
|
+
createdAt: machineId.createdAt,
|
|
72
|
+
platform: os.platform(),
|
|
73
|
+
user: this._getUsername()
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get hostname safely
|
|
79
|
+
* @returns {string}
|
|
80
|
+
* @private
|
|
81
|
+
*/
|
|
82
|
+
_getHostname() {
|
|
83
|
+
try {
|
|
84
|
+
return os.hostname() || 'unknown-host';
|
|
85
|
+
} catch {
|
|
86
|
+
return 'unknown-host';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get username safely
|
|
92
|
+
* @returns {string}
|
|
93
|
+
* @private
|
|
94
|
+
*/
|
|
95
|
+
_getUsername() {
|
|
96
|
+
try {
|
|
97
|
+
return os.userInfo().username || process.env.USER || process.env.USERNAME || 'unknown-user';
|
|
98
|
+
} catch {
|
|
99
|
+
return process.env.USER || process.env.USERNAME || 'unknown-user';
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Validate machine ID structure
|
|
105
|
+
* @param {Object} machineId
|
|
106
|
+
* @returns {boolean}
|
|
107
|
+
* @private
|
|
108
|
+
*/
|
|
109
|
+
_isValidMachineId(machineId) {
|
|
110
|
+
return (
|
|
111
|
+
machineId &&
|
|
112
|
+
typeof machineId.id === 'string' &&
|
|
113
|
+
typeof machineId.hostname === 'string' &&
|
|
114
|
+
typeof machineId.createdAt === 'string' &&
|
|
115
|
+
machineId.id.length > 0
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Persist machine ID to config file
|
|
121
|
+
* @param {MachineId} machineId
|
|
122
|
+
* @returns {Promise<void>}
|
|
123
|
+
* @private
|
|
124
|
+
*/
|
|
125
|
+
async _persistMachineId(machineId) {
|
|
126
|
+
try {
|
|
127
|
+
await fs.mkdir(this.configDir, { recursive: true });
|
|
128
|
+
await fs.writeFile(this.configFile, JSON.stringify(machineId, null, 2), 'utf8');
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.warn(`Warning: Could not persist machine ID: ${error.message}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = { MachineIdentifier };
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SteeringFileLock - Protects concurrent writes to Steering files
|
|
3
|
+
*
|
|
4
|
+
* Uses file-level locks with exclusive create (`wx`) to serialize writes
|
|
5
|
+
* to `.kiro/steering/` files. When lock acquisition fails after retries,
|
|
6
|
+
* falls back to writing a pending file for later merge.
|
|
7
|
+
*
|
|
8
|
+
* Lock file path: `.kiro/steering/{filename}.lock`
|
|
9
|
+
* Pending file path: `.kiro/steering/{filename}.pending.{agentId}`
|
|
10
|
+
*
|
|
11
|
+
* Requirements: 5.1, 5.2, 5.3, 5.4
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs').promises;
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const crypto = require('crypto');
|
|
17
|
+
const fsUtils = require('../utils/fs-utils');
|
|
18
|
+
|
|
19
|
+
const MAX_RETRIES = 3;
|
|
20
|
+
const BASE_DELAY_MS = 100;
|
|
21
|
+
const STALE_LOCK_MS = 30000; // 30 seconds
|
|
22
|
+
|
|
23
|
+
class SteeringFileLock {
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} workspaceRoot - Absolute path to the project root
|
|
26
|
+
*/
|
|
27
|
+
constructor(workspaceRoot) {
|
|
28
|
+
this._workspaceRoot = workspaceRoot;
|
|
29
|
+
this._steeringDir = path.join(workspaceRoot, '.kiro', 'steering');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Lock file path for a given steering filename.
|
|
34
|
+
* @param {string} filename
|
|
35
|
+
* @returns {string}
|
|
36
|
+
* @private
|
|
37
|
+
*/
|
|
38
|
+
_lockPath(filename) {
|
|
39
|
+
return path.join(this._steeringDir, `${filename}.lock`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Acquire a write lock for a Steering file.
|
|
44
|
+
* Retries up to MAX_RETRIES times with exponential backoff (Req 5.2).
|
|
45
|
+
*
|
|
46
|
+
* @param {string} filename - Steering filename (e.g. "CURRENT_CONTEXT.md")
|
|
47
|
+
* @returns {Promise<{success: boolean, lockId?: string, error?: string}>}
|
|
48
|
+
*/
|
|
49
|
+
async acquireLock(filename) {
|
|
50
|
+
const lockFile = this._lockPath(filename);
|
|
51
|
+
const lockId = crypto.randomUUID();
|
|
52
|
+
const lockData = JSON.stringify({
|
|
53
|
+
lockId,
|
|
54
|
+
acquiredAt: new Date().toISOString(),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
58
|
+
if (attempt > 0) {
|
|
59
|
+
const delayMs = BASE_DELAY_MS * Math.pow(2, attempt - 1);
|
|
60
|
+
await this._sleep(delayMs);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
await fsUtils.ensureDirectory(this._steeringDir);
|
|
65
|
+
await fs.writeFile(lockFile, lockData, { flag: 'wx' });
|
|
66
|
+
return { success: true, lockId };
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (err.code === 'EEXIST') {
|
|
69
|
+
// Lock held — check for staleness
|
|
70
|
+
const claimed = await this._tryClaimStaleLock(lockFile, lockData);
|
|
71
|
+
if (claimed) {
|
|
72
|
+
return { success: true, lockId };
|
|
73
|
+
}
|
|
74
|
+
continue; // retry
|
|
75
|
+
}
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { success: false, error: 'Failed to acquire lock: retries exhausted' };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Release a previously acquired write lock.
|
|
85
|
+
* Only the holder (matching lockId) can release the lock.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} filename - Steering filename
|
|
88
|
+
* @param {string} lockId - The lockId returned by acquireLock
|
|
89
|
+
* @returns {Promise<{success: boolean, error?: string}>}
|
|
90
|
+
*/
|
|
91
|
+
async releaseLock(filename, lockId) {
|
|
92
|
+
const lockFile = this._lockPath(filename);
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const raw = await fs.readFile(lockFile, 'utf8');
|
|
96
|
+
const data = JSON.parse(raw);
|
|
97
|
+
|
|
98
|
+
if (data.lockId !== lockId) {
|
|
99
|
+
return { success: false, error: 'Lock owned by different caller' };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
await fs.unlink(lockFile);
|
|
103
|
+
return { success: true };
|
|
104
|
+
} catch (err) {
|
|
105
|
+
if (err.code === 'ENOENT') {
|
|
106
|
+
// Lock already gone — treat as success
|
|
107
|
+
return { success: true };
|
|
108
|
+
}
|
|
109
|
+
if (err instanceof SyntaxError) {
|
|
110
|
+
// Corrupted lock file — remove it
|
|
111
|
+
try { await fs.unlink(lockFile); } catch (_) { /* ignore */ }
|
|
112
|
+
return { success: true };
|
|
113
|
+
}
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Execute a callback while holding the Steering file lock.
|
|
120
|
+
* Acquires the lock, runs the callback, then releases the lock.
|
|
121
|
+
* If lock acquisition fails, throws an error.
|
|
122
|
+
*
|
|
123
|
+
* @param {string} filename - Steering filename
|
|
124
|
+
* @param {Function} callback - async () => result
|
|
125
|
+
* @returns {Promise<*>} The callback's return value
|
|
126
|
+
*/
|
|
127
|
+
async withLock(filename, callback) {
|
|
128
|
+
const { success, lockId, error } = await this.acquireLock(filename);
|
|
129
|
+
if (!success) {
|
|
130
|
+
throw new Error(error || 'Failed to acquire steering file lock');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
return await callback();
|
|
135
|
+
} finally {
|
|
136
|
+
await this.releaseLock(filename, lockId);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Write content to a pending file as a fallback when lock acquisition fails.
|
|
142
|
+
* Pending file path: `.kiro/steering/{filename}.pending.{agentId}`
|
|
143
|
+
* Uses atomic write for file integrity (Req 5.3, 5.4).
|
|
144
|
+
*
|
|
145
|
+
* @param {string} filename - Steering filename
|
|
146
|
+
* @param {string} content - Content to write
|
|
147
|
+
* @param {string} agentId - Agent identifier
|
|
148
|
+
* @returns {Promise<{pendingPath: string}>}
|
|
149
|
+
*/
|
|
150
|
+
async writePending(filename, content, agentId) {
|
|
151
|
+
await fsUtils.ensureDirectory(this._steeringDir);
|
|
152
|
+
const pendingPath = path.join(this._steeringDir, `${filename}.pending.${agentId}`);
|
|
153
|
+
await fsUtils.atomicWrite(pendingPath, content);
|
|
154
|
+
return { pendingPath };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── Private helpers ──────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* If the existing lock file is older than STALE_LOCK_MS, remove it and
|
|
161
|
+
* re-attempt acquisition.
|
|
162
|
+
*
|
|
163
|
+
* @param {string} lockFile
|
|
164
|
+
* @param {string} lockData
|
|
165
|
+
* @returns {Promise<boolean>}
|
|
166
|
+
* @private
|
|
167
|
+
*/
|
|
168
|
+
async _tryClaimStaleLock(lockFile, lockData) {
|
|
169
|
+
try {
|
|
170
|
+
const stat = await fs.stat(lockFile);
|
|
171
|
+
const ageMs = Date.now() - stat.mtimeMs;
|
|
172
|
+
if (ageMs > STALE_LOCK_MS) {
|
|
173
|
+
await fs.unlink(lockFile);
|
|
174
|
+
try {
|
|
175
|
+
await fs.writeFile(lockFile, lockData, { flag: 'wx' });
|
|
176
|
+
return true;
|
|
177
|
+
} catch (retryErr) {
|
|
178
|
+
if (retryErr.code === 'EEXIST') return false;
|
|
179
|
+
throw retryErr;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} catch (statErr) {
|
|
183
|
+
if (statErr.code === 'ENOENT') {
|
|
184
|
+
try {
|
|
185
|
+
await fs.writeFile(lockFile, lockData, { flag: 'wx' });
|
|
186
|
+
return true;
|
|
187
|
+
} catch (retryErr) {
|
|
188
|
+
if (retryErr.code === 'EEXIST') return false;
|
|
189
|
+
throw retryErr;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Promise-based sleep helper.
|
|
198
|
+
* @param {number} ms
|
|
199
|
+
* @returns {Promise<void>}
|
|
200
|
+
* @private
|
|
201
|
+
*/
|
|
202
|
+
_sleep(ms) {
|
|
203
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
module.exports = { SteeringFileLock };
|