@matt82198/aesop 0.1.0 → 0.3.1

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 (130) hide show
  1. package/CHANGELOG.md +117 -2
  2. package/README.md +59 -218
  3. package/bin/cli.js +168 -41
  4. package/daemons/run-watchdog.sh +16 -4
  5. package/daemons/selfheal.sh +231 -0
  6. package/docs/ANY-REPO.md +427 -0
  7. package/docs/CONTRIBUTING.md +72 -0
  8. package/docs/DEMO.md +334 -0
  9. package/docs/HOOK-INSTALL.md +15 -56
  10. package/docs/INSTALL.md +74 -4
  11. package/docs/PORTING.md +166 -0
  12. package/docs/README.md +33 -3
  13. package/docs/TEAM-STATE.md +372 -0
  14. package/docs/reproduce.md +33 -3
  15. package/driver/CLAUDE.md +150 -0
  16. package/driver/README.md +383 -0
  17. package/driver/aesop.config.example.json +80 -0
  18. package/driver/agent_driver.py +355 -0
  19. package/driver/backend_config.py +253 -0
  20. package/driver/claude_code_driver.py +198 -0
  21. package/driver/codex_driver.py +673 -0
  22. package/driver/openai_compatible_driver.py +249 -0
  23. package/driver/openai_transport.py +179 -0
  24. package/driver/verification_policy.py +75 -0
  25. package/driver/wave_bridge.py +254 -0
  26. package/driver/wave_loop.py +1408 -0
  27. package/driver/wave_scheduler.py +890 -0
  28. package/hooks/pre-push-policy.sh +131 -33
  29. package/mcp/server.mjs +320 -4
  30. package/monitor/collect-signals.mjs +69 -0
  31. package/package.json +22 -14
  32. package/skills/CLAUDE.md +132 -2
  33. package/skills/buildsystem/SKILL.md +330 -0
  34. package/skills/buildsystem/wave-flat-dispatch.template.mjs +658 -0
  35. package/skills/fleet/SKILL.md +113 -0
  36. package/skills/power/SKILL.md +246 -131
  37. package/state_store/__init__.py +4 -2
  38. package/state_store/api.py +19 -4
  39. package/state_store/coordination.py +209 -0
  40. package/state_store/identity.py +51 -0
  41. package/state_store/projections.py +63 -0
  42. package/state_store/read_api.py +156 -0
  43. package/state_store/store.py +185 -73
  44. package/state_store/write_api.py +462 -0
  45. package/templates/wave-presets/data.json +65 -0
  46. package/templates/wave-presets/library.json +65 -0
  47. package/templates/wave-presets/saas.json +64 -0
  48. package/tools/audit_report.py +388 -0
  49. package/tools/bench_runner.py +100 -3
  50. package/tools/ci_merge_wait.py +256 -35
  51. package/tools/ci_workflow_lint.py +430 -0
  52. package/tools/claudemd_drift.py +394 -0
  53. package/tools/claudemd_lint.py +359 -0
  54. package/tools/common.py +39 -3
  55. package/tools/cost_ceiling.py +166 -43
  56. package/tools/cost_econ.py +480 -0
  57. package/tools/cost_projection.py +559 -0
  58. package/tools/crossos_drift.py +394 -0
  59. package/tools/defect_escape.py +252 -0
  60. package/tools/doctor.js +1 -1
  61. package/tools/eod_sweep.py +188 -26
  62. package/tools/fleet.js +260 -0
  63. package/tools/fleet_ledger.py +209 -7
  64. package/tools/git_identity_check.py +315 -0
  65. package/tools/health-score.js +40 -0
  66. package/tools/health_score.py +361 -0
  67. package/tools/metrics_gate.py +13 -4
  68. package/tools/mutation_test.py +523 -0
  69. package/tools/portability_check.py +206 -0
  70. package/tools/proposals.mjs +47 -2
  71. package/tools/reconcile.py +7 -4
  72. package/tools/reproduce.js +405 -0
  73. package/tools/secret_scan.py +207 -65
  74. package/tools/self_stats.py +20 -0
  75. package/tools/stall_check.py +247 -16
  76. package/tools/stateapi_lint.py +325 -0
  77. package/tools/test_battery.py +173 -0
  78. package/tools/transcript_digest.py +380 -0
  79. package/tools/verify_activity_filter.py +437 -0
  80. package/tools/verify_agent_inspector.py +2 -0
  81. package/tools/verify_cost_panel.py +345 -0
  82. package/tools/verify_dash.py +2 -0
  83. package/tools/verify_dispatch_panel.py +301 -0
  84. package/tools/verify_failure_drilldown.py +188 -0
  85. package/tools/verify_prboard.py +2 -0
  86. package/tools/verify_scorecards.py +281 -0
  87. package/tools/verify_submit_encoding.py +2 -0
  88. package/tools/verify_ui_trio.py +409 -0
  89. package/tools/verify_wave_telemetry.py +268 -0
  90. package/tools/wave_backlog_analyzer.py +490 -0
  91. package/tools/wave_ledger_hook.py +150 -0
  92. package/tools/wave_preflight.py +779 -0
  93. package/tools/wave_resume.py +215 -0
  94. package/tools/wave_templates.py +340 -0
  95. package/ui/agents.py +68 -14
  96. package/ui/collectors.py +81 -55
  97. package/ui/config.py +7 -2
  98. package/ui/cost.py +231 -12
  99. package/ui/handler.py +383 -55
  100. package/ui/quality_scorecard.py +232 -0
  101. package/ui/sse.py +3 -3
  102. package/ui/wave_audit_tail.py +213 -0
  103. package/ui/wave_dispatch.py +280 -0
  104. package/ui/wave_failure.py +288 -0
  105. package/ui/wave_gantt.py +152 -0
  106. package/ui/wave_reasoning_tail.py +176 -0
  107. package/ui/wave_telemetry.py +383 -0
  108. package/ui/web/dist/assets/index-CNQxaiOW.css +1 -0
  109. package/ui/web/dist/assets/index-CP68RIh3.js +9 -0
  110. package/ui/web/dist/index.html +2 -2
  111. package/bin/CLAUDE.md +0 -76
  112. package/daemons/CLAUDE.md +0 -36
  113. package/dash/CLAUDE.md +0 -32
  114. package/docs/archive/README.md +0 -3
  115. package/docs/archive/spikes/tiered-cognition/ACTIVATION.md +0 -125
  116. package/docs/archive/spikes/tiered-cognition/DESIGN.md +0 -287
  117. package/docs/archive/spikes/tiered-cognition/FINDINGS.md +0 -113
  118. package/docs/archive/spikes/tiered-cognition/README.md +0 -27
  119. package/docs/archive/spikes/tiered-cognition/aesop-cognition.example.md +0 -32
  120. package/docs/archive/spikes/tiered-cognition/force-model-policy.merged.mjs +0 -673
  121. package/docs/archive/spikes/tiered-cognition/strip-tools-hook.mjs +0 -434
  122. package/hooks/CLAUDE.md +0 -89
  123. package/mcp/CLAUDE.md +0 -213
  124. package/monitor/CLAUDE.md +0 -40
  125. package/scan/CLAUDE.md +0 -30
  126. package/state_store/CLAUDE.md +0 -39
  127. package/tools/CLAUDE.md +0 -79
  128. package/ui/CLAUDE.md +0 -127
  129. package/ui/web/dist/assets/index-0qQYnvMC.js +0 -9
  130. package/ui/web/dist/assets/index-BdIlFieV.css +0 -1
