bmalph 2.4.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/README.md +93 -47
  2. package/dist/cli.js +17 -2
  3. package/dist/cli.js.map +1 -0
  4. package/dist/commands/check-updates.js +2 -17
  5. package/dist/commands/check-updates.js.map +1 -0
  6. package/dist/commands/doctor.js +6 -57
  7. package/dist/commands/doctor.js.map +1 -0
  8. package/dist/commands/implement.js +6 -4
  9. package/dist/commands/implement.js.map +1 -0
  10. package/dist/commands/init.js +28 -47
  11. package/dist/commands/init.js.map +1 -0
  12. package/dist/commands/reset.js +7 -10
  13. package/dist/commands/reset.js.map +1 -0
  14. package/dist/commands/run.js +46 -0
  15. package/dist/commands/run.js.map +1 -0
  16. package/dist/commands/status.js +18 -3
  17. package/dist/commands/status.js.map +1 -0
  18. package/dist/commands/upgrade.js +7 -10
  19. package/dist/commands/upgrade.js.map +1 -0
  20. package/dist/commands/watch.js +19 -0
  21. package/dist/commands/watch.js.map +1 -0
  22. package/dist/installer.js +11 -38
  23. package/dist/installer.js.map +1 -0
  24. package/dist/platform/aider.js +7 -63
  25. package/dist/platform/aider.js.map +1 -0
  26. package/dist/platform/claude-code.js +5 -39
  27. package/dist/platform/claude-code.js.map +1 -0
  28. package/dist/platform/codex.js +7 -59
  29. package/dist/platform/codex.js.map +1 -0
  30. package/dist/platform/copilot.js +10 -65
  31. package/dist/platform/copilot.js.map +1 -0
  32. package/dist/platform/cursor.js +7 -63
  33. package/dist/platform/cursor.js.map +1 -0
  34. package/dist/platform/detect.js +2 -1
  35. package/dist/platform/detect.js.map +1 -0
  36. package/dist/platform/doctor-checks.js +61 -0
  37. package/dist/platform/doctor-checks.js.map +1 -0
  38. package/dist/platform/index.js +1 -0
  39. package/dist/platform/index.js.map +1 -0
  40. package/dist/platform/instructions-snippet.js +75 -0
  41. package/dist/platform/instructions-snippet.js.map +1 -0
  42. package/dist/platform/registry.js +7 -0
  43. package/dist/platform/registry.js.map +1 -0
  44. package/dist/platform/resolve.js +1 -0
  45. package/dist/platform/resolve.js.map +1 -0
  46. package/dist/platform/types.js +1 -0
  47. package/dist/platform/types.js.map +1 -0
  48. package/dist/platform/windsurf.js +7 -63
  49. package/dist/platform/windsurf.js.map +1 -0
  50. package/dist/reset.js +6 -20
  51. package/dist/reset.js.map +1 -0
  52. package/dist/run/ralph-process.js +89 -0
  53. package/dist/run/ralph-process.js.map +1 -0
  54. package/dist/run/run-dashboard.js +104 -0
  55. package/dist/run/run-dashboard.js.map +1 -0
  56. package/dist/run/types.js +2 -0
  57. package/dist/run/types.js.map +1 -0
  58. package/dist/transition/artifact-scan.js +3 -2
  59. package/dist/transition/artifact-scan.js.map +1 -0
  60. package/dist/transition/artifacts.js +2 -28
  61. package/dist/transition/artifacts.js.map +1 -0
  62. package/dist/transition/context.js +1 -0
  63. package/dist/transition/context.js.map +1 -0
  64. package/dist/transition/fix-plan.js +1 -0
  65. package/dist/transition/fix-plan.js.map +1 -0
  66. package/dist/transition/index.js +2 -1
  67. package/dist/transition/index.js.map +1 -0
  68. package/dist/transition/orchestration.js +3 -2
  69. package/dist/transition/orchestration.js.map +1 -0
  70. package/dist/transition/preflight.js +1 -0
  71. package/dist/transition/preflight.js.map +1 -0
  72. package/dist/transition/specs-changelog.js +3 -2
  73. package/dist/transition/specs-changelog.js.map +1 -0
  74. package/dist/transition/specs-index.js +3 -2
  75. package/dist/transition/specs-index.js.map +1 -0
  76. package/dist/transition/story-parsing.js +1 -0
  77. package/dist/transition/story-parsing.js.map +1 -0
  78. package/dist/transition/tech-stack.js +1 -0
  79. package/dist/transition/tech-stack.js.map +1 -0
  80. package/dist/transition/types.js +1 -0
  81. package/dist/transition/types.js.map +1 -0
  82. package/dist/utils/config.js +3 -2
  83. package/dist/utils/config.js.map +1 -0
  84. package/dist/utils/constants.js +8 -49
  85. package/dist/utils/constants.js.map +1 -0
  86. package/dist/utils/dryrun.js +1 -20
  87. package/dist/utils/dryrun.js.map +1 -0
  88. package/dist/utils/errors.js +1 -19
  89. package/dist/utils/errors.js.map +1 -0
  90. package/dist/utils/file-system.js +34 -3
  91. package/dist/utils/file-system.js.map +1 -0
  92. package/dist/utils/github.js +21 -8
  93. package/dist/utils/github.js.map +1 -0
  94. package/dist/utils/json.js +2 -1
  95. package/dist/utils/json.js.map +1 -0
  96. package/dist/utils/logger.js +1 -14
  97. package/dist/utils/logger.js.map +1 -0
  98. package/dist/utils/state.js +3 -2
  99. package/dist/utils/state.js.map +1 -0
  100. package/dist/utils/validate.js +10 -1
  101. package/dist/utils/validate.js.map +1 -0
  102. package/dist/watch/dashboard.js +61 -0
  103. package/dist/watch/dashboard.js.map +1 -0
  104. package/dist/watch/file-watcher.js +30 -0
  105. package/dist/watch/file-watcher.js.map +1 -0
  106. package/dist/watch/renderer.js +242 -0
  107. package/dist/watch/renderer.js.map +1 -0
  108. package/dist/watch/state-reader.js +200 -0
  109. package/dist/watch/state-reader.js.map +1 -0
  110. package/dist/watch/types.js +2 -0
  111. package/dist/watch/types.js.map +1 -0
  112. package/package.json +11 -8
  113. package/ralph/drivers/copilot.sh +89 -0
  114. package/ralph/lib/circuit_breaker.sh +86 -59
  115. package/ralph/lib/enable_core.sh +3 -6
  116. package/ralph/lib/response_analyzer.sh +5 -29
  117. package/ralph/lib/task_sources.sh +45 -11
  118. package/ralph/lib/wizard_utils.sh +9 -0
  119. package/ralph/ralph_import.sh +7 -2
  120. package/ralph/ralph_loop.sh +29 -34
  121. package/ralph/ralph_monitor.sh +4 -0
  122. package/ralph/templates/ralphrc.template +1 -1
  123. package/slash-commands/bmalph-watch.md +22 -0
  124. package/dist/cli.d.ts +0 -1
  125. package/dist/commands/check-updates.d.ts +0 -5
  126. package/dist/commands/doctor.d.ts +0 -51
  127. package/dist/commands/implement.d.ts +0 -6
  128. package/dist/commands/init.d.ts +0 -9
  129. package/dist/commands/reset.d.ts +0 -7
  130. package/dist/commands/status.d.ts +0 -7
  131. package/dist/commands/upgrade.d.ts +0 -7
  132. package/dist/installer.d.ts +0 -39
  133. package/dist/platform/aider.d.ts +0 -2
  134. package/dist/platform/claude-code.d.ts +0 -2
  135. package/dist/platform/codex.d.ts +0 -2
  136. package/dist/platform/copilot.d.ts +0 -2
  137. package/dist/platform/cursor.d.ts +0 -2
  138. package/dist/platform/detect.d.ts +0 -7
  139. package/dist/platform/index.d.ts +0 -4
  140. package/dist/platform/registry.d.ts +0 -4
  141. package/dist/platform/resolve.d.ts +0 -8
  142. package/dist/platform/types.d.ts +0 -41
  143. package/dist/platform/windsurf.d.ts +0 -2
  144. package/dist/reset.d.ts +0 -18
  145. package/dist/transition/artifact-scan.d.ts +0 -27
  146. package/dist/transition/artifacts.d.ts +0 -3
  147. package/dist/transition/context.d.ts +0 -19
  148. package/dist/transition/fix-plan.d.ts +0 -21
  149. package/dist/transition/index.d.ts +0 -9
  150. package/dist/transition/orchestration.d.ts +0 -2
  151. package/dist/transition/preflight.d.ts +0 -6
  152. package/dist/transition/specs-changelog.d.ts +0 -3
  153. package/dist/transition/specs-index.d.ts +0 -22
  154. package/dist/transition/story-parsing.d.ts +0 -7
  155. package/dist/transition/tech-stack.d.ts +0 -3
  156. package/dist/transition/types.d.ts +0 -82
  157. package/dist/utils/config.d.ts +0 -13
  158. package/dist/utils/constants.d.ts +0 -70
  159. package/dist/utils/dryrun.d.ts +0 -7
  160. package/dist/utils/errors.d.ts +0 -63
  161. package/dist/utils/file-system.d.ts +0 -24
  162. package/dist/utils/github.d.ts +0 -83
  163. package/dist/utils/json.d.ts +0 -7
  164. package/dist/utils/logger.d.ts +0 -9
  165. package/dist/utils/state.d.ts +0 -26
  166. package/dist/utils/validate.d.ts +0 -44
