aiwcli 0.10.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/run.js +1 -1
- package/dist/commands/clear.d.ts +11 -6
- package/dist/commands/clear.js +229 -381
- package/dist/commands/init/index.d.ts +1 -17
- package/dist/commands/init/index.js +22 -107
- package/dist/lib/gitignore-manager.d.ts +32 -0
- package/dist/lib/gitignore-manager.js +141 -2
- package/dist/lib/template-installer.d.ts +7 -12
- package/dist/lib/template-installer.js +69 -193
- package/dist/lib/template-settings-reconstructor.d.ts +35 -0
- package/dist/lib/template-settings-reconstructor.js +130 -0
- package/dist/templates/CLAUDE.md +8 -8
- package/dist/templates/_shared/.claude/commands/handoff-resume.md +64 -0
- package/dist/templates/_shared/.claude/commands/handoff.md +16 -10
- package/dist/templates/_shared/.claude/settings.json +7 -7
- package/dist/templates/_shared/hooks-ts/_utils/git-state.ts +2 -0
- package/dist/templates/_shared/hooks-ts/archive_plan.ts +159 -0
- package/dist/templates/_shared/hooks-ts/context_monitor.ts +147 -0
- package/dist/templates/_shared/hooks-ts/file-suggestion.ts +130 -0
- package/dist/templates/_shared/hooks-ts/pre_compact.ts +49 -0
- package/dist/templates/_shared/hooks-ts/session_end.ts +104 -0
- package/dist/templates/_shared/hooks-ts/session_start.ts +144 -0
- package/dist/templates/_shared/hooks-ts/task_create_capture.ts +48 -0
- package/dist/templates/_shared/hooks-ts/task_update_capture.ts +74 -0
- package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +83 -0
- package/dist/templates/_shared/lib-ts/CLAUDE.md +318 -0
- package/dist/templates/_shared/lib-ts/base/atomic-write.ts +138 -0
- package/dist/templates/_shared/lib-ts/base/constants.ts +306 -0
- package/dist/templates/_shared/lib-ts/base/git-state.ts +58 -0
- package/dist/templates/_shared/lib-ts/base/hook-utils.ts +439 -0
- package/dist/templates/_shared/lib-ts/base/inference.ts +252 -0
- package/dist/templates/_shared/lib-ts/base/logger.ts +250 -0
- package/dist/templates/_shared/lib-ts/base/state-io.ts +116 -0
- package/dist/templates/_shared/lib-ts/base/stop-words.ts +184 -0
- package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +162 -0
- package/dist/templates/_shared/lib-ts/base/utils.ts +184 -0
- package/dist/templates/_shared/lib-ts/context/context-formatter.ts +438 -0
- package/dist/templates/_shared/lib-ts/context/context-selector.ts +515 -0
- package/dist/templates/_shared/lib-ts/context/context-store.ts +707 -0
- package/dist/templates/_shared/lib-ts/context/plan-manager.ts +316 -0
- package/dist/templates/_shared/lib-ts/context/task-tracker.ts +185 -0
- package/dist/templates/_shared/lib-ts/handoff/document-generator.ts +216 -0
- package/dist/templates/_shared/lib-ts/handoff/handoff-reader.ts +159 -0
- package/dist/templates/_shared/lib-ts/package.json +21 -0
- package/dist/templates/_shared/lib-ts/templates/formatters.ts +104 -0
- package/dist/templates/_shared/{lib/templates/plan_context.py → lib-ts/templates/plan-context.ts} +14 -22
- package/dist/templates/_shared/lib-ts/tsconfig.json +13 -0
- package/dist/templates/_shared/lib-ts/types.ts +164 -0
- package/dist/templates/_shared/scripts/resolve_context.ts +24 -0
- package/dist/templates/_shared/scripts/resume_handoff.ts +321 -0
- package/dist/templates/_shared/scripts/save_handoff.ts +359 -0
- package/dist/templates/_shared/scripts/status_line.ts +733 -0
- package/dist/templates/cc-native/.claude/settings.json +175 -185
- package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +15 -17
- package/dist/templates/cc-native/_cc-native/agents/ARCH-EVOLUTION.md +63 -0
- package/dist/templates/cc-native/_cc-native/agents/ARCH-PATTERNS.md +62 -0
- package/dist/templates/cc-native/_cc-native/agents/ARCH-STRUCTURE.md +63 -0
- package/dist/templates/cc-native/_cc-native/agents/{ASSUMPTION-CHAIN-TRACER.md → ASSUMPTION-TRACER.md} +6 -10
- package/dist/templates/cc-native/_cc-native/agents/CLARITY-AUDITOR.md +6 -10
- package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +74 -3
- package/dist/templates/cc-native/_cc-native/agents/COMPLETENESS-FEASIBILITY.md +67 -0
- package/dist/templates/cc-native/_cc-native/agents/COMPLETENESS-GAPS.md +71 -0
- package/dist/templates/cc-native/_cc-native/agents/COMPLETENESS-ORDERING.md +63 -0
- package/dist/templates/cc-native/_cc-native/agents/CONSTRAINT-VALIDATOR.md +73 -0
- package/dist/templates/cc-native/_cc-native/agents/DESIGN-ADR-VALIDATOR.md +62 -0
- package/dist/templates/cc-native/_cc-native/agents/DESIGN-SCALE-MATCHER.md +65 -0
- package/dist/templates/cc-native/_cc-native/agents/DEVILS-ADVOCATE.md +6 -9
- package/dist/templates/cc-native/_cc-native/agents/DOCUMENTATION-PHILOSOPHY.md +87 -0
- package/dist/templates/cc-native/_cc-native/agents/HANDOFF-READINESS.md +5 -9
- package/dist/templates/cc-native/_cc-native/agents/{HIDDEN-COMPLEXITY-DETECTOR.md → HIDDEN-COMPLEXITY.md} +6 -10
- package/dist/templates/cc-native/_cc-native/agents/INCREMENTAL-DELIVERY.md +67 -0
- package/dist/templates/cc-native/_cc-native/agents/PLAN-ORCHESTRATOR.md +91 -18
- package/dist/templates/cc-native/_cc-native/agents/RISK-DEPENDENCY.md +63 -0
- package/dist/templates/cc-native/_cc-native/agents/RISK-FMEA.md +67 -0
- package/dist/templates/cc-native/_cc-native/agents/RISK-PREMORTEM.md +72 -0
- package/dist/templates/cc-native/_cc-native/agents/RISK-REVERSIBILITY.md +75 -0
- package/dist/templates/cc-native/_cc-native/agents/SCOPE-BOUNDARY.md +78 -0
- package/dist/templates/cc-native/_cc-native/agents/SIMPLICITY-GUARDIAN.md +5 -9
- package/dist/templates/cc-native/_cc-native/agents/SKEPTIC.md +16 -12
- package/dist/templates/cc-native/_cc-native/agents/TESTDRIVEN-BEHAVIOR-AUDITOR.md +62 -0
- package/dist/templates/cc-native/_cc-native/agents/TESTDRIVEN-CHARACTERIZATION.md +72 -0
- package/dist/templates/cc-native/_cc-native/agents/TESTDRIVEN-FIRST-VALIDATOR.md +62 -0
- package/dist/templates/cc-native/_cc-native/agents/TESTDRIVEN-PYRAMID-ANALYZER.md +62 -0
- package/dist/templates/cc-native/_cc-native/agents/TRADEOFF-COSTS.md +68 -0
- package/dist/templates/cc-native/_cc-native/agents/TRADEOFF-STAKEHOLDERS.md +66 -0
- package/dist/templates/cc-native/_cc-native/agents/VERIFY-COVERAGE.md +75 -0
- package/dist/templates/cc-native/_cc-native/agents/VERIFY-STRENGTH.md +70 -0
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +109 -135
- package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.ts +119 -0
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +921 -0
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +157 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +709 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +199 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +124 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +80 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +119 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +162 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/nul +3 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +249 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +155 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/codex.ts +130 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/gemini.ts +106 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +10 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +23 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +243 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +310 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +72 -0
- package/dist/templates/cc-native/_cc-native/plan-review.config.json +12 -16
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
- package/dist/lib/template-merger.d.ts +0 -47
- package/dist/lib/template-merger.js +0 -162
- package/dist/templates/_shared/hooks/__init__.py +0 -16
- package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/archive_plan.py +0 -169
- package/dist/templates/_shared/hooks/context_monitor.py +0 -270
- package/dist/templates/_shared/hooks/file-suggestion.py +0 -215
- package/dist/templates/_shared/hooks/pre_compact.py +0 -104
- package/dist/templates/_shared/hooks/session_end.py +0 -173
- package/dist/templates/_shared/hooks/session_start.py +0 -206
- package/dist/templates/_shared/hooks/task_create_capture.py +0 -108
- package/dist/templates/_shared/hooks/task_update_capture.py +0 -145
- package/dist/templates/_shared/hooks/user_prompt_submit.py +0 -139
- package/dist/templates/_shared/lib/__init__.py +0 -1
- package/dist/templates/_shared/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__init__.py +0 -65
- package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/atomic_write.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/stop_words.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/subprocess_utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/atomic_write.py +0 -180
- package/dist/templates/_shared/lib/base/constants.py +0 -358
- package/dist/templates/_shared/lib/base/hook_utils.py +0 -341
- package/dist/templates/_shared/lib/base/inference.py +0 -318
- package/dist/templates/_shared/lib/base/logger.py +0 -291
- package/dist/templates/_shared/lib/base/stop_words.py +0 -213
- package/dist/templates/_shared/lib/base/subprocess_utils.py +0 -46
- package/dist/templates/_shared/lib/base/utils.py +0 -242
- package/dist/templates/_shared/lib/context/__init__.py +0 -102
- package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/cache.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_extractor.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/event_log.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/plan_archive.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/task_sync.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/context_formatter.py +0 -317
- package/dist/templates/_shared/lib/context/context_selector.py +0 -508
- package/dist/templates/_shared/lib/context/context_store.py +0 -653
- package/dist/templates/_shared/lib/context/plan_manager.py +0 -204
- package/dist/templates/_shared/lib/context/task_tracker.py +0 -188
- package/dist/templates/_shared/lib/handoff/__init__.py +0 -22
- package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/handoff/document_generator.py +0 -278
- package/dist/templates/_shared/lib/templates/README.md +0 -206
- package/dist/templates/_shared/lib/templates/__init__.py +0 -36
- package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/persona_questions.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/formatters.py +0 -146
- package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
- package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
- package/dist/templates/_shared/scripts/save_handoff.py +0 -357
- package/dist/templates/_shared/scripts/status_line.py +0 -701
- package/dist/templates/cc-native/.claude/commands/cc-native/fresh-perspective.md +0 -8
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/fresh-perspective.md +0 -8
- package/dist/templates/cc-native/MIGRATION.md +0 -86
- package/dist/templates/cc-native/_cc-native/agents/ACCESSIBILITY-TESTER.md +0 -79
- package/dist/templates/cc-native/_cc-native/agents/ARCHITECT-REVIEWER.md +0 -48
- package/dist/templates/cc-native/_cc-native/agents/CODE-REVIEWER.md +0 -70
- package/dist/templates/cc-native/_cc-native/agents/COMPLETENESS-CHECKER.md +0 -59
- package/dist/templates/cc-native/_cc-native/agents/CONTEXT-EXTRACTOR.md +0 -92
- package/dist/templates/cc-native/_cc-native/agents/DOCUMENTATION-REVIEWER.md +0 -51
- package/dist/templates/cc-native/_cc-native/agents/FEASIBILITY-ANALYST.md +0 -57
- package/dist/templates/cc-native/_cc-native/agents/FRESH-PERSPECTIVE.md +0 -54
- package/dist/templates/cc-native/_cc-native/agents/INCENTIVE-MAPPER.md +0 -61
- package/dist/templates/cc-native/_cc-native/agents/PENETRATION-TESTER.md +0 -79
- package/dist/templates/cc-native/_cc-native/agents/PERFORMANCE-ENGINEER.md +0 -75
- package/dist/templates/cc-native/_cc-native/agents/PRECEDENT-FINDER.md +0 -70
- package/dist/templates/cc-native/_cc-native/agents/REVERSIBILITY-ANALYST.md +0 -61
- package/dist/templates/cc-native/_cc-native/agents/RISK-ASSESSOR.md +0 -58
- package/dist/templates/cc-native/_cc-native/agents/SECOND-ORDER-ANALYST.md +0 -61
- package/dist/templates/cc-native/_cc-native/agents/STAKEHOLDER-ADVOCATE.md +0 -55
- package/dist/templates/cc-native/_cc-native/agents/TRADE-OFF-ILLUMINATOR.md +0 -204
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +0 -130
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +0 -869
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +0 -81
- package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +0 -340
- package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +0 -265
- package/dist/templates/cc-native/_cc-native/lib/__init__.py +0 -53
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/atomic_write.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/constants.py +0 -45
- package/dist/templates/cc-native/_cc-native/lib/debug.py +0 -139
- package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +0 -362
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__init__.py +0 -28
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +0 -215
- package/dist/templates/cc-native/_cc-native/lib/reviewers/base.py +0 -88
- package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +0 -124
- package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +0 -108
- package/dist/templates/cc-native/_cc-native/lib/state.py +0 -268
- package/dist/templates/cc-native/_cc-native/lib/utils.py +0 -1027
- package/dist/templates/cc-native/_cc-native/scripts/__pycache__/aggregate_agents.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/scripts/aggregate_agents.py +0 -168
- package/dist/templates/cc-native/_cc-native/workflows/fresh-perspective.md +0 -134
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""PostToolUse hook - captures TaskUpdate operations for persistence.
|
|
3
|
-
|
|
4
|
-
This hook runs after Claude uses the TaskUpdate tool and automatically
|
|
5
|
-
records the update in the context's state.json.
|
|
6
|
-
|
|
7
|
-
Hook input (from Claude Code):
|
|
8
|
-
{
|
|
9
|
-
"hook_event_name": "PostToolUse",
|
|
10
|
-
"tool_name": "TaskUpdate",
|
|
11
|
-
"tool_input": {
|
|
12
|
-
"taskId": "1",
|
|
13
|
-
"status": "completed",
|
|
14
|
-
"metadata": {"evidence": "...", "work_summary": "...", ...},
|
|
15
|
-
"addBlockedBy": ["2"],
|
|
16
|
-
...
|
|
17
|
-
},
|
|
18
|
-
"tool_response": {...},
|
|
19
|
-
"session_id": "abc123",
|
|
20
|
-
"cwd": "/path/to/project"
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
Hook output:
|
|
24
|
-
- Silent on success (no stdout output)
|
|
25
|
-
- Logs to stderr for debugging
|
|
26
|
-
"""
|
|
27
|
-
import sys
|
|
28
|
-
from pathlib import Path
|
|
29
|
-
from typing import Dict, Any
|
|
30
|
-
|
|
31
|
-
# Add parent directories to path for imports
|
|
32
|
-
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
33
|
-
SHARED_LIB = SCRIPT_DIR.parent / "lib"
|
|
34
|
-
sys.path.insert(0, str(SHARED_LIB.parent))
|
|
35
|
-
|
|
36
|
-
from lib.base.hook_utils import (
|
|
37
|
-
load_hook_input,
|
|
38
|
-
validate_hook_event,
|
|
39
|
-
get_tool_input,
|
|
40
|
-
check_skip_persistence,
|
|
41
|
-
safe_hook_main,
|
|
42
|
-
run_hook,
|
|
43
|
-
log_debug,
|
|
44
|
-
log_info,
|
|
45
|
-
log_warn,
|
|
46
|
-
log_error,
|
|
47
|
-
)
|
|
48
|
-
from lib.base.utils import project_dir
|
|
49
|
-
from lib.context.context_store import get_context_by_session_id
|
|
50
|
-
from lib.context.task_tracker import update_task, delete_task
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def get_persistent_task_id(
|
|
54
|
-
claude_task_id: str,
|
|
55
|
-
tool_input: Dict[str, Any]
|
|
56
|
-
) -> str:
|
|
57
|
-
"""Convert Claude's ephemeral task ID to persistent task ID."""
|
|
58
|
-
metadata = tool_input.get("metadata", {})
|
|
59
|
-
if isinstance(metadata, dict):
|
|
60
|
-
persistent_id = metadata.get("persistent_id")
|
|
61
|
-
if persistent_id:
|
|
62
|
-
return persistent_id
|
|
63
|
-
return f"aiw-{claude_task_id}"
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
@safe_hook_main("task_update_capture")
|
|
67
|
-
def main() -> int:
|
|
68
|
-
"""Main hook entry point."""
|
|
69
|
-
payload = load_hook_input()
|
|
70
|
-
if not payload:
|
|
71
|
-
return 0
|
|
72
|
-
|
|
73
|
-
if not validate_hook_event(payload, "PostToolUse", "TaskUpdate"):
|
|
74
|
-
return 0
|
|
75
|
-
|
|
76
|
-
tool_input = get_tool_input(payload)
|
|
77
|
-
if not tool_input:
|
|
78
|
-
log_warn("task_update_capture", "Invalid tool_input: not a dict")
|
|
79
|
-
return 0
|
|
80
|
-
|
|
81
|
-
if check_skip_persistence(payload, "task_update_capture"):
|
|
82
|
-
return 0
|
|
83
|
-
|
|
84
|
-
project_root = project_dir(payload)
|
|
85
|
-
session_id = payload.get("session_id", "")
|
|
86
|
-
|
|
87
|
-
# Find context by session ID
|
|
88
|
-
state = get_context_by_session_id(session_id, project_root)
|
|
89
|
-
if not state:
|
|
90
|
-
log_debug("task_update_capture", "No context available - skipping persistence")
|
|
91
|
-
return 0
|
|
92
|
-
|
|
93
|
-
context_id = state.id
|
|
94
|
-
|
|
95
|
-
# Extract task ID
|
|
96
|
-
claude_task_id = tool_input.get("taskId")
|
|
97
|
-
if not claude_task_id:
|
|
98
|
-
log_warn("task_update_capture", "Missing required field: taskId")
|
|
99
|
-
return 0
|
|
100
|
-
|
|
101
|
-
persistent_task_id = get_persistent_task_id(claude_task_id, tool_input)
|
|
102
|
-
|
|
103
|
-
status = tool_input.get("status")
|
|
104
|
-
metadata = tool_input.get("metadata", {})
|
|
105
|
-
|
|
106
|
-
events_recorded = []
|
|
107
|
-
|
|
108
|
-
if status == "deleted":
|
|
109
|
-
if delete_task(context_id, persistent_task_id, project_root):
|
|
110
|
-
events_recorded.append("task_deleted")
|
|
111
|
-
elif status:
|
|
112
|
-
# Extract completion metadata
|
|
113
|
-
evidence = ""
|
|
114
|
-
work_summary = ""
|
|
115
|
-
files_changed = None
|
|
116
|
-
if isinstance(metadata, dict):
|
|
117
|
-
evidence = metadata.get("evidence", "")
|
|
118
|
-
work_summary = metadata.get("work_summary", "")
|
|
119
|
-
files_changed = metadata.get("files_changed")
|
|
120
|
-
if files_changed and not isinstance(files_changed, list):
|
|
121
|
-
files_changed = None
|
|
122
|
-
|
|
123
|
-
success = update_task(
|
|
124
|
-
context_id=context_id,
|
|
125
|
-
task_id=persistent_task_id,
|
|
126
|
-
status=status,
|
|
127
|
-
evidence=evidence,
|
|
128
|
-
work_summary=work_summary,
|
|
129
|
-
files_changed=files_changed,
|
|
130
|
-
session_id=session_id,
|
|
131
|
-
project_root=project_root,
|
|
132
|
-
)
|
|
133
|
-
if success:
|
|
134
|
-
events_recorded.append(f"task_{status}")
|
|
135
|
-
|
|
136
|
-
if events_recorded:
|
|
137
|
-
log_info("task_update_capture", f"Recorded {', '.join(events_recorded)} for {persistent_task_id} in {context_id}")
|
|
138
|
-
else:
|
|
139
|
-
log_debug("task_update_capture", f"No relevant changes for {persistent_task_id}")
|
|
140
|
-
|
|
141
|
-
return 0
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if __name__ == "__main__":
|
|
145
|
-
run_hook(main, "task_update_capture")
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Unified UserPromptSubmit hook entry point.
|
|
3
|
-
|
|
4
|
-
This hook runs on every UserPromptSubmit and handles:
|
|
5
|
-
- Context enforcement - ensures all work happens in a tracked context
|
|
6
|
-
|
|
7
|
-
Note: Context monitoring (handoff warnings) is handled separately by
|
|
8
|
-
context_monitor.py on PostToolUse events, which fires during Claude's
|
|
9
|
-
work rather than waiting for user input.
|
|
10
|
-
|
|
11
|
-
Hook input (from Claude Code):
|
|
12
|
-
{
|
|
13
|
-
"hook_type": "UserPromptSubmit",
|
|
14
|
-
"prompt": "user's message text",
|
|
15
|
-
"session_id": "abc123",
|
|
16
|
-
...
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
Hook output:
|
|
20
|
-
- Prints system reminders to stdout for context enforcement
|
|
21
|
-
"""
|
|
22
|
-
import sys
|
|
23
|
-
from pathlib import Path
|
|
24
|
-
from typing import List
|
|
25
|
-
|
|
26
|
-
# Add parent directories to path for imports
|
|
27
|
-
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
28
|
-
SHARED_LIB = SCRIPT_DIR.parent / "lib"
|
|
29
|
-
sys.path.insert(0, str(SHARED_LIB.parent))
|
|
30
|
-
|
|
31
|
-
from lib.base.hook_utils import load_hook_input, log_debug, log_info, log_warn, log_error, log_diagnostic
|
|
32
|
-
from lib.base.utils import project_dir
|
|
33
|
-
from lib.context.context_store import (
|
|
34
|
-
get_context,
|
|
35
|
-
get_context_by_session_id,
|
|
36
|
-
bind_session,
|
|
37
|
-
maybe_activate,
|
|
38
|
-
save_state,
|
|
39
|
-
)
|
|
40
|
-
from lib.context.context_selector import determine_context, BlockRequest
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def _update_in_flight_status(context_id: str, hook_input: dict, project_root: Path) -> None:
|
|
44
|
-
"""
|
|
45
|
-
Update context mode based on permission mode.
|
|
46
|
-
|
|
47
|
-
- permission_mode == "plan": no-op (planning is runtime-only, not persisted)
|
|
48
|
-
- permission_mode != "plan" and mode == "idle": set to "active"
|
|
49
|
-
- permission_mode != "plan" and mode == "has_plan": set to "active" (plan was accepted)
|
|
50
|
-
"""
|
|
51
|
-
permission_mode = hook_input.get("permission_mode", "default")
|
|
52
|
-
log_debug("user_prompt_submit", f"context_id={context_id}, permission_mode={permission_mode}")
|
|
53
|
-
maybe_activate(context_id, permission_mode, project_root=project_root, caller="user_prompt_submit")
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def main():
|
|
57
|
-
"""
|
|
58
|
-
Main entry point for UserPromptSubmit hook.
|
|
59
|
-
|
|
60
|
-
Handles context enforcement for all user prompts.
|
|
61
|
-
Uses session_id to detect first prompt vs subsequent prompts.
|
|
62
|
-
"""
|
|
63
|
-
try:
|
|
64
|
-
hook_input = load_hook_input()
|
|
65
|
-
if not hook_input:
|
|
66
|
-
return
|
|
67
|
-
|
|
68
|
-
user_prompt = hook_input.get("prompt", "")
|
|
69
|
-
project_root = project_dir(hook_input)
|
|
70
|
-
session_id = hook_input.get("session_id", "unknown")
|
|
71
|
-
|
|
72
|
-
log_diagnostic("user_prompt_submit", "receive", f"session={session_id[:8]}, prompt_len={len(user_prompt)}",
|
|
73
|
-
inputs={"session_id": session_id[:12], "prompt_length": len(user_prompt)})
|
|
74
|
-
|
|
75
|
-
outputs: List[str] = []
|
|
76
|
-
active_context_id = None
|
|
77
|
-
|
|
78
|
-
# First-prompt detection: check if session_id is already bound to a context
|
|
79
|
-
existing_context = get_context_by_session_id(session_id, project_root)
|
|
80
|
-
|
|
81
|
-
if existing_context:
|
|
82
|
-
# NOT first prompt - session already bound to context
|
|
83
|
-
log_debug("user_prompt_submit", f"Session {session_id[:8]}... already bound to {existing_context.id}")
|
|
84
|
-
log_diagnostic("user_prompt_submit", "decide", f"Session already bound to {existing_context.id}",
|
|
85
|
-
decision="session_match", reasoning="session_id found in existing context",
|
|
86
|
-
inputs={"context_id": existing_context.id, "mode": existing_context.mode})
|
|
87
|
-
_update_in_flight_status(existing_context.id, hook_input, project_root)
|
|
88
|
-
active_context_id = existing_context.id
|
|
89
|
-
elif user_prompt:
|
|
90
|
-
# FIRST prompt - need context detection
|
|
91
|
-
try:
|
|
92
|
-
context_id, method, context_output = determine_context(user_prompt, session_id, project_root)
|
|
93
|
-
log_info("user_prompt_submit", f"Context: {method} -> {context_id}")
|
|
94
|
-
log_diagnostic("user_prompt_submit", "decide", f"Context detected via {method}: {context_id}",
|
|
95
|
-
decision=method, reasoning=f"determine_context returned method={method}",
|
|
96
|
-
inputs={"context_id": context_id, "has_output": bool(context_output)})
|
|
97
|
-
|
|
98
|
-
if context_id:
|
|
99
|
-
# Bind session to context
|
|
100
|
-
bind_session(context_id, session_id, project_root)
|
|
101
|
-
log_info("user_prompt_submit", f"Bound session {session_id[:8]}... to context '{context_id}'")
|
|
102
|
-
|
|
103
|
-
# Update mode based on permission mode
|
|
104
|
-
_update_in_flight_status(context_id, hook_input, project_root)
|
|
105
|
-
active_context_id = context_id
|
|
106
|
-
|
|
107
|
-
# Clear handoff_path after it's been injected via context_selector
|
|
108
|
-
try:
|
|
109
|
-
ctx = get_context(context_id, project_root)
|
|
110
|
-
if ctx and ctx.handoff_path:
|
|
111
|
-
ctx.handoff_path = None
|
|
112
|
-
save_state(ctx, project_root)
|
|
113
|
-
log_debug("user_prompt_submit", f"Cleared handoff_path for {context_id}")
|
|
114
|
-
except Exception as e:
|
|
115
|
-
log_warn("user_prompt_submit", f"Failed to clear handoff_path: {e}")
|
|
116
|
-
|
|
117
|
-
if context_output:
|
|
118
|
-
outputs.append(context_output)
|
|
119
|
-
|
|
120
|
-
except BlockRequest as e:
|
|
121
|
-
log_error("user_prompt_submit", e.message)
|
|
122
|
-
sys.exit(2)
|
|
123
|
-
|
|
124
|
-
log_diagnostic("user_prompt_submit", "result", f"context={active_context_id}, outputs={len(outputs)}",
|
|
125
|
-
inputs={"active_context_id": active_context_id, "output_count": len(outputs)})
|
|
126
|
-
|
|
127
|
-
if outputs:
|
|
128
|
-
print("\n\n".join(outputs))
|
|
129
|
-
|
|
130
|
-
except Exception as e:
|
|
131
|
-
import traceback
|
|
132
|
-
tb = traceback.format_exc()
|
|
133
|
-
from lib.base.hook_utils import log_hook_error
|
|
134
|
-
log_hook_error("user_prompt_submit", e, "UserPromptSubmit", traceback_str=tb)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if __name__ == "__main__":
|
|
138
|
-
from lib.base.hook_utils import run_hook
|
|
139
|
-
run_hook(main, "user_prompt_submit")
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"""Shared library for AIW CLI templates."""
|
|
Binary file
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
"""Base utilities for shared context management."""
|
|
2
|
-
from .atomic_write import atomic_write, atomic_append
|
|
3
|
-
from .constants import (
|
|
4
|
-
OUTPUT_DIR,
|
|
5
|
-
CONTEXTS_DIR,
|
|
6
|
-
INDEX_FILENAME,
|
|
7
|
-
validate_context_id,
|
|
8
|
-
get_output_dir,
|
|
9
|
-
get_contexts_dir,
|
|
10
|
-
get_context_dir,
|
|
11
|
-
get_context_plans_dir,
|
|
12
|
-
get_context_handoffs_dir,
|
|
13
|
-
get_index_path,
|
|
14
|
-
get_context_file_path,
|
|
15
|
-
get_events_file_path,
|
|
16
|
-
)
|
|
17
|
-
from .utils import (
|
|
18
|
-
eprint,
|
|
19
|
-
now_local,
|
|
20
|
-
now_iso,
|
|
21
|
-
project_dir,
|
|
22
|
-
sanitize_filename,
|
|
23
|
-
sanitize_title,
|
|
24
|
-
generate_context_id,
|
|
25
|
-
)
|
|
26
|
-
from .logger import (
|
|
27
|
-
hook_log,
|
|
28
|
-
log_debug,
|
|
29
|
-
log_info,
|
|
30
|
-
log_warn,
|
|
31
|
-
log_error,
|
|
32
|
-
log_hook_error,
|
|
33
|
-
set_context_path,
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
__all__ = [
|
|
37
|
-
"atomic_write",
|
|
38
|
-
"atomic_append",
|
|
39
|
-
"OUTPUT_DIR",
|
|
40
|
-
"CONTEXTS_DIR",
|
|
41
|
-
"INDEX_FILENAME",
|
|
42
|
-
"validate_context_id",
|
|
43
|
-
"get_output_dir",
|
|
44
|
-
"get_contexts_dir",
|
|
45
|
-
"get_context_dir",
|
|
46
|
-
"get_context_plans_dir",
|
|
47
|
-
"get_context_handoffs_dir",
|
|
48
|
-
"get_index_path",
|
|
49
|
-
"get_context_file_path",
|
|
50
|
-
"get_events_file_path",
|
|
51
|
-
"eprint",
|
|
52
|
-
"now_local",
|
|
53
|
-
"now_iso",
|
|
54
|
-
"project_dir",
|
|
55
|
-
"sanitize_filename",
|
|
56
|
-
"sanitize_title",
|
|
57
|
-
"generate_context_id",
|
|
58
|
-
"hook_log",
|
|
59
|
-
"log_debug",
|
|
60
|
-
"log_info",
|
|
61
|
-
"log_warn",
|
|
62
|
-
"log_error",
|
|
63
|
-
"log_hook_error",
|
|
64
|
-
"set_context_path",
|
|
65
|
-
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
"""Cross-platform atomic file writes with security.
|
|
2
|
-
|
|
3
|
-
Provides crash-safe file writes by writing to a temp file first,
|
|
4
|
-
then atomically replacing the target. This prevents corrupted files
|
|
5
|
-
if the process crashes mid-write.
|
|
6
|
-
|
|
7
|
-
Note: This is for crash-safety, NOT for concurrent access.
|
|
8
|
-
The shared context system assumes single-session-per-context.
|
|
9
|
-
"""
|
|
10
|
-
import os
|
|
11
|
-
import sys
|
|
12
|
-
import tempfile
|
|
13
|
-
import time
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
from typing import Optional, Tuple
|
|
16
|
-
|
|
17
|
-
if sys.platform == 'win32':
|
|
18
|
-
import ctypes
|
|
19
|
-
from ctypes import wintypes
|
|
20
|
-
|
|
21
|
-
# Windows MoveFileEx flags
|
|
22
|
-
MOVEFILE_REPLACE_EXISTING = 0x1
|
|
23
|
-
MOVEFILE_WRITE_THROUGH = 0x8
|
|
24
|
-
|
|
25
|
-
def _atomic_replace_windows(src: Path, dst: Path) -> None:
|
|
26
|
-
"""Atomic file replacement on Windows using MoveFileEx."""
|
|
27
|
-
kernel32 = ctypes.windll.kernel32
|
|
28
|
-
|
|
29
|
-
# Set proper function prototypes for 64-bit safety
|
|
30
|
-
kernel32.MoveFileExW.argtypes = [wintypes.LPCWSTR, wintypes.LPCWSTR, wintypes.DWORD]
|
|
31
|
-
kernel32.MoveFileExW.restype = wintypes.BOOL
|
|
32
|
-
|
|
33
|
-
result = kernel32.MoveFileExW(
|
|
34
|
-
str(src),
|
|
35
|
-
str(dst),
|
|
36
|
-
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH
|
|
37
|
-
)
|
|
38
|
-
if not result:
|
|
39
|
-
error_code = kernel32.GetLastError()
|
|
40
|
-
raise ctypes.WinError(error_code)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def atomic_write(
|
|
44
|
-
path: Path,
|
|
45
|
-
content: str,
|
|
46
|
-
max_attempts: int = 2,
|
|
47
|
-
backoff_ms: Optional[list] = None
|
|
48
|
-
) -> Tuple[bool, Optional[str]]:
|
|
49
|
-
"""
|
|
50
|
-
Write file atomically with retry logic.
|
|
51
|
-
|
|
52
|
-
Creates a temp file in the same directory, writes content,
|
|
53
|
-
then atomically replaces the target file. This ensures the
|
|
54
|
-
file is never left in a corrupted state.
|
|
55
|
-
|
|
56
|
-
Args:
|
|
57
|
-
path: Target file path
|
|
58
|
-
content: Content to write
|
|
59
|
-
max_attempts: Maximum retry attempts (default: 2)
|
|
60
|
-
backoff_ms: Retry backoff in milliseconds (default: [500, 1000])
|
|
61
|
-
|
|
62
|
-
Returns:
|
|
63
|
-
Tuple of (success: bool, error_message: Optional[str])
|
|
64
|
-
"""
|
|
65
|
-
if backoff_ms is None:
|
|
66
|
-
backoff_ms = [500, 1000]
|
|
67
|
-
|
|
68
|
-
# Ensure parent directory exists
|
|
69
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
70
|
-
|
|
71
|
-
for attempt in range(max_attempts):
|
|
72
|
-
try:
|
|
73
|
-
# Create temp file in same directory for atomic rename
|
|
74
|
-
temp_fd, temp_path_str = tempfile.mkstemp(
|
|
75
|
-
dir=path.parent,
|
|
76
|
-
prefix=f".{path.stem}_",
|
|
77
|
-
suffix=".tmp"
|
|
78
|
-
)
|
|
79
|
-
temp_path = Path(temp_path_str)
|
|
80
|
-
|
|
81
|
-
try:
|
|
82
|
-
# Write content to temp file
|
|
83
|
-
with os.fdopen(temp_fd, 'w', encoding='utf-8') as f:
|
|
84
|
-
f.write(content)
|
|
85
|
-
f.flush()
|
|
86
|
-
os.fsync(f.fileno()) # Force write to disk
|
|
87
|
-
|
|
88
|
-
# Set restrictive permissions before rename (chmod 600)
|
|
89
|
-
try:
|
|
90
|
-
os.chmod(temp_path, 0o600)
|
|
91
|
-
except OSError:
|
|
92
|
-
pass # chmod may fail on some filesystems
|
|
93
|
-
|
|
94
|
-
# Platform-specific atomic rename
|
|
95
|
-
if sys.platform == 'win32':
|
|
96
|
-
_atomic_replace_windows(temp_path, path)
|
|
97
|
-
else:
|
|
98
|
-
temp_path.replace(path) # POSIX atomic
|
|
99
|
-
|
|
100
|
-
return (True, None)
|
|
101
|
-
|
|
102
|
-
except Exception:
|
|
103
|
-
# Clean up temp file on failure
|
|
104
|
-
try:
|
|
105
|
-
temp_path.unlink()
|
|
106
|
-
except Exception:
|
|
107
|
-
pass # Cleanup is best-effort
|
|
108
|
-
raise
|
|
109
|
-
|
|
110
|
-
except Exception as e:
|
|
111
|
-
if attempt < max_attempts - 1:
|
|
112
|
-
# Bounds-safe backoff indexing
|
|
113
|
-
wait_ms = backoff_ms[min(attempt, len(backoff_ms) - 1)]
|
|
114
|
-
time.sleep(wait_ms / 1000.0)
|
|
115
|
-
else:
|
|
116
|
-
# Sanitize error message (no paths, no stack trace)
|
|
117
|
-
error_type = type(e).__name__
|
|
118
|
-
error_msg = str(e).split('\n')[0][:200] # First line only, max 200 chars
|
|
119
|
-
return (False, f"{error_type}: {error_msg}")
|
|
120
|
-
|
|
121
|
-
return (False, "Max retry attempts exceeded")
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def atomic_append(
|
|
125
|
-
path: Path,
|
|
126
|
-
content: str,
|
|
127
|
-
max_attempts: int = 2,
|
|
128
|
-
backoff_ms: Optional[list] = None
|
|
129
|
-
) -> Tuple[bool, Optional[str]]:
|
|
130
|
-
"""
|
|
131
|
-
Append to file atomically with retry logic.
|
|
132
|
-
|
|
133
|
-
For JSONL files, this is safe because each line is independent.
|
|
134
|
-
If process crashes mid-append, only the last partial line is lost,
|
|
135
|
-
which read_events() handles gracefully.
|
|
136
|
-
|
|
137
|
-
Args:
|
|
138
|
-
path: Target file path
|
|
139
|
-
content: Content to append (should include newline if needed)
|
|
140
|
-
max_attempts: Maximum retry attempts (default: 2)
|
|
141
|
-
backoff_ms: Retry backoff in milliseconds (default: [500, 1000])
|
|
142
|
-
|
|
143
|
-
Returns:
|
|
144
|
-
Tuple of (success: bool, error_message: Optional[str])
|
|
145
|
-
"""
|
|
146
|
-
if backoff_ms is None:
|
|
147
|
-
backoff_ms = [500, 1000]
|
|
148
|
-
|
|
149
|
-
# Ensure parent directory exists
|
|
150
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
151
|
-
|
|
152
|
-
# Check if file is being created (for permission setting)
|
|
153
|
-
is_new_file = not path.exists()
|
|
154
|
-
|
|
155
|
-
for attempt in range(max_attempts):
|
|
156
|
-
try:
|
|
157
|
-
with open(path, 'a', encoding='utf-8') as f:
|
|
158
|
-
f.write(content)
|
|
159
|
-
f.flush()
|
|
160
|
-
os.fsync(f.fileno()) # Force write to disk
|
|
161
|
-
|
|
162
|
-
# Set restrictive permissions on newly created files (chmod 600)
|
|
163
|
-
if is_new_file:
|
|
164
|
-
try:
|
|
165
|
-
os.chmod(path, 0o600)
|
|
166
|
-
except OSError:
|
|
167
|
-
pass # chmod may fail on some filesystems
|
|
168
|
-
|
|
169
|
-
return (True, None)
|
|
170
|
-
|
|
171
|
-
except Exception as e:
|
|
172
|
-
if attempt < max_attempts - 1:
|
|
173
|
-
wait_ms = backoff_ms[min(attempt, len(backoff_ms) - 1)]
|
|
174
|
-
time.sleep(wait_ms / 1000.0)
|
|
175
|
-
else:
|
|
176
|
-
error_type = type(e).__name__
|
|
177
|
-
error_msg = str(e).split('\n')[0][:200]
|
|
178
|
-
return (False, f"{error_type}: {error_msg}")
|
|
179
|
-
|
|
180
|
-
return (False, "Max retry attempts exceeded")
|