@@ -14,19 +14,33 @@ Configuration (aesop.config.json):
14
14
 
15
15
  Spend source: an explicit --spent/spent= figure always wins. Otherwise spend
16
16
  is read from the cost ledger (tools/fleet_ledger.py's OUTCOMES-LEDGER.md under
17
- the resolved state dir): sum of tokens_in + tokens_out across all ledger rows.
17
+ the resolved state dir): sum of tokens_in + tokens_out across all ledger rows
18
+ within the specified window (or all rows if no window specified).
18
19
  Missing/unreadable ledger -> spend of 0 (never trips on a ledger that doesn't
19
20
  exist yet).
20
21
 
22
+ WINDOW CONTRACT (shared with cost_projection.py):
23
+ Both tools filter ledger rows by the SAME window parameters to ensure
24
+ consistent spend calculations. The helper calculate_window_bounds() is
25
+ imported by both modules to ensure identical window calculations.
26
+
27
+ When both cost_projection.project(window_minutes=W) and cost_ceiling.check(window_minutes=W)
28
+ are called with the same W value, they will produce identical spend figures.
29
+
21
30
  API:
22
- check(spent=None, period="wave", config=None, state_dir=None, trip=True) -> dict
31
+ check(spent=None, period="wave", config=None, state_dir=None, trip=True,
32
+ window_minutes=None) -> dict
23
33
  Returns {"period", "ceiling", "spent", "exceeded", "tripped"}.