@@ -0,0 +1,200 @@
1
+ import { join } from "node:path";
2
+ import { open, readFile } from "node:fs/promises";
3
+ import { readJsonFile } from "../utils/json.js";
4
+ import { RALPH_DIR } from "../utils/constants.js";
5
+ import { parseFixPlan } from "../transition/fix-plan.js";
6
+ import { debug } from "../utils/logger.js";
7
+ import { formatError } from "../utils/errors.js";
8
+ import { validateCircuitBreakerState, validateRalphSession, normalizeRalphStatus, } from "../utils/validate.js";
9
+ const LOG_LINE_PATTERN = /^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] \[(\w+)\] (.+)$/;
10
+ const DEFAULT_MAX_LOG_LINES = 8;
11
+ const TAIL_BYTES = 4096;
12
+ export async function readDashboardState(projectDir) {
13
+ const [loop, circuitBreaker, stories, analysis, execution, session, recentLogs] = await Promise.all([
14
+ readLoopInfo(projectDir),
15
+ readCircuitBreakerInfo(projectDir),
16
+ readStoryProgress(projectDir),
17
+ readAnalysisInfo(projectDir),
18
+ readExecutionProgress(projectDir),
19
+ readSessionInfo(projectDir),
20
+ readRecentLogs(projectDir),
21
+ ]);
22
+ const ralphCompleted = loop !== null && loop.status === "completed";
23
+ return {
24
+ loop,
25
+ circuitBreaker,
26
+ stories,
27
+ analysis,
28
+ execution,
29
+ session,
30
+ recentLogs,
31
+ ralphCompleted,
32
+ lastUpdated: new Date(),
33
+ };
34
+ }
35
+ export async function readLoopInfo(projectDir) {
36
+ try {
37
+ const data = await readJsonFile(join(projectDir, RALPH_DIR, "status.json"));
38
+ if (data === null)
39
+ return null;
40
+ const normalized = normalizeRalphStatus(data);
41
+ const lastAction = typeof data.last_action === "string" ? data.last_action : "";
42
+ const callsMadeThisHour = typeof data.calls_made_this_hour === "number" ? data.calls_made_this_hour : 0;
43
+ const maxCallsPerHour = typeof data.max_calls_per_hour === "number" ? data.max_calls_per_hour : 0;
44
+ return {
45
+ loopCount: normalized.loopCount,
46
+ status: normalized.status,
47
+ lastAction,
48
+ callsMadeThisHour,
49
+ maxCallsPerHour,
50
+ };
51
+ }
52
+ catch (err) {
53
+ debug(`Failed to read loop info: ${formatError(err)}`);
54
+ return null;
55
+ }
56
+ }
57
+ export async function readCircuitBreakerInfo(projectDir) {
58
+ try {
59
+ const data = await readJsonFile(join(projectDir, RALPH_DIR, ".circuit_breaker_state"));
60
+ if (data === null)
61
+ return null;
62
+ const validated = validateCircuitBreakerState(data);
63
+ const totalOpens = typeof data.total_opens === "number" ? data.total_opens : 0;
64
+ return {
65
+ state: validated.state,
66
+ consecutiveNoProgress: validated.consecutive_no_progress,
67
+ totalOpens,
68
+ reason: validated.reason,
69
+ };
70
+ }
71
+ catch (err) {
72
+ debug(`Failed to read circuit breaker info: ${formatError(err)}`);
73
+ return null;
74
+ }
75
+ }
76
+ export async function readStoryProgress(projectDir) {
77
+ let content;
78
+ try {
79
+ content = await readFile(join(projectDir, RALPH_DIR, "@fix_plan.md"), "utf-8");
80
+ }
81
+ catch (err) {
82
+ debug(`Failed to read fix plan: ${formatError(err)}`);
83
+ return null;
84
+ }
85
+ const items = parseFixPlan(content);
86
+ const completed = items.filter((item) => item.completed).length;
87
+ const total = items.length;
88
+ const nextItem = items.find((item) => !item.completed);
89
+ const nextStory = nextItem ? `Story ${nextItem.id}: ${nextItem.title ?? ""}`.trim() : null;
90
+ return { completed, total, nextStory };
91
+ }
92
+ export async function readAnalysisInfo(projectDir) {
93
+ try {
94
+ const data = await readJsonFile(join(projectDir, RALPH_DIR, ".response_analysis"));
95
+ if (data === null)
96
+ return null;
97
+ const analysis = data.analysis;
98
+ if (typeof analysis !== "object" || analysis === null)
99
+ return null;
100
+ const a = analysis;
101
+ const filesModified = typeof a.files_modified === "number" ? a.files_modified : 0;
102
+ const confidenceScore = typeof a.confidence_score === "number" ? a.confidence_score : 0;
103
+ const isTestOnly = typeof a.is_test_only === "boolean" ? a.is_test_only : false;
104
+ const isStuck = typeof a.is_stuck === "boolean" ? a.is_stuck : false;
105
+ const exitSignal = typeof a.exit_signal === "boolean" ? a.exit_signal : false;
106
+ const hasPermissionDenials = typeof a.has_permission_denials === "boolean" ? a.has_permission_denials : false;
107
+ const permissionDenialCount = typeof a.permission_denial_count === "number" ? a.permission_denial_count : 0;
108
+ return {
109
+ filesModified,
110
+ confidenceScore,
111
+ isTestOnly,
112
+ isStuck,
113
+ exitSignal,
114
+ hasPermissionDenials,
115
+ permissionDenialCount,
116
+ };
117
+ }
118
+ catch (err) {
119
+ debug(`Failed to read analysis info: ${formatError(err)}`);
120
+ return null;
121
+ }
122
+ }
123
+ export async function readExecutionProgress(projectDir) {
124
+ try {
125
+ const data = await readJsonFile(join(projectDir, RALPH_DIR, "progress.json"));
126
+ if (data === null)
127
+ return null;
128
+ const status = typeof data.status === "string" ? data.status : "";
129
+ if (status !== "executing")
130
+ return null;
131
+ const elapsedSeconds = typeof data.elapsed_seconds === "number" ? data.elapsed_seconds : 0;
132
+ return { status, elapsedSeconds };
133
+ }
134
+ catch (err) {
135
+ debug(`Failed to read execution progress: ${formatError(err)}`);
136
+ return null;
137
+ }
138
+ }
139
+ export async function readSessionInfo(projectDir) {
140
+ try {
141
+ const data = await readJsonFile(join(projectDir, RALPH_DIR, ".ralph_session"));
142
+ if (data === null)
143
+ return null;
144
+ const validated = validateRalphSession(data);
145
+ return {
146
+ createdAt: validated.created_at,
147
+ lastUsed: validated.last_used,
148
+ };
149
+ }
150
+ catch (err) {
151
+ debug(`Failed to read session info: ${formatError(err)}`);
152
+ return null;
153
+ }
154
+ }
155
+ export async function readRecentLogs(projectDir, maxLines = DEFAULT_MAX_LOG_LINES) {
156
+ const logPath = join(projectDir, RALPH_DIR, "logs", "ralph.log");
157
+ let content;
158
+ try {
159
+ const fh = await open(logPath, "r");
160
+ try {
161
+ const stats = await fh.stat();
162
+ if (stats.size === 0) {
163
+ return [];
164
+ }
165
+ if (stats.size <= TAIL_BYTES) {
166
+ content = await fh.readFile("utf-8");
167
+ }
168
+ else {
169
+ const position = stats.size - TAIL_BYTES;
170
+ const buf = Buffer.alloc(TAIL_BYTES);
171
+ const { bytesRead } = await fh.read(buf, 0, TAIL_BYTES, position);
172
+ const raw = buf.toString("utf-8", 0, bytesRead);
173
+ const newlineIdx = raw.indexOf("\n");
174
+ content = newlineIdx >= 0 ? raw.slice(newlineIdx + 1) : raw;
175
+ }
176
+ }
177
+ finally {
178
+ await fh.close();
179
+ }
180
+ }
181
+ catch (err) {
182
+ debug(`Failed to read recent logs: ${formatError(err)}`);
183
+ return [];
184
+ }
185
+ const lines = content.split(/\r?\n/).filter((line) => line.length > 0);
186
+ const tail = lines.slice(-maxLines);
187
+ const entries = [];
188
+ for (const line of tail) {
189
+ const match = LOG_LINE_PATTERN.exec(line);
190
+ if (match) {
191
+ entries.push({
192
+ timestamp: match[1],
193
+ level: match[2],
194
+ message: match[3],
195
+ });
196
+ }
197
+ }
198
+ return entries;
199
+ }
200
+ //# sourceMappingURL=state-reader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-reader.js","sourceRoot":"","sources":["../../src/watch/state-reader.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/watch/types.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,24 +1,23 @@
1
1
  {
2
2
  "name": "bmalph",
3
- "version": "2.4.0",
4
- "description": "Unified AI Development Framework - BMAD phases with Ralph execution loop for Claude Code",
3
+ "version": "2.6.0",
4
+ "description": "Unified AI Development Framework - BMAD phases with Ralph execution loop",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "bmalph": "./bin/bmalph.js"
8
8
  },
