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.
Files changed (336) hide show
  1. package/CHANGELOG.md +2513 -0
  2. package/LICENSE +21 -0
  3. package/README.md +765 -0
  4. package/README.zh.md +630 -0
  5. package/bin/kiro-spec-engine.js +796 -0
  6. package/bin/kse.js +3 -0
  7. package/bin/sce.js +3 -0
  8. package/bin/sco.js +3 -0
  9. package/docs/331-poc-adaptation-roadmap.md +156 -0
  10. package/docs/331-poc-dual-track-integration-guide.md +120 -0
  11. package/docs/331-poc-weekly-delivery-checklist.md +52 -0
  12. package/docs/OFFLINE_INSTALL.md +96 -0
  13. package/docs/README.md +279 -0
  14. package/docs/adopt-migration-guide.md +599 -0
  15. package/docs/adoption-guide.md +616 -0
  16. package/docs/agent-hooks-analysis.md +815 -0
  17. package/docs/architecture.md +733 -0
  18. package/docs/articles/ai-driven-development-philosophy-and-practice-review.md +208 -0
  19. package/docs/articles/ai-driven-development-philosophy-and-practice.en.md +459 -0
  20. package/docs/articles/ai-driven-development-philosophy-and-practice.md +492 -0
  21. package/docs/autonomous-control-guide.md +851 -0
  22. package/docs/command-reference.md +1368 -0
  23. package/docs/community.md +115 -0
  24. package/docs/cross-tool-guide.md +555 -0
  25. package/docs/developer-guide.md +619 -0
  26. package/docs/document-governance.md +865 -0
  27. package/docs/environment-management-guide.md +526 -0
  28. package/docs/examples/add-export-command/design.md +194 -0
  29. package/docs/examples/add-export-command/requirements.md +110 -0
  30. package/docs/examples/add-export-command/tasks.md +88 -0
  31. package/docs/examples/add-rest-api/design.md +855 -0
  32. package/docs/examples/add-rest-api/requirements.md +323 -0
  33. package/docs/examples/add-rest-api/tasks.md +355 -0
  34. package/docs/examples/add-user-dashboard/design.md +192 -0
  35. package/docs/examples/add-user-dashboard/requirements.md +143 -0
  36. package/docs/examples/add-user-dashboard/tasks.md +91 -0
  37. package/docs/faq.md +697 -0
  38. package/docs/handoffs/evidence/ontology/moqui-template-baseline-2026-02-17-232922.json +156 -0
  39. package/docs/handoffs/evidence/ontology/moqui-template-baseline-2026-02-17-232922.md +24 -0
  40. package/docs/images/wechat-qr.png +0 -0
  41. package/docs/integration-modes.md +529 -0
  42. package/docs/integration-philosophy.md +313 -0
  43. package/docs/knowledge-management-guide.md +263 -0
  44. package/docs/manual-workflows-guide.md +418 -0
  45. package/docs/moqui-capability-matrix.md +73 -0
  46. package/docs/moqui-template-core-library-playbook.md +109 -0
  47. package/docs/multi-agent-coordination-guide.md +553 -0
  48. package/docs/multi-repo-management-guide.md +1344 -0
  49. package/docs/quick-start-with-ai-tools.md +375 -0
  50. package/docs/quick-start.md +146 -0
  51. package/docs/release-checklist.md +121 -0
  52. package/docs/releases/README.md +13 -0
  53. package/docs/releases/v1.46.2-validation.md +45 -0
  54. package/docs/releases/v1.46.2.md +50 -0
  55. package/docs/scene-runtime-guide.md +347 -0
  56. package/docs/spec-collaboration-guide.md +369 -0
  57. package/docs/spec-locking-guide.md +225 -0
  58. package/docs/spec-numbering-guide.md +348 -0
  59. package/docs/spec-workflow.md +519 -0
  60. package/docs/steering-strategy-guide.md +196 -0
  61. package/docs/team-collaboration-guide.md +465 -0
  62. package/docs/testing-strategy.md +272 -0
  63. package/docs/tools/claude-guide.md +654 -0
  64. package/docs/tools/cursor-guide.md +706 -0
  65. package/docs/tools/generic-guide.md +446 -0
  66. package/docs/tools/kiro-guide.md +308 -0
  67. package/docs/tools/vscode-guide.md +445 -0
  68. package/docs/tools/windsurf-guide.md +391 -0
  69. package/docs/troubleshooting.md +1135 -0
  70. package/docs/upgrade-guide.md +639 -0
  71. package/docs/value-observability-guide.md +127 -0
  72. package/docs/zh/README.md +341 -0
  73. package/docs/zh/quick-start.md +764 -0
  74. package/docs/zh/release-checklist.md +121 -0
  75. package/docs/zh/releases/README.md +13 -0
  76. package/docs/zh/releases/v1.46.2-validation.md +45 -0
  77. package/docs/zh/releases/v1.46.2.md +50 -0
  78. package/docs/zh/spec-numbering-guide.md +348 -0
  79. package/docs/zh/tools/claude-guide.md +349 -0
  80. package/docs/zh/tools/cursor-guide.md +281 -0
  81. package/docs/zh/tools/generic-guide.md +499 -0
  82. package/docs/zh/tools/kiro-guide.md +342 -0
  83. package/docs/zh/tools/vscode-guide.md +449 -0
  84. package/docs/zh/tools/windsurf-guide.md +378 -0
  85. package/docs/zh/value-observability-guide.md +127 -0
  86. package/docs//344/272/244/344/273/230/346/270/205/345/215/225.md +75 -0
  87. package/lib/adoption/adoption-logger.js +487 -0
  88. package/lib/adoption/adoption-strategy.js +538 -0
  89. package/lib/adoption/backup-manager.js +420 -0
  90. package/lib/adoption/conflict-resolver.js +410 -0
  91. package/lib/adoption/detection-engine.js +275 -0
  92. package/lib/adoption/diff-viewer.js +226 -0
  93. package/lib/adoption/error-formatter.js +509 -0
  94. package/lib/adoption/file-classifier.js +385 -0
  95. package/lib/adoption/progress-reporter.js +534 -0
  96. package/lib/adoption/smart-orchestrator.js +470 -0
  97. package/lib/adoption/strategy-selector.js +218 -0
  98. package/lib/adoption/summary-generator.js +493 -0
  99. package/lib/adoption/template-sync.js +605 -0
  100. package/lib/auto/autonomous-engine.js +485 -0
  101. package/lib/auto/checkpoint-manager.js +300 -0
  102. package/lib/auto/close-loop-runner.js +2476 -0
  103. package/lib/auto/config-schema.js +176 -0
  104. package/lib/auto/decision-engine.js +344 -0
  105. package/lib/auto/error-recovery-manager.js +580 -0
  106. package/lib/auto/goal-decomposer.js +278 -0
  107. package/lib/auto/progress-tracker.js +502 -0
  108. package/lib/auto/safety-manager.js +186 -0
  109. package/lib/auto/semantic-decomposer.js +137 -0
  110. package/lib/auto/state-manager.js +126 -0
  111. package/lib/auto/task-queue-manager.js +340 -0
  112. package/lib/backup/backup-system.js +372 -0
  113. package/lib/backup/selective-backup.js +207 -0
  114. package/lib/collab/agent-registry.js +240 -0
  115. package/lib/collab/collab-manager.js +285 -0
  116. package/lib/collab/contract-manager.js +320 -0
  117. package/lib/collab/coordinator.js +370 -0
  118. package/lib/collab/dependency-manager.js +280 -0
  119. package/lib/collab/index.js +20 -0
  120. package/lib/collab/integration-manager.js +202 -0
  121. package/lib/collab/merge-coordinator.js +252 -0
  122. package/lib/collab/metadata-manager.js +233 -0
  123. package/lib/collab/multi-agent-config.js +120 -0
  124. package/lib/collab/spec-lifecycle-manager.js +304 -0
  125. package/lib/collab/sync-barrier.js +88 -0
  126. package/lib/collab/visualizer.js +208 -0
  127. package/lib/commands/adopt.js +749 -0
  128. package/lib/commands/auto.js +19559 -0
  129. package/lib/commands/collab.js +275 -0
  130. package/lib/commands/context.js +99 -0
  131. package/lib/commands/docs.js +808 -0
  132. package/lib/commands/doctor.js +273 -0
  133. package/lib/commands/env.js +420 -0
  134. package/lib/commands/knowledge.js +309 -0
  135. package/lib/commands/lock.js +235 -0
  136. package/lib/commands/ops.js +409 -0
  137. package/lib/commands/orchestrate.js +446 -0
  138. package/lib/commands/prompt.js +105 -0
  139. package/lib/commands/repo.js +118 -0
  140. package/lib/commands/rollback.js +219 -0
  141. package/lib/commands/scene.js +15549 -0
  142. package/lib/commands/spec-bootstrap.js +147 -0
  143. package/lib/commands/spec-gate.js +157 -0
  144. package/lib/commands/spec-pipeline.js +205 -0
  145. package/lib/commands/status.js +321 -0
  146. package/lib/commands/task.js +199 -0
  147. package/lib/commands/templates.js +654 -0
  148. package/lib/commands/upgrade.js +231 -0
  149. package/lib/commands/value.js +569 -0
  150. package/lib/commands/watch.js +684 -0
  151. package/lib/commands/workflows.js +240 -0
  152. package/lib/commands/workspace-multi.js +325 -0
  153. package/lib/commands/workspace.js +189 -0
  154. package/lib/context/context-exporter.js +378 -0
  155. package/lib/context/prompt-generator.js +482 -0
  156. package/lib/data/moqui-capability-lexicon.json +45 -0
  157. package/lib/environment/backup-system.js +189 -0
  158. package/lib/environment/environment-manager.js +379 -0
  159. package/lib/environment/environment-registry.js +168 -0
  160. package/lib/gitignore/gitignore-backup.js +229 -0
  161. package/lib/gitignore/gitignore-detector.js +239 -0
  162. package/lib/gitignore/gitignore-integration.js +267 -0
  163. package/lib/gitignore/gitignore-transformer.js +193 -0
  164. package/lib/gitignore/layered-rules-template.js +42 -0
  165. package/lib/governance/archive-tool.js +284 -0
  166. package/lib/governance/cleanup-tool.js +237 -0
  167. package/lib/governance/config-manager.js +186 -0
  168. package/lib/governance/diagnostic-engine.js +271 -0
  169. package/lib/governance/doc-reference-checker.js +200 -0
  170. package/lib/governance/execution-logger.js +243 -0
  171. package/lib/governance/file-scanner.js +285 -0
  172. package/lib/governance/hooks-manager.js +333 -0
  173. package/lib/governance/reporter.js +337 -0
  174. package/lib/governance/validation-engine.js +181 -0
  175. package/lib/i18n.js +79 -0
  176. package/lib/knowledge/entry-manager.js +208 -0
  177. package/lib/knowledge/index-manager.js +261 -0
  178. package/lib/knowledge/knowledge-manager.js +273 -0
  179. package/lib/knowledge/template-manager.js +191 -0
  180. package/lib/lock/index.js +21 -0
  181. package/lib/lock/lock-file.js +192 -0
  182. package/lib/lock/lock-manager.js +321 -0
  183. package/lib/lock/machine-identifier.js +135 -0
  184. package/lib/lock/steering-file-lock.js +207 -0
  185. package/lib/lock/task-lock-manager.js +345 -0
  186. package/lib/operations/audit-logger.js +293 -0
  187. package/lib/operations/feedback-manager.js +1147 -0
  188. package/lib/operations/index.js +23 -0
  189. package/lib/operations/models/index.js +170 -0
  190. package/lib/operations/operations-manager.js +151 -0
  191. package/lib/operations/operations-validator.js +280 -0
  192. package/lib/operations/permission-manager.js +354 -0
  193. package/lib/operations/template-loader.js +143 -0
  194. package/lib/orchestrator/agent-spawner.js +629 -0
  195. package/lib/orchestrator/bootstrap-prompt-builder.js +236 -0
  196. package/lib/orchestrator/index.js +19 -0
  197. package/lib/orchestrator/orchestration-engine.js +1270 -0
  198. package/lib/orchestrator/orchestrator-config.js +173 -0
  199. package/lib/orchestrator/status-monitor.js +591 -0
  200. package/lib/python-checker.js +209 -0
  201. package/lib/repo/config-manager.js +580 -0
  202. package/lib/repo/errors/config-error.js +13 -0
  203. package/lib/repo/errors/git-error.js +15 -0
  204. package/lib/repo/errors/repo-error.js +14 -0
  205. package/lib/repo/git-operations.js +181 -0
  206. package/lib/repo/handlers/.gitkeep +1 -0
  207. package/lib/repo/handlers/exec-handler.js +155 -0
  208. package/lib/repo/handlers/health-handler.js +169 -0
  209. package/lib/repo/handlers/init-handler.js +197 -0
  210. package/lib/repo/handlers/status-handler.js +176 -0
  211. package/lib/repo/output-formatter.js +184 -0
  212. package/lib/repo/path-resolver.js +178 -0
  213. package/lib/repo/repo-manager.js +514 -0
  214. package/lib/scene-runtime/audit-emitter.js +59 -0
  215. package/lib/scene-runtime/binding-plugin-loader.js +351 -0
  216. package/lib/scene-runtime/binding-registry.js +349 -0
  217. package/lib/scene-runtime/eval-bridge.js +44 -0
  218. package/lib/scene-runtime/index.js +19 -0
  219. package/lib/scene-runtime/moqui-adapter.js +620 -0
  220. package/lib/scene-runtime/moqui-client.js +606 -0
  221. package/lib/scene-runtime/moqui-extractor.js +2029 -0
  222. package/lib/scene-runtime/plan-compiler.js +208 -0
  223. package/lib/scene-runtime/policy-gate.js +58 -0
  224. package/lib/scene-runtime/runtime-executor.js +358 -0
  225. package/lib/scene-runtime/scene-loader.js +96 -0
  226. package/lib/scene-runtime/scene-ontology.js +959 -0
  227. package/lib/scene-runtime/scene-template-linter.js +852 -0
  228. package/lib/scene-runtime/templates/scene-template-erp-query-v0.1.yaml +28 -0
  229. package/lib/scene-runtime/templates/scene-template-hybrid-shadow-v0.1.yaml +34 -0
  230. package/lib/spec/bootstrap/context-collector.js +48 -0
  231. package/lib/spec/bootstrap/draft-generator.js +158 -0
  232. package/lib/spec/bootstrap/questionnaire-engine.js +70 -0
  233. package/lib/spec/bootstrap/trace-emitter.js +59 -0
  234. package/lib/spec/multi-spec-orchestrate.js +93 -0
  235. package/lib/spec/pipeline/constants.js +6 -0
  236. package/lib/spec/pipeline/stage-adapters.js +118 -0
  237. package/lib/spec/pipeline/stage-runner.js +146 -0
  238. package/lib/spec/pipeline/state-store.js +119 -0
  239. package/lib/spec-gate/engine/gate-engine.js +165 -0
  240. package/lib/spec-gate/policy/default-policy.js +22 -0
  241. package/lib/spec-gate/policy/policy-loader.js +103 -0
  242. package/lib/spec-gate/result-emitter.js +81 -0
  243. package/lib/spec-gate/rules/default-rules.js +156 -0
  244. package/lib/spec-gate/rules/rule-registry.js +51 -0
  245. package/lib/steering/adoption-config.js +164 -0
  246. package/lib/steering/compliance-auto-fixer.js +204 -0
  247. package/lib/steering/compliance-cache.js +99 -0
  248. package/lib/steering/compliance-error-reporter.js +70 -0
  249. package/lib/steering/context-sync-manager.js +273 -0
  250. package/lib/steering/index.js +92 -0
  251. package/lib/steering/spec-steering.js +230 -0
  252. package/lib/steering/steering-compliance-checker.js +73 -0
  253. package/lib/steering/steering-loader.js +144 -0
  254. package/lib/steering/steering-manager.js +289 -0
  255. package/lib/task/index.js +12 -0
  256. package/lib/task/task-claimer.js +489 -0
  257. package/lib/task/task-status-store.js +418 -0
  258. package/lib/templates/cache-manager.js +440 -0
  259. package/lib/templates/content-generalizer.js +247 -0
  260. package/lib/templates/frontmatter-generator.js +128 -0
  261. package/lib/templates/git-handler.js +471 -0
  262. package/lib/templates/metadata-collector.js +328 -0
  263. package/lib/templates/path-utils.js +144 -0
  264. package/lib/templates/registry-parser.js +505 -0
  265. package/lib/templates/spec-reader.js +216 -0
  266. package/lib/templates/template-applicator.js +249 -0
  267. package/lib/templates/template-creator.js +256 -0
  268. package/lib/templates/template-error.js +143 -0
  269. package/lib/templates/template-exporter.js +502 -0
  270. package/lib/templates/template-manager.js +782 -0
  271. package/lib/templates/template-validator.js +361 -0
  272. package/lib/upgrade/migration-engine.js +382 -0
  273. package/lib/upgrade/migrations/.gitkeep +52 -0
  274. package/lib/upgrade/migrations/1.0.0-to-1.1.0.js +78 -0
  275. package/lib/utils/file-diff.js +177 -0
  276. package/lib/utils/fs-utils.js +274 -0
  277. package/lib/utils/tool-detector.js +383 -0
  278. package/lib/utils/validation.js +324 -0
  279. package/lib/value/gate-summary-emitter.js +99 -0
  280. package/lib/value/metric-contract-loader.js +210 -0
  281. package/lib/value/risk-evaluator.js +117 -0
  282. package/lib/value/weekly-snapshot-builder.js +61 -0
  283. package/lib/version/version-checker.js +156 -0
  284. package/lib/version/version-manager.js +327 -0
  285. package/lib/watch/action-executor.js +458 -0
  286. package/lib/watch/event-debouncer.js +323 -0
  287. package/lib/watch/execution-logger.js +550 -0
  288. package/lib/watch/file-watcher.js +499 -0
  289. package/lib/watch/presets.js +266 -0
  290. package/lib/watch/watch-manager.js +533 -0
  291. package/lib/workspace/multi/global-config.js +150 -0
  292. package/lib/workspace/multi/index.js +22 -0
  293. package/lib/workspace/multi/path-utils.js +173 -0
  294. package/lib/workspace/multi/workspace-context-resolver.js +244 -0
  295. package/lib/workspace/multi/workspace-registry.js +196 -0
  296. package/lib/workspace/multi/workspace-state-manager.js +537 -0
  297. package/lib/workspace/multi/workspace.js +90 -0
  298. package/lib/workspace/workspace-manager.js +370 -0
  299. package/lib/workspace/workspace-sync.js +356 -0
  300. package/locales/en.json +114 -0
  301. package/locales/zh.json +114 -0
  302. package/package.json +102 -0
  303. package/template/.kiro/README.md +247 -0
  304. package/template/.kiro/hooks/check-spec-on-create.kiro.hook +17 -0
  305. package/template/.kiro/hooks/run-tests-on-save.kiro.hook +13 -0
  306. package/template/.kiro/hooks/sync-tasks-on-edit.kiro.hook +16 -0
  307. package/template/.kiro/specs/SPEC_WORKFLOW_GUIDE.md +134 -0
  308. package/template/.kiro/steering/CORE_PRINCIPLES.md +133 -0
  309. package/template/.kiro/steering/CURRENT_CONTEXT.md +30 -0
  310. package/template/.kiro/steering/ENVIRONMENT.md +35 -0
  311. package/template/.kiro/steering/RULES_GUIDE.md +46 -0
  312. package/template/.kiro/templates/operations/default/change-impact.md +112 -0
  313. package/template/.kiro/templates/operations/default/deployment.md +91 -0
  314. package/template/.kiro/templates/operations/default/feedback-response.md +269 -0
  315. package/template/.kiro/templates/operations/default/migration-plan.md +172 -0
  316. package/template/.kiro/templates/operations/default/monitoring.md +135 -0
  317. package/template/.kiro/templates/operations/default/operations.md +135 -0
  318. package/template/.kiro/templates/operations/default/rollback.md +143 -0
  319. package/template/.kiro/templates/operations/default/tools.yaml +364 -0
  320. package/template/.kiro/templates/operations/default/troubleshooting.md +123 -0
  321. package/template/.kiro/tools/backup_manager.py +295 -0
  322. package/template/.kiro/tools/configuration_manager.py +218 -0
  323. package/template/.kiro/tools/document_evaluator.py +550 -0
  324. package/template/.kiro/tools/enhancement_logger.py +168 -0
  325. package/template/.kiro/tools/error_handler.py +335 -0
  326. package/template/.kiro/tools/improvement_identifier.py +444 -0
  327. package/template/.kiro/tools/modification_applicator.py +737 -0
  328. package/template/.kiro/tools/quality_gate_enforcer.py +207 -0
  329. package/template/.kiro/tools/quality_scorer.py +305 -0
  330. package/template/.kiro/tools/report_generator.py +154 -0
  331. package/template/.kiro/tools/ultrawork_enhancer.py +676 -0
  332. package/template/.kiro/tools/ultrawork_enhancer_refactored.py +0 -0
  333. package/template/.kiro/tools/ultrawork_enhancer_v2.py +463 -0
  334. package/template/.kiro/tools/ultrawork_enhancer_v3.py +606 -0
  335. package/template/.kiro/tools/workflow_quality_gate.py +100 -0
  336. 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 };