24
34
  When exceeded and trip=True, calls tools/halt.py's halt() with a reason
25
35
  describing the breach, and "tripped" is True. When ceiling is None
26
36
  (unconfigured), exceeded is always False and nothing is ever tripped.
27
37
 
38
+ window_minutes: Optional time window in minutes for ledger filtering.
39
+ If None, uses all ledger rows (backward compat). If specified, only rows
40
+ within the window contribute to spend calculation.
41
+
28
42
  CLI:
29
- python tools/cost_ceiling.py --check --spent N [--period wave|daily]
43
+ python tools/cost_ceiling.py --check --spent N [--period wave|daily] [--window MINUTES]
30
44
  Exit 0 if not exceeded (or ceiling unconfigured), exit 1 if exceeded
31
45
  (and thus tripped, unless already halted).
32
46
  """
@@ -35,7 +49,7 @@ import argparse
35
49
  import json
36
50
  import sys
37
51
  from pathlib import Path
38
- from datetime import datetime, timezone
52
+ from datetime import datetime, timezone, timedelta
39
53
 
40
54
  try:
41
55
  import halt
@@ -45,6 +59,26 @@ except ImportError:
45
59
  from tools import fleet_ledger
46
60
 
47
61
 
62
+ def calculate_window_bounds(window_minutes):
63
+ """Calculate UTC window start and end times for ledger filtering.
64
+
65
+ Shared contract between cost_projection.py and cost_ceiling.py to ensure
66
+ consistent ledger windowing. Both tools import and use this helper to
67
+ guarantee agreement on spend figures when using the same window.
68
+
69
+ Args:
70
+ window_minutes: Time window in minutes (e.g., 30 = last 30 minutes)
71
+
72
+ Returns:
73
+ Tuple of (window_start_utc, window_end_utc) as datetime objects with UTC timezone.
74
+ window_end_utc is always datetime.now(timezone.utc).
75
+ window_start_utc is window_end_utc minus window_minutes.
76
+ """
77
+ now_utc = datetime.now(timezone.utc)
78
+ window_start = now_utc - timedelta(minutes=window_minutes)
79
+ return window_start, now_utc
80
+
81
+
48
82
  def load_config():
49
83
  """Load aesop.config.json from current directory, return dict (or {} if absent/bad)."""
50
84
  config_file = Path("aesop.config.json")
@@ -73,15 +107,22 @@ def get_ceiling(config, period):
73
107
  return None
74
108
 
75
109
 
76
- def read_ledger_total_tokens(state_dir, period="wave"):
77
- """Sum tokens_in + tokens_out from OUTCOMES-LEDGER.md.
110
+ def read_ledger_total_tokens(state_dir, period="wave", window_minutes=None):
111
+ """Sum tokens_in + tokens_out from OUTCOMES-LEDGER.md with optional windowing.
78
112
 
79
113
  Args:
80
114
  state_dir: path to state directory
81
115
  period: "wave" (all rows) or "daily" (today's rows only, filtered by UTC date)
116
+ window_minutes: Optional time window in minutes. If specified, only rows
117
+ within the last window_minutes are included. Default None
118
+ means include all rows (backward compat).
82
119
 
83
120
  Returns 0 if the ledger doesn't exist or is unreadable/empty.
84
121
  Uses fleet_ledger.py's shared parser (single source of truth).
122
+
123
+ Window contract: When both cost_projection and cost_ceiling use the same
124
+ window_minutes value, they produce identical spend figures (single source
125
+ of truth is the ledger parse result, and the same filtering is applied).
85
126
  """
86
127
  # Set the state root temporarily so fleet_ledger can find the ledger
87
128
  import os
@@ -98,6 +139,12 @@ def read_ledger_total_tokens(state_dir, period="wave"):
98
139
  if not rows:
99
140
  return 0
100
141
 