9
9
  "scripts": {
10
- "prepare": "tsc",
11
10
  "build": "tsc",
12
11
  "test": "vitest run",
13
12
  "test:watch": "vitest",
14
13
  "test:e2e": "vitest run --config vitest.config.e2e.ts",
15
14
  "test:coverage": "vitest run --coverage",
16
- "test:all": "npm run test && npm run test:e2e",
15
+ "test:bash": "bash -c 'command -v bats &>/dev/null && bats tests/bash/*.bats tests/bash/drivers/*.bats || echo \"[skip] bats not installed\"'",
16
+ "test:all": "npm run test && npm run test:e2e && npm run test:bash",
17
17
  "check": "npm run lint && npm run build && npm test",
18
18
  "dev": "tsc --watch",
19
19
  "lint": "eslint src tests",
20
20
  "lint:fix": "eslint src tests --fix",
21
- "format": "prettier --write .",
22
21
  "fmt:check": "prettier --check .",
23
22
  "fmt:fix": "prettier --write .",
24
23
  "type-check": "tsc --noEmit",
@@ -31,7 +30,11 @@
31
30
  "ai",
32
31
  "development",
33
32
  "framework",
34
- "agents"
33
+ "agents",
34
+ "bmad",
35
+ "ralph",
36
+ "autonomous",
37
+ "coding-assistant"
35
38
  ],
