claude-flow-novice 2.15.3 → 2.15.5
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/.claude/cfn-extras/skills/advanced-features/cfn-agent-swap/recommend-swap.sh +59 -59
- package/.claude/cfn-extras/skills/analytics/cfn-improvement-recommender/recommend-improvements.sh +91 -91
- package/.claude/cfn-extras/skills/analytics/cfn-pattern-extraction/extract-patterns.sh +79 -79
- package/.claude/cfn-extras/skills/analytics/cfn-retrospective-report/generate-report.sh +100 -100
- package/.claude/cfn-extras/skills/analytics/cfn-telemetry/start-telemetry.sh +110 -110
- package/.claude/cfn-extras/skills/deprecated/cfn-ace-system/add-bullet.sh +145 -145
- package/.claude/cfn-extras/skills/deprecated/cfn-ace-system/log-merge.sh +67 -67
- package/.claude/cfn-extras/skills/deprecated/cfn-ace-system/monitor-injection-performance.sh +137 -137
- package/.claude/cfn-extras/skills/deprecated/cfn-ace-system/optimize-injection-pipeline.sh +168 -168
- package/.claude/cfn-extras/skills/deprecated/cfn-ace-system/query-reflections.sh +35 -35
- package/.claude/cfn-extras/skills/deprecated/cfn-ace-system/store-reflection.sh +45 -45
- package/.claude/cfn-extras/skills/deprecated/cfn-ace-system/track-ab-test.sh +41 -41
- package/.claude/cfn-extras/skills/deprecated/cfn-ace-system/update-reflection.sh +41 -41
- package/.claude/cfn-extras/skills/deprecated/cfn-cli-setup/validate-cli-environment.sh +191 -191
- package/.claude/cfn-extras/skills/marketing/cfn-marketing-ad-campaigns/operations/create-campaign.sh +231 -231
- package/.claude/cfn-extras/skills/marketing/cfn-marketing-ad-campaigns/operations/get-campaign-performance.sh +190 -190
- package/.claude/cfn-extras/skills/marketing/cfn-marketing-ad-campaigns/operations/pause-campaign.sh +142 -142
- package/.claude/cfn-extras/skills/marketing/cfn-marketing-ad-campaigns/operations/set-budget.sh +181 -181
- package/.claude/cfn-extras/skills/marketing/cfn-marketing-ad-campaigns/operations/update-bid-strategy.sh +133 -133
- package/.claude/cfn-extras/skills/marketing/cfn-marketing-chatbot-conversations/operations/get-conversation-history.sh +121 -121
- package/.claude/cfn-extras/skills/marketing/cfn-marketing-chatbot-conversations/operations/qualify-lead.sh +156 -156
- package/.claude/cfn-extras/skills/marketing/cfn-marketing-chatbot-conversations/operations/schedule-demo.sh +181 -181
- package/.claude/cfn-extras/skills/marketing/cfn-marketing-chatbot-conversations/operations/send-message.sh +137 -137
- package/.claude/cfn-extras/skills/marketing/cfn-marketing-chatbot-conversations/operations/transfer-to-human.sh +179 -179
- package/.claude/cfn-extras/skills/marketing/cfn-marketing-sms-campaigns/operations/create-campaign.sh +183 -183
- package/.claude/cfn-extras/skills/marketing/cfn-marketing-sms-campaigns/operations/get-delivery-status.sh +139 -139
- package/.claude/cfn-extras/skills/marketing/cfn-marketing-sms-campaigns/operations/opt-out.sh +150 -150
- package/.claude/cfn-extras/skills/marketing/cfn-marketing-sms-campaigns/operations/schedule-campaign.sh +187 -187
- package/.claude/cfn-extras/skills/marketing/cfn-marketing-sms-campaigns/operations/send-sms.sh +181 -181
- package/.claude/cfn-extras/skills/ui-portal/cfn-web-portal/test-web-portal-skill.sh +50 -50
- package/.claude/cfn-extras/skills/ui-portal/cfn-web-portal/validate-deployment.sh +84 -84
- package/.claude/cfn-extras/skills/utility/cfn-environment-sanitization/sanitize-environment.sh +243 -243
- package/.claude/commands/cfn-loop-cli.md +29 -6
- package/.claude/commands/switch-api.md +31 -10
- package/.claude/hooks/cfn-lint-sql-injection.sh +61 -0
- package/.claude/hooks/cfn-post-edit-cfn-retrospective.sh +33 -2
- package/.claude/hooks/cfn-pre-edit-security-warning.sh +40 -0
- package/.claude/skills/cfn-agent-spawning/spawn-agent.sh +22 -24
- package/.claude/skills/cfn-docker-agent-spawning/SKILL.md +28 -4
- package/.claude/skills/cfn-docker-agent-spawning/spawn-agent.sh +3 -1
- package/.claude/skills/cfn-docker-loop-orchestration/orchestrate.sh +224 -20
- package/.claude/skills/cfn-loop-orchestration/helpers/gate-check.sh +550 -46
- package/.claude/skills/cfn-loop-orchestration/helpers/parse-test-results.sh +277 -0
- package/.claude/skills/cfn-loop-orchestration/orchestrate.sh +238 -29
- package/.claude/skills/cfn-loop-orchestration/security_utils.sh +24 -0
- package/.claude/skills/cfn-loop-orchestration/test-iteration-context-injection.sh +366 -0
- package/.claude/skills/cfn-redis-coordination/CENTRALIZED_REDIS_WRAPPER.md +319 -0
- package/.claude/skills/cfn-redis-coordination/agent-log.sh +4 -0
- package/.claude/skills/cfn-redis-coordination/agent-log.sh.bak +124 -0
- package/.claude/skills/cfn-redis-coordination/agent-recovery.sh +2 -2
- package/.claude/skills/cfn-redis-coordination/collect-confidence-scores.sh +30 -0
- package/.claude/skills/cfn-redis-coordination/get-context.sh +33 -0
- package/.claude/skills/cfn-redis-coordination/get-success-criteria.sh +54 -0
- package/.claude/skills/cfn-redis-coordination/invoke-waiting-mode.sh +6 -2
- package/.claude/skills/cfn-redis-coordination/redis-cli-wrapper.sh +24 -3
- package/.claude/skills/cfn-redis-coordination/redis-functions.sh +34 -0
- package/.claude/skills/cfn-redis-coordination/report-completion.sh +24 -31
- package/.claude/skills/cfn-redis-coordination/store-context.sh +4 -0
- package/.claude/skills/cfn-redis-coordination/store-success-criteria.sh +85 -0
- package/.claude/skills/cfn-redis-coordination/update-all-scripts.sh +67 -0
- package/.claude/skills/cfn-sqlite-memory/ttl-cleanup.sh +17 -25
- package/.claude/skills/cfn-transparency-middleware/test-e2e.sh +15 -0
- package/.claude/skills/cfn-transparency-middleware/tests/input-validation.sh +15 -0
- package/README.md +116 -475
- package/claude-assets/agents/cfn-dev-team/README.md +103 -0
- package/claude-assets/agents/cfn-dev-team/architecture/goal-planner.md +1 -1
- package/claude-assets/agents/cfn-dev-team/coordinators/cfn-frontend-coordinator.md +77 -15
- package/claude-assets/agents/cfn-dev-team/coordinators/cfn-v3-coordinator.md +355 -6
- package/claude-assets/agents/cfn-dev-team/coordinators/consensus-builder.md +82 -1
- package/claude-assets/agents/cfn-dev-team/coordinators/handoff-coordinator.md +82 -1
- package/claude-assets/agents/cfn-dev-team/coordinators/multi-sprint-coordinator.md +77 -15
- package/claude-assets/agents/cfn-dev-team/dev-ops/docker-specialist.md +99 -12
- package/claude-assets/agents/cfn-dev-team/dev-ops/github-commit-agent.md +1 -1
- package/claude-assets/agents/cfn-dev-team/dev-ops/kubernetes-specialist.md +97 -0
- package/claude-assets/agents/cfn-dev-team/dev-ops/monitoring-specialist.md +20 -1
- package/claude-assets/agents/cfn-dev-team/developers/api-gateway-specialist.md +97 -0
- package/claude-assets/agents/cfn-dev-team/developers/backend-developer.md +110 -13
- package/claude-assets/agents/cfn-dev-team/developers/data/data-engineer.md +106 -15
- package/claude-assets/agents/cfn-dev-team/developers/database/database-architect.md +115 -11
- package/claude-assets/agents/cfn-dev-team/developers/frontend/mobile-dev.md +94 -7
- package/claude-assets/agents/cfn-dev-team/developers/frontend/react-frontend-engineer.md +87 -9
- package/claude-assets/agents/cfn-dev-team/developers/frontend/typescript-specialist.md +85 -7
- package/claude-assets/agents/cfn-dev-team/developers/frontend/ui-designer.md +160 -28
- package/claude-assets/agents/cfn-dev-team/developers/graphql-specialist.md +101 -19
- package/claude-assets/agents/cfn-dev-team/developers/rust-developer.md +108 -14
- package/claude-assets/agents/cfn-dev-team/reviewers/{reviewer.md → code-reviewer.md} +95 -8
- package/claude-assets/agents/cfn-dev-team/reviewers/quality/code-quality-validator.md +107 -7
- package/claude-assets/agents/cfn-dev-team/reviewers/quality/perf-analyzer.md +98 -7
- package/claude-assets/agents/cfn-dev-team/reviewers/quality/performance-benchmarker.md +95 -7
- package/claude-assets/agents/cfn-dev-team/reviewers/quality/security-specialist.md +136 -9
- package/claude-assets/agents/cfn-dev-team/testers/api-testing-specialist.md +108 -1
- package/claude-assets/agents/cfn-dev-team/testers/chaos-engineering-specialist.md +107 -13
- package/claude-assets/agents/cfn-dev-team/testers/contract-tester.md +737 -0
- package/claude-assets/agents/cfn-dev-team/testers/e2e/playwright-tester.md +1 -1
- package/claude-assets/agents/cfn-dev-team/testers/integration-tester.md +828 -0
- package/claude-assets/agents/cfn-dev-team/testers/interaction-tester.md +106 -7
- package/claude-assets/agents/cfn-dev-team/testers/load-testing-specialist.md +77 -0
- package/claude-assets/agents/cfn-dev-team/testers/mutation-testing-specialist.md +684 -0
- package/claude-assets/agents/cfn-dev-team/testers/playwright-tester.md +110 -1
- package/claude-assets/agents/cfn-dev-team/testers/tester.md +94 -7
- package/claude-assets/agents/cfn-dev-team/utility/code-booster.md +1 -3
- package/claude-assets/agents/cfn-dev-team/utility/epic-creator.md +87 -13
- package/claude-assets/agents/cfn-dev-team/utility/memory-leak-specialist.md +103 -7
- package/claude-assets/agents/cfn-dev-team/utility/researcher.md +1 -3
- package/claude-assets/agents/cfn-dev-team/utility/z-ai-specialist.md +94 -7
- package/claude-assets/agents/docker-coordinators/cfn-docker-v3-coordinator.md +46 -0
- package/claude-assets/agents/project-only-agents/npm-package-specialist.md +1 -1
- package/claude-assets/cfn-extras/skills/advanced-features/cfn-agent-swap/recommend-swap.sh +59 -59
- package/claude-assets/cfn-extras/skills/analytics/cfn-improvement-recommender/recommend-improvements.sh +91 -91
- package/claude-assets/cfn-extras/skills/analytics/cfn-pattern-extraction/extract-patterns.sh +79 -79
- package/claude-assets/cfn-extras/skills/analytics/cfn-retrospective-report/generate-report.sh +100 -100
- package/claude-assets/cfn-extras/skills/analytics/cfn-telemetry/start-telemetry.sh +110 -110
- package/claude-assets/cfn-extras/skills/deprecated/cfn-ace-system/add-bullet.sh +145 -145
- package/claude-assets/cfn-extras/skills/deprecated/cfn-ace-system/log-merge.sh +67 -67
- package/claude-assets/cfn-extras/skills/deprecated/cfn-ace-system/monitor-injection-performance.sh +137 -137
- package/claude-assets/cfn-extras/skills/deprecated/cfn-ace-system/optimize-injection-pipeline.sh +168 -168
- package/claude-assets/cfn-extras/skills/deprecated/cfn-ace-system/query-reflections.sh +35 -35
- package/claude-assets/cfn-extras/skills/deprecated/cfn-ace-system/store-reflection.sh +45 -45
- package/claude-assets/cfn-extras/skills/deprecated/cfn-ace-system/track-ab-test.sh +41 -41
- package/claude-assets/cfn-extras/skills/deprecated/cfn-ace-system/update-reflection.sh +41 -41
- package/claude-assets/cfn-extras/skills/deprecated/cfn-cli-setup/validate-cli-environment.sh +191 -191
- package/claude-assets/cfn-extras/skills/marketing/cfn-marketing-ad-campaigns/operations/create-campaign.sh +231 -231
- package/claude-assets/cfn-extras/skills/marketing/cfn-marketing-ad-campaigns/operations/get-campaign-performance.sh +190 -190
- package/claude-assets/cfn-extras/skills/marketing/cfn-marketing-ad-campaigns/operations/pause-campaign.sh +142 -142
- package/claude-assets/cfn-extras/skills/marketing/cfn-marketing-ad-campaigns/operations/set-budget.sh +181 -181
- package/claude-assets/cfn-extras/skills/marketing/cfn-marketing-ad-campaigns/operations/update-bid-strategy.sh +133 -133
- package/claude-assets/cfn-extras/skills/marketing/cfn-marketing-chatbot-conversations/operations/get-conversation-history.sh +121 -121
- package/claude-assets/cfn-extras/skills/marketing/cfn-marketing-chatbot-conversations/operations/qualify-lead.sh +156 -156
- package/claude-assets/cfn-extras/skills/marketing/cfn-marketing-chatbot-conversations/operations/schedule-demo.sh +181 -181
- package/claude-assets/cfn-extras/skills/marketing/cfn-marketing-chatbot-conversations/operations/send-message.sh +137 -137
- package/claude-assets/cfn-extras/skills/marketing/cfn-marketing-chatbot-conversations/operations/transfer-to-human.sh +179 -179
- package/claude-assets/cfn-extras/skills/marketing/cfn-marketing-sms-campaigns/operations/create-campaign.sh +183 -183
- package/claude-assets/cfn-extras/skills/marketing/cfn-marketing-sms-campaigns/operations/get-delivery-status.sh +139 -139
- package/claude-assets/cfn-extras/skills/marketing/cfn-marketing-sms-campaigns/operations/opt-out.sh +150 -150
- package/claude-assets/cfn-extras/skills/marketing/cfn-marketing-sms-campaigns/operations/schedule-campaign.sh +187 -187
- package/claude-assets/cfn-extras/skills/marketing/cfn-marketing-sms-campaigns/operations/send-sms.sh +181 -181
- package/claude-assets/cfn-extras/skills/ui-portal/cfn-web-portal/test-web-portal-skill.sh +50 -50
- package/claude-assets/cfn-extras/skills/ui-portal/cfn-web-portal/validate-deployment.sh +84 -84
- package/claude-assets/cfn-extras/skills/utility/cfn-environment-sanitization/sanitize-environment.sh +243 -243
- package/claude-assets/commands/cfn-loop-cli.md +29 -6
- package/claude-assets/commands/switch-api.md +31 -10
- package/claude-assets/hooks/cfn-lint-sql-injection.sh +61 -0
- package/claude-assets/hooks/cfn-post-edit-cfn-retrospective.sh +33 -2
- package/claude-assets/hooks/cfn-pre-edit-security-warning.sh +40 -0
- package/claude-assets/hooks/detect-hardcoded-credentials.sh +212 -0
- package/claude-assets/skills/SKILL_TEMPLATE.md +774 -0
- package/claude-assets/skills/agent-lifecycle/execute-lifecycle-hook.sh +84 -113
- package/claude-assets/skills/agent-lifecycle/simple-audit.sh +33 -6
- package/claude-assets/skills/agent-template-generator/SKILL.md +440 -0
- package/claude-assets/skills/agent-template-generator/generate-agent.sh +405 -0
- package/claude-assets/skills/agent-validation-linter/SKILL.md +589 -0
- package/claude-assets/skills/agent-validation-linter/lint-agents.sh +271 -0
- package/claude-assets/skills/bootstrap/bash-fundamentals.md +786 -0
- package/claude-assets/skills/bootstrap/database-connection.md +464 -0
- package/claude-assets/skills/bootstrap/error-handling.md +580 -0
- package/claude-assets/skills/bootstrap/file-operations.md +699 -0
- package/claude-assets/skills/bootstrap/skill-loader.md +616 -0
- package/claude-assets/skills/bootstrap/sqlite-params.sh +287 -0
- package/claude-assets/skills/cfn-agent-spawning/spawn-agent.sh +22 -24
- package/claude-assets/skills/cfn-automatic-memory-persistence/test-memory-persistence.sh +17 -16
- package/claude-assets/skills/cfn-deployment/SKILL.md +293 -0
- package/claude-assets/skills/cfn-deployment/execute.sh +21 -0
- package/claude-assets/skills/cfn-docker-agent-spawning/SKILL.md +28 -4
- package/claude-assets/skills/cfn-docker-agent-spawning/spawn-agent.sh +3 -1
- package/claude-assets/skills/cfn-docker-loop-orchestration/orchestrate.sh +224 -20
- package/claude-assets/skills/cfn-environment-sanitization/sanitize-environment.sh +38 -0
- package/claude-assets/skills/cfn-error-batching-strategy/lib/core-functions.sh +47 -47
- package/claude-assets/skills/cfn-file-operations/SKILL.md +290 -0
- package/claude-assets/skills/cfn-file-operations/execute.sh +129 -0
- package/claude-assets/skills/cfn-file-operations/lib/atomic-write.sh +294 -0
- package/claude-assets/skills/cfn-file-operations/lib/lock.sh +361 -0
- package/claude-assets/skills/cfn-file-operations/test.sh +369 -0
- package/claude-assets/skills/cfn-log-operations/SKILL.md +308 -0
- package/claude-assets/skills/cfn-log-operations/execute.sh +420 -0
- package/claude-assets/skills/cfn-log-operations/lib/rotate.sh +406 -0
- package/claude-assets/skills/cfn-log-operations/lib/search.sh +448 -0
- package/claude-assets/skills/cfn-log-operations/test.sh +394 -0
- package/claude-assets/skills/cfn-loop-orchestration/helpers/gate-check.sh +550 -46
- package/claude-assets/skills/cfn-loop-orchestration/helpers/parse-test-results.sh +277 -0
- package/claude-assets/skills/cfn-loop-orchestration/orchestrate.sh +238 -29
- package/claude-assets/skills/cfn-loop-orchestration/security_utils.sh +24 -0
- package/claude-assets/skills/cfn-loop-orchestration/test-iteration-context-injection.sh +366 -0
- package/claude-assets/skills/cfn-parameterized-queries/SKILL.md +339 -0
- package/claude-assets/skills/cfn-playbook/query-playbook.sh +19 -15
- package/claude-assets/skills/cfn-playbook/update-playbook.sh +25 -14
- package/claude-assets/skills/cfn-process-instrumentation/instrument-process.sh +44 -0
- package/claude-assets/skills/cfn-promotion/SKILL.md +305 -0
- package/claude-assets/skills/cfn-redis-coordination/CENTRALIZED_REDIS_WRAPPER.md +319 -0
- package/claude-assets/skills/cfn-redis-coordination/agent-log.sh +4 -0
- package/claude-assets/skills/cfn-redis-coordination/agent-log.sh.bak +124 -0
- package/claude-assets/skills/cfn-redis-coordination/agent-recovery.sh +2 -2
- package/claude-assets/skills/cfn-redis-coordination/collect-confidence-scores.sh +30 -0
- package/claude-assets/skills/cfn-redis-coordination/get-context.sh +33 -0
- package/claude-assets/skills/cfn-redis-coordination/get-success-criteria.sh +54 -0
- package/claude-assets/skills/cfn-redis-coordination/invoke-waiting-mode.sh +6 -2
- package/claude-assets/skills/cfn-redis-coordination/redis-cli-wrapper.sh +24 -3
- package/claude-assets/skills/cfn-redis-coordination/redis-functions.sh +34 -0
- package/claude-assets/skills/cfn-redis-coordination/report-completion.sh +24 -31
- package/claude-assets/skills/cfn-redis-coordination/store-context.sh +4 -0
- package/claude-assets/skills/cfn-redis-coordination/store-success-criteria.sh +85 -0
- package/claude-assets/skills/cfn-redis-coordination/update-all-scripts.sh +67 -0
- package/claude-assets/skills/cfn-skill-loader/SKILL.md +466 -0
- package/claude-assets/skills/cfn-skill-loader/execute.sh +344 -0
- package/claude-assets/skills/cfn-sqlite-memory/ttl-cleanup.sh +17 -25
- package/claude-assets/skills/cfn-task-audit/get-audit-data.sh +42 -21
- package/claude-assets/skills/cfn-task-audit/store-task-audit.sh +17 -10
- package/claude-assets/skills/cfn-test-runner/detect-regressions.sh +17 -14
- package/claude-assets/skills/cfn-test-runner/detect-regressions.sh.backup-1763392821 +55 -0
- package/claude-assets/skills/cfn-test-runner/store-benchmarks.sh +17 -19
- package/claude-assets/skills/cfn-transparency-middleware/test-e2e.sh +15 -0
- package/claude-assets/skills/cfn-transparency-middleware/tests/input-validation.sh +15 -0
- package/claude-assets/skills/cfn-utilities/SKILL.md +237 -0
- package/claude-assets/skills/cfn-utilities/execute.sh +32 -0
- package/claude-assets/skills/cfn-utilities/lib/errors.sh +56 -0
- package/claude-assets/skills/cfn-utilities/lib/file-ops.sh +164 -0
- package/claude-assets/skills/cfn-utilities/lib/logging.sh +77 -0
- package/claude-assets/skills/cfn-utilities/lib/retry.sh +127 -0
- package/claude-assets/skills/cfn-utilities/test.sh +317 -0
- package/claude-assets/skills/integration/agent-handoff.sh +62 -64
- package/claude-assets/skills/json-validation/SKILL.md +431 -0
- package/claude-assets/skills/json-validation/test-validate-success-criteria.sh +421 -0
- package/claude-assets/skills/json-validation/validate-success-criteria.sh +197 -0
- package/claude-assets/skills/redis-coordination/validate-parameters.sh +34 -0
- package/claude-assets/skills/workflow-codification/DEPLOY_QUICK_REFERENCE.md +106 -0
- package/claude-assets/skills/workflow-codification/PROPAGATE_UPDATE_QUICK_REFERENCE.md +366 -0
- package/claude-assets/skills/workflow-codification/deploy-approved-skill.sh +481 -0
- package/claude-assets/skills/workflow-codification/deploy-approved-skill.sh.backup-1763392820 +512 -0
- package/claude-assets/skills/workflow-codification/lib/security-utils.sh +204 -0
- package/claude-assets/skills/workflow-codification/propagate-skill-update.sh +648 -0
- package/claude-assets/skills/workflow-codification/propagate-skill-update.sh.backup-1763392820 +664 -0
- package/claude-assets/skills/workflow-codification/test-integration.sh +15 -0
- package/claude-assets/skills/workflow-codification/test-metadata-update.sh +350 -0
- package/claude-assets/skills/workflow-codification/track-cost-savings.sh +55 -14
- package/claude-assets/skills/workflow-codification/track-cost-savings.sh.backup-1763392821 +445 -0
- package/claude-assets/skills/workflow-codification/track-edge-case.sh +27 -60
- package/claude-assets/skills/workflow-codification/workflow-codification.db +0 -0
- package/dist/ace/ace-curator.js +10 -2
- package/dist/ace/ace-curator.js.map +1 -1
- package/dist/ace/ace-generator.js +4 -0
- package/dist/ace/ace-generator.js.map +1 -1
- package/dist/ace/ace-reflector.js +1 -1
- package/dist/ace/ace-reflector.js.map +1 -1
- package/dist/ace/context-injection.js +24 -2
- package/dist/ace/context-injection.js.map +1 -1
- package/dist/agents/task-agent-integration.js +1 -1
- package/dist/agents/task-agent-integration.js.map +1 -1
- package/dist/api/health-endpoints.js +390 -0
- package/dist/api/health-endpoints.js.map +1 -0
- package/dist/cli/agent-executor.js +4 -1
- package/dist/cli/agent-executor.js.map +1 -1
- package/dist/cli/agent-prompt-builder.js +89 -1
- package/dist/cli/agent-prompt-builder.js.map +1 -1
- package/dist/cli/agent-spawn.js +130 -37
- package/dist/cli/agent-spawn.js.map +1 -1
- package/dist/cli/config-manager.js +109 -91
- package/dist/cli/config-manager.js.map +1 -1
- package/dist/cli/conversation-fork-cleanup.js +201 -0
- package/dist/cli/conversation-fork-cleanup.js.map +1 -0
- package/dist/cli/conversation-fork.js +16 -3
- package/dist/cli/conversation-fork.js.map +1 -1
- package/dist/cli/skill-cache-validator.js +412 -0
- package/dist/cli/skill-cache-validator.js.map +1 -0
- package/dist/cli/skill-cli.js +991 -0
- package/dist/cli/skill-cli.js.map +1 -0
- package/dist/cli/skill-execution-logger.js +284 -0
- package/dist/cli/skill-execution-logger.js.map +1 -0
- package/dist/cli/skill-loader.js +457 -0
- package/dist/cli/skill-loader.js.map +1 -0
- package/dist/coordination/event-bus.js +2 -2
- package/dist/coordination/event-bus.js.map +1 -1
- package/dist/coordination/fleet-manager.js +1 -1
- package/dist/coordination/fleet-manager.js.map +1 -1
- package/dist/coordination/index.js +23 -9
- package/dist/coordination/index.js.map +1 -1
- package/dist/coordination/types/fleet-manager.types.js.map +1 -1
- package/dist/db/migration-manager.js +483 -0
- package/dist/db/migration-manager.js.map +1 -0
- package/dist/db/skills-query.js +535 -0
- package/dist/db/skills-query.js.map +1 -0
- package/dist/integration/DatabaseHandoff.js +1 -1
- package/dist/integration/DatabaseHandoff.js.map +1 -1
- package/dist/jobs/edge-case-analyzer.js +367 -0
- package/dist/jobs/edge-case-analyzer.js.map +1 -0
- package/dist/jobs/promotion-sla-enforcer.js +288 -0
- package/dist/jobs/promotion-sla-enforcer.js.map +1 -0
- package/dist/lib/agent-output-parser.js.map +1 -1
- package/dist/lib/agent-output-validator.js.map +1 -1
- package/dist/lib/agent-workspace.js +281 -0
- package/dist/lib/agent-workspace.js.map +1 -0
- package/dist/lib/atomic-file-writer.js +377 -0
- package/dist/lib/atomic-file-writer.js.map +1 -0
- package/dist/lib/backup-manager.js +779 -0
- package/dist/lib/backup-manager.js.map +1 -0
- package/dist/lib/checkpoint-manager.js +837 -0
- package/dist/lib/checkpoint-manager.js.map +1 -0
- package/dist/lib/circuit-breaker.js +340 -0
- package/dist/lib/circuit-breaker.js.map +1 -0
- package/dist/lib/completion-signal-handler.js +243 -0
- package/dist/lib/completion-signal-handler.js.map +1 -0
- package/dist/lib/config-manager.js +312 -0
- package/dist/lib/config-manager.js.map +1 -0
- package/dist/lib/config-migrator.js +386 -0
- package/dist/lib/config-migrator.js.map +1 -0
- package/dist/lib/config-validator.js.map +1 -1
- package/dist/lib/correlation-cache.js +311 -0
- package/dist/lib/correlation-cache.js.map +1 -0
- package/dist/lib/correlation.js +263 -0
- package/dist/lib/correlation.js.map +1 -0
- package/dist/lib/database-service/connection-pool-manager.js +520 -0
- package/dist/lib/database-service/connection-pool-manager.js.map +1 -0
- package/dist/lib/database-service/correlation.js +329 -0
- package/dist/lib/database-service/correlation.js.map +1 -0
- package/dist/lib/database-service/errors.js +120 -0
- package/dist/lib/database-service/errors.js.map +1 -0
- package/dist/lib/database-service/index.js +168 -0
- package/dist/lib/database-service/index.js.map +1 -0
- package/dist/lib/database-service/postgres-adapter.js +526 -0
- package/dist/lib/database-service/postgres-adapter.js.map +1 -0
- package/dist/lib/database-service/redis-adapter.js +360 -0
- package/dist/lib/database-service/redis-adapter.js.map +1 -0
- package/dist/lib/database-service/sqlite-adapter.js +544 -0
- package/dist/lib/database-service/sqlite-adapter.js.map +1 -0
- package/dist/lib/database-service/transaction-manager.js +773 -0
- package/dist/lib/database-service/transaction-manager.js.map +1 -0
- package/dist/lib/database-service/types.js +23 -0
- package/dist/lib/database-service/types.js.map +1 -0
- package/dist/lib/deadlock-resolver.js +292 -0
- package/dist/lib/deadlock-resolver.js.map +1 -0
- package/dist/lib/distributed-lock.js +451 -0
- package/dist/lib/distributed-lock.js.map +1 -0
- package/dist/lib/edge-case-deduplicator.js +227 -0
- package/dist/lib/edge-case-deduplicator.js.map +1 -0
- package/dist/lib/encryption-manager.js +322 -0
- package/dist/lib/encryption-manager.js.map +1 -0
- package/dist/lib/error-aggregator.js +234 -0
- package/dist/lib/error-aggregator.js.map +1 -0
- package/dist/lib/errors.js +287 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/file-lock-manager.js +578 -0
- package/dist/lib/file-lock-manager.js.map +1 -0
- package/dist/lib/file-operations.js +367 -0
- package/dist/lib/file-operations.js.map +1 -0
- package/dist/lib/idempotent-write.js +237 -0
- package/dist/lib/idempotent-write.js.map +1 -0
- package/dist/lib/integration-schema-validator.js +522 -0
- package/dist/lib/integration-schema-validator.js.map +1 -0
- package/dist/lib/lock-health-monitor.js +298 -0
- package/dist/lib/lock-health-monitor.js.map +1 -0
- package/dist/lib/log-shipper.js +422 -0
- package/dist/lib/log-shipper.js.map +1 -0
- package/dist/lib/logging.js +146 -0
- package/dist/lib/logging.js.map +1 -0
- package/dist/lib/message-deduplicator.js +439 -0
- package/dist/lib/message-deduplicator.js.map +1 -0
- package/dist/lib/multi-system-query.js +604 -0
- package/dist/lib/multi-system-query.js.map +1 -0
- package/dist/lib/orphan-detector.js +332 -0
- package/dist/lib/orphan-detector.js.map +1 -0
- package/dist/lib/password-generator.js +166 -0
- package/dist/lib/password-generator.js.map +1 -0
- package/dist/lib/path-validator.js +429 -0
- package/dist/lib/path-validator.js.map +1 -0
- package/dist/lib/query-translator.js +905 -0
- package/dist/lib/query-translator.js.map +1 -0
- package/dist/lib/queue-recovery.js +469 -0
- package/dist/lib/queue-recovery.js.map +1 -0
- package/dist/lib/redis-queue-manager.js +512 -0
- package/dist/lib/redis-queue-manager.js.map +1 -0
- package/dist/lib/reflection-archiver.js +272 -0
- package/dist/lib/reflection-archiver.js.map +1 -0
- package/dist/lib/retry-manager.js +453 -0
- package/dist/lib/retry-manager.js.map +1 -0
- package/dist/lib/retry.js +262 -0
- package/dist/lib/retry.js.map +1 -0
- package/dist/lib/schema-transform.js +695 -0
- package/dist/lib/schema-transform.js.map +1 -0
- package/dist/lib/schema-validator.js +491 -0
- package/dist/lib/schema-validator.js.map +1 -0
- package/dist/lib/skill-cache.js +297 -0
- package/dist/lib/skill-cache.js.map +1 -0
- package/dist/lib/skill-content-manager.js +337 -0
- package/dist/lib/skill-content-manager.js.map +1 -0
- package/dist/lib/skill-frontmatter-parser.js +237 -0
- package/dist/lib/skill-frontmatter-parser.js.map +1 -0
- package/dist/lib/skill-git-integration.js +275 -0
- package/dist/lib/skill-git-integration.js.map +1 -0
- package/dist/lib/skill-markdown-validator.js +396 -0
- package/dist/lib/skill-markdown-validator.js.map +1 -0
- package/dist/lib/skill-output-parser.js +312 -0
- package/dist/lib/skill-output-parser.js.map +1 -0
- package/dist/lib/unified-query-api.js +467 -0
- package/dist/lib/unified-query-api.js.map +1 -0
- package/dist/middleware/auth-middleware.js +350 -0
- package/dist/middleware/auth-middleware.js.map +1 -0
- package/dist/middleware/schema-validation.js +347 -0
- package/dist/middleware/schema-validation.js.map +1 -0
- package/dist/providers/anthropic-provider.js +1 -1
- package/dist/providers/anthropic-provider.js.map +1 -1
- package/dist/providers/provider-factory.js +2 -2
- package/dist/providers/provider-factory.js.map +1 -1
- package/dist/services/edge-case-analyzer.js +321 -0
- package/dist/services/edge-case-analyzer.js.map +1 -0
- package/dist/services/edge-case-deduplicator.js +266 -0
- package/dist/services/edge-case-deduplicator.js.map +1 -0
- package/dist/services/edge-case-detector.js +337 -0
- package/dist/services/edge-case-detector.js.map +1 -0
- package/dist/services/edge-case-tracker.js +547 -0
- package/dist/services/edge-case-tracker.js.map +1 -0
- package/dist/services/health-check-system.js +586 -0
- package/dist/services/health-check-system.js.map +1 -0
- package/dist/services/metrics-logger.js +412 -0
- package/dist/services/metrics-logger.js.map +1 -0
- package/dist/services/patch-generator.js +378 -0
- package/dist/services/patch-generator.js.map +1 -0
- package/dist/services/patch-validator.js +337 -0
- package/dist/services/patch-validator.js.map +1 -0
- package/dist/services/performance-monitor.js +811 -0
- package/dist/services/performance-monitor.js.map +1 -0
- package/dist/services/promotion-pipeline.js +918 -0
- package/dist/services/promotion-pipeline.js.map +1 -0
- package/dist/services/promotion-validator.js +394 -0
- package/dist/services/promotion-validator.js.map +1 -0
- package/dist/services/reflection-logger.js +388 -0
- package/dist/services/reflection-logger.js.map +1 -0
- package/dist/services/skill-deployment.js +472 -0
- package/dist/services/skill-deployment.js.map +1 -0
- package/dist/services/skill-loader.js +427 -0
- package/dist/services/skill-loader.js.map +1 -0
- package/dist/services/skill-promotion.js +372 -0
- package/dist/services/skill-promotion.js.map +1 -0
- package/dist/services/skill-validator.js +454 -0
- package/dist/services/skill-validator.js.map +1 -0
- package/dist/services/skill-versioning.js +244 -0
- package/dist/services/skill-versioning.js.map +1 -0
- package/dist/services/workspace-supervisor.js +597 -0
- package/dist/services/workspace-supervisor.js.map +1 -0
- package/dist/types/edge-case.js +45 -0
- package/dist/types/edge-case.js.map +1 -0
- package/docs/BUG_19_MEMORY_LEAK_TASK_MODE.md +405 -0
- package/docs/MEMORY_CLEANUP_GUIDE.md +358 -0
- package/docs/MEMORY_LEAK_FIX_SUMMARY.md +322 -0
- package/docs/REDIS_CLEANUP_EXECUTIVE_SUMMARY.md +319 -0
- package/docs/REDIS_CLEANUP_VERIFICATION_REPORT.md +574 -0
- package/package.json +35 -4
- package/readme/README.md +53 -5
- package/scripts/backup-cleanup.sh +627 -0
- package/scripts/cleanup-workspaces.sh +412 -0
- package/scripts/cleanup-yaml-configs.sh +141 -0
- package/scripts/deploy-approved-skills.sh +263 -0
- package/scripts/health-check.sh +447 -0
- package/scripts/log-aggregator.sh +554 -0
- package/scripts/log-monitor.sh +629 -0
- package/scripts/manage-agent-workspaces.sh +434 -0
- package/scripts/migrate-schema.sh +533 -0
- package/scripts/promote-staged-skills.sh +423 -0
- package/scripts/verify-no-secrets.sh +88 -35
- package/scripts/verify-redis-cleanup.sh +173 -0
- package/tests/README.md +84 -0
- package/tests/test-memory-leak-task-mode.sh +435 -0
- package/.claude/cfn-extras/agents/deprecated-coordinators/adaptive-coordinator.md.backup +0 -161
- package/.claude/cfn-extras/agents/deprecated-coordinators/blocking-coordinator-example.md.backup +0 -728
- package/.claude/cfn-extras/agents/deprecated-coordinators/mesh-coordinator.md.backup +0 -131
- package/.claude/skills/agent-lifecycle/SKILL.md +0 -60
- package/.claude/skills/agent-lifecycle/execute-lifecycle-hook.sh +0 -573
- package/.claude/skills/agent-lifecycle/simple-audit.sh +0 -31
- package/.claude/skills/cfn-agent-spawning/spawn-agent.sh.backup +0 -273
- package/.claude/skills/cfn-loop-orchestration/orchestrate.sh.backup +0 -949
- package/README.md.backup_before_replace +0 -781
- package/claude-assets/cfn-extras/agents/deprecated-coordinators/adaptive-coordinator.md.backup +0 -161
- package/claude-assets/cfn-extras/agents/deprecated-coordinators/blocking-coordinator-example.md.backup +0 -728
- package/claude-assets/cfn-extras/agents/deprecated-coordinators/mesh-coordinator.md.backup +0 -131
- package/claude-assets/skills/cfn-agent-spawning/spawn-agent.sh.backup +0 -273
- package/claude-assets/skills/cfn-loop-orchestration/orchestrate.sh.backup +0 -949
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Operations Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides atomic file writes and file locking mechanisms for safe concurrent operations.
|
|
5
|
+
* Part of Task 0.5: Implementation Tooling & Utilities (Foundation)
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* await atomicWrite('/path/to/file.json', JSON.stringify(data));
|
|
9
|
+
* const lock = await acquireLock('/path/to/file.json');
|
|
10
|
+
* // ... perform operations ...
|
|
11
|
+
* await releaseLock(lock);
|
|
12
|
+
*/ import * as fs from 'fs';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
import { promisify } from 'util';
|
|
15
|
+
import { randomUUID } from 'crypto';
|
|
16
|
+
import { createError, ErrorCode, createTimeoutError } from './errors.js';
|
|
17
|
+
import { createLogger } from './logging.js';
|
|
18
|
+
const logger = createLogger('file-operations');
|
|
19
|
+
const fsWriteFile = promisify(fs.writeFile);
|
|
20
|
+
const fsReadFile = promisify(fs.readFile);
|
|
21
|
+
const fsRename = promisify(fs.rename);
|
|
22
|
+
const fsUnlink = promisify(fs.unlink);
|
|
23
|
+
const fsStat = promisify(fs.stat);
|
|
24
|
+
const fsMkdir = promisify(fs.mkdir);
|
|
25
|
+
const fsAccess = promisify(fs.access);
|
|
26
|
+
/**
|
|
27
|
+
* Default lock options
|
|
28
|
+
*/ const DEFAULT_LOCK_OPTIONS = {
|
|
29
|
+
timeoutMs: 30000,
|
|
30
|
+
retryIntervalMs: 100,
|
|
31
|
+
staleTimeoutMs: 60000
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Atomically write content to a file
|
|
35
|
+
*
|
|
36
|
+
* Pattern: Write to temp file → verify → move to target
|
|
37
|
+
* This ensures that the file is never left in a partially written state.
|
|
38
|
+
*
|
|
39
|
+
* @param filePath - Target file path
|
|
40
|
+
* @param content - Content to write
|
|
41
|
+
* @param encoding - File encoding (default: 'utf8')
|
|
42
|
+
* @returns Promise that resolves when write is complete
|
|
43
|
+
* @throws FILE_WRITE_FAILED if write operation fails
|
|
44
|
+
*/ export async function atomicWrite(filePath, content, encoding = 'utf8') {
|
|
45
|
+
const absolutePath = path.resolve(filePath);
|
|
46
|
+
const dir = path.dirname(absolutePath);
|
|
47
|
+
const tempPath = path.join(dir, `.${path.basename(absolutePath)}.${randomUUID()}.tmp`);
|
|
48
|
+
try {
|
|
49
|
+
// Ensure directory exists
|
|
50
|
+
await ensureDirectory(dir);
|
|
51
|
+
// Write to temporary file
|
|
52
|
+
logger.debug('Writing to temporary file', {
|
|
53
|
+
tempPath
|
|
54
|
+
});
|
|
55
|
+
await fsWriteFile(tempPath, content, encoding);
|
|
56
|
+
// Verify write by reading back
|
|
57
|
+
const written = await fsReadFile(tempPath, encoding);
|
|
58
|
+
if (written !== content) {
|
|
59
|
+
throw createError(ErrorCode.FILE_WRITE_FAILED, 'Content verification failed after write', {
|
|
60
|
+
filePath: absolutePath,
|
|
61
|
+
tempPath
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
// Atomically move temp file to target
|
|
65
|
+
logger.debug('Moving temp file to target', {
|
|
66
|
+
tempPath,
|
|
67
|
+
target: absolutePath
|
|
68
|
+
});
|
|
69
|
+
await fsRename(tempPath, absolutePath);
|
|
70
|
+
logger.info('Atomic write completed', {
|
|
71
|
+
filePath: absolutePath
|
|
72
|
+
});
|
|
73
|
+
} catch (error) {
|
|
74
|
+
// Clean up temp file if it exists
|
|
75
|
+
try {
|
|
76
|
+
await fsUnlink(tempPath);
|
|
77
|
+
} catch {
|
|
78
|
+
// Ignore cleanup errors
|
|
79
|
+
}
|
|
80
|
+
throw createError(ErrorCode.FILE_WRITE_FAILED, `Failed to write file: ${absolutePath}`, {
|
|
81
|
+
filePath: absolutePath,
|
|
82
|
+
tempPath
|
|
83
|
+
}, error instanceof Error ? error : undefined);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Acquire a lock on a file
|
|
88
|
+
*
|
|
89
|
+
* Creates a lock file with metadata about the lock holder.
|
|
90
|
+
* Implements stale lock detection and automatic cleanup.
|
|
91
|
+
*
|
|
92
|
+
* @param filePath - File path to lock
|
|
93
|
+
* @param options - Lock options
|
|
94
|
+
* @returns FileLock object
|
|
95
|
+
* @throws LOCK_TIMEOUT if lock cannot be acquired within timeout
|
|
96
|
+
*/ export async function acquireLock(filePath, options = {}) {
|
|
97
|
+
const opts = {
|
|
98
|
+
...DEFAULT_LOCK_OPTIONS,
|
|
99
|
+
...options
|
|
100
|
+
};
|
|
101
|
+
const absolutePath = path.resolve(filePath);
|
|
102
|
+
const lockPath = `${absolutePath}.lock`;
|
|
103
|
+
const lockId = randomUUID();
|
|
104
|
+
const startTime = Date.now();
|
|
105
|
+
logger.debug('Acquiring lock', {
|
|
106
|
+
filePath: absolutePath,
|
|
107
|
+
lockId
|
|
108
|
+
});
|
|
109
|
+
while(Date.now() - startTime < opts.timeoutMs){
|
|
110
|
+
try {
|
|
111
|
+
// Check if lock file exists
|
|
112
|
+
const lockExists = await fileExists(lockPath);
|
|
113
|
+
if (lockExists) {
|
|
114
|
+
// Check if lock is stale
|
|
115
|
+
const isStale = await isLockStale(lockPath, opts.staleTimeoutMs);
|
|
116
|
+
if (isStale) {
|
|
117
|
+
logger.warn('Removing stale lock', {
|
|
118
|
+
lockPath
|
|
119
|
+
});
|
|
120
|
+
await fsUnlink(lockPath);
|
|
121
|
+
} else {
|
|
122
|
+
// Lock is held by another process, wait and retry
|
|
123
|
+
await sleep(opts.retryIntervalMs);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Attempt to create lock file
|
|
128
|
+
const lockData = {
|
|
129
|
+
filePath: absolutePath,
|
|
130
|
+
lockPath,
|
|
131
|
+
acquired: new Date(),
|
|
132
|
+
lockId,
|
|
133
|
+
pid: process.pid
|
|
134
|
+
};
|
|
135
|
+
// Write lock file atomically
|
|
136
|
+
await atomicWrite(lockPath, JSON.stringify(lockData, null, 2));
|
|
137
|
+
// Verify we actually acquired the lock (check for race condition)
|
|
138
|
+
const verifyData = await fsReadFile(lockPath, 'utf8');
|
|
139
|
+
const verifyLock = JSON.parse(verifyData);
|
|
140
|
+
if (verifyLock.lockId !== lockId) {
|
|
141
|
+
// Another process won the race
|
|
142
|
+
logger.debug('Lost lock race', {
|
|
143
|
+
lockId,
|
|
144
|
+
winnerId: verifyLock.lockId
|
|
145
|
+
});
|
|
146
|
+
await sleep(opts.retryIntervalMs);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
logger.info('Lock acquired', {
|
|
150
|
+
filePath: absolutePath,
|
|
151
|
+
lockId
|
|
152
|
+
});
|
|
153
|
+
return lockData;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
logger.error('Error acquiring lock', error instanceof Error ? error : undefined, {
|
|
156
|
+
filePath: absolutePath,
|
|
157
|
+
lockId
|
|
158
|
+
});
|
|
159
|
+
throw createError(ErrorCode.LOCK_TIMEOUT, `Failed to acquire lock: ${absolutePath}`, {
|
|
160
|
+
filePath: absolutePath,
|
|
161
|
+
lockPath
|
|
162
|
+
}, error instanceof Error ? error : undefined);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Timeout reached
|
|
166
|
+
throw createTimeoutError(`acquire lock on ${absolutePath}`, opts.timeoutMs);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Release a file lock
|
|
170
|
+
*
|
|
171
|
+
* @param lock - FileLock object from acquireLock
|
|
172
|
+
* @returns Promise that resolves when lock is released
|
|
173
|
+
*/ export async function releaseLock(lock) {
|
|
174
|
+
try {
|
|
175
|
+
// Verify lock still exists and belongs to us
|
|
176
|
+
const lockExists = await fileExists(lock.lockPath);
|
|
177
|
+
if (!lockExists) {
|
|
178
|
+
logger.warn('Lock file already removed', {
|
|
179
|
+
lockPath: lock.lockPath
|
|
180
|
+
});
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const currentData = await fsReadFile(lock.lockPath, 'utf8');
|
|
184
|
+
const currentLock = JSON.parse(currentData);
|
|
185
|
+
if (currentLock.lockId !== lock.lockId) {
|
|
186
|
+
logger.warn('Lock was taken by another process', {
|
|
187
|
+
ourLockId: lock.lockId,
|
|
188
|
+
currentLockId: currentLock.lockId
|
|
189
|
+
});
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
// Remove lock file
|
|
193
|
+
await fsUnlink(lock.lockPath);
|
|
194
|
+
logger.info('Lock released', {
|
|
195
|
+
filePath: lock.filePath,
|
|
196
|
+
lockId: lock.lockId
|
|
197
|
+
});
|
|
198
|
+
} catch (error) {
|
|
199
|
+
logger.error('Error releasing lock', error instanceof Error ? error : undefined, {
|
|
200
|
+
lockPath: lock.lockPath,
|
|
201
|
+
lockId: lock.lockId
|
|
202
|
+
});
|
|
203
|
+
throw createError(ErrorCode.FILE_WRITE_FAILED, `Failed to release lock: ${lock.lockPath}`, {
|
|
204
|
+
lockPath: lock.lockPath,
|
|
205
|
+
lockId: lock.lockId
|
|
206
|
+
}, error instanceof Error ? error : undefined);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Execute a function with a file lock
|
|
211
|
+
*
|
|
212
|
+
* @param filePath - File path to lock
|
|
213
|
+
* @param fn - Function to execute while holding the lock
|
|
214
|
+
* @param options - Lock options
|
|
215
|
+
* @returns Result of the function
|
|
216
|
+
*/ export async function withLock(filePath, fn, options = {}) {
|
|
217
|
+
const lock = await acquireLock(filePath, options);
|
|
218
|
+
try {
|
|
219
|
+
return await fn();
|
|
220
|
+
} finally{
|
|
221
|
+
await releaseLock(lock);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Check if a lock is stale
|
|
226
|
+
*
|
|
227
|
+
* @param lockPath - Path to lock file
|
|
228
|
+
* @param staleTimeoutMs - Stale timeout in milliseconds
|
|
229
|
+
* @returns True if lock is stale
|
|
230
|
+
*/ async function isLockStale(lockPath, staleTimeoutMs) {
|
|
231
|
+
try {
|
|
232
|
+
const lockData = await fsReadFile(lockPath, 'utf8');
|
|
233
|
+
const lock = JSON.parse(lockData);
|
|
234
|
+
const acquiredTime = new Date(lock.acquired).getTime();
|
|
235
|
+
const now = Date.now();
|
|
236
|
+
const age = now - acquiredTime;
|
|
237
|
+
return age > staleTimeoutMs;
|
|
238
|
+
} catch {
|
|
239
|
+
// If we can't read the lock file, consider it stale
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Check if a file exists
|
|
245
|
+
*
|
|
246
|
+
* @param filePath - File path to check
|
|
247
|
+
* @returns True if file exists
|
|
248
|
+
*/ export async function fileExists(filePath) {
|
|
249
|
+
try {
|
|
250
|
+
await fsAccess(filePath, fs.constants.F_OK);
|
|
251
|
+
return true;
|
|
252
|
+
} catch {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Ensure directory exists, creating it if necessary
|
|
258
|
+
*
|
|
259
|
+
* @param dirPath - Directory path
|
|
260
|
+
* @returns Promise that resolves when directory exists
|
|
261
|
+
*/ export async function ensureDirectory(dirPath) {
|
|
262
|
+
try {
|
|
263
|
+
await fsMkdir(dirPath, {
|
|
264
|
+
recursive: true
|
|
265
|
+
});
|
|
266
|
+
} catch (error) {
|
|
267
|
+
// Ignore error if directory already exists
|
|
268
|
+
if (error.code !== 'EEXIST') {
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Sleep for specified duration
|
|
275
|
+
*
|
|
276
|
+
* @param ms - Duration in milliseconds
|
|
277
|
+
* @returns Promise that resolves after duration
|
|
278
|
+
*/ function sleep(ms) {
|
|
279
|
+
return new Promise((resolve)=>setTimeout(resolve, ms));
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Read file with automatic retry
|
|
283
|
+
*
|
|
284
|
+
* @param filePath - File path to read
|
|
285
|
+
* @param encoding - File encoding (default: 'utf8')
|
|
286
|
+
* @param maxRetries - Maximum retry attempts (default: 3)
|
|
287
|
+
* @returns File content
|
|
288
|
+
*/ export async function readFileWithRetry(filePath, encoding = 'utf8', maxRetries = 3) {
|
|
289
|
+
let lastError;
|
|
290
|
+
for(let attempt = 0; attempt < maxRetries; attempt++){
|
|
291
|
+
try {
|
|
292
|
+
return await fsReadFile(filePath, encoding);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
295
|
+
if (attempt < maxRetries - 1) {
|
|
296
|
+
await sleep(100 * Math.pow(2, attempt)); // Exponential backoff
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
throw createError(ErrorCode.FILE_NOT_FOUND, `Failed to read file after ${maxRetries} attempts: ${filePath}`, {
|
|
301
|
+
filePath,
|
|
302
|
+
maxRetries
|
|
303
|
+
}, lastError);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Write file with automatic retry
|
|
307
|
+
*
|
|
308
|
+
* @param filePath - File path to write
|
|
309
|
+
* @param content - Content to write
|
|
310
|
+
* @param encoding - File encoding (default: 'utf8')
|
|
311
|
+
* @param maxRetries - Maximum retry attempts (default: 3)
|
|
312
|
+
* @returns Promise that resolves when write is complete
|
|
313
|
+
*/ export async function writeFileWithRetry(filePath, content, encoding = 'utf8', maxRetries = 3) {
|
|
314
|
+
let lastError;
|
|
315
|
+
for(let attempt = 0; attempt < maxRetries; attempt++){
|
|
316
|
+
try {
|
|
317
|
+
await atomicWrite(filePath, content, encoding);
|
|
318
|
+
return;
|
|
319
|
+
} catch (error) {
|
|
320
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
321
|
+
if (attempt < maxRetries - 1) {
|
|
322
|
+
await sleep(100 * Math.pow(2, attempt)); // Exponential backoff
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
throw createError(ErrorCode.FILE_WRITE_FAILED, `Failed to write file after ${maxRetries} attempts: ${filePath}`, {
|
|
327
|
+
filePath,
|
|
328
|
+
maxRetries
|
|
329
|
+
}, lastError);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Copy file atomically
|
|
333
|
+
*
|
|
334
|
+
* @param sourcePath - Source file path
|
|
335
|
+
* @param targetPath - Target file path
|
|
336
|
+
* @returns Promise that resolves when copy is complete
|
|
337
|
+
*/ export async function atomicCopy(sourcePath, targetPath) {
|
|
338
|
+
const content = await fsReadFile(sourcePath, 'utf8');
|
|
339
|
+
await atomicWrite(targetPath, content);
|
|
340
|
+
logger.info('Atomic copy completed', {
|
|
341
|
+
source: sourcePath,
|
|
342
|
+
target: targetPath
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Move file atomically
|
|
347
|
+
*
|
|
348
|
+
* @param sourcePath - Source file path
|
|
349
|
+
* @param targetPath - Target file path
|
|
350
|
+
* @returns Promise that resolves when move is complete
|
|
351
|
+
*/ export async function atomicMove(sourcePath, targetPath) {
|
|
352
|
+
const absoluteSource = path.resolve(sourcePath);
|
|
353
|
+
const absoluteTarget = path.resolve(targetPath);
|
|
354
|
+
try {
|
|
355
|
+
await fsRename(absoluteSource, absoluteTarget);
|
|
356
|
+
logger.info('Atomic move completed', {
|
|
357
|
+
source: absoluteSource,
|
|
358
|
+
target: absoluteTarget
|
|
359
|
+
});
|
|
360
|
+
} catch (error) {
|
|
361
|
+
// If rename fails (e.g., cross-device), fall back to copy + delete
|
|
362
|
+
await atomicCopy(absoluteSource, absoluteTarget);
|
|
363
|
+
await fsUnlink(absoluteSource);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
//# sourceMappingURL=file-operations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/lib/file-operations.ts"],"sourcesContent":["/**\r\n * File Operations Utilities\r\n *\r\n * Provides atomic file writes and file locking mechanisms for safe concurrent operations.\r\n * Part of Task 0.5: Implementation Tooling & Utilities (Foundation)\r\n *\r\n * Usage:\r\n * await atomicWrite('/path/to/file.json', JSON.stringify(data));\r\n * const lock = await acquireLock('/path/to/file.json');\r\n * // ... perform operations ...\r\n * await releaseLock(lock);\r\n */\r\n\r\nimport * as fs from 'fs';\r\nimport * as path from 'path';\r\nimport { promisify } from 'util';\r\nimport { randomUUID } from 'crypto';\r\nimport { createError, ErrorCode, createTimeoutError } from './errors.js';\r\nimport { createLogger } from './logging.js';\r\n\r\nconst logger = createLogger('file-operations');\r\n\r\nconst fsWriteFile = promisify(fs.writeFile);\r\nconst fsReadFile = promisify(fs.readFile);\r\nconst fsRename = promisify(fs.rename);\r\nconst fsUnlink = promisify(fs.unlink);\r\nconst fsStat = promisify(fs.stat);\r\nconst fsMkdir = promisify(fs.mkdir);\r\nconst fsAccess = promisify(fs.access);\r\n\r\n/**\r\n * File lock information\r\n */\r\nexport interface FileLock {\r\n /** Path to the file being locked */\r\n filePath: string;\r\n /** Path to the lock file */\r\n lockPath: string;\r\n /** When the lock was acquired */\r\n acquired: Date;\r\n /** Lock ID (unique identifier) */\r\n lockId: string;\r\n /** Process ID that acquired the lock */\r\n pid: number;\r\n}\r\n\r\n/**\r\n * Lock options\r\n */\r\nexport interface LockOptions {\r\n /** Timeout in milliseconds (default: 30000) */\r\n timeoutMs?: number;\r\n /** Retry interval in milliseconds (default: 100) */\r\n retryIntervalMs?: number;\r\n /** Stale lock timeout in milliseconds (default: 60000) */\r\n staleTimeoutMs?: number;\r\n}\r\n\r\n/**\r\n * Default lock options\r\n */\r\nconst DEFAULT_LOCK_OPTIONS: Required<LockOptions> = {\r\n timeoutMs: 30000, // 30 seconds\r\n retryIntervalMs: 100, // 100ms\r\n staleTimeoutMs: 60000, // 60 seconds\r\n};\r\n\r\n/**\r\n * Atomically write content to a file\r\n *\r\n * Pattern: Write to temp file → verify → move to target\r\n * This ensures that the file is never left in a partially written state.\r\n *\r\n * @param filePath - Target file path\r\n * @param content - Content to write\r\n * @param encoding - File encoding (default: 'utf8')\r\n * @returns Promise that resolves when write is complete\r\n * @throws FILE_WRITE_FAILED if write operation fails\r\n */\r\nexport async function atomicWrite(\r\n filePath: string,\r\n content: string,\r\n encoding: BufferEncoding = 'utf8'\r\n): Promise<void> {\r\n const absolutePath = path.resolve(filePath);\r\n const dir = path.dirname(absolutePath);\r\n const tempPath = path.join(dir, `.${path.basename(absolutePath)}.${randomUUID()}.tmp`);\r\n\r\n try {\r\n // Ensure directory exists\r\n await ensureDirectory(dir);\r\n\r\n // Write to temporary file\r\n logger.debug('Writing to temporary file', { tempPath });\r\n await fsWriteFile(tempPath, content, encoding);\r\n\r\n // Verify write by reading back\r\n const written = await fsReadFile(tempPath, encoding);\r\n if (written !== content) {\r\n throw createError(\r\n ErrorCode.FILE_WRITE_FAILED,\r\n 'Content verification failed after write',\r\n { filePath: absolutePath, tempPath }\r\n );\r\n }\r\n\r\n // Atomically move temp file to target\r\n logger.debug('Moving temp file to target', { tempPath, target: absolutePath });\r\n await fsRename(tempPath, absolutePath);\r\n\r\n logger.info('Atomic write completed', { filePath: absolutePath });\r\n } catch (error) {\r\n // Clean up temp file if it exists\r\n try {\r\n await fsUnlink(tempPath);\r\n } catch {\r\n // Ignore cleanup errors\r\n }\r\n\r\n throw createError(\r\n ErrorCode.FILE_WRITE_FAILED,\r\n `Failed to write file: ${absolutePath}`,\r\n { filePath: absolutePath, tempPath },\r\n error instanceof Error ? error : undefined\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Acquire a lock on a file\r\n *\r\n * Creates a lock file with metadata about the lock holder.\r\n * Implements stale lock detection and automatic cleanup.\r\n *\r\n * @param filePath - File path to lock\r\n * @param options - Lock options\r\n * @returns FileLock object\r\n * @throws LOCK_TIMEOUT if lock cannot be acquired within timeout\r\n */\r\nexport async function acquireLock(\r\n filePath: string,\r\n options: LockOptions = {}\r\n): Promise<FileLock> {\r\n const opts = { ...DEFAULT_LOCK_OPTIONS, ...options };\r\n const absolutePath = path.resolve(filePath);\r\n const lockPath = `${absolutePath}.lock`;\r\n const lockId = randomUUID();\r\n const startTime = Date.now();\r\n\r\n logger.debug('Acquiring lock', { filePath: absolutePath, lockId });\r\n\r\n while (Date.now() - startTime < opts.timeoutMs) {\r\n try {\r\n // Check if lock file exists\r\n const lockExists = await fileExists(lockPath);\r\n\r\n if (lockExists) {\r\n // Check if lock is stale\r\n const isStale = await isLockStale(lockPath, opts.staleTimeoutMs);\r\n\r\n if (isStale) {\r\n logger.warn('Removing stale lock', { lockPath });\r\n await fsUnlink(lockPath);\r\n } else {\r\n // Lock is held by another process, wait and retry\r\n await sleep(opts.retryIntervalMs);\r\n continue;\r\n }\r\n }\r\n\r\n // Attempt to create lock file\r\n const lockData: FileLock = {\r\n filePath: absolutePath,\r\n lockPath,\r\n acquired: new Date(),\r\n lockId,\r\n pid: process.pid,\r\n };\r\n\r\n // Write lock file atomically\r\n await atomicWrite(lockPath, JSON.stringify(lockData, null, 2));\r\n\r\n // Verify we actually acquired the lock (check for race condition)\r\n const verifyData = await fsReadFile(lockPath, 'utf8');\r\n const verifyLock = JSON.parse(verifyData) as FileLock;\r\n\r\n if (verifyLock.lockId !== lockId) {\r\n // Another process won the race\r\n logger.debug('Lost lock race', { lockId, winnerId: verifyLock.lockId });\r\n await sleep(opts.retryIntervalMs);\r\n continue;\r\n }\r\n\r\n logger.info('Lock acquired', { filePath: absolutePath, lockId });\r\n return lockData;\r\n } catch (error) {\r\n logger.error('Error acquiring lock', error instanceof Error ? error : undefined, {\r\n filePath: absolutePath,\r\n lockId,\r\n });\r\n throw createError(\r\n ErrorCode.LOCK_TIMEOUT,\r\n `Failed to acquire lock: ${absolutePath}`,\r\n { filePath: absolutePath, lockPath },\r\n error instanceof Error ? error : undefined\r\n );\r\n }\r\n }\r\n\r\n // Timeout reached\r\n throw createTimeoutError(`acquire lock on ${absolutePath}`, opts.timeoutMs);\r\n}\r\n\r\n/**\r\n * Release a file lock\r\n *\r\n * @param lock - FileLock object from acquireLock\r\n * @returns Promise that resolves when lock is released\r\n */\r\nexport async function releaseLock(lock: FileLock): Promise<void> {\r\n try {\r\n // Verify lock still exists and belongs to us\r\n const lockExists = await fileExists(lock.lockPath);\r\n\r\n if (!lockExists) {\r\n logger.warn('Lock file already removed', { lockPath: lock.lockPath });\r\n return;\r\n }\r\n\r\n const currentData = await fsReadFile(lock.lockPath, 'utf8');\r\n const currentLock = JSON.parse(currentData) as FileLock;\r\n\r\n if (currentLock.lockId !== lock.lockId) {\r\n logger.warn('Lock was taken by another process', {\r\n ourLockId: lock.lockId,\r\n currentLockId: currentLock.lockId,\r\n });\r\n return;\r\n }\r\n\r\n // Remove lock file\r\n await fsUnlink(lock.lockPath);\r\n logger.info('Lock released', { filePath: lock.filePath, lockId: lock.lockId });\r\n } catch (error) {\r\n logger.error('Error releasing lock', error instanceof Error ? error : undefined, {\r\n lockPath: lock.lockPath,\r\n lockId: lock.lockId,\r\n });\r\n throw createError(\r\n ErrorCode.FILE_WRITE_FAILED,\r\n `Failed to release lock: ${lock.lockPath}`,\r\n { lockPath: lock.lockPath, lockId: lock.lockId },\r\n error instanceof Error ? error : undefined\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Execute a function with a file lock\r\n *\r\n * @param filePath - File path to lock\r\n * @param fn - Function to execute while holding the lock\r\n * @param options - Lock options\r\n * @returns Result of the function\r\n */\r\nexport async function withLock<T>(\r\n filePath: string,\r\n fn: () => Promise<T>,\r\n options: LockOptions = {}\r\n): Promise<T> {\r\n const lock = await acquireLock(filePath, options);\r\n\r\n try {\r\n return await fn();\r\n } finally {\r\n await releaseLock(lock);\r\n }\r\n}\r\n\r\n/**\r\n * Check if a lock is stale\r\n *\r\n * @param lockPath - Path to lock file\r\n * @param staleTimeoutMs - Stale timeout in milliseconds\r\n * @returns True if lock is stale\r\n */\r\nasync function isLockStale(lockPath: string, staleTimeoutMs: number): Promise<boolean> {\r\n try {\r\n const lockData = await fsReadFile(lockPath, 'utf8');\r\n const lock = JSON.parse(lockData) as FileLock;\r\n\r\n const acquiredTime = new Date(lock.acquired).getTime();\r\n const now = Date.now();\r\n const age = now - acquiredTime;\r\n\r\n return age > staleTimeoutMs;\r\n } catch {\r\n // If we can't read the lock file, consider it stale\r\n return true;\r\n }\r\n}\r\n\r\n/**\r\n * Check if a file exists\r\n *\r\n * @param filePath - File path to check\r\n * @returns True if file exists\r\n */\r\nexport async function fileExists(filePath: string): Promise<boolean> {\r\n try {\r\n await fsAccess(filePath, fs.constants.F_OK);\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * Ensure directory exists, creating it if necessary\r\n *\r\n * @param dirPath - Directory path\r\n * @returns Promise that resolves when directory exists\r\n */\r\nexport async function ensureDirectory(dirPath: string): Promise<void> {\r\n try {\r\n await fsMkdir(dirPath, { recursive: true });\r\n } catch (error) {\r\n // Ignore error if directory already exists\r\n if ((error as NodeJS.ErrnoException).code !== 'EEXIST') {\r\n throw error;\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Sleep for specified duration\r\n *\r\n * @param ms - Duration in milliseconds\r\n * @returns Promise that resolves after duration\r\n */\r\nfunction sleep(ms: number): Promise<void> {\r\n return new Promise((resolve) => setTimeout(resolve, ms));\r\n}\r\n\r\n/**\r\n * Read file with automatic retry\r\n *\r\n * @param filePath - File path to read\r\n * @param encoding - File encoding (default: 'utf8')\r\n * @param maxRetries - Maximum retry attempts (default: 3)\r\n * @returns File content\r\n */\r\nexport async function readFileWithRetry(\r\n filePath: string,\r\n encoding: BufferEncoding = 'utf8',\r\n maxRetries: number = 3\r\n): Promise<string> {\r\n let lastError: Error | undefined;\r\n\r\n for (let attempt = 0; attempt < maxRetries; attempt++) {\r\n try {\r\n return await fsReadFile(filePath, encoding);\r\n } catch (error) {\r\n lastError = error instanceof Error ? error : new Error(String(error));\r\n if (attempt < maxRetries - 1) {\r\n await sleep(100 * Math.pow(2, attempt)); // Exponential backoff\r\n }\r\n }\r\n }\r\n\r\n throw createError(\r\n ErrorCode.FILE_NOT_FOUND,\r\n `Failed to read file after ${maxRetries} attempts: ${filePath}`,\r\n { filePath, maxRetries },\r\n lastError\r\n );\r\n}\r\n\r\n/**\r\n * Write file with automatic retry\r\n *\r\n * @param filePath - File path to write\r\n * @param content - Content to write\r\n * @param encoding - File encoding (default: 'utf8')\r\n * @param maxRetries - Maximum retry attempts (default: 3)\r\n * @returns Promise that resolves when write is complete\r\n */\r\nexport async function writeFileWithRetry(\r\n filePath: string,\r\n content: string,\r\n encoding: BufferEncoding = 'utf8',\r\n maxRetries: number = 3\r\n): Promise<void> {\r\n let lastError: Error | undefined;\r\n\r\n for (let attempt = 0; attempt < maxRetries; attempt++) {\r\n try {\r\n await atomicWrite(filePath, content, encoding);\r\n return;\r\n } catch (error) {\r\n lastError = error instanceof Error ? error : new Error(String(error));\r\n if (attempt < maxRetries - 1) {\r\n await sleep(100 * Math.pow(2, attempt)); // Exponential backoff\r\n }\r\n }\r\n }\r\n\r\n throw createError(\r\n ErrorCode.FILE_WRITE_FAILED,\r\n `Failed to write file after ${maxRetries} attempts: ${filePath}`,\r\n { filePath, maxRetries },\r\n lastError\r\n );\r\n}\r\n\r\n/**\r\n * Copy file atomically\r\n *\r\n * @param sourcePath - Source file path\r\n * @param targetPath - Target file path\r\n * @returns Promise that resolves when copy is complete\r\n */\r\nexport async function atomicCopy(sourcePath: string, targetPath: string): Promise<void> {\r\n const content = await fsReadFile(sourcePath, 'utf8');\r\n await atomicWrite(targetPath, content);\r\n logger.info('Atomic copy completed', { source: sourcePath, target: targetPath });\r\n}\r\n\r\n/**\r\n * Move file atomically\r\n *\r\n * @param sourcePath - Source file path\r\n * @param targetPath - Target file path\r\n * @returns Promise that resolves when move is complete\r\n */\r\nexport async function atomicMove(sourcePath: string, targetPath: string): Promise<void> {\r\n const absoluteSource = path.resolve(sourcePath);\r\n const absoluteTarget = path.resolve(targetPath);\r\n\r\n try {\r\n await fsRename(absoluteSource, absoluteTarget);\r\n logger.info('Atomic move completed', { source: absoluteSource, target: absoluteTarget });\r\n } catch (error) {\r\n // If rename fails (e.g., cross-device), fall back to copy + delete\r\n await atomicCopy(absoluteSource, absoluteTarget);\r\n await fsUnlink(absoluteSource);\r\n }\r\n}\r\n"],"names":["fs","path","promisify","randomUUID","createError","ErrorCode","createTimeoutError","createLogger","logger","fsWriteFile","writeFile","fsReadFile","readFile","fsRename","rename","fsUnlink","unlink","fsStat","stat","fsMkdir","mkdir","fsAccess","access","DEFAULT_LOCK_OPTIONS","timeoutMs","retryIntervalMs","staleTimeoutMs","atomicWrite","filePath","content","encoding","absolutePath","resolve","dir","dirname","tempPath","join","basename","ensureDirectory","debug","written","FILE_WRITE_FAILED","target","info","error","Error","undefined","acquireLock","options","opts","lockPath","lockId","startTime","Date","now","lockExists","fileExists","isStale","isLockStale","warn","sleep","lockData","acquired","pid","process","JSON","stringify","verifyData","verifyLock","parse","winnerId","LOCK_TIMEOUT","releaseLock","lock","currentData","currentLock","ourLockId","currentLockId","withLock","fn","acquiredTime","getTime","age","constants","F_OK","dirPath","recursive","code","ms","Promise","setTimeout","readFileWithRetry","maxRetries","lastError","attempt","String","Math","pow","FILE_NOT_FOUND","writeFileWithRetry","atomicCopy","sourcePath","targetPath","source","atomicMove","absoluteSource","absoluteTarget"],"mappings":"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"}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idempotent Write Utility
|
|
3
|
+
*
|
|
4
|
+
* Provides content-based deduplication for metrics logging using SHA256 hashing.
|
|
5
|
+
* Part of Task 2.3: Unified Metrics and Execution Logging
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Content-based idempotency keys (prevents duplicate metrics)
|
|
9
|
+
* - TTL-based cleanup (24-hour retention)
|
|
10
|
+
* - Atomic write tracking across databases
|
|
11
|
+
*/ import * as crypto from 'crypto';
|
|
12
|
+
import { createLogger } from './logging.js';
|
|
13
|
+
const logger = createLogger('idempotent-write');
|
|
14
|
+
/**
|
|
15
|
+
* Default TTL for idempotency keys (24 hours)
|
|
16
|
+
*/ const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
17
|
+
/**
|
|
18
|
+
* Create idempotency key from execution metrics
|
|
19
|
+
*
|
|
20
|
+
* Key is based on: agent_id + task_id + timestamp + duration_ms
|
|
21
|
+
* This combination ensures uniqueness while allowing retries to be detected
|
|
22
|
+
*
|
|
23
|
+
* @param metrics - Execution metrics to create key from
|
|
24
|
+
* @returns SHA256 hash as idempotency key
|
|
25
|
+
*/ export function createIdempotentKey(metrics) {
|
|
26
|
+
// Normalize timestamp to ISO string for consistent hashing
|
|
27
|
+
const timestampStr = metrics.timestamp instanceof Date ? metrics.timestamp.toISOString() : new Date(metrics.timestamp).toISOString();
|
|
28
|
+
// Build content string for hashing
|
|
29
|
+
const content = [
|
|
30
|
+
metrics.agent_id,
|
|
31
|
+
metrics.task_id,
|
|
32
|
+
timestampStr,
|
|
33
|
+
metrics.duration_ms.toString(),
|
|
34
|
+
metrics.status
|
|
35
|
+
].join(':');
|
|
36
|
+
// Generate SHA256 hash
|
|
37
|
+
const hash = crypto.createHash('sha256').update(content).digest('hex');
|
|
38
|
+
logger.debug('Created idempotency key', {
|
|
39
|
+
agent_id: metrics.agent_id,
|
|
40
|
+
task_id: metrics.task_id,
|
|
41
|
+
key: hash.substring(0, 16) + '...'
|
|
42
|
+
});
|
|
43
|
+
return hash;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Check if metrics have already been written
|
|
47
|
+
*
|
|
48
|
+
* @param key - Idempotency key to check
|
|
49
|
+
* @param db - Database adapter
|
|
50
|
+
* @returns True if already written, false otherwise
|
|
51
|
+
*/ export async function hasBeenWritten(key, db) {
|
|
52
|
+
try {
|
|
53
|
+
// Query idempotency_keys table
|
|
54
|
+
const result = await db.get(`idempotency_keys:${key}`);
|
|
55
|
+
if (!result) {
|
|
56
|
+
logger.debug('Idempotency key not found', {
|
|
57
|
+
key: key.substring(0, 16) + '...'
|
|
58
|
+
});
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
// Check if key has expired
|
|
62
|
+
const expiresAt = new Date(result.expires_at);
|
|
63
|
+
const now = new Date();
|
|
64
|
+
if (expiresAt < now) {
|
|
65
|
+
logger.debug('Idempotency key expired', {
|
|
66
|
+
key: key.substring(0, 16) + '...',
|
|
67
|
+
expired_at: expiresAt.toISOString()
|
|
68
|
+
});
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
logger.debug('Idempotency key found', {
|
|
72
|
+
key: key.substring(0, 16) + '...',
|
|
73
|
+
written_at: result.written_at
|
|
74
|
+
});
|
|
75
|
+
return true;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
logger.error('Failed to check idempotency key', {
|
|
78
|
+
key: key.substring(0, 16) + '...',
|
|
79
|
+
error: error instanceof Error ? error.message : String(error)
|
|
80
|
+
});
|
|
81
|
+
// On error, assume not written to allow retry
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Mark metrics as written
|
|
87
|
+
*
|
|
88
|
+
* @param key - Idempotency key
|
|
89
|
+
* @param db - Database adapter
|
|
90
|
+
* @param metricsId - Optional metrics ID to link
|
|
91
|
+
* @param ttlMs - Time-to-live in milliseconds (default: 24 hours)
|
|
92
|
+
*/ export async function markWritten(key, db, metricsId, ttlMs = DEFAULT_TTL_MS) {
|
|
93
|
+
try {
|
|
94
|
+
const now = new Date();
|
|
95
|
+
const expiresAt = new Date(now.getTime() + ttlMs);
|
|
96
|
+
const idempotencyRecord = {
|
|
97
|
+
key,
|
|
98
|
+
metrics_id: metricsId,
|
|
99
|
+
written_at: now,
|
|
100
|
+
expires_at: expiresAt
|
|
101
|
+
};
|
|
102
|
+
// Insert into idempotency_keys table
|
|
103
|
+
await db.insert('idempotency_keys', idempotencyRecord);
|
|
104
|
+
logger.debug('Marked idempotency key as written', {
|
|
105
|
+
key: key.substring(0, 16) + '...',
|
|
106
|
+
metrics_id: metricsId,
|
|
107
|
+
expires_at: expiresAt.toISOString()
|
|
108
|
+
});
|
|
109
|
+
} catch (error) {
|
|
110
|
+
logger.error('Failed to mark idempotency key', {
|
|
111
|
+
key: key.substring(0, 16) + '...',
|
|
112
|
+
error: error instanceof Error ? error.message : String(error)
|
|
113
|
+
});
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Batch check for written metrics
|
|
119
|
+
*
|
|
120
|
+
* @param keys - Array of idempotency keys
|
|
121
|
+
* @param db - Database adapter
|
|
122
|
+
* @returns Map of key -> boolean (written status)
|
|
123
|
+
*/ export async function batchCheckWritten(keys, db) {
|
|
124
|
+
const results = new Map();
|
|
125
|
+
try {
|
|
126
|
+
// Check all keys in parallel
|
|
127
|
+
const checks = keys.map(async (key)=>{
|
|
128
|
+
const written = await hasBeenWritten(key, db);
|
|
129
|
+
return {
|
|
130
|
+
key,
|
|
131
|
+
written
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
const checkResults = await Promise.all(checks);
|
|
135
|
+
// Build result map
|
|
136
|
+
checkResults.forEach(({ key, written })=>{
|
|
137
|
+
results.set(key, written);
|
|
138
|
+
});
|
|
139
|
+
logger.debug('Batch idempotency check complete', {
|
|
140
|
+
total_keys: keys.length,
|
|
141
|
+
already_written: Array.from(results.values()).filter((v)=>v).length
|
|
142
|
+
});
|
|
143
|
+
return results;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
logger.error('Failed to batch check idempotency keys', {
|
|
146
|
+
total_keys: keys.length,
|
|
147
|
+
error: error instanceof Error ? error.message : String(error)
|
|
148
|
+
});
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Batch mark metrics as written
|
|
154
|
+
*
|
|
155
|
+
* @param entries - Array of { key, metricsId? } entries
|
|
156
|
+
* @param db - Database adapter
|
|
157
|
+
* @param ttlMs - Time-to-live in milliseconds (default: 24 hours)
|
|
158
|
+
*/ export async function batchMarkWritten(entries, db, ttlMs = DEFAULT_TTL_MS) {
|
|
159
|
+
try {
|
|
160
|
+
const now = new Date();
|
|
161
|
+
const expiresAt = new Date(now.getTime() + ttlMs);
|
|
162
|
+
const records = entries.map(({ key, metricsId })=>({
|
|
163
|
+
key,
|
|
164
|
+
metrics_id: metricsId,
|
|
165
|
+
written_at: now,
|
|
166
|
+
expires_at: expiresAt
|
|
167
|
+
}));
|
|
168
|
+
// Batch insert
|
|
169
|
+
await db.insertMany('idempotency_keys', records);
|
|
170
|
+
logger.debug('Batch marked idempotency keys as written', {
|
|
171
|
+
total_keys: entries.length,
|
|
172
|
+
expires_at: expiresAt.toISOString()
|
|
173
|
+
});
|
|
174
|
+
} catch (error) {
|
|
175
|
+
logger.error('Failed to batch mark idempotency keys', {
|
|
176
|
+
total_keys: entries.length,
|
|
177
|
+
error: error instanceof Error ? error.message : String(error)
|
|
178
|
+
});
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Cleanup expired idempotency keys
|
|
184
|
+
*
|
|
185
|
+
* @param db - Database adapter
|
|
186
|
+
* @returns Number of keys deleted
|
|
187
|
+
*/ export async function cleanupExpiredKeys(db) {
|
|
188
|
+
try {
|
|
189
|
+
const now = new Date();
|
|
190
|
+
// Query for expired keys
|
|
191
|
+
const expiredKeys = await db.query('idempotency_keys', [
|
|
192
|
+
{
|
|
193
|
+
field: 'expires_at',
|
|
194
|
+
operator: 'lt',
|
|
195
|
+
value: now
|
|
196
|
+
}
|
|
197
|
+
]);
|
|
198
|
+
// Delete expired keys
|
|
199
|
+
let deletedCount = 0;
|
|
200
|
+
for (const key of expiredKeys){
|
|
201
|
+
await db.delete('idempotency_keys', key.key);
|
|
202
|
+
deletedCount++;
|
|
203
|
+
}
|
|
204
|
+
logger.info('Cleaned up expired idempotency keys', {
|
|
205
|
+
deleted_count: deletedCount
|
|
206
|
+
});
|
|
207
|
+
return deletedCount;
|
|
208
|
+
} catch (error) {
|
|
209
|
+
logger.error('Failed to cleanup expired idempotency keys', {
|
|
210
|
+
error: error instanceof Error ? error.message : String(error)
|
|
211
|
+
});
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Validate cost accuracy (within $0.001 precision)
|
|
217
|
+
*
|
|
218
|
+
* @param cost - Cost value to validate
|
|
219
|
+
* @returns True if cost is accurate to $0.001
|
|
220
|
+
*/ export function validateCostAccuracy(cost) {
|
|
221
|
+
const rounded = parseFloat(cost.toFixed(3));
|
|
222
|
+
return Math.abs(cost - rounded) < 0.0001;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Round cost to $0.001 precision
|
|
226
|
+
*
|
|
227
|
+
* Uses standard "round half up" behavior for consistent rounding.
|
|
228
|
+
*
|
|
229
|
+
* @param cost - Cost value to round
|
|
230
|
+
* @returns Cost rounded to 3 decimal places
|
|
231
|
+
*/ export function roundCost(cost) {
|
|
232
|
+
// Use Math.round for proper "round half up" behavior
|
|
233
|
+
// Multiply by 1000, round, then divide by 1000 for $0.001 precision
|
|
234
|
+
return Math.round(cost * 1000) / 1000;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
//# sourceMappingURL=idempotent-write.js.map
|