142
+ # Calculate window bounds if windowing is requested
143
+ if window_minutes is not None:
144
+ window_start, window_end = calculate_window_bounds(window_minutes)
145
+ else:
146
+ window_start = None # No window filtering
147
+
101
148
  total = 0
102
149
 
103
150
  if period == "daily":
@@ -107,56 +154,125 @@ def read_ledger_total_tokens(state_dir, period="wave"):
107
154
  # Extract date from ISO timestamp (format: YYYY-MM-DDTHH:MM:SSZ or similar)
108
155
  try:
109
156
  iso_ts = row['iso_ts']
110
- # Parse the date part (first 10 characters: YYYY-MM-DD)
111
- row_date = datetime.fromisoformat(iso_ts.replace('Z', '+00:00')).date()
112
- if row_date == today_utc:
113
- total += row['tokens_in'] + row['tokens_out']
114
- except (ValueError, IndexError, KeyError):
157
+ # Parse ISO timestamp
158
+ ts_clean = iso_ts.replace('Z', '+00:00') if 'Z' in iso_ts else iso_ts
159
+ row_dt = datetime.fromisoformat(ts_clean)
160
+ if row_dt.tzinfo is None:
161
+ row_dt = row_dt.replace(tzinfo=timezone.utc)
162
+
163
+ # Check date filter
164
+ row_date = row_dt.date()
165
+ if row_date != today_utc:
166
+ continue
167
+
168
+ # Check window filter (if specified)
169
+ if window_start is not None and row_dt < window_start:
170
+ continue
171
+
172
+ total += row['tokens_in'] + row['tokens_out']
173
+ except (ValueError, IndexError, KeyError, AttributeError):
115
174
  # Skip malformed timestamps
116
175
  continue
117
176
  else:
118
- # period == "wave": sum all rows
177
+ # period == "wave": sum rows within window (or all if no window)
119
178
  for row in rows:
179
+ # Check window filter (if specified)
180
+ if window_start is not None:
181
+ try:
182
+ iso_ts = row['iso_ts']
183
+ ts_clean = iso_ts.replace('Z', '+00:00') if 'Z' in iso_ts else iso_ts
184
+ row_dt = datetime.fromisoformat(ts_clean)
185
+ if row_dt.tzinfo is None:
186
+ row_dt = row_dt.replace(tzinfo=timezone.utc)
187
+
188
+ if row_dt < window_start:
189
+ continue
190
+ except (ValueError, AttributeError):
191
+ # Skip malformed timestamps
192
+ continue
193
+
120
194
  total += row['tokens_in'] + row['tokens_out']
121
195
 
122
196
  return total
123
197
 
124
198
 
125
- def check(spent=None, period="wave", config=None, state_dir=None, trip=True):
199
+ def check(spent=None, period="wave", config=None, state_dir=None, trip=True, window_minutes=None):
126
200
  """Check spend against the configured ceiling for `period`.
127
201
 
128
- Returns a dict: {"period", "ceiling", "spent", "exceeded", "tripped"}.
129
- """
130
- if config is None:
131
- config = load_config()
132
- if state_dir is None:
133
- state_dir = halt.resolve_state_dir(config=config)
134
- state_dir = Path(state_dir)
135
-
136
- ceiling = get_ceiling(config, period)
137
-
138
- if spent is None:
139
- spent = read_ledger_total_tokens(state_dir, period=period)
140
- spent = int(spent)
202
+ Returns a dict: {"period", "ceiling", "spent", "exceeded", "tripped", "reason"}.
141
203
 
142
- exceeded = ceiling is not None and spent >= ceiling
143
- tripped = False
204
+ Window contract: when cost_ceiling and cost_projection both pass the same
205
+ window_minutes value, they will compute identical spend figures from the
206
+ ledger, ensuring agreement on cost tracking.
144
207
 
145
- if exceeded and trip:
146
- reason = (
147
- f"cost ceiling exceeded: {period} spend {spent} tokens >= "
148
- f"ceiling {ceiling} tokens"
149
- )
150
- halt.halt(reason, state_dir=state_dir)
151
- tripped = True
208
+ Distinctions:
209
+ - Genuine ceiling breach (ceiling is configured, spent >= ceiling): when trip=True,
210
+ writes persistent .HALT sentinel via halt.halt() and sets tripped=True.
211
+ - Exception during spend computation (ledger read/create failure, etc.): signals
212
+ abort of current wave (exceeded=True) but does NOT write persistent sentinel
213
+ (tripped=False). This preserves fleet availability across transient I/O errors
214
+ (e.g., momentary file lock, disk full) while still aborting the current wave.
152
215
 