36
39
  "author": "Lars Cowe",
37
40
  "license": "MIT",
@@ -58,9 +61,9 @@
58
61
  "bundled-versions.json"
59
62
  ],
60
63
  "dependencies": {
64
+ "@inquirer/prompts": "^8.3.0",
61
65
  "chalk": "^5.6.2",
62
- "commander": "^14.0.2",
63
- "inquirer": "^13.2.1"
66
+ "commander": "^14.0.2"
64
67
  },
65
68
  "devDependencies": {
66
69
  "@eslint/js": "^10.0.1",
@@ -0,0 +1,89 @@
1
+ #!/bin/bash
2
+ # GitHub Copilot CLI driver for Ralph (EXPERIMENTAL)
3
+ # Provides platform-specific CLI invocation logic for Copilot CLI.
4
+ #
5
+ # Known limitations:
6
+ # - No session continuity (session IDs not capturable from -p output)
7
+ # - No structured output (plain text only, no --json flag)
8
+ # - Coarse tool permissions (only shell, shell(git:*), shell(npm:*), write)
9
+ # - CLI is new (GA Feb 25, 2026) — scripting interface may change
10
+
11
+ driver_name() {
12
+ echo "copilot"
13
+ }
14
+
15
+ driver_display_name() {
16
+ echo "GitHub Copilot CLI"
17
+ }
18
+
19
+ driver_cli_binary() {
20
+ echo "copilot"
21
+ }
22
+
23
+ driver_min_version() {
24
+ echo "0.0.418"
25
+ }
26
+
27
+ driver_check_available() {
28
+ command -v "$(driver_cli_binary)" &>/dev/null
29
+ }
30
+
31
+ # Copilot CLI tool names
32
+ driver_valid_tools() {
33
+ VALID_TOOL_PATTERNS=(
34
+ "shell"
35
+ "shell(git:*)"
36
+ "shell(npm:*)"
37
+ "write"
38
+ )
39
+ }
40
+
41
+ # Build Copilot CLI command
42
+ # Context is prepended to the prompt (same pattern as Codex driver).
43
+ # Uses --autopilot --yolo for autonomous mode, -s to strip stats, -p for prompt.
44
+ driver_build_command() {
45
+ local prompt_file=$1
46
+ local loop_context=$2
47
+ # $3 (session_id) is intentionally ignored — Copilot CLI does not
48
+ # expose session IDs in -p output, so resume is not possible.
49
+
50
+ CLAUDE_CMD_ARGS=("$(driver_cli_binary)")
51
+
52
+ if [[ ! -f "$prompt_file" ]]; then
53
+ echo "ERROR: Prompt file not found: $prompt_file" >&2
54
+ return 1
55
+ fi
56
+
57
+ # Autonomous execution flags
58
+ CLAUDE_CMD_ARGS+=("--autopilot" "--yolo")
59
+
60
+ # Limit auto-continuation loops
61
+ CLAUDE_CMD_ARGS+=("--max-autopilot-continues" "50")
62
+
63
+ # Disable interactive prompts
64
+ CLAUDE_CMD_ARGS+=("--no-ask-user")
65
+
66
+ # Strip stats for cleaner output
67
+ CLAUDE_CMD_ARGS+=("-s")
68
+
69
+ # Build prompt with context prepended
70
+ local prompt_content
71
+ prompt_content=$(cat "$prompt_file")
72
+ if [[ -n "$loop_context" ]]; then
73
+ prompt_content="$loop_context
74
+
75
+ $prompt_content"
76
+ fi
77
+
78
+ CLAUDE_CMD_ARGS+=("-p" "$prompt_content")
79
+ }
80
+
81
+ driver_supports_sessions() {
82
+ return 1 # false — session IDs not capturable from -p output
83
+ }
84
+
85
+ # Copilot CLI outputs plain text only (no JSON streaming).
86
+ # Passthrough filter — no transformation needed.
87
+ driver_stream_filter() {
88
+ echo '.'
89
+ }
@@ -43,18 +43,19 @@ init_circuit_breaker() {
43
43
  fi
44
44
 
45
45
  if [[ ! -f "$CB_STATE_FILE" ]]; then
46
- cat > "$CB_STATE_FILE" << EOF
47
- {
48
- "state": "$CB_STATE_CLOSED",
49
- "last_change": "$(get_iso_timestamp)",
50
- "consecutive_no_progress": 0,
51
- "consecutive_same_error": 0,
52
- "consecutive_permission_denials": 0,
53
- "last_progress_loop": 0,
54
- "total_opens": 0,
55
- "reason": ""
56
- }
57
- EOF
46
+ jq -n \
47
+ --arg state "$CB_STATE_CLOSED" \
48
+ --arg last_change "$(get_iso_timestamp)" \
49
+ '{
50
+ state: $state,
51
+ last_change: $last_change,
52
+ consecutive_no_progress: 0,
53
+ consecutive_same_error: 0,
54
+ consecutive_permission_denials: 0,
55
+ last_progress_loop: 0,
56
+ total_opens: 0,
57
+ reason: ""
58
+ }' > "$CB_STATE_FILE"
58
59
  fi
59
60
 
60
61
  # Ensure history file exists before any transition logging
@@ -81,18 +82,20 @@ EOF
81
82
  total_opens=$(jq -r '.total_opens // 0' "$CB_STATE_FILE" 2>/dev/null || echo "0")
82
83
  log_circuit_transition "$CB_STATE_OPEN" "$CB_STATE_CLOSED" "Auto-reset on startup (CB_AUTO_RESET=true)" "$current_loop"
83
84
 
84
- cat > "$CB_STATE_FILE" << EOF
85
- {
86
- "state": "$CB_STATE_CLOSED",
87
- "last_change": "$(get_iso_timestamp)",
88
- "consecutive_no_progress": 0,
89
- "consecutive_same_error": 0,
90
- "consecutive_permission_denials": 0,
91
- "last_progress_loop": 0,
92
- "total_opens": $total_opens,
93
- "reason": "Auto-reset on startup"
94
- }
95
- EOF
85
+ jq -n \
86
+ --arg state "$CB_STATE_CLOSED" \
87
+ --arg last_change "$(get_iso_timestamp)" \
88
+ --argjson total_opens "$total_opens" \
89
+ '{
90
+ state: $state,
91
+ last_change: $last_change,
92
+ consecutive_no_progress: 0,
93
+ consecutive_same_error: 0,
94
+ consecutive_permission_denials: 0,
95
+ last_progress_loop: 0,
96
+ total_opens: $total_opens,
97
+ reason: "Auto-reset on startup"
98
+ }' > "$CB_STATE_FILE"
96
99
  else
