@liangjie559567/ultrapower 5.5.12 → 5.5.13
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-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/LICENSE +21 -21
- package/bridge/gyoshu_bridge.py +846 -846
- package/bridge/mcp-server.cjs +145 -38
- package/commands/wizard.md +5 -0
- package/dist/agents/__tests__/agent-wrapper.test.d.ts +2 -0
- package/dist/agents/__tests__/agent-wrapper.test.d.ts.map +1 -0
- package/dist/agents/__tests__/agent-wrapper.test.js +37 -0
- package/dist/agents/__tests__/agent-wrapper.test.js.map +1 -0
- package/dist/agents/__tests__/timeout-config.test.d.ts +2 -0
- package/dist/agents/__tests__/timeout-config.test.d.ts.map +1 -0
- package/dist/agents/__tests__/timeout-config.test.js +35 -0
- package/dist/agents/__tests__/timeout-config.test.js.map +1 -0
- package/dist/agents/__tests__/timeout-manager.test.d.ts +2 -0
- package/dist/agents/__tests__/timeout-manager.test.d.ts.map +1 -0
- package/dist/agents/__tests__/timeout-manager.test.js +37 -0
- package/dist/agents/__tests__/timeout-manager.test.js.map +1 -0
- package/dist/agents/agent-wrapper.d.ts +22 -0
- package/dist/agents/agent-wrapper.d.ts.map +1 -0
- package/dist/agents/agent-wrapper.js +51 -0
- package/dist/agents/agent-wrapper.js.map +1 -0
- package/dist/agents/coordinator-deprecated.d.ts +18 -0
- package/dist/agents/coordinator-deprecated.d.ts.map +1 -0
- package/dist/agents/coordinator-deprecated.js +38 -0
- package/dist/agents/coordinator-deprecated.js.map +1 -0
- package/dist/agents/index.d.ts +3 -0
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +4 -0
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/timeout-config.d.ts +19 -0
- package/dist/agents/timeout-config.d.ts.map +1 -0
- package/dist/agents/timeout-config.js +57 -0
- package/dist/agents/timeout-config.js.map +1 -0
- package/dist/agents/timeout-manager.d.ts +30 -0
- package/dist/agents/timeout-manager.d.ts.map +1 -0
- package/dist/agents/timeout-manager.js +47 -0
- package/dist/agents/timeout-manager.js.map +1 -0
- package/dist/analytics/analytics-summary.d.ts.map +1 -1
- package/dist/analytics/analytics-summary.js +7 -1
- package/dist/analytics/analytics-summary.js.map +1 -1
- package/dist/analytics/metrics-collector.d.ts.map +1 -1
- package/dist/analytics/metrics-collector.js +9 -1
- package/dist/analytics/metrics-collector.js.map +1 -1
- package/dist/analytics/query-engine.d.ts.map +1 -1
- package/dist/analytics/query-engine.js +21 -3
- package/dist/analytics/query-engine.js.map +1 -1
- package/dist/analytics/token-tracker.js +3 -3
- package/dist/analytics/token-tracker.js.map +1 -1
- package/dist/analytics/transcript-scanner.d.ts.map +1 -1
- package/dist/analytics/transcript-scanner.js +1 -0
- package/dist/analytics/transcript-scanner.js.map +1 -1
- package/dist/audit/logger.d.ts +28 -0
- package/dist/audit/logger.d.ts.map +1 -0
- package/dist/audit/logger.js +78 -0
- package/dist/audit/logger.js.map +1 -0
- package/dist/audit/verify-cli.d.ts +2 -0
- package/dist/audit/verify-cli.d.ts.map +1 -0
- package/dist/audit/verify-cli.js +10 -0
- package/dist/audit/verify-cli.js.map +1 -0
- package/dist/core/hud-config.d.ts +19 -0
- package/dist/core/hud-config.d.ts.map +1 -0
- package/dist/core/hud-config.js +6 -0
- package/dist/core/hud-config.js.map +1 -0
- package/dist/core/job-types.d.ts +22 -0
- package/dist/core/job-types.d.ts.map +1 -0
- package/dist/core/job-types.js +6 -0
- package/dist/core/job-types.js.map +1 -0
- package/dist/features/diagnostics/error-matcher.d.ts +12 -0
- package/dist/features/diagnostics/error-matcher.d.ts.map +1 -0
- package/dist/features/diagnostics/error-matcher.js +41 -0
- package/dist/features/diagnostics/error-matcher.js.map +1 -0
- package/dist/features/diagnostics/index.d.ts +3 -0
- package/dist/features/diagnostics/index.d.ts.map +1 -0
- package/dist/features/diagnostics/index.js +3 -0
- package/dist/features/diagnostics/index.js.map +1 -0
- package/dist/features/diagnostics/solution-suggester.d.ts +12 -0
- package/dist/features/diagnostics/solution-suggester.d.ts.map +1 -0
- package/dist/features/diagnostics/solution-suggester.js +46 -0
- package/dist/features/diagnostics/solution-suggester.js.map +1 -0
- package/dist/features/diagnostics/types.d.ts +25 -0
- package/dist/features/diagnostics/types.d.ts.map +1 -0
- package/dist/features/diagnostics/types.js +5 -0
- package/dist/features/diagnostics/types.js.map +1 -0
- package/dist/features/state-manager/__tests__/cache.test.js +17 -17
- package/dist/features/state-manager/__tests__/cache.test.js.map +1 -1
- package/dist/features/state-manager/__tests__/encryption-performance.test.d.ts +2 -0
- package/dist/features/state-manager/__tests__/encryption-performance.test.d.ts.map +1 -0
- package/dist/features/state-manager/__tests__/encryption-performance.test.js +42 -0
- package/dist/features/state-manager/__tests__/encryption-performance.test.js.map +1 -0
- package/dist/features/state-manager/__tests__/encryption.test.d.ts +2 -0
- package/dist/features/state-manager/__tests__/encryption.test.d.ts.map +1 -0
- package/dist/features/state-manager/__tests__/encryption.test.js +68 -0
- package/dist/features/state-manager/__tests__/encryption.test.js.map +1 -0
- package/dist/features/state-manager/encryption.d.ts +24 -0
- package/dist/features/state-manager/encryption.d.ts.map +1 -0
- package/dist/features/state-manager/encryption.js +86 -0
- package/dist/features/state-manager/encryption.js.map +1 -0
- package/dist/features/state-manager/index.d.ts +4 -0
- package/dist/features/state-manager/index.d.ts.map +1 -1
- package/dist/features/state-manager/index.js +94 -6
- package/dist/features/state-manager/index.js.map +1 -1
- package/dist/features/state-manager/tiered-writer.d.ts +44 -0
- package/dist/features/state-manager/tiered-writer.d.ts.map +1 -0
- package/dist/features/state-manager/tiered-writer.js +76 -0
- package/dist/features/state-manager/tiered-writer.js.map +1 -0
- package/dist/features/state-manager/wal.d.ts +21 -0
- package/dist/features/state-manager/wal.d.ts.map +1 -0
- package/dist/features/state-manager/wal.js +75 -0
- package/dist/features/state-manager/wal.js.map +1 -0
- package/dist/features/task-templates/index.d.ts +13 -0
- package/dist/features/task-templates/index.d.ts.map +1 -0
- package/dist/features/task-templates/index.js +31 -0
- package/dist/features/task-templates/index.js.map +1 -0
- package/dist/features/task-templates/wizard-integration.d.ts +15 -0
- package/dist/features/task-templates/wizard-integration.d.ts.map +1 -0
- package/dist/features/task-templates/wizard-integration.js +27 -0
- package/dist/features/task-templates/wizard-integration.js.map +1 -0
- package/dist/features/wizard/__tests__/engine.test.d.ts +2 -0
- package/dist/features/wizard/__tests__/engine.test.d.ts.map +1 -0
- package/dist/features/wizard/__tests__/engine.test.js +78 -0
- package/dist/features/wizard/__tests__/engine.test.js.map +1 -0
- package/dist/features/wizard/__tests__/recommendation-engine.test.d.ts +2 -0
- package/dist/features/wizard/__tests__/recommendation-engine.test.d.ts.map +1 -0
- package/dist/features/wizard/__tests__/recommendation-engine.test.js +43 -0
- package/dist/features/wizard/__tests__/recommendation-engine.test.js.map +1 -0
- package/dist/features/wizard/engine.d.ts +15 -0
- package/dist/features/wizard/engine.d.ts.map +1 -0
- package/dist/features/wizard/engine.js +74 -0
- package/dist/features/wizard/engine.js.map +1 -0
- package/dist/features/wizard/index.d.ts +8 -0
- package/dist/features/wizard/index.d.ts.map +1 -0
- package/dist/features/wizard/index.js +7 -0
- package/dist/features/wizard/index.js.map +1 -0
- package/dist/features/wizard/questions.d.ts +6 -0
- package/dist/features/wizard/questions.d.ts.map +1 -0
- package/dist/features/wizard/questions.js +64 -0
- package/dist/features/wizard/questions.js.map +1 -0
- package/dist/features/wizard/recommendation-engine.d.ts +6 -0
- package/dist/features/wizard/recommendation-engine.d.ts.map +1 -0
- package/dist/features/wizard/recommendation-engine.js +33 -0
- package/dist/features/wizard/recommendation-engine.js.map +1 -0
- package/dist/features/wizard/types.d.ts +23 -0
- package/dist/features/wizard/types.d.ts.map +1 -0
- package/dist/features/wizard/types.js +5 -0
- package/dist/features/wizard/types.js.map +1 -0
- package/dist/features/workflow-recommender/context-analyzer.d.ts +6 -0
- package/dist/features/workflow-recommender/context-analyzer.d.ts.map +1 -0
- package/dist/features/workflow-recommender/context-analyzer.js +20 -0
- package/dist/features/workflow-recommender/context-analyzer.js.map +1 -0
- package/dist/features/workflow-recommender/index.d.ts +8 -0
- package/dist/features/workflow-recommender/index.d.ts.map +1 -0
- package/dist/features/workflow-recommender/index.js +7 -0
- package/dist/features/workflow-recommender/index.js.map +1 -0
- package/dist/features/workflow-recommender/intent-classifier.d.ts +6 -0
- package/dist/features/workflow-recommender/intent-classifier.d.ts.map +1 -0
- package/dist/features/workflow-recommender/intent-classifier.js +24 -0
- package/dist/features/workflow-recommender/intent-classifier.js.map +1 -0
- package/dist/features/workflow-recommender/recommendation-engine.d.ts +6 -0
- package/dist/features/workflow-recommender/recommendation-engine.d.ts.map +1 -0
- package/dist/features/workflow-recommender/recommendation-engine.js +110 -0
- package/dist/features/workflow-recommender/recommendation-engine.js.map +1 -0
- package/dist/features/workflow-recommender/types.d.ts +20 -0
- package/dist/features/workflow-recommender/types.d.ts.map +1 -0
- package/dist/features/workflow-recommender/types.js +5 -0
- package/dist/features/workflow-recommender/types.js.map +1 -0
- package/dist/hooks/__tests__/bridge-normalize.test.d.ts +2 -0
- package/dist/hooks/__tests__/bridge-normalize.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/bridge-normalize.test.js +90 -0
- package/dist/hooks/__tests__/bridge-normalize.test.js.map +1 -0
- package/dist/hooks/__tests__/bridge-security.test.js +23 -41
- package/dist/hooks/__tests__/bridge-security.test.js.map +1 -1
- package/dist/hooks/auto-slash-command/__tests__/detector.test.d.ts +2 -0
- package/dist/hooks/auto-slash-command/__tests__/detector.test.d.ts.map +1 -0
- package/dist/hooks/auto-slash-command/__tests__/detector.test.js +70 -0
- package/dist/hooks/auto-slash-command/__tests__/detector.test.js.map +1 -0
- package/dist/hooks/auto-slash-command/__tests__/executor.test.d.ts +2 -0
- package/dist/hooks/auto-slash-command/__tests__/executor.test.d.ts.map +1 -0
- package/dist/hooks/auto-slash-command/__tests__/executor.test.js +55 -0
- package/dist/hooks/auto-slash-command/__tests__/executor.test.js.map +1 -0
- package/dist/hooks/auto-slash-command/__tests__/index.test.d.ts +2 -0
- package/dist/hooks/auto-slash-command/__tests__/index.test.d.ts.map +1 -0
- package/dist/hooks/auto-slash-command/__tests__/index.test.js +50 -0
- package/dist/hooks/auto-slash-command/__tests__/index.test.js.map +1 -0
- package/dist/hooks/autopilot/__tests__/prompts.test.js +19 -1
- package/dist/hooks/autopilot/__tests__/prompts.test.js.map +1 -1
- package/dist/hooks/autopilot/enforcement.d.ts +1 -1
- package/dist/hooks/autopilot/enforcement.d.ts.map +1 -1
- package/dist/hooks/autopilot/enforcement.js +1 -1
- package/dist/hooks/autopilot/enforcement.js.map +1 -1
- package/dist/hooks/bridge-normalize.d.ts +43 -3
- package/dist/hooks/bridge-normalize.d.ts.map +1 -1
- package/dist/hooks/bridge-normalize.js +110 -15
- package/dist/hooks/bridge-normalize.js.map +1 -1
- package/dist/hooks/bridge-types.d.ts +48 -0
- package/dist/hooks/bridge-types.d.ts.map +1 -0
- package/dist/hooks/bridge-types.js +6 -0
- package/dist/hooks/bridge-types.js.map +1 -0
- package/dist/hooks/bridge.d.ts +1 -43
- package/dist/hooks/bridge.d.ts.map +1 -1
- package/dist/hooks/bridge.js +18 -2
- package/dist/hooks/bridge.js.map +1 -1
- package/dist/hooks/dependency-analyzer.d.ts +32 -0
- package/dist/hooks/dependency-analyzer.d.ts.map +1 -0
- package/dist/hooks/dependency-analyzer.js +199 -0
- package/dist/hooks/dependency-analyzer.js.map +1 -0
- package/dist/hooks/index.d.ts +2 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/learner/__tests__/detector.test.d.ts +2 -0
- package/dist/hooks/learner/__tests__/detector.test.d.ts.map +1 -0
- package/dist/hooks/learner/__tests__/detector.test.js +170 -0
- package/dist/hooks/learner/__tests__/detector.test.js.map +1 -0
- package/dist/hooks/learner/__tests__/index.test.d.ts +2 -0
- package/dist/hooks/learner/__tests__/index.test.d.ts.map +1 -0
- package/dist/hooks/learner/__tests__/index.test.js +48 -0
- package/dist/hooks/learner/__tests__/index.test.js.map +1 -0
- package/dist/hooks/learner/__tests__/matcher.test.d.ts +2 -0
- package/dist/hooks/learner/__tests__/matcher.test.d.ts.map +1 -0
- package/dist/hooks/learner/__tests__/matcher.test.js +114 -0
- package/dist/hooks/learner/__tests__/matcher.test.js.map +1 -0
- package/dist/hooks/learner/__tests__/promotion.test.d.ts +2 -0
- package/dist/hooks/learner/__tests__/promotion.test.d.ts.map +1 -0
- package/dist/hooks/learner/__tests__/promotion.test.js +146 -0
- package/dist/hooks/learner/__tests__/promotion.test.js.map +1 -0
- package/dist/hooks/learner/__tests__/validator.test.d.ts +2 -0
- package/dist/hooks/learner/__tests__/validator.test.d.ts.map +1 -0
- package/dist/hooks/learner/__tests__/validator.test.js +123 -0
- package/dist/hooks/learner/__tests__/validator.test.js.map +1 -0
- package/dist/hooks/learner/__tests__/writer.test.d.ts +2 -0
- package/dist/hooks/learner/__tests__/writer.test.d.ts.map +1 -0
- package/dist/hooks/learner/__tests__/writer.test.js +141 -0
- package/dist/hooks/learner/__tests__/writer.test.js.map +1 -0
- package/dist/hooks/learner/detection-hook.js +2 -2
- package/dist/hooks/learner/detection-hook.js.map +1 -1
- package/dist/hooks/parallel-executor.d.ts +24 -0
- package/dist/hooks/parallel-executor.d.ts.map +1 -0
- package/dist/hooks/parallel-executor.js +82 -0
- package/dist/hooks/parallel-executor.js.map +1 -0
- package/dist/hooks/persistent-mode/index.d.ts +2 -21
- package/dist/hooks/persistent-mode/index.d.ts.map +1 -1
- package/dist/hooks/persistent-mode/index.js +4 -85
- package/dist/hooks/persistent-mode/index.js.map +1 -1
- package/dist/hooks/persistent-mode/tool-error.d.ts +15 -0
- package/dist/hooks/persistent-mode/tool-error.d.ts.map +1 -0
- package/dist/hooks/persistent-mode/tool-error.js +80 -0
- package/dist/hooks/persistent-mode/tool-error.js.map +1 -0
- package/dist/hooks/pre-compact/index.d.ts.map +1 -1
- package/dist/hooks/pre-compact/index.js +0 -1
- package/dist/hooks/pre-compact/index.js.map +1 -1
- package/dist/hooks/project-memory/learner.d.ts +13 -1
- package/dist/hooks/project-memory/learner.d.ts.map +1 -1
- package/dist/hooks/project-memory/learner.js +24 -12
- package/dist/hooks/project-memory/learner.js.map +1 -1
- package/dist/hooks/race-detector.d.ts +51 -0
- package/dist/hooks/race-detector.d.ts.map +1 -0
- package/dist/hooks/race-detector.js +121 -0
- package/dist/hooks/race-detector.js.map +1 -0
- package/dist/hooks/ralph/__tests__/loop.test.d.ts +2 -0
- package/dist/hooks/ralph/__tests__/loop.test.d.ts.map +1 -0
- package/dist/hooks/ralph/__tests__/loop.test.js +268 -0
- package/dist/hooks/ralph/__tests__/loop.test.js.map +1 -0
- package/dist/hooks/ralph/__tests__/prd.test.d.ts +2 -0
- package/dist/hooks/ralph/__tests__/prd.test.d.ts.map +1 -0
- package/dist/hooks/ralph/__tests__/prd.test.js +197 -0
- package/dist/hooks/ralph/__tests__/prd.test.js.map +1 -0
- package/dist/hooks/ralph/__tests__/progress.test.d.ts +2 -0
- package/dist/hooks/ralph/__tests__/progress.test.d.ts.map +1 -0
- package/dist/hooks/ralph/__tests__/progress.test.js +120 -0
- package/dist/hooks/ralph/__tests__/progress.test.js.map +1 -0
- package/dist/hooks/ralph/__tests__/verifier.test.d.ts +2 -0
- package/dist/hooks/ralph/__tests__/verifier.test.d.ts.map +1 -0
- package/dist/hooks/ralph/__tests__/verifier.test.js +75 -0
- package/dist/hooks/ralph/__tests__/verifier.test.js.map +1 -0
- package/dist/hooks/recovery/__tests__/context-window.test.d.ts +2 -0
- package/dist/hooks/recovery/__tests__/context-window.test.d.ts.map +1 -0
- package/dist/hooks/recovery/__tests__/context-window.test.js +131 -0
- package/dist/hooks/recovery/__tests__/context-window.test.js.map +1 -0
- package/dist/hooks/recovery/__tests__/edit-error.test.d.ts +2 -0
- package/dist/hooks/recovery/__tests__/edit-error.test.d.ts.map +1 -0
- package/dist/hooks/recovery/__tests__/edit-error.test.js +88 -0
- package/dist/hooks/recovery/__tests__/edit-error.test.js.map +1 -0
- package/dist/hooks/recovery/__tests__/index.test.d.ts +2 -0
- package/dist/hooks/recovery/__tests__/index.test.d.ts.map +1 -0
- package/dist/hooks/recovery/__tests__/index.test.js +270 -0
- package/dist/hooks/recovery/__tests__/index.test.js.map +1 -0
- package/dist/hooks/recovery/__tests__/session-recovery.test.d.ts +2 -0
- package/dist/hooks/recovery/__tests__/session-recovery.test.d.ts.map +1 -0
- package/dist/hooks/recovery/__tests__/session-recovery.test.js +129 -0
- package/dist/hooks/recovery/__tests__/session-recovery.test.js.map +1 -0
- package/dist/hooks/recovery/__tests__/storage.test.d.ts +2 -0
- package/dist/hooks/recovery/__tests__/storage.test.d.ts.map +1 -0
- package/dist/hooks/recovery/__tests__/storage.test.js +549 -0
- package/dist/hooks/recovery/__tests__/storage.test.js.map +1 -0
- package/dist/hooks/rules-injector/__tests__/finder.test.d.ts +2 -0
- package/dist/hooks/rules-injector/__tests__/finder.test.d.ts.map +1 -0
- package/dist/hooks/rules-injector/__tests__/finder.test.js +68 -0
- package/dist/hooks/rules-injector/__tests__/finder.test.js.map +1 -0
- package/dist/hooks/rules-injector/__tests__/index.test.d.ts +2 -0
- package/dist/hooks/rules-injector/__tests__/index.test.d.ts.map +1 -0
- package/dist/hooks/rules-injector/__tests__/index.test.js +58 -0
- package/dist/hooks/rules-injector/__tests__/index.test.js.map +1 -0
- package/dist/hooks/rules-injector/__tests__/matcher.test.d.ts +2 -0
- package/dist/hooks/rules-injector/__tests__/matcher.test.d.ts.map +1 -0
- package/dist/hooks/rules-injector/__tests__/matcher.test.js +86 -0
- package/dist/hooks/rules-injector/__tests__/matcher.test.js.map +1 -0
- package/dist/hooks/rules-injector/__tests__/parser.test.d.ts +2 -0
- package/dist/hooks/rules-injector/__tests__/parser.test.d.ts.map +1 -0
- package/dist/hooks/rules-injector/__tests__/parser.test.js +86 -0
- package/dist/hooks/rules-injector/__tests__/parser.test.js.map +1 -0
- package/dist/hooks/session-end/__tests__/index.test.d.ts +2 -0
- package/dist/hooks/session-end/__tests__/index.test.d.ts.map +1 -0
- package/dist/hooks/session-end/__tests__/index.test.js +77 -0
- package/dist/hooks/session-end/__tests__/index.test.js.map +1 -0
- package/dist/hooks/session-end/callbacks.d.ts +1 -1
- package/dist/hooks/session-end/index.d.ts +2 -21
- package/dist/hooks/session-end/index.d.ts.map +1 -1
- package/dist/hooks/session-end/index.js.map +1 -1
- package/dist/hooks/session-end/types.d.ts +26 -0
- package/dist/hooks/session-end/types.d.ts.map +1 -0
- package/dist/hooks/session-end/types.js +6 -0
- package/dist/hooks/session-end/types.js.map +1 -0
- package/dist/hooks/setup/__tests__/index.test.d.ts +2 -0
- package/dist/hooks/setup/__tests__/index.test.d.ts.map +1 -0
- package/dist/hooks/setup/__tests__/index.test.js +68 -0
- package/dist/hooks/setup/__tests__/index.test.js.map +1 -0
- package/dist/hooks/team-pipeline/__tests__/state.test.d.ts +2 -0
- package/dist/hooks/team-pipeline/__tests__/state.test.d.ts.map +1 -0
- package/dist/hooks/team-pipeline/__tests__/state.test.js +94 -0
- package/dist/hooks/team-pipeline/__tests__/state.test.js.map +1 -0
- package/dist/hud/elements/autopilot.d.ts +1 -1
- package/dist/hud/elements/autopilot.d.ts.map +1 -1
- package/dist/hud/state.d.ts.map +1 -1
- package/dist/hud/state.js +69 -1
- package/dist/hud/state.js.map +1 -1
- package/dist/hud/types.d.ts +2 -15
- package/dist/hud/types.d.ts.map +1 -1
- package/dist/hud/types.js.map +1 -1
- package/dist/lib/__tests__/validateMode.test.d.ts +2 -0
- package/dist/lib/__tests__/validateMode.test.d.ts.map +1 -0
- package/dist/lib/__tests__/validateMode.test.js +61 -0
- package/dist/lib/__tests__/validateMode.test.js.map +1 -0
- package/dist/lib/path-validator.d.ts +25 -0
- package/dist/lib/path-validator.d.ts.map +1 -0
- package/dist/lib/path-validator.js +81 -0
- package/dist/lib/path-validator.js.map +1 -0
- package/dist/lib/validateMode.d.ts +3 -0
- package/dist/lib/validateMode.d.ts.map +1 -1
- package/dist/lib/validateMode.js +28 -2
- package/dist/lib/validateMode.js.map +1 -1
- package/dist/mcp/__tests__/codex-core.test.d.ts +2 -0
- package/dist/mcp/__tests__/codex-core.test.d.ts.map +1 -0
- package/dist/mcp/__tests__/codex-core.test.js +143 -0
- package/dist/mcp/__tests__/codex-core.test.js.map +1 -0
- package/dist/mcp/__tests__/gemini-core.test.d.ts +2 -0
- package/dist/mcp/__tests__/gemini-core.test.d.ts.map +1 -0
- package/dist/mcp/__tests__/gemini-core.test.js +53 -0
- package/dist/mcp/__tests__/gemini-core.test.js.map +1 -0
- package/dist/mcp/__tests__/job-state-db-deprecation.test.js +48 -1
- package/dist/mcp/__tests__/job-state-db-deprecation.test.js.map +1 -1
- package/dist/mcp/__tests__/omc-tools-server.test.d.ts +2 -0
- package/dist/mcp/__tests__/omc-tools-server.test.d.ts.map +1 -0
- package/dist/mcp/__tests__/omc-tools-server.test.js +108 -0
- package/dist/mcp/__tests__/omc-tools-server.test.js.map +1 -0
- package/dist/mcp/job-state-db.d.ts +1 -1
- package/dist/mcp/job-state-db.d.ts.map +1 -1
- package/dist/mcp/prompt-persistence.d.ts +2 -17
- package/dist/mcp/prompt-persistence.d.ts.map +1 -1
- package/dist/mcp/prompt-persistence.js.map +1 -1
- package/dist/team/__tests__/deadlock-detector.test.d.ts +2 -0
- package/dist/team/__tests__/deadlock-detector.test.d.ts.map +1 -0
- package/dist/team/__tests__/deadlock-detector.test.js +50 -0
- package/dist/team/__tests__/deadlock-detector.test.js.map +1 -0
- package/dist/team/__tests__/dependency-graph.test.d.ts +2 -0
- package/dist/team/__tests__/dependency-graph.test.d.ts.map +1 -0
- package/dist/team/__tests__/dependency-graph.test.js +29 -0
- package/dist/team/__tests__/dependency-graph.test.js.map +1 -0
- package/dist/team/capabilities.d.ts +1 -2
- package/dist/team/capabilities.d.ts.map +1 -1
- package/dist/team/capabilities.js.map +1 -1
- package/dist/team/deadlock-detector.d.ts +16 -0
- package/dist/team/deadlock-detector.d.ts.map +1 -0
- package/dist/team/deadlock-detector.js +52 -0
- package/dist/team/deadlock-detector.js.map +1 -0
- package/dist/team/dependency-graph.d.ts +23 -0
- package/dist/team/dependency-graph.d.ts.map +1 -0
- package/dist/team/dependency-graph.js +35 -0
- package/dist/team/dependency-graph.js.map +1 -0
- package/dist/team/index.d.ts +3 -0
- package/dist/team/index.d.ts.map +1 -1
- package/dist/team/index.js +2 -0
- package/dist/team/index.js.map +1 -1
- package/dist/team/types.d.ts +15 -4
- package/dist/team/types.d.ts.map +1 -1
- package/dist/team/types.js +0 -1
- package/dist/team/types.js.map +1 -1
- package/dist/team/unified-team.d.ts +2 -11
- package/dist/team/unified-team.d.ts.map +1 -1
- package/dist/team/unified-team.js.map +1 -1
- package/dist/tools/__tests__/ast-tools.test.d.ts +2 -0
- package/dist/tools/__tests__/ast-tools.test.d.ts.map +1 -0
- package/dist/tools/__tests__/ast-tools.test.js +178 -0
- package/dist/tools/__tests__/ast-tools.test.js.map +1 -0
- package/dist/tools/__tests__/lsp-tools.test.d.ts +2 -0
- package/dist/tools/__tests__/lsp-tools.test.d.ts.map +1 -0
- package/dist/tools/__tests__/lsp-tools.test.js +252 -0
- package/dist/tools/__tests__/lsp-tools.test.js.map +1 -0
- package/dist/tools/diagnostics/__tests__/index.test.d.ts +2 -0
- package/dist/tools/diagnostics/__tests__/index.test.d.ts.map +1 -0
- package/dist/tools/diagnostics/__tests__/index.test.js +111 -0
- package/dist/tools/diagnostics/__tests__/index.test.js.map +1 -0
- package/dist/tools/diagnostics/__tests__/lsp-aggregator.test.d.ts +2 -0
- package/dist/tools/diagnostics/__tests__/lsp-aggregator.test.d.ts.map +1 -0
- package/dist/tools/diagnostics/__tests__/lsp-aggregator.test.js +120 -0
- package/dist/tools/diagnostics/__tests__/lsp-aggregator.test.js.map +1 -0
- package/dist/tools/diagnostics/__tests__/tsc-runner.test.d.ts +2 -0
- package/dist/tools/diagnostics/__tests__/tsc-runner.test.d.ts.map +1 -0
- package/dist/tools/diagnostics/__tests__/tsc-runner.test.js +86 -0
- package/dist/tools/diagnostics/__tests__/tsc-runner.test.js.map +1 -0
- package/dist/tools/diagnostics/constants.d.ts +5 -0
- package/dist/tools/diagnostics/constants.d.ts.map +1 -0
- package/dist/tools/diagnostics/constants.js +5 -0
- package/dist/tools/diagnostics/constants.js.map +1 -0
- package/dist/tools/diagnostics/index.d.ts +2 -1
- package/dist/tools/diagnostics/index.d.ts.map +1 -1
- package/dist/tools/diagnostics/index.js +2 -1
- package/dist/tools/diagnostics/index.js.map +1 -1
- package/dist/tools/diagnostics/lsp-aggregator.js +1 -1
- package/dist/tools/diagnostics/lsp-aggregator.js.map +1 -1
- package/dist/tools/lsp/__tests__/utils.test.d.ts +2 -0
- package/dist/tools/lsp/__tests__/utils.test.d.ts.map +1 -0
- package/dist/tools/lsp/__tests__/utils.test.js +338 -0
- package/dist/tools/lsp/__tests__/utils.test.js.map +1 -0
- package/dist/tools/lsp/utils.d.ts.map +1 -1
- package/dist/tools/lsp/utils.js +2 -2
- package/dist/tools/lsp/utils.js.map +1 -1
- package/dist/tools/python-repl/__tests__/bridge-manager.test.d.ts +2 -0
- package/dist/tools/python-repl/__tests__/bridge-manager.test.d.ts.map +1 -0
- package/dist/tools/python-repl/__tests__/bridge-manager.test.js +338 -0
- package/dist/tools/python-repl/__tests__/bridge-manager.test.js.map +1 -0
- package/dist/tools/python-repl/__tests__/socket-client.test.d.ts +2 -0
- package/dist/tools/python-repl/__tests__/socket-client.test.d.ts.map +1 -0
- package/dist/tools/python-repl/__tests__/socket-client.test.js +155 -0
- package/dist/tools/python-repl/__tests__/socket-client.test.js.map +1 -0
- package/dist/tools/python-repl/bridge-manager.d.ts +4 -0
- package/dist/tools/python-repl/bridge-manager.d.ts.map +1 -1
- package/dist/tools/python-repl/bridge-manager.js +4 -1
- package/dist/tools/python-repl/bridge-manager.js.map +1 -1
- package/docs/guides/task-templates-guide.md +153 -0
- package/docs/guides/troubleshooting-guide.md +110 -0
- package/docs/guides/wizard-user-guide.md +85 -0
- package/docs/guides/workflow-recommendation-guide.md +97 -0
- package/docs/reviews/ultrapower-security/review_critic.md +450 -0
- package/docs/reviews/ultrapower-tech-review/review_tech.md +180 -0
- package/docs/troubleshooting/agent-timeouts.md +37 -0
- package/docs/troubleshooting/common-errors.md +37 -0
- package/docs/troubleshooting/hook-failures.md +29 -0
- package/docs/troubleshooting/performance-issues.md +41 -0
- package/docs/troubleshooting/state-corruption.md +36 -0
- package/hooks/run-hook.cmd +0 -0
- package/hooks/session-start +0 -0
- package/package.json +1 -1
- package/scripts/analyze-dependencies.ts +47 -0
- package/scripts/analyze-hook-coverage.ts +55 -0
- package/scripts/performance-regression.ts +28 -0
- package/scripts/persistent-mode.cjs +605 -605
- package/scripts/profiling.ts +95 -0
- package/scripts/run-profiling.ts +64 -0
- package/scripts/test-parallel-execution.ts +72 -0
- package/scripts/test-race-detection.ts +57 -0
- package/scripts/test-tiered-writer.ts +60 -0
- package/scripts/test-wal-integration.ts +29 -0
- package/scripts/test-wal.ts +48 -0
- package/skills/next-step-router/SKILL.md +17 -0
- package/skills/systematic-debugging/find-polluter.sh +0 -0
- package/skills/wizard/SKILL.md +103 -72
- package/skills/writing-skills/graphviz-conventions.dot +171 -171
- package/skills/writing-skills/render-graphs.js +0 -0
- package/templates/axiom/scripts/Check-Memory.ps1 +68 -68
- package/templates/axiom/scripts/Poll-Memory.ps1 +36 -36
- package/templates/axiom/scripts/Watch-Memory.ps1 +149 -149
- package/templates/axiom/scripts/agent-runner.ps1 +101 -101
- package/templates/axiom/scripts/start-reviews.ps1 +19 -19
- package/templates/tasks/README.md +45 -0
- package/templates/tasks/bug-fix.md +37 -0
- package/templates/tasks/code-review.md +36 -0
- package/templates/tasks/feature-development.md +43 -0
- package/templates/tasks/refactoring.md +37 -0
- package/templates/tasks/security-audit.md +37 -0
|
@@ -1,605 +1,605 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* OMC Persistent Mode Hook (Node.js)
|
|
5
|
-
* Minimal continuation enforcer for all OMC modes.
|
|
6
|
-
* Stripped down for reliability — no optional imports, no PRD, no notepad pruning.
|
|
7
|
-
*
|
|
8
|
-
* Supported modes: ralph, autopilot, ultrapilot, swarm, ultrawork, ultraqa, pipeline
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
const {
|
|
12
|
-
existsSync,
|
|
13
|
-
readFileSync,
|
|
14
|
-
writeFileSync,
|
|
15
|
-
readdirSync,
|
|
16
|
-
mkdirSync,
|
|
17
|
-
} = require("fs");
|
|
18
|
-
const { join, dirname, resolve, normalize } = require("path");
|
|
19
|
-
const { homedir } = require("os");
|
|
20
|
-
|
|
21
|
-
async function readStdin(timeoutMs = 5000) {
|
|
22
|
-
return new Promise((resolve) => {
|
|
23
|
-
const chunks = [];
|
|
24
|
-
let settled = false;
|
|
25
|
-
const timeout = setTimeout(() => {
|
|
26
|
-
if (!settled) { settled = true; process.stdin.removeAllListeners(); process.stdin.destroy(); resolve(Buffer.concat(chunks).toString("utf-8")); }
|
|
27
|
-
}, timeoutMs);
|
|
28
|
-
process.stdin.on("data", (chunk) => { chunks.push(chunk); });
|
|
29
|
-
process.stdin.on("end", () => { if (!settled) { settled = true; clearTimeout(timeout); resolve(Buffer.concat(chunks).toString("utf-8")); } });
|
|
30
|
-
process.stdin.on("error", () => { if (!settled) { settled = true; clearTimeout(timeout); resolve(""); } });
|
|
31
|
-
if (process.stdin.readableEnded) { if (!settled) { settled = true; clearTimeout(timeout); resolve(Buffer.concat(chunks).toString("utf-8")); } }
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function readJsonFile(path) {
|
|
36
|
-
try {
|
|
37
|
-
if (!existsSync(path)) return null;
|
|
38
|
-
return JSON.parse(readFileSync(path, "utf-8"));
|
|
39
|
-
} catch {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function writeJsonFile(path, data) {
|
|
45
|
-
try {
|
|
46
|
-
// Ensure directory exists
|
|
47
|
-
const dir = dirname(path);
|
|
48
|
-
if (dir && dir !== "." && !existsSync(dir)) {
|
|
49
|
-
mkdirSync(dir, { recursive: true });
|
|
50
|
-
}
|
|
51
|
-
writeFileSync(path, JSON.stringify(data, null, 2));
|
|
52
|
-
return true;
|
|
53
|
-
} catch {
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Send stop notification (fire-and-forget, non-blocking).
|
|
60
|
-
* Only notifies on first stop to avoid spam.
|
|
61
|
-
*/
|
|
62
|
-
async function sendStopNotification(modeName, stateData, sessionId, directory) {
|
|
63
|
-
// Only notify once per mode activation
|
|
64
|
-
if (stateData._stopNotified) return;
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT;
|
|
68
|
-
if (!pluginRoot) return;
|
|
69
|
-
|
|
70
|
-
const { pathToFileURL } = require('url');
|
|
71
|
-
const { notify } = await import(pathToFileURL(join(pluginRoot, 'dist', 'notifications', 'index.js')).href);
|
|
72
|
-
|
|
73
|
-
await notify('session-stop', {
|
|
74
|
-
sessionId: sessionId,
|
|
75
|
-
projectPath: directory,
|
|
76
|
-
activeMode: modeName,
|
|
77
|
-
iteration: stateData.iteration || stateData.reinforcement_count || 1,
|
|
78
|
-
maxIterations: stateData.max_iterations || stateData.max_reinforcements || 100,
|
|
79
|
-
incompleteTasks: undefined, // Caller can override
|
|
80
|
-
}).catch(() => {});
|
|
81
|
-
|
|
82
|
-
// Mark as notified to prevent duplicate notifications
|
|
83
|
-
stateData._stopNotified = true;
|
|
84
|
-
} catch {
|
|
85
|
-
// Notification module not available, skip silently
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Staleness threshold for mode states (2 hours in milliseconds).
|
|
91
|
-
* States older than this are treated as inactive to prevent stale state
|
|
92
|
-
* from causing the stop hook to malfunction in new sessions.
|
|
93
|
-
*/
|
|
94
|
-
const STALE_STATE_THRESHOLD_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Check if a state is stale based on its timestamps.
|
|
98
|
-
* A state is considered stale if it hasn't been updated recently.
|
|
99
|
-
* We check both `last_checked_at` and `started_at` - using whichever is more recent.
|
|
100
|
-
*/
|
|
101
|
-
function isStaleState(state) {
|
|
102
|
-
if (!state) return true;
|
|
103
|
-
|
|
104
|
-
const lastChecked = state.last_checked_at
|
|
105
|
-
? new Date(state.last_checked_at).getTime()
|
|
106
|
-
: 0;
|
|
107
|
-
const startedAt = state.started_at ? new Date(state.started_at).getTime() : 0;
|
|
108
|
-
const mostRecent = Math.max(lastChecked, startedAt);
|
|
109
|
-
|
|
110
|
-
if (mostRecent === 0) return true; // No valid timestamps
|
|
111
|
-
|
|
112
|
-
const age = Date.now() - mostRecent;
|
|
113
|
-
return age > STALE_STATE_THRESHOLD_MS;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Normalize a path for comparison.
|
|
118
|
-
*/
|
|
119
|
-
function normalizePath(p) {
|
|
120
|
-
if (!p) return "";
|
|
121
|
-
let normalized = resolve(p);
|
|
122
|
-
normalized = normalize(normalized);
|
|
123
|
-
normalized = normalized.replace(/[\/\\]+$/, "");
|
|
124
|
-
if (process.platform === "win32") {
|
|
125
|
-
normalized = normalized.toLowerCase();
|
|
126
|
-
}
|
|
127
|
-
return normalized;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Check if a state belongs to the requesting session.
|
|
132
|
-
* When sessionId is known: require exact match with state.session_id.
|
|
133
|
-
* When sessionId is empty/unknown: only match state without session_id (legacy compat).
|
|
134
|
-
*/
|
|
135
|
-
function isSessionMatch(state, sessionId) {
|
|
136
|
-
if (!state) return false;
|
|
137
|
-
if (sessionId) {
|
|
138
|
-
// Session is known: require exact match
|
|
139
|
-
return state.session_id === sessionId;
|
|
140
|
-
}
|
|
141
|
-
// No session_id from hook: only match legacy state (no session_id in state)
|
|
142
|
-
return !state.session_id;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Check if a state belongs to the current project.
|
|
147
|
-
*/
|
|
148
|
-
function isStateForCurrentProject(
|
|
149
|
-
state,
|
|
150
|
-
currentDirectory,
|
|
151
|
-
isGlobalState = false,
|
|
152
|
-
) {
|
|
153
|
-
if (!state) return true;
|
|
154
|
-
|
|
155
|
-
if (!state.project_path) {
|
|
156
|
-
if (isGlobalState) {
|
|
157
|
-
return false;
|
|
158
|
-
}
|
|
159
|
-
return true;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return normalizePath(state.project_path) === normalizePath(currentDirectory);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Read state file from local location only.
|
|
167
|
-
*/
|
|
168
|
-
function readStateFile(stateDir, filename) {
|
|
169
|
-
const localPath = join(stateDir, filename);
|
|
170
|
-
const state = readJsonFile(localPath);
|
|
171
|
-
return { state, path: localPath, isGlobal: false };
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Read state file with session-scoped path support and fallback to legacy path.
|
|
176
|
-
*/
|
|
177
|
-
function readStateFileWithSession(stateDir, filename, sessionId) {
|
|
178
|
-
// Try session-scoped path first (and ONLY) when sessionId is available
|
|
179
|
-
if (sessionId && /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,255}$/.test(sessionId)) {
|
|
180
|
-
const sessionsDir = join(stateDir, 'sessions', sessionId);
|
|
181
|
-
const sessionPath = join(sessionsDir, filename);
|
|
182
|
-
const state = readJsonFile(sessionPath);
|
|
183
|
-
if (state) {
|
|
184
|
-
return { state, path: sessionPath, isGlobal: false };
|
|
185
|
-
}
|
|
186
|
-
// Session path not found — do NOT fall back to legacy
|
|
187
|
-
return { state: null, path: null, isGlobal: false };
|
|
188
|
-
}
|
|
189
|
-
// No sessionId: fall back to legacy path (backward compat)
|
|
190
|
-
return readStateFile(stateDir, filename);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Count incomplete Tasks from Claude Code's native Task system.
|
|
195
|
-
*/
|
|
196
|
-
function countIncompleteTasks(sessionId) {
|
|
197
|
-
if (!sessionId || typeof sessionId !== "string") return 0;
|
|
198
|
-
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,255}$/.test(sessionId)) return 0;
|
|
199
|
-
|
|
200
|
-
const cfgDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
|
|
201
|
-
const taskDir = join(cfgDir, "tasks", sessionId);
|
|
202
|
-
if (!existsSync(taskDir)) return 0;
|
|
203
|
-
|
|
204
|
-
let count = 0;
|
|
205
|
-
try {
|
|
206
|
-
const files = readdirSync(taskDir).filter(
|
|
207
|
-
(f) => f.endsWith(".json") && f !== ".lock",
|
|
208
|
-
);
|
|
209
|
-
for (const file of files) {
|
|
210
|
-
try {
|
|
211
|
-
const content = readFileSync(join(taskDir, file), "utf-8");
|
|
212
|
-
const task = JSON.parse(content);
|
|
213
|
-
if (task.status === "pending" || task.status === "in_progress") count++;
|
|
214
|
-
} catch {
|
|
215
|
-
/* skip */
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
} catch {
|
|
219
|
-
/* skip */
|
|
220
|
-
}
|
|
221
|
-
return count;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function countIncompleteTodos(sessionId, projectDir) {
|
|
225
|
-
let count = 0;
|
|
226
|
-
|
|
227
|
-
// Session-specific todos only (no global scan)
|
|
228
|
-
if (
|
|
229
|
-
sessionId &&
|
|
230
|
-
typeof sessionId === "string" &&
|
|
231
|
-
/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,255}$/.test(sessionId)
|
|
232
|
-
) {
|
|
233
|
-
const sessionTodoPath = join(
|
|
234
|
-
homedir(),
|
|
235
|
-
".claude",
|
|
236
|
-
"todos",
|
|
237
|
-
`${sessionId}.json`,
|
|
238
|
-
);
|
|
239
|
-
try {
|
|
240
|
-
const data = readJsonFile(sessionTodoPath);
|
|
241
|
-
const todos = Array.isArray(data)
|
|
242
|
-
? data
|
|
243
|
-
: Array.isArray(data?.todos)
|
|
244
|
-
? data.todos
|
|
245
|
-
: [];
|
|
246
|
-
count += todos.filter(
|
|
247
|
-
(t) => t.status !== "completed" && t.status !== "cancelled",
|
|
248
|
-
).length;
|
|
249
|
-
} catch {
|
|
250
|
-
/* skip */
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Project-local todos only
|
|
255
|
-
for (const path of [
|
|
256
|
-
join(projectDir, ".omc", "todos.json"),
|
|
257
|
-
join(projectDir, ".claude", "todos.json"),
|
|
258
|
-
]) {
|
|
259
|
-
try {
|
|
260
|
-
const data = readJsonFile(path);
|
|
261
|
-
const todos = Array.isArray(data)
|
|
262
|
-
? data
|
|
263
|
-
: Array.isArray(data?.todos)
|
|
264
|
-
? data.todos
|
|
265
|
-
: [];
|
|
266
|
-
count += todos.filter(
|
|
267
|
-
(t) => t.status !== "completed" && t.status !== "cancelled",
|
|
268
|
-
).length;
|
|
269
|
-
} catch {
|
|
270
|
-
/* skip */
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
return count;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Detect if stop was triggered by context-limit related reasons.
|
|
279
|
-
* When context is exhausted, Claude Code needs to stop so it can compact.
|
|
280
|
-
* Blocking these stops causes a deadlock: can't compact because can't stop,
|
|
281
|
-
* can't continue because context is full.
|
|
282
|
-
*
|
|
283
|
-
* See: https://github.com/liangjie559567/ultrapower/issues/213
|
|
284
|
-
*/
|
|
285
|
-
function isContextLimitStop(data) {
|
|
286
|
-
const reason = (data.stop_reason || data.stopReason || "").toLowerCase();
|
|
287
|
-
|
|
288
|
-
const contextPatterns = [
|
|
289
|
-
"context_limit",
|
|
290
|
-
"context_window",
|
|
291
|
-
"context_exceeded",
|
|
292
|
-
"context_full",
|
|
293
|
-
"max_context",
|
|
294
|
-
"token_limit",
|
|
295
|
-
"max_tokens",
|
|
296
|
-
"conversation_too_long",
|
|
297
|
-
"input_too_long",
|
|
298
|
-
];
|
|
299
|
-
|
|
300
|
-
if (contextPatterns.some((p) => reason.includes(p))) {
|
|
301
|
-
return true;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const endTurnReason = (
|
|
305
|
-
data.end_turn_reason ||
|
|
306
|
-
data.endTurnReason ||
|
|
307
|
-
""
|
|
308
|
-
).toLowerCase();
|
|
309
|
-
if (endTurnReason && contextPatterns.some((p) => endTurnReason.includes(p))) {
|
|
310
|
-
return true;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
return false;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Detect if stop was triggered by user abort (Ctrl+C, cancel button, etc.)
|
|
318
|
-
*/
|
|
319
|
-
function isUserAbort(data) {
|
|
320
|
-
if (data.user_requested || data.userRequested) return true;
|
|
321
|
-
|
|
322
|
-
const reason = (data.stop_reason || data.stopReason || "").toLowerCase();
|
|
323
|
-
// Exact-match patterns: short generic words that cause false positives with .includes()
|
|
324
|
-
const exactPatterns = ["aborted", "abort", "cancel", "interrupt"];
|
|
325
|
-
// Substring patterns: compound words safe for .includes() matching
|
|
326
|
-
const substringPatterns = [
|
|
327
|
-
"user_cancel",
|
|
328
|
-
"user_interrupt",
|
|
329
|
-
"ctrl_c",
|
|
330
|
-
"manual_stop",
|
|
331
|
-
];
|
|
332
|
-
|
|
333
|
-
return (
|
|
334
|
-
exactPatterns.some((p) => reason === p) ||
|
|
335
|
-
substringPatterns.some((p) => reason.includes(p))
|
|
336
|
-
);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
async function main() {
|
|
340
|
-
try {
|
|
341
|
-
const input = await readStdin();
|
|
342
|
-
let data = {};
|
|
343
|
-
try {
|
|
344
|
-
data = JSON.parse(input);
|
|
345
|
-
} catch {}
|
|
346
|
-
|
|
347
|
-
const directory = data.cwd || data.directory || process.cwd();
|
|
348
|
-
const sessionId = data.session_id || data.sessionId || "";
|
|
349
|
-
const stateDir = join(directory, ".omc", "state");
|
|
350
|
-
|
|
351
|
-
// CRITICAL: Never block context-limit stops.
|
|
352
|
-
// Blocking these causes a deadlock where Claude Code cannot compact.
|
|
353
|
-
// See: https://github.com/liangjie559567/ultrapower/issues/213
|
|
354
|
-
if (isContextLimitStop(data)) {
|
|
355
|
-
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Respect user abort (Ctrl+C, cancel)
|
|
360
|
-
if (isUserAbort(data)) {
|
|
361
|
-
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// Read all mode states (session-scoped with legacy fallback)
|
|
366
|
-
const ralph = readStateFileWithSession(stateDir, "ralph-state.json", sessionId);
|
|
367
|
-
const autopilot = readStateFileWithSession(stateDir, "autopilot-state.json", sessionId);
|
|
368
|
-
const ultrapilot = readStateFileWithSession(stateDir, "ultrapilot-state.json", sessionId);
|
|
369
|
-
const ultrawork = readStateFileWithSession(stateDir, "ultrawork-state.json", sessionId);
|
|
370
|
-
const ultraqa = readStateFileWithSession(stateDir, "ultraqa-state.json", sessionId);
|
|
371
|
-
const pipeline = readStateFileWithSession(stateDir, "pipeline-state.json", sessionId);
|
|
372
|
-
|
|
373
|
-
// Swarm uses swarm-summary.json (not swarm-state.json) + marker file
|
|
374
|
-
const swarmMarker = existsSync(join(stateDir, "swarm-active.marker"));
|
|
375
|
-
const swarmSummary = readJsonFile(join(stateDir, "swarm-summary.json"));
|
|
376
|
-
|
|
377
|
-
// Count incomplete items (session-specific + project-local only)
|
|
378
|
-
const taskCount = countIncompleteTasks(sessionId);
|
|
379
|
-
const todoCount = countIncompleteTodos(sessionId, directory);
|
|
380
|
-
const totalIncomplete = taskCount + todoCount;
|
|
381
|
-
|
|
382
|
-
// Priority 1: Ralph Loop (explicit persistence mode)
|
|
383
|
-
// Skip if state is stale (older than 2 hours) - prevents blocking new sessions
|
|
384
|
-
if (ralph.state?.active && !isStaleState(ralph.state) && isSessionMatch(ralph.state, sessionId)) {
|
|
385
|
-
const iteration = ralph.state.iteration || 1;
|
|
386
|
-
const maxIter = ralph.state.max_iterations || 100;
|
|
387
|
-
|
|
388
|
-
if (iteration < maxIter) {
|
|
389
|
-
ralph.state.iteration = iteration + 1;
|
|
390
|
-
ralph.state.last_checked_at = new Date().toISOString();
|
|
391
|
-
writeJsonFile(ralph.path, ralph.state);
|
|
392
|
-
|
|
393
|
-
// Fire-and-forget notification
|
|
394
|
-
sendStopNotification('ralph', ralph.state, sessionId, directory).catch(() => {});
|
|
395
|
-
|
|
396
|
-
console.log(
|
|
397
|
-
JSON.stringify({
|
|
398
|
-
decision: "block",
|
|
399
|
-
reason: `[RALPH LOOP - ITERATION ${iteration + 1}/${maxIter}] Work is NOT done. Continue working.\nWhen FULLY complete (after Architect verification), run /ultrapower:cancel to cleanly exit ralph mode and clean up all state files. If cancel fails, retry with /ultrapower:cancel --force.\n${ralph.state.prompt ? `Task: ${ralph.state.prompt}` : ""}`,
|
|
400
|
-
}),
|
|
401
|
-
);
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// Priority 2: Autopilot (high-level orchestration)
|
|
407
|
-
if (autopilot.state?.active && !isStaleState(autopilot.state) && isSessionMatch(autopilot.state, sessionId)) {
|
|
408
|
-
const phase = autopilot.state.phase || "unknown";
|
|
409
|
-
if (phase !== "complete") {
|
|
410
|
-
const newCount = (autopilot.state.reinforcement_count || 0) + 1;
|
|
411
|
-
if (newCount <= 20) {
|
|
412
|
-
autopilot.state.reinforcement_count = newCount;
|
|
413
|
-
autopilot.state.last_checked_at = new Date().toISOString();
|
|
414
|
-
writeJsonFile(autopilot.path, autopilot.state);
|
|
415
|
-
|
|
416
|
-
// Fire-and-forget notification
|
|
417
|
-
sendStopNotification('autopilot', autopilot.state, sessionId, directory).catch(() => {});
|
|
418
|
-
|
|
419
|
-
console.log(
|
|
420
|
-
JSON.stringify({
|
|
421
|
-
decision: "block",
|
|
422
|
-
reason: `[AUTOPILOT - Phase: ${phase}] Autopilot not complete. Continue working. When all phases are complete, run /ultrapower:cancel to cleanly exit and clean up state files. If cancel fails, retry with /ultrapower:cancel --force.`,
|
|
423
|
-
}),
|
|
424
|
-
);
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// Priority 3: Ultrapilot (parallel autopilot)
|
|
431
|
-
if (ultrapilot.state?.active && !isStaleState(ultrapilot.state) && isSessionMatch(ultrapilot.state, sessionId)) {
|
|
432
|
-
const workers = ultrapilot.state.workers || [];
|
|
433
|
-
const incomplete = workers.filter(
|
|
434
|
-
(w) => w.status !== "complete" && w.status !== "failed",
|
|
435
|
-
).length;
|
|
436
|
-
if (incomplete > 0) {
|
|
437
|
-
const newCount = (ultrapilot.state.reinforcement_count || 0) + 1;
|
|
438
|
-
if (newCount <= 20) {
|
|
439
|
-
ultrapilot.state.reinforcement_count = newCount;
|
|
440
|
-
ultrapilot.state.last_checked_at = new Date().toISOString();
|
|
441
|
-
writeJsonFile(ultrapilot.path, ultrapilot.state);
|
|
442
|
-
|
|
443
|
-
// Fire-and-forget notification
|
|
444
|
-
sendStopNotification('ultrapilot', ultrapilot.state, sessionId, directory).catch(() => {});
|
|
445
|
-
|
|
446
|
-
console.log(
|
|
447
|
-
JSON.stringify({
|
|
448
|
-
decision: "block",
|
|
449
|
-
reason: `[ULTRAPILOT] ${incomplete} workers still running. Continue working. When all workers complete, run /ultrapower:cancel to cleanly exit and clean up state files. If cancel fails, retry with /ultrapower:cancel --force.`,
|
|
450
|
-
}),
|
|
451
|
-
);
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// Priority 4: Swarm (coordinated agents with SQLite)
|
|
458
|
-
if (swarmMarker && swarmSummary?.active && !isStaleState(swarmSummary)) {
|
|
459
|
-
const pending =
|
|
460
|
-
(swarmSummary.tasks_pending || 0) + (swarmSummary.tasks_claimed || 0);
|
|
461
|
-
if (pending > 0) {
|
|
462
|
-
const newCount = (swarmSummary.reinforcement_count || 0) + 1;
|
|
463
|
-
if (newCount <= 15) {
|
|
464
|
-
swarmSummary.reinforcement_count = newCount;
|
|
465
|
-
swarmSummary.last_checked_at = new Date().toISOString();
|
|
466
|
-
writeJsonFile(join(stateDir, "swarm-summary.json"), swarmSummary);
|
|
467
|
-
|
|
468
|
-
// Fire-and-forget notification
|
|
469
|
-
sendStopNotification('swarm', swarmSummary, sessionId, directory).catch(() => {});
|
|
470
|
-
|
|
471
|
-
console.log(
|
|
472
|
-
JSON.stringify({
|
|
473
|
-
decision: "block",
|
|
474
|
-
reason: `[SWARM ACTIVE] ${pending} tasks remain. Continue working. When all tasks are done, run /ultrapower:cancel to cleanly exit and clean up state files. If cancel fails, retry with /ultrapower:cancel --force.`,
|
|
475
|
-
}),
|
|
476
|
-
);
|
|
477
|
-
return;
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// Priority 5: Pipeline (sequential stages)
|
|
483
|
-
if (pipeline.state?.active && !isStaleState(pipeline.state) && isSessionMatch(pipeline.state, sessionId)) {
|
|
484
|
-
const currentStage = pipeline.state.current_stage || 0;
|
|
485
|
-
const totalStages = pipeline.state.stages?.length || 0;
|
|
486
|
-
if (currentStage < totalStages) {
|
|
487
|
-
const newCount = (pipeline.state.reinforcement_count || 0) + 1;
|
|
488
|
-
if (newCount <= 15) {
|
|
489
|
-
pipeline.state.reinforcement_count = newCount;
|
|
490
|
-
pipeline.state.last_checked_at = new Date().toISOString();
|
|
491
|
-
writeJsonFile(pipeline.path, pipeline.state);
|
|
492
|
-
|
|
493
|
-
// Fire-and-forget notification
|
|
494
|
-
sendStopNotification('pipeline', pipeline.state, sessionId, directory).catch(() => {});
|
|
495
|
-
|
|
496
|
-
console.log(
|
|
497
|
-
JSON.stringify({
|
|
498
|
-
decision: "block",
|
|
499
|
-
reason: `[PIPELINE - Stage ${currentStage + 1}/${totalStages}] Pipeline not complete. Continue working. When all stages complete, run /ultrapower:cancel to cleanly exit and clean up state files. If cancel fails, retry with /ultrapower:cancel --force.`,
|
|
500
|
-
}),
|
|
501
|
-
);
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Priority 6: UltraQA (QA cycling)
|
|
508
|
-
if (ultraqa.state?.active && !isStaleState(ultraqa.state) && isSessionMatch(ultraqa.state, sessionId)) {
|
|
509
|
-
const cycle = ultraqa.state.cycle || 1;
|
|
510
|
-
const maxCycles = ultraqa.state.max_cycles || 10;
|
|
511
|
-
if (cycle < maxCycles && !ultraqa.state.all_passing) {
|
|
512
|
-
ultraqa.state.cycle = cycle + 1;
|
|
513
|
-
ultraqa.state.last_checked_at = new Date().toISOString();
|
|
514
|
-
writeJsonFile(ultraqa.path, ultraqa.state);
|
|
515
|
-
|
|
516
|
-
// Fire-and-forget notification
|
|
517
|
-
sendStopNotification('ultraqa', ultraqa.state, sessionId, directory).catch(() => {});
|
|
518
|
-
|
|
519
|
-
console.log(
|
|
520
|
-
JSON.stringify({
|
|
521
|
-
decision: "block",
|
|
522
|
-
reason: `[ULTRAQA - Cycle ${cycle + 1}/${maxCycles}] Tests not all passing. Continue fixing. When all tests pass, run /ultrapower:cancel to cleanly exit and clean up state files. If cancel fails, retry with /ultrapower:cancel --force.`,
|
|
523
|
-
}),
|
|
524
|
-
);
|
|
525
|
-
return;
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// Priority 7: Ultrawork - ALWAYS continue while active (not just when tasks exist)
|
|
530
|
-
// This prevents false stops from bash errors, transient failures, etc.
|
|
531
|
-
// Session isolation: only block if state belongs to this session (issue #311)
|
|
532
|
-
// Project isolation: only block if state belongs to this project
|
|
533
|
-
if (
|
|
534
|
-
ultrawork.state?.active &&
|
|
535
|
-
!isStaleState(ultrawork.state) &&
|
|
536
|
-
isSessionMatch(ultrawork.state, sessionId) &&
|
|
537
|
-
isStateForCurrentProject(ultrawork.state, directory, ultrawork.isGlobal)
|
|
538
|
-
) {
|
|
539
|
-
const newCount = (ultrawork.state.reinforcement_count || 0) + 1;
|
|
540
|
-
const maxReinforcements = ultrawork.state.max_reinforcements || 50;
|
|
541
|
-
|
|
542
|
-
if (newCount > maxReinforcements) {
|
|
543
|
-
// Max reinforcements reached - allow stop
|
|
544
|
-
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
ultrawork.state.reinforcement_count = newCount;
|
|
549
|
-
ultrawork.state.last_checked_at = new Date().toISOString();
|
|
550
|
-
writeJsonFile(ultrawork.path, ultrawork.state);
|
|
551
|
-
|
|
552
|
-
// Fire-and-forget notification
|
|
553
|
-
sendStopNotification('ultrawork', ultrawork.state, sessionId, directory).catch(() => {});
|
|
554
|
-
|
|
555
|
-
let reason = `[ULTRAWORK #${newCount}/${maxReinforcements}] Mode active.`;
|
|
556
|
-
|
|
557
|
-
if (totalIncomplete > 0) {
|
|
558
|
-
const itemType = taskCount > 0 ? "Tasks" : "todos";
|
|
559
|
-
reason += ` ${totalIncomplete} incomplete ${itemType} remain. Continue working.`;
|
|
560
|
-
} else if (newCount >= 3) {
|
|
561
|
-
// Only suggest cancel after minimum iterations (guard against no-tasks-created scenario)
|
|
562
|
-
reason += ` If all work is complete, run /ultrapower:cancel to cleanly exit ultrawork mode and clean up state files. If cancel fails, retry with /ultrapower:cancel --force. Otherwise, continue working.`;
|
|
563
|
-
} else {
|
|
564
|
-
// Early iterations with no tasks yet - just tell LLM to continue
|
|
565
|
-
reason += ` Continue working - create Tasks to track your progress.`;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
if (ultrawork.state.original_prompt) {
|
|
569
|
-
reason += `\nTask: ${ultrawork.state.original_prompt}`;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
console.log(JSON.stringify({ decision: "block", reason }));
|
|
573
|
-
return;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
// No blocking needed — Claude is truly idle.
|
|
577
|
-
// Send session-idle notification (fire-and-forget) so external integrations
|
|
578
|
-
// (Telegram, Discord) know the session went idle without any active mode.
|
|
579
|
-
if (sessionId) {
|
|
580
|
-
try {
|
|
581
|
-
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT;
|
|
582
|
-
if (pluginRoot) {
|
|
583
|
-
const { pathToFileURL } = require('url');
|
|
584
|
-
import(pathToFileURL(join(pluginRoot, 'dist', 'notifications', 'index.js')).href)
|
|
585
|
-
.then(({ notify }) =>
|
|
586
|
-
notify('session-idle', {
|
|
587
|
-
sessionId,
|
|
588
|
-
projectPath: directory,
|
|
589
|
-
}).catch(() => {})
|
|
590
|
-
)
|
|
591
|
-
.catch(() => {});
|
|
592
|
-
}
|
|
593
|
-
} catch {
|
|
594
|
-
// Notification module not available, skip silently
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
598
|
-
} catch (error) {
|
|
599
|
-
// On any error, allow stop rather than blocking forever
|
|
600
|
-
console.error(`[persistent-mode] Error: ${error.message}`);
|
|
601
|
-
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
main();
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* OMC Persistent Mode Hook (Node.js)
|
|
5
|
+
* Minimal continuation enforcer for all OMC modes.
|
|
6
|
+
* Stripped down for reliability — no optional imports, no PRD, no notepad pruning.
|
|
7
|
+
*
|
|
8
|
+
* Supported modes: ralph, autopilot, ultrapilot, swarm, ultrawork, ultraqa, pipeline
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
existsSync,
|
|
13
|
+
readFileSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
readdirSync,
|
|
16
|
+
mkdirSync,
|
|
17
|
+
} = require("fs");
|
|
18
|
+
const { join, dirname, resolve, normalize } = require("path");
|
|
19
|
+
const { homedir } = require("os");
|
|
20
|
+
|
|
21
|
+
async function readStdin(timeoutMs = 5000) {
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
const chunks = [];
|
|
24
|
+
let settled = false;
|
|
25
|
+
const timeout = setTimeout(() => {
|
|
26
|
+
if (!settled) { settled = true; process.stdin.removeAllListeners(); process.stdin.destroy(); resolve(Buffer.concat(chunks).toString("utf-8")); }
|
|
27
|
+
}, timeoutMs);
|
|
28
|
+
process.stdin.on("data", (chunk) => { chunks.push(chunk); });
|
|
29
|
+
process.stdin.on("end", () => { if (!settled) { settled = true; clearTimeout(timeout); resolve(Buffer.concat(chunks).toString("utf-8")); } });
|
|
30
|
+
process.stdin.on("error", () => { if (!settled) { settled = true; clearTimeout(timeout); resolve(""); } });
|
|
31
|
+
if (process.stdin.readableEnded) { if (!settled) { settled = true; clearTimeout(timeout); resolve(Buffer.concat(chunks).toString("utf-8")); } }
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readJsonFile(path) {
|
|
36
|
+
try {
|
|
37
|
+
if (!existsSync(path)) return null;
|
|
38
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function writeJsonFile(path, data) {
|
|
45
|
+
try {
|
|
46
|
+
// Ensure directory exists
|
|
47
|
+
const dir = dirname(path);
|
|
48
|
+
if (dir && dir !== "." && !existsSync(dir)) {
|
|
49
|
+
mkdirSync(dir, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
writeFileSync(path, JSON.stringify(data, null, 2));
|
|
52
|
+
return true;
|
|
53
|
+
} catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Send stop notification (fire-and-forget, non-blocking).
|
|
60
|
+
* Only notifies on first stop to avoid spam.
|
|
61
|
+
*/
|
|
62
|
+
async function sendStopNotification(modeName, stateData, sessionId, directory) {
|
|
63
|
+
// Only notify once per mode activation
|
|
64
|
+
if (stateData._stopNotified) return;
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT;
|
|
68
|
+
if (!pluginRoot) return;
|
|
69
|
+
|
|
70
|
+
const { pathToFileURL } = require('url');
|
|
71
|
+
const { notify } = await import(pathToFileURL(join(pluginRoot, 'dist', 'notifications', 'index.js')).href);
|
|
72
|
+
|
|
73
|
+
await notify('session-stop', {
|
|
74
|
+
sessionId: sessionId,
|
|
75
|
+
projectPath: directory,
|
|
76
|
+
activeMode: modeName,
|
|
77
|
+
iteration: stateData.iteration || stateData.reinforcement_count || 1,
|
|
78
|
+
maxIterations: stateData.max_iterations || stateData.max_reinforcements || 100,
|
|
79
|
+
incompleteTasks: undefined, // Caller can override
|
|
80
|
+
}).catch(() => {});
|
|
81
|
+
|
|
82
|
+
// Mark as notified to prevent duplicate notifications
|
|
83
|
+
stateData._stopNotified = true;
|
|
84
|
+
} catch {
|
|
85
|
+
// Notification module not available, skip silently
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Staleness threshold for mode states (2 hours in milliseconds).
|
|
91
|
+
* States older than this are treated as inactive to prevent stale state
|
|
92
|
+
* from causing the stop hook to malfunction in new sessions.
|
|
93
|
+
*/
|
|
94
|
+
const STALE_STATE_THRESHOLD_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Check if a state is stale based on its timestamps.
|
|
98
|
+
* A state is considered stale if it hasn't been updated recently.
|
|
99
|
+
* We check both `last_checked_at` and `started_at` - using whichever is more recent.
|
|
100
|
+
*/
|
|
101
|
+
function isStaleState(state) {
|
|
102
|
+
if (!state) return true;
|
|
103
|
+
|
|
104
|
+
const lastChecked = state.last_checked_at
|
|
105
|
+
? new Date(state.last_checked_at).getTime()
|
|
106
|
+
: 0;
|
|
107
|
+
const startedAt = state.started_at ? new Date(state.started_at).getTime() : 0;
|
|
108
|
+
const mostRecent = Math.max(lastChecked, startedAt);
|
|
109
|
+
|
|
110
|
+
if (mostRecent === 0) return true; // No valid timestamps
|
|
111
|
+
|
|
112
|
+
const age = Date.now() - mostRecent;
|
|
113
|
+
return age > STALE_STATE_THRESHOLD_MS;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Normalize a path for comparison.
|
|
118
|
+
*/
|
|
119
|
+
function normalizePath(p) {
|
|
120
|
+
if (!p) return "";
|
|
121
|
+
let normalized = resolve(p);
|
|
122
|
+
normalized = normalize(normalized);
|
|
123
|
+
normalized = normalized.replace(/[\/\\]+$/, "");
|
|
124
|
+
if (process.platform === "win32") {
|
|
125
|
+
normalized = normalized.toLowerCase();
|
|
126
|
+
}
|
|
127
|
+
return normalized;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Check if a state belongs to the requesting session.
|
|
132
|
+
* When sessionId is known: require exact match with state.session_id.
|
|
133
|
+
* When sessionId is empty/unknown: only match state without session_id (legacy compat).
|
|
134
|
+
*/
|
|
135
|
+
function isSessionMatch(state, sessionId) {
|
|
136
|
+
if (!state) return false;
|
|
137
|
+
if (sessionId) {
|
|
138
|
+
// Session is known: require exact match
|
|
139
|
+
return state.session_id === sessionId;
|
|
140
|
+
}
|
|
141
|
+
// No session_id from hook: only match legacy state (no session_id in state)
|
|
142
|
+
return !state.session_id;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Check if a state belongs to the current project.
|
|
147
|
+
*/
|
|
148
|
+
function isStateForCurrentProject(
|
|
149
|
+
state,
|
|
150
|
+
currentDirectory,
|
|
151
|
+
isGlobalState = false,
|
|
152
|
+
) {
|
|
153
|
+
if (!state) return true;
|
|
154
|
+
|
|
155
|
+
if (!state.project_path) {
|
|
156
|
+
if (isGlobalState) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return normalizePath(state.project_path) === normalizePath(currentDirectory);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Read state file from local location only.
|
|
167
|
+
*/
|
|
168
|
+
function readStateFile(stateDir, filename) {
|
|
169
|
+
const localPath = join(stateDir, filename);
|
|
170
|
+
const state = readJsonFile(localPath);
|
|
171
|
+
return { state, path: localPath, isGlobal: false };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Read state file with session-scoped path support and fallback to legacy path.
|
|
176
|
+
*/
|
|
177
|
+
function readStateFileWithSession(stateDir, filename, sessionId) {
|
|
178
|
+
// Try session-scoped path first (and ONLY) when sessionId is available
|
|
179
|
+
if (sessionId && /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,255}$/.test(sessionId)) {
|
|
180
|
+
const sessionsDir = join(stateDir, 'sessions', sessionId);
|
|
181
|
+
const sessionPath = join(sessionsDir, filename);
|
|
182
|
+
const state = readJsonFile(sessionPath);
|
|
183
|
+
if (state) {
|
|
184
|
+
return { state, path: sessionPath, isGlobal: false };
|
|
185
|
+
}
|
|
186
|
+
// Session path not found — do NOT fall back to legacy
|
|
187
|
+
return { state: null, path: null, isGlobal: false };
|
|
188
|
+
}
|
|
189
|
+
// No sessionId: fall back to legacy path (backward compat)
|
|
190
|
+
return readStateFile(stateDir, filename);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Count incomplete Tasks from Claude Code's native Task system.
|
|
195
|
+
*/
|
|
196
|
+
function countIncompleteTasks(sessionId) {
|
|
197
|
+
if (!sessionId || typeof sessionId !== "string") return 0;
|
|
198
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,255}$/.test(sessionId)) return 0;
|
|
199
|
+
|
|
200
|
+
const cfgDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
|
|
201
|
+
const taskDir = join(cfgDir, "tasks", sessionId);
|
|
202
|
+
if (!existsSync(taskDir)) return 0;
|
|
203
|
+
|
|
204
|
+
let count = 0;
|
|
205
|
+
try {
|
|
206
|
+
const files = readdirSync(taskDir).filter(
|
|
207
|
+
(f) => f.endsWith(".json") && f !== ".lock",
|
|
208
|
+
);
|
|
209
|
+
for (const file of files) {
|
|
210
|
+
try {
|
|
211
|
+
const content = readFileSync(join(taskDir, file), "utf-8");
|
|
212
|
+
const task = JSON.parse(content);
|
|
213
|
+
if (task.status === "pending" || task.status === "in_progress") count++;
|
|
214
|
+
} catch {
|
|
215
|
+
/* skip */
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} catch {
|
|
219
|
+
/* skip */
|
|
220
|
+
}
|
|
221
|
+
return count;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function countIncompleteTodos(sessionId, projectDir) {
|
|
225
|
+
let count = 0;
|
|
226
|
+
|
|
227
|
+
// Session-specific todos only (no global scan)
|
|
228
|
+
if (
|
|
229
|
+
sessionId &&
|
|
230
|
+
typeof sessionId === "string" &&
|
|
231
|
+
/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,255}$/.test(sessionId)
|
|
232
|
+
) {
|
|
233
|
+
const sessionTodoPath = join(
|
|
234
|
+
homedir(),
|
|
235
|
+
".claude",
|
|
236
|
+
"todos",
|
|
237
|
+
`${sessionId}.json`,
|
|
238
|
+
);
|
|
239
|
+
try {
|
|
240
|
+
const data = readJsonFile(sessionTodoPath);
|
|
241
|
+
const todos = Array.isArray(data)
|
|
242
|
+
? data
|
|
243
|
+
: Array.isArray(data?.todos)
|
|
244
|
+
? data.todos
|
|
245
|
+
: [];
|
|
246
|
+
count += todos.filter(
|
|
247
|
+
(t) => t.status !== "completed" && t.status !== "cancelled",
|
|
248
|
+
).length;
|
|
249
|
+
} catch {
|
|
250
|
+
/* skip */
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Project-local todos only
|
|
255
|
+
for (const path of [
|
|
256
|
+
join(projectDir, ".omc", "todos.json"),
|
|
257
|
+
join(projectDir, ".claude", "todos.json"),
|
|
258
|
+
]) {
|
|
259
|
+
try {
|
|
260
|
+
const data = readJsonFile(path);
|
|
261
|
+
const todos = Array.isArray(data)
|
|
262
|
+
? data
|
|
263
|
+
: Array.isArray(data?.todos)
|
|
264
|
+
? data.todos
|
|
265
|
+
: [];
|
|
266
|
+
count += todos.filter(
|
|
267
|
+
(t) => t.status !== "completed" && t.status !== "cancelled",
|
|
268
|
+
).length;
|
|
269
|
+
} catch {
|
|
270
|
+
/* skip */
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return count;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Detect if stop was triggered by context-limit related reasons.
|
|
279
|
+
* When context is exhausted, Claude Code needs to stop so it can compact.
|
|
280
|
+
* Blocking these stops causes a deadlock: can't compact because can't stop,
|
|
281
|
+
* can't continue because context is full.
|
|
282
|
+
*
|
|
283
|
+
* See: https://github.com/liangjie559567/ultrapower/issues/213
|
|
284
|
+
*/
|
|
285
|
+
function isContextLimitStop(data) {
|
|
286
|
+
const reason = (data.stop_reason || data.stopReason || "").toLowerCase();
|
|
287
|
+
|
|
288
|
+
const contextPatterns = [
|
|
289
|
+
"context_limit",
|
|
290
|
+
"context_window",
|
|
291
|
+
"context_exceeded",
|
|
292
|
+
"context_full",
|
|
293
|
+
"max_context",
|
|
294
|
+
"token_limit",
|
|
295
|
+
"max_tokens",
|
|
296
|
+
"conversation_too_long",
|
|
297
|
+
"input_too_long",
|
|
298
|
+
];
|
|
299
|
+
|
|
300
|
+
if (contextPatterns.some((p) => reason.includes(p))) {
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const endTurnReason = (
|
|
305
|
+
data.end_turn_reason ||
|
|
306
|
+
data.endTurnReason ||
|
|
307
|
+
""
|
|
308
|
+
).toLowerCase();
|
|
309
|
+
if (endTurnReason && contextPatterns.some((p) => endTurnReason.includes(p))) {
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Detect if stop was triggered by user abort (Ctrl+C, cancel button, etc.)
|
|
318
|
+
*/
|
|
319
|
+
function isUserAbort(data) {
|
|
320
|
+
if (data.user_requested || data.userRequested) return true;
|
|
321
|
+
|
|
322
|
+
const reason = (data.stop_reason || data.stopReason || "").toLowerCase();
|
|
323
|
+
// Exact-match patterns: short generic words that cause false positives with .includes()
|
|
324
|
+
const exactPatterns = ["aborted", "abort", "cancel", "interrupt"];
|
|
325
|
+
// Substring patterns: compound words safe for .includes() matching
|
|
326
|
+
const substringPatterns = [
|
|
327
|
+
"user_cancel",
|
|
328
|
+
"user_interrupt",
|
|
329
|
+
"ctrl_c",
|
|
330
|
+
"manual_stop",
|
|
331
|
+
];
|
|
332
|
+
|
|
333
|
+
return (
|
|
334
|
+
exactPatterns.some((p) => reason === p) ||
|
|
335
|
+
substringPatterns.some((p) => reason.includes(p))
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function main() {
|
|
340
|
+
try {
|
|
341
|
+
const input = await readStdin();
|
|
342
|
+
let data = {};
|
|
343
|
+
try {
|
|
344
|
+
data = JSON.parse(input);
|
|
345
|
+
} catch {}
|
|
346
|
+
|
|
347
|
+
const directory = data.cwd || data.directory || process.cwd();
|
|
348
|
+
const sessionId = data.session_id || data.sessionId || "";
|
|
349
|
+
const stateDir = join(directory, ".omc", "state");
|
|
350
|
+
|
|
351
|
+
// CRITICAL: Never block context-limit stops.
|
|
352
|
+
// Blocking these causes a deadlock where Claude Code cannot compact.
|
|
353
|
+
// See: https://github.com/liangjie559567/ultrapower/issues/213
|
|
354
|
+
if (isContextLimitStop(data)) {
|
|
355
|
+
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Respect user abort (Ctrl+C, cancel)
|
|
360
|
+
if (isUserAbort(data)) {
|
|
361
|
+
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Read all mode states (session-scoped with legacy fallback)
|
|
366
|
+
const ralph = readStateFileWithSession(stateDir, "ralph-state.json", sessionId);
|
|
367
|
+
const autopilot = readStateFileWithSession(stateDir, "autopilot-state.json", sessionId);
|
|
368
|
+
const ultrapilot = readStateFileWithSession(stateDir, "ultrapilot-state.json", sessionId);
|
|
369
|
+
const ultrawork = readStateFileWithSession(stateDir, "ultrawork-state.json", sessionId);
|
|
370
|
+
const ultraqa = readStateFileWithSession(stateDir, "ultraqa-state.json", sessionId);
|
|
371
|
+
const pipeline = readStateFileWithSession(stateDir, "pipeline-state.json", sessionId);
|
|
372
|
+
|
|
373
|
+
// Swarm uses swarm-summary.json (not swarm-state.json) + marker file
|
|
374
|
+
const swarmMarker = existsSync(join(stateDir, "swarm-active.marker"));
|
|
375
|
+
const swarmSummary = readJsonFile(join(stateDir, "swarm-summary.json"));
|
|
376
|
+
|
|
377
|
+
// Count incomplete items (session-specific + project-local only)
|
|
378
|
+
const taskCount = countIncompleteTasks(sessionId);
|
|
379
|
+
const todoCount = countIncompleteTodos(sessionId, directory);
|
|
380
|
+
const totalIncomplete = taskCount + todoCount;
|
|
381
|
+
|
|
382
|
+
// Priority 1: Ralph Loop (explicit persistence mode)
|
|
383
|
+
// Skip if state is stale (older than 2 hours) - prevents blocking new sessions
|
|
384
|
+
if (ralph.state?.active && !isStaleState(ralph.state) && isSessionMatch(ralph.state, sessionId)) {
|
|
385
|
+
const iteration = ralph.state.iteration || 1;
|
|
386
|
+
const maxIter = ralph.state.max_iterations || 100;
|
|
387
|
+
|
|
388
|
+
if (iteration < maxIter) {
|
|
389
|
+
ralph.state.iteration = iteration + 1;
|
|
390
|
+
ralph.state.last_checked_at = new Date().toISOString();
|
|
391
|
+
writeJsonFile(ralph.path, ralph.state);
|
|
392
|
+
|
|
393
|
+
// Fire-and-forget notification
|
|
394
|
+
sendStopNotification('ralph', ralph.state, sessionId, directory).catch(() => {});
|
|
395
|
+
|
|
396
|
+
console.log(
|
|
397
|
+
JSON.stringify({
|
|
398
|
+
decision: "block",
|
|
399
|
+
reason: `[RALPH LOOP - ITERATION ${iteration + 1}/${maxIter}] Work is NOT done. Continue working.\nWhen FULLY complete (after Architect verification), run /ultrapower:cancel to cleanly exit ralph mode and clean up all state files. If cancel fails, retry with /ultrapower:cancel --force.\n${ralph.state.prompt ? `Task: ${ralph.state.prompt}` : ""}`,
|
|
400
|
+
}),
|
|
401
|
+
);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Priority 2: Autopilot (high-level orchestration)
|
|
407
|
+
if (autopilot.state?.active && !isStaleState(autopilot.state) && isSessionMatch(autopilot.state, sessionId)) {
|
|
408
|
+
const phase = autopilot.state.phase || "unknown";
|
|
409
|
+
if (phase !== "complete") {
|
|
410
|
+
const newCount = (autopilot.state.reinforcement_count || 0) + 1;
|
|
411
|
+
if (newCount <= 20) {
|
|
412
|
+
autopilot.state.reinforcement_count = newCount;
|
|
413
|
+
autopilot.state.last_checked_at = new Date().toISOString();
|
|
414
|
+
writeJsonFile(autopilot.path, autopilot.state);
|
|
415
|
+
|
|
416
|
+
// Fire-and-forget notification
|
|
417
|
+
sendStopNotification('autopilot', autopilot.state, sessionId, directory).catch(() => {});
|
|
418
|
+
|
|
419
|
+
console.log(
|
|
420
|
+
JSON.stringify({
|
|
421
|
+
decision: "block",
|
|
422
|
+
reason: `[AUTOPILOT - Phase: ${phase}] Autopilot not complete. Continue working. When all phases are complete, run /ultrapower:cancel to cleanly exit and clean up state files. If cancel fails, retry with /ultrapower:cancel --force.`,
|
|
423
|
+
}),
|
|
424
|
+
);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Priority 3: Ultrapilot (parallel autopilot)
|
|
431
|
+
if (ultrapilot.state?.active && !isStaleState(ultrapilot.state) && isSessionMatch(ultrapilot.state, sessionId)) {
|
|
432
|
+
const workers = ultrapilot.state.workers || [];
|
|
433
|
+
const incomplete = workers.filter(
|
|
434
|
+
(w) => w.status !== "complete" && w.status !== "failed",
|
|
435
|
+
).length;
|
|
436
|
+
if (incomplete > 0) {
|
|
437
|
+
const newCount = (ultrapilot.state.reinforcement_count || 0) + 1;
|
|
438
|
+
if (newCount <= 20) {
|
|
439
|
+
ultrapilot.state.reinforcement_count = newCount;
|
|
440
|
+
ultrapilot.state.last_checked_at = new Date().toISOString();
|
|
441
|
+
writeJsonFile(ultrapilot.path, ultrapilot.state);
|
|
442
|
+
|
|
443
|
+
// Fire-and-forget notification
|
|
444
|
+
sendStopNotification('ultrapilot', ultrapilot.state, sessionId, directory).catch(() => {});
|
|
445
|
+
|
|
446
|
+
console.log(
|
|
447
|
+
JSON.stringify({
|
|
448
|
+
decision: "block",
|
|
449
|
+
reason: `[ULTRAPILOT] ${incomplete} workers still running. Continue working. When all workers complete, run /ultrapower:cancel to cleanly exit and clean up state files. If cancel fails, retry with /ultrapower:cancel --force.`,
|
|
450
|
+
}),
|
|
451
|
+
);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Priority 4: Swarm (coordinated agents with SQLite)
|
|
458
|
+
if (swarmMarker && swarmSummary?.active && !isStaleState(swarmSummary)) {
|
|
459
|
+
const pending =
|
|
460
|
+
(swarmSummary.tasks_pending || 0) + (swarmSummary.tasks_claimed || 0);
|
|
461
|
+
if (pending > 0) {
|
|
462
|
+
const newCount = (swarmSummary.reinforcement_count || 0) + 1;
|
|
463
|
+
if (newCount <= 15) {
|
|
464
|
+
swarmSummary.reinforcement_count = newCount;
|
|
465
|
+
swarmSummary.last_checked_at = new Date().toISOString();
|
|
466
|
+
writeJsonFile(join(stateDir, "swarm-summary.json"), swarmSummary);
|
|
467
|
+
|
|
468
|
+
// Fire-and-forget notification
|
|
469
|
+
sendStopNotification('swarm', swarmSummary, sessionId, directory).catch(() => {});
|
|
470
|
+
|
|
471
|
+
console.log(
|
|
472
|
+
JSON.stringify({
|
|
473
|
+
decision: "block",
|
|
474
|
+
reason: `[SWARM ACTIVE] ${pending} tasks remain. Continue working. When all tasks are done, run /ultrapower:cancel to cleanly exit and clean up state files. If cancel fails, retry with /ultrapower:cancel --force.`,
|
|
475
|
+
}),
|
|
476
|
+
);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Priority 5: Pipeline (sequential stages)
|
|
483
|
+
if (pipeline.state?.active && !isStaleState(pipeline.state) && isSessionMatch(pipeline.state, sessionId)) {
|
|
484
|
+
const currentStage = pipeline.state.current_stage || 0;
|
|
485
|
+
const totalStages = pipeline.state.stages?.length || 0;
|
|
486
|
+
if (currentStage < totalStages) {
|
|
487
|
+
const newCount = (pipeline.state.reinforcement_count || 0) + 1;
|
|
488
|
+
if (newCount <= 15) {
|
|
489
|
+
pipeline.state.reinforcement_count = newCount;
|
|
490
|
+
pipeline.state.last_checked_at = new Date().toISOString();
|
|
491
|
+
writeJsonFile(pipeline.path, pipeline.state);
|
|
492
|
+
|
|
493
|
+
// Fire-and-forget notification
|
|
494
|
+
sendStopNotification('pipeline', pipeline.state, sessionId, directory).catch(() => {});
|
|
495
|
+
|
|
496
|
+
console.log(
|
|
497
|
+
JSON.stringify({
|
|
498
|
+
decision: "block",
|
|
499
|
+
reason: `[PIPELINE - Stage ${currentStage + 1}/${totalStages}] Pipeline not complete. Continue working. When all stages complete, run /ultrapower:cancel to cleanly exit and clean up state files. If cancel fails, retry with /ultrapower:cancel --force.`,
|
|
500
|
+
}),
|
|
501
|
+
);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Priority 6: UltraQA (QA cycling)
|
|
508
|
+
if (ultraqa.state?.active && !isStaleState(ultraqa.state) && isSessionMatch(ultraqa.state, sessionId)) {
|
|
509
|
+
const cycle = ultraqa.state.cycle || 1;
|
|
510
|
+
const maxCycles = ultraqa.state.max_cycles || 10;
|
|
511
|
+
if (cycle < maxCycles && !ultraqa.state.all_passing) {
|
|
512
|
+
ultraqa.state.cycle = cycle + 1;
|
|
513
|
+
ultraqa.state.last_checked_at = new Date().toISOString();
|
|
514
|
+
writeJsonFile(ultraqa.path, ultraqa.state);
|
|
515
|
+
|
|
516
|
+
// Fire-and-forget notification
|
|
517
|
+
sendStopNotification('ultraqa', ultraqa.state, sessionId, directory).catch(() => {});
|
|
518
|
+
|
|
519
|
+
console.log(
|
|
520
|
+
JSON.stringify({
|
|
521
|
+
decision: "block",
|
|
522
|
+
reason: `[ULTRAQA - Cycle ${cycle + 1}/${maxCycles}] Tests not all passing. Continue fixing. When all tests pass, run /ultrapower:cancel to cleanly exit and clean up state files. If cancel fails, retry with /ultrapower:cancel --force.`,
|
|
523
|
+
}),
|
|
524
|
+
);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Priority 7: Ultrawork - ALWAYS continue while active (not just when tasks exist)
|
|
530
|
+
// This prevents false stops from bash errors, transient failures, etc.
|
|
531
|
+
// Session isolation: only block if state belongs to this session (issue #311)
|
|
532
|
+
// Project isolation: only block if state belongs to this project
|
|
533
|
+
if (
|
|
534
|
+
ultrawork.state?.active &&
|
|
535
|
+
!isStaleState(ultrawork.state) &&
|
|
536
|
+
isSessionMatch(ultrawork.state, sessionId) &&
|
|
537
|
+
isStateForCurrentProject(ultrawork.state, directory, ultrawork.isGlobal)
|
|
538
|
+
) {
|
|
539
|
+
const newCount = (ultrawork.state.reinforcement_count || 0) + 1;
|
|
540
|
+
const maxReinforcements = ultrawork.state.max_reinforcements || 50;
|
|
541
|
+
|
|
542
|
+
if (newCount > maxReinforcements) {
|
|
543
|
+
// Max reinforcements reached - allow stop
|
|
544
|
+
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
ultrawork.state.reinforcement_count = newCount;
|
|
549
|
+
ultrawork.state.last_checked_at = new Date().toISOString();
|
|
550
|
+
writeJsonFile(ultrawork.path, ultrawork.state);
|
|
551
|
+
|
|
552
|
+
// Fire-and-forget notification
|
|
553
|
+
sendStopNotification('ultrawork', ultrawork.state, sessionId, directory).catch(() => {});
|
|
554
|
+
|
|
555
|
+
let reason = `[ULTRAWORK #${newCount}/${maxReinforcements}] Mode active.`;
|
|
556
|
+
|
|
557
|
+
if (totalIncomplete > 0) {
|
|
558
|
+
const itemType = taskCount > 0 ? "Tasks" : "todos";
|
|
559
|
+
reason += ` ${totalIncomplete} incomplete ${itemType} remain. Continue working.`;
|
|
560
|
+
} else if (newCount >= 3) {
|
|
561
|
+
// Only suggest cancel after minimum iterations (guard against no-tasks-created scenario)
|
|
562
|
+
reason += ` If all work is complete, run /ultrapower:cancel to cleanly exit ultrawork mode and clean up state files. If cancel fails, retry with /ultrapower:cancel --force. Otherwise, continue working.`;
|
|
563
|
+
} else {
|
|
564
|
+
// Early iterations with no tasks yet - just tell LLM to continue
|
|
565
|
+
reason += ` Continue working - create Tasks to track your progress.`;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (ultrawork.state.original_prompt) {
|
|
569
|
+
reason += `\nTask: ${ultrawork.state.original_prompt}`;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
console.log(JSON.stringify({ decision: "block", reason }));
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// No blocking needed — Claude is truly idle.
|
|
577
|
+
// Send session-idle notification (fire-and-forget) so external integrations
|
|
578
|
+
// (Telegram, Discord) know the session went idle without any active mode.
|
|
579
|
+
if (sessionId) {
|
|
580
|
+
try {
|
|
581
|
+
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT;
|
|
582
|
+
if (pluginRoot) {
|
|
583
|
+
const { pathToFileURL } = require('url');
|
|
584
|
+
import(pathToFileURL(join(pluginRoot, 'dist', 'notifications', 'index.js')).href)
|
|
585
|
+
.then(({ notify }) =>
|
|
586
|
+
notify('session-idle', {
|
|
587
|
+
sessionId,
|
|
588
|
+
projectPath: directory,
|
|
589
|
+
}).catch(() => {})
|
|
590
|
+
)
|
|
591
|
+
.catch(() => {});
|
|
592
|
+
}
|
|
593
|
+
} catch {
|
|
594
|
+
// Notification module not available, skip silently
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
598
|
+
} catch (error) {
|
|
599
|
+
// On any error, allow stop rather than blocking forever
|
|
600
|
+
console.error(`[persistent-mode] Error: ${error.message}`);
|
|
601
|
+
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
main();
|