153
- return {
154
- "period": period,
155
- "ceiling": ceiling,
156
- "spent": spent,
157
- "exceeded": exceeded,
158
- "tripped": tripped,
159
- }
216
+ Args:
217
+ spent: Optional explicit spend figure in tokens. If provided, overrides ledger.
218
+ period: "wave" (all rows) or "daily" (today's rows only)
219
+ config: aesop.config.json dict, or None to load from disk
220
+ state_dir: path to state directory, or None to resolve from config
221
+ trip: If True and exceeded, trip the .HALT sentinel
222
+ window_minutes: Optional time window in minutes for ledger filtering.
223
+ If None, all ledger rows are included (backward compat).
224
+ When specified, only rows within the last window_minutes
225
+ are included. MUST match the window_minutes used in
226
+ cost_projection.project() to ensure agreement.
227
+ """
228
+ try:
229
+ if config is None:
230
+ config = load_config()
231
+ if state_dir is None:
232
+ state_dir = halt.resolve_state_dir(config=config)
233
+ state_dir = Path(state_dir)
234
+
235
+ ceiling = get_ceiling(config, period)
236
+
237
+ if spent is None:
238
+ spent = read_ledger_total_tokens(state_dir, period=period, window_minutes=window_minutes)
239
+ spent = int(spent)
240
+
241
+ exceeded = ceiling is not None and spent >= ceiling
242
+ tripped = False
243
+
244
+ if exceeded and trip:
245
+ reason = (
246
+ f"cost ceiling exceeded: {period} spend {spent} tokens >= "
247
+ f"ceiling {ceiling} tokens"
248
+ )
249
+ halt.halt(reason, state_dir=state_dir)
250
+ tripped = True
251
+
252
+ return {
253
+ "period": period,
254
+ "ceiling": ceiling,
255
+ "spent": spent,
256
+ "exceeded": exceeded,
257
+ "tripped": tripped,
258
+ "reason": None,
259
+ }
260
+ except Exception as e:
261
+ # Exception during spend computation (e.g., ledger read/create failure,
262
+ # file lock, transient I/O error). This is fail-SAFE: abort the current
263
+ # wave (exceeded=True) to prevent runaway work, but do NOT write the
264
+ # persistent .HALT sentinel (tripped=False). The distinction ensures a
265
+ # transient I/O hiccup does not permanently wedge the fleet.
266
+ reason_text = f"cost_check_error: {type(e).__name__}: {str(e)[:100]}"
267
+ return {
268
+ "period": period,
269
+ "ceiling": None,
270
+ "spent": None,
271
+ "exceeded": True,
272
+ "tripped": False,
273
+ "error": str(e),
274
+ "reason": reason_text,
275
+ }
160
276
 
161
277
 
162
278
  def main(argv=None):
@@ -164,13 +280,20 @@ def main(argv=None):
164
280
  parser.add_argument("--check", action="store_true", help="Run the ceiling check (required).")
165
281
  parser.add_argument("--spent", type=int, default=None, help="Explicit spend figure in tokens; defaults to ledger total.")
166
282
  parser.add_argument("--period", choices=("wave", "daily"), default="wave", help="Which ceiling to check against (default: wave).")
283
+ parser.add_argument("--window", type=int, default=None, help="Optional time window in minutes for ledger filtering; None means all rows (default, backward compat).")
167
284
  args = parser.parse_args(argv)
168
285
 
169
286
  if not args.check:
170
287
  parser.print_usage(sys.stderr)
171
288
  return 2
172
289
 
173
- result = check(spent=args.spent, period=args.period)
290
+ result = check(spent=args.spent, period=args.period, window_minutes=args.window)
291
+
292
+ # Check for errors FIRST (fail-closed): exception during check() means abort the wave
293
+ if "error" in result:
294
+ reason = result.get("reason", result["error"])
295
+ print(f"[cost_ceiling] FATAL: {reason}", file=sys.stderr)
296
+ return 1
174
297
 
175
298
  if result["ceiling"] is None:
176
299
  print(f"[cost_ceiling] no {args.period} ceiling configured — skipping (spent={result['spent']})")