97
100
  # Cooldown: check if enough time has elapsed to transition to HALF_OPEN
98
101
  local opened_at
@@ -296,20 +299,34 @@ record_loop_result() {
296
299
  opened_at=$(echo "$state_data" | jq -r '.opened_at // .last_change // ""' 2>/dev/null)
297
300
  fi
298
301
 
299
- cat > "$CB_STATE_FILE" << EOF
300
- {
301
- "state": "$new_state",
302
- "last_change": "$(get_iso_timestamp)",
303
- "consecutive_no_progress": $consecutive_no_progress,
304
- "consecutive_same_error": $consecutive_same_error,
305
- "consecutive_permission_denials": $consecutive_permission_denials,
306
- "last_progress_loop": $last_progress_loop,
307
- "total_opens": $total_opens,
308
- "reason": "$reason",
309
- "current_loop": $loop_number$(if [[ -n "$opened_at" ]]; then echo ",
310
- \"opened_at\": \"$opened_at\""; fi)
311
- }
312
- EOF
302
+ jq -n \
303
+ --arg state "$new_state" \
304
+ --arg last_change "$(get_iso_timestamp)" \
305
+ --argjson consecutive_no_progress "$consecutive_no_progress" \
306
+ --argjson consecutive_same_error "$consecutive_same_error" \
307
+ --argjson consecutive_permission_denials "$consecutive_permission_denials" \
308
+ --argjson last_progress_loop "$last_progress_loop" \
309
+ --argjson total_opens "$total_opens" \
310
+ --arg reason "$reason" \
311
+ --argjson current_loop "$loop_number" \
312
+ '{
313
+ state: $state,
314
+ last_change: $last_change,
315
+ consecutive_no_progress: $consecutive_no_progress,
316
+ consecutive_same_error: $consecutive_same_error,
317
+ consecutive_permission_denials: $consecutive_permission_denials,
318
+ last_progress_loop: $last_progress_loop,
319
+ total_opens: $total_opens,
320
+ reason: $reason,
321
+ current_loop: $current_loop
322
+ }' > "$CB_STATE_FILE"
323
+
324
+ # Add opened_at if set (entering or staying in OPEN state)
325
+ if [[ -n "$opened_at" ]]; then
326
+ local tmp
327
+ tmp=$(jq --arg opened_at "$opened_at" '. + {opened_at: $opened_at}' "$CB_STATE_FILE")
328
+ echo "$tmp" > "$CB_STATE_FILE"
329
+ fi
313
330
 
314
331
  # Log state transition
315
332
  if [[ "$new_state" != "$current_state" ]]; then
@@ -331,15 +348,23 @@ log_circuit_transition() {
331
348
  local reason=$3
332
349
  local loop_number=$4
333
350
 
334
- local history=$(cat "$CB_HISTORY_FILE")
335
- local transition="{
336
- \"timestamp\": \"$(get_iso_timestamp)\",
337
- \"loop\": $loop_number,
338
- \"from_state\": \"$from_state\",
339
- \"to_state\": \"$to_state\",
340
- \"reason\": \"$reason\"
341
- }"
342
-
351
+ local transition
352
+ transition=$(jq -n -c \
353
+ --arg timestamp "$(get_iso_timestamp)" \
354
+ --argjson loop "$loop_number" \
355
+ --arg from_state "$from_state" \
356
+ --arg to_state "$to_state" \
357
+ --arg reason "$reason" \
358
+ '{
359
+ timestamp: $timestamp,
360
+ loop: $loop,
361
+ from_state: $from_state,
362
+ to_state: $to_state,
363
+ reason: $reason
364
+ }')
365
+
366
+ local history
367
+ history=$(cat "$CB_HISTORY_FILE")
343
368
  history=$(echo "$history" | jq ". += [$transition]")
344
369
  echo "$history" > "$CB_HISTORY_FILE"
345
370
 
@@ -406,18 +431,20 @@ show_circuit_status() {
406
431
  reset_circuit_breaker() {
407
432
  local reason=${1:-"Manual reset"}
408
433
 
409
- cat > "$CB_STATE_FILE" << EOF
410
- {
411
- "state": "$CB_STATE_CLOSED",
412
- "last_change": "$(get_iso_timestamp)",
413
- "consecutive_no_progress": 0,
414
- "consecutive_same_error": 0,
415
- "consecutive_permission_denials": 0,
416
- "last_progress_loop": 0,
417
- "total_opens": 0,
418
- "reason": "$reason"
419
- }
420
- EOF
434
+ jq -n \
435
+ --arg state "$CB_STATE_CLOSED" \
436
+ --arg last_change "$(get_iso_timestamp)" \
437
+ --arg reason "$reason" \
438
+ '{
439
+ state: $state,
440
+ last_change: $last_change,
441
+ consecutive_no_progress: 0,
442
+ consecutive_same_error: 0,
443
+ consecutive_permission_denials: 0,
444
+ last_progress_loop: 0,
445
+ total_opens: 0,
446
+ reason: $reason
447
+ }' > "$CB_STATE_FILE"
421
448
 
422
449
  echo -e "${GREEN}✅ Circuit breaker reset to CLOSED state${NC}"
423
450
  }
@@ -328,10 +328,9 @@ detect_project_context() {
328
328
  DETECTED_TEST_CMD="pnpm test"
329
329
  DETECTED_RUN_CMD="pnpm start"
330
330
  fi
331
- fi
332
331
 
333
332
  # Detect from pyproject.toml or setup.py (Python)
334
- if [[ -f "pyproject.toml" ]] || [[ -f "setup.py" ]]; then
333
+ elif [[ -f "pyproject.toml" ]] || [[ -f "setup.py" ]]; then
335
334
  DETECTED_PROJECT_TYPE="python"
336
335
 
337
336
  # Extract project name from pyproject.toml
@@ -358,19 +357,17 @@ detect_project_context() {
358
357
  DETECTED_TEST_CMD="pytest"
359
358
  DETECTED_RUN_CMD="python -m ${DETECTED_PROJECT_NAME:-main}"
360
359
  fi
361
- fi
362
360
 
363
361
  # Detect from Cargo.toml (Rust)
364
- if [[ -f "Cargo.toml" ]]; then
362
+ elif [[ -f "Cargo.toml" ]]; then
365
363
  DETECTED_PROJECT_TYPE="rust"
366
364
  DETECTED_PROJECT_NAME=$(grep -m1 '^name' Cargo.toml | sed 's/.*= *"\([^"]*\)".*/\1/' 2>/dev/null)
367
365
  DETECTED_BUILD_CMD="cargo build"
368
366
  DETECTED_TEST_CMD="cargo test"
369
367
  DETECTED_RUN_CMD="cargo run"
370
- fi
371
368
 
372
369
  # Detect from go.mod (Go)
373
- if [[ -f "go.mod" ]]; then
370
+ elif [[ -f "go.mod" ]]; then
374
371
  DETECTED_PROJECT_TYPE="go"
375
372
  DETECTED_PROJECT_NAME=$(head -1 go.mod | sed 's/module //' 2>/dev/null)
376
373
  DETECTED_BUILD_CMD="go build"
@@ -829,37 +829,13 @@ should_resume_session() {
829
829
  fi
830
830
 
831
831
  # Calculate session age using date utilities
832
- local now=$(get_epoch_seconds)
832
+ local now
833
+ now=$(get_epoch_seconds)
833
834
  local session_time
835
+ session_time=$(parse_iso_to_epoch "$timestamp")
834
836
 
835
- # Parse ISO timestamp to epoch - try multiple formats for cross-platform compatibility
836
- # Strip milliseconds if present (e.g., 2026-01-09T10:30:00.123+00:00 2026-01-09T10:30:00+00:00)
837
- local clean_timestamp="${timestamp}"
838
- if [[ "$timestamp" =~ \.[0-9]+[+-Z] ]]; then
839
- clean_timestamp=$(echo "$timestamp" | sed 's/\.[0-9]*\([+-Z]\)/\1/')
840
- fi
841
-
842
- if command -v gdate &>/dev/null; then
843
- # macOS with coreutils
844
- session_time=$(gdate -d "$clean_timestamp" +%s 2>/dev/null)
845
- elif date --version 2>&1 | grep -q GNU; then
846
- # GNU date (Linux)
847
- session_time=$(date -d "$clean_timestamp" +%s 2>/dev/null)
848
- else
849
- # BSD date (macOS without coreutils) - try parsing ISO format
850
- # Format: 2026-01-09T10:30:00+00:00 or 2026-01-09T10:30:00Z
851
- # Strip timezone suffix for BSD date parsing
852
- local date_only="${clean_timestamp%[+-Z]*}"
853
- session_time=$(date -j -f "%Y-%m-%dT%H:%M:%S" "$date_only" +%s 2>/dev/null)
854
- fi
855
-
856
- # If we couldn't parse the timestamp, consider session expired
857
- if [[ -z "$session_time" || ! "$session_time" =~ ^[0-9]+$ ]]; then
858
- echo "false"
859
- return 1
860
- fi
861
-
862
- # Calculate age in seconds
837
+ # If parse_iso_to_epoch fell back to current epoch, session_time now age 0.
838
+ # That's a safe default: treat unparseable timestamps as fresh rather than expired.
863
839
  local age=$((now - session_time))
864
840
 
865
841
  # Check if session is still valid (less than expiration time)