@matt82198/aesop 0.1.0-beta.2 → 0.1.0-beta.4

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 (108) hide show
  1. package/CHANGELOG.md +37 -94
  2. package/CLAUDE-TEMPLATE.md +115 -0
  3. package/README.md +89 -264
  4. package/aesop.config.example.json +27 -26
  5. package/bin/CLAUDE.md +39 -0
  6. package/bin/cli.js +368 -24
  7. package/daemons/CLAUDE.md +33 -0
  8. package/daemons/backup-fleet.sh +92 -21
  9. package/daemons/run-watchdog.sh +90 -10
  10. package/dash/CLAUDE.md +32 -0
  11. package/dash/dash-extra.mjs +194 -21
  12. package/dash/watchdog-gui.sh +23 -6
  13. package/docs/BEHAVIORAL-PR-REVIEW.md +86 -0
  14. package/docs/CHECKPOINTING.md +100 -0
  15. package/docs/DISPATCH-MODEL.md +11 -7
  16. package/docs/FORENSICS.md +204 -0
  17. package/docs/GOVERNANCE.md +4 -2
  18. package/docs/HOOK-INSTALL.md +307 -0
  19. package/docs/HOW-THE-LOOP-WORKS.md +175 -0
  20. package/docs/MEMORY-TEMPLATE.md +61 -0
  21. package/docs/README.md +44 -0
  22. package/docs/RELIABILITY.md +61 -0
  23. package/docs/RESTORE.md +397 -0
  24. package/docs/SCRIPTS-POLICY.md +97 -0
  25. package/docs/spikes/tiered-cognition/ACTIVATION.md +125 -0
  26. package/docs/spikes/tiered-cognition/DESIGN.md +287 -0
  27. package/docs/spikes/tiered-cognition/FINDINGS.md +113 -0
  28. package/docs/spikes/tiered-cognition/README.md +29 -0
  29. package/docs/spikes/tiered-cognition/aesop-cognition.example.md +32 -0
  30. package/docs/spikes/tiered-cognition/force-model-policy.merged.mjs +673 -0
  31. package/docs/spikes/tiered-cognition/strip-tools-hook.mjs +434 -0
  32. package/hooks/CLAUDE.md +38 -0
  33. package/hooks/claude/force-model-policy.mjs +159 -0
  34. package/hooks/pre-push-policy.sh +753 -0
  35. package/monitor/.signal-state.json +3 -0
  36. package/monitor/ACTIONS.log +1 -0
  37. package/monitor/BRIEF.md +24 -0
  38. package/monitor/CHARTER.md +29 -8
  39. package/monitor/CLAUDE.md +40 -0
  40. package/monitor/SIGNALS.json +54 -0
  41. package/monitor/collect-signals.mjs +529 -62
  42. package/package.json +12 -3
  43. package/skills/CLAUDE.md +3 -0
  44. package/skills/power/SKILL.md +161 -0
  45. package/tools/CLAUDE.md +191 -0
  46. package/tools/__pycache__/buildlog.cpython-314.pyc +0 -0
  47. package/tools/__pycache__/ci_merge_wait.cpython-314.pyc +0 -0
  48. package/tools/__pycache__/ensure_state.cpython-314.pyc +0 -0
  49. package/tools/__pycache__/eod_sweep.cpython-314.pyc +0 -0
  50. package/tools/__pycache__/fleet_ledger.cpython-314.pyc +0 -0
  51. package/tools/__pycache__/heartbeat.cpython-314.pyc +0 -0
  52. package/tools/__pycache__/inbox_drain.cpython-314.pyc +0 -0
  53. package/tools/__pycache__/launch_tui.cpython-314.pyc +0 -0
  54. package/tools/__pycache__/orchestrator_status.cpython-314.pyc +0 -0
  55. package/tools/__pycache__/power_selftest.cpython-314.pyc +0 -0
  56. package/tools/__pycache__/prepublish_scan.cpython-314.pyc +0 -0
  57. package/tools/__pycache__/rotate_logs.cpython-314.pyc +0 -0
  58. package/tools/__pycache__/scanner_selftest.cpython-314.pyc +0 -0
  59. package/tools/__pycache__/secret_scan.cpython-314.pyc +0 -0
  60. package/tools/__pycache__/stall_check.cpython-314.pyc +0 -0
  61. package/tools/__pycache__/verify_dash.cpython-314.pyc +0 -0
  62. package/tools/__pycache__/verify_submit_encoding.cpython-314.pyc +0 -0
  63. package/tools/agent-forensics.sh +186 -0
  64. package/tools/buildlog.py +182 -0
  65. package/tools/ci_merge_wait.py +204 -0
  66. package/tools/ensure_state.py +94 -0
  67. package/tools/eod_sweep.py +239 -0
  68. package/tools/fleet_ledger.py +272 -0
  69. package/tools/heartbeat.py +131 -0
  70. package/tools/inbox_drain.py +249 -0
  71. package/tools/launch_tui.py +51 -3
  72. package/tools/lock.mjs +211 -0
  73. package/tools/metrics_gate.py +205 -0
  74. package/tools/orchestrator_status.py +99 -0
  75. package/tools/power_selftest.py +386 -0
  76. package/tools/prepublish_scan.py +84 -0
  77. package/tools/proposals.mjs +248 -0
  78. package/tools/reconstitute.sh +467 -0
  79. package/tools/rotate_logs.py +228 -0
  80. package/tools/scanner_selftest.py +303 -0
  81. package/tools/secret_scan.py +131 -31
  82. package/tools/stall_check.py +172 -0
  83. package/tools/verify_dash.py +694 -0
  84. package/tools/verify_submit_encoding.py +194 -0
  85. package/ui/CLAUDE.md +109 -0
  86. package/ui/__pycache__/agents.cpython-314.pyc +0 -0
  87. package/ui/__pycache__/collectors.cpython-314.pyc +0 -0
  88. package/ui/__pycache__/config.cpython-314.pyc +0 -0
  89. package/ui/__pycache__/csrf.cpython-314.pyc +0 -0
  90. package/ui/__pycache__/handler.cpython-314.pyc +0 -0
  91. package/ui/__pycache__/render.cpython-314.pyc +0 -0
  92. package/ui/__pycache__/serve.cpython-314.pyc +0 -0
  93. package/ui/__pycache__/sse.cpython-314.pyc +0 -0
  94. package/ui/agents.py +179 -0
  95. package/ui/api/__init__.py +84 -0
  96. package/ui/api/__pycache__/__init__.cpython-314.pyc +0 -0
  97. package/ui/api/__pycache__/submit.cpython-314.pyc +0 -0
  98. package/ui/api/__pycache__/tracker.cpython-314.pyc +0 -0
  99. package/ui/api/submit.py +58 -0
  100. package/ui/api/tracker.py +116 -0
  101. package/ui/collectors.py +586 -0
  102. package/ui/config.py +114 -0
  103. package/ui/csrf.py +140 -0
  104. package/ui/handler.py +426 -0
  105. package/ui/render.py +29 -0
  106. package/ui/serve.py +68 -796
  107. package/ui/sse.py +168 -0
  108. package/ui/templates/dashboard.html +1202 -0
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env python3
2
+ r"""
3
+ Append uniform BUILDLOG entries with optional git HEAD reference.
4
+
5
+ Usage:
6
+ python buildlog.py "<message>" [--state-dir DIR] [--head] [--repo-path PATH]
7
+
8
+ Purpose:
9
+ Ensures consistent BUILDLOG.md formatting across agents: one line per entry,
10
+ timestamped, with optional git HEAD reference. Prevents hand-formatting variance.
11
+
12
+ Args:
13
+ message: Entry text to append (positional, required).
14
+ --state-dir: Path to state directory for BUILDLOG.md (default: AESOP_STATE_ROOT or ./state).
15
+ --head: Include git HEAD (short-hash subject) from specified repo or current directory.
16
+ --repo-path: Path to git repo for HEAD extraction (default: current working directory).
17
+
18
+ Output:
19
+ Prints the exact line appended to stderr; exit 0 always.
20
+
21
+ Behavior:
22
+ - Appends ONE line formatted: ### [YYYY-MM-DD HH:MM] <message>
23
+ - With --head, appends: | HEAD: <short-hash> <subject>
24
+ - If repo not found or not a git repo, omits HEAD and notes "(no-repo)".
25
+ - Creates BUILDLOG.md with append-only header if missing.
26
+ - Never overwrites; always returns the line appended.
27
+ """
28
+
29
+ import argparse
30
+ import os
31
+ import subprocess
32
+ import sys
33
+ from datetime import datetime
34
+ from pathlib import Path
35
+
36
+
37
+ def get_state_dir():
38
+ """Resolve state directory from env var or current working directory."""
39
+ if os.environ.get("AESOP_STATE_ROOT"):
40
+ return Path(os.environ["AESOP_STATE_ROOT"])
41
+ # Default to ./state (relative to cwd)
42
+ return Path.cwd() / "state"
43
+
44
+
45
+ def get_git_head(repo_path):
46
+ """Get short hash and subject from git HEAD.
47
+
48
+ Args:
49
+ repo_path: Path to git repository.
50
+
51
+ Returns:
52
+ Tuple of (short_hash, subject) or (None, None) if not a git repo.
53
+ """
54
+ try:
55
+ if not Path(repo_path / ".git").exists():
56
+ return None, None
57
+
58
+ result = subprocess.run(
59
+ ["git", "log", "-1", "--pretty=%h %s"],
60
+ cwd=repo_path,
61
+ capture_output=True,
62
+ text=True,
63
+ timeout=5,
64
+ )
65
+ if result.returncode == 0 and result.stdout.strip():
66
+ parts = result.stdout.strip().split(None, 1)
67
+ if len(parts) == 2:
68
+ return parts[0], parts[1]
69
+ elif len(parts) == 1:
70
+ return parts[0], "(no subject)"
71
+ except Exception:
72
+ pass
73
+
74
+ return None, None
75
+
76
+
77
+ def build_entry_line(message, short_hash=None, subject=None):
78
+ """Format BUILDLOG entry line.
79
+
80
+ Args:
81
+ message: Entry message.
82
+ short_hash: Git short hash (optional).
83
+ subject: Git subject (optional).
84
+
85
+ Returns:
86
+ Formatted entry line.
87
+ """
88
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
89
+ line = f"### [{timestamp}] {message}"
90
+
91
+ if short_hash and subject:
92
+ line += f" | HEAD: {short_hash} {subject}"
93
+ elif short_hash:
94
+ line += f" | HEAD: {short_hash} (no subject)"
95
+
96
+ return line
97
+
98
+
99
+ def ensure_buildlog_header(buildlog_path):
100
+ """Create BUILDLOG.md with header if missing.
101
+
102
+ Args:
103
+ buildlog_path: Path to BUILDLOG.md file.
104
+ """
105
+ if buildlog_path.exists():
106
+ return
107
+
108
+ header = "# Build Log (append-only)\n"
109
+ buildlog_path.parent.mkdir(parents=True, exist_ok=True)
110
+ buildlog_path.write_text(header)
111
+
112
+
113
+ def append_entry(buildlog_path, line):
114
+ """Append line to BUILDLOG.md.
115
+
116
+ Args:
117
+ buildlog_path: Path to BUILDLOG.md file.
118
+ line: Entry line to append.
119
+ """
120
+ ensure_buildlog_header(buildlog_path)
121
+
122
+ with open(buildlog_path, "a", encoding="utf-8") as f:
123
+ f.write(line + "\n")
124
+
125
+
126
+ def main():
127
+ """Main entry point."""
128
+ parser = argparse.ArgumentParser(
129
+ description="Append uniform BUILDLOG entries with optional git HEAD reference.",
130
+ formatter_class=argparse.RawDescriptionHelpFormatter,
131
+ )
132
+ parser.add_argument("message", help="Entry message to append")
133
+ parser.add_argument(
134
+ "--state-dir",
135
+ default=None,
136
+ help="Path to state directory (default: AESOP_STATE_ROOT or ./state)",
137
+ )
138
+ parser.add_argument(
139
+ "--head",
140
+ action="store_true",
141
+ help="Include git HEAD (short-hash subject) reference",
142
+ )
143
+ parser.add_argument(
144
+ "--repo-path",
145
+ default=None,
146
+ help="Path to git repo for HEAD extraction (default: current working directory)",
147
+ )
148
+
149
+ args = parser.parse_args()
150
+
151
+ # Resolve state directory
152
+ if args.state_dir:
153
+ state_dir = Path(args.state_dir)
154
+ else:
155
+ state_dir = get_state_dir()
156
+
157
+ # Derive repo path and get git HEAD if requested
158
+ short_hash = None
159
+ subject = None
160
+ repo_note = ""
161
+
162
+ if args.head:
163
+ repo_path = Path(args.repo_path) if args.repo_path else Path.cwd()
164
+ short_hash, subject = get_git_head(repo_path)
165
+
166
+ if not short_hash:
167
+ repo_note = " (no-repo)"
168
+
169
+ # Build and format entry
170
+ line = build_entry_line(args.message, short_hash, subject)
171
+
172
+ # Append to BUILDLOG.md
173
+ buildlog_path = state_dir / "BUILDLOG.md"
174
+ append_entry(buildlog_path, line)
175
+
176
+ # Print the exact line appended (with repo note if --head requested and failed)
177
+ output_line = line + repo_note if repo_note else line
178
+ print(output_line, file=sys.stderr)
179
+
180
+
181
+ if __name__ == "__main__":
182
+ main()
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ ci_merge_wait.py — CI-gated merge helper: wait for PR checks to conclude, then merge.
4
+
5
+ Polls gh pr view until all status checks conclude (SUCCESS/FAILURE), then merges ONLY
6
+ if all checks are SUCCESS. The gh pr merge call is STRUCTURALLY UNREACHABLE unless the
7
+ status is SUCCESS — this is the whole point (prevents merge-on-CI-failure edge cases).
8
+
9
+ Usage:
10
+ python ci_merge_wait.py <PR-number> [--timeout SECONDS] [--poll SECONDS] [--merge-method merge|squash|rebase]
11
+
12
+ Options:
13
+ --timeout SECONDS Max seconds to wait for CI to conclude (default: 3600)
14
+ --poll SECONDS Poll interval in seconds (default: 10)
15
+ --merge-method METHOD Merge strategy: merge, squash, rebase (default: merge)
16
+
17
+ Exit codes:
18
+ 0 = PR merged successfully
19
+ 2 = CI checks failed (do NOT merge, prints which check failed)
20
+ 3 = Timeout waiting for CI to conclude
21
+ 4 = PR not mergeable or has merge conflicts
22
+
23
+ Requires: gh CLI available on PATH. Gracefully exits with error if gh is missing.
24
+ """
25
+
26
+ import argparse
27
+ import json
28
+ import subprocess
29
+ import sys
30
+ import time
31
+
32
+
33
+ def run_gh_command(args):
34
+ """
35
+ Run gh CLI command; return parsed JSON or None if gh missing/error.
36
+ Raises subprocess.CalledProcessError on non-zero exit.
37
+ """
38
+ try:
39
+ result = subprocess.run(
40
+ args,
41
+ capture_output=True,
42
+ text=True,
43
+ timeout=30,
44
+ )
45
+ if result.returncode != 0:
46
+ if "not found" in result.stderr or "No such file" in result.stderr:
47
+ return None
48
+ # Re-raise for non-zero return (will be caught by caller)
49
+ result.check_returncode()
50
+ return json.loads(result.stdout) if result.stdout.strip() else None
51
+ except subprocess.TimeoutExpired:
52
+ print("ERROR: gh command timed out")
53
+ sys.exit(1)
54
+ except FileNotFoundError:
55
+ print("ERROR: gh CLI not found on PATH")
56
+ sys.exit(1)
57
+
58
+
59
+ def get_pr_status(pr_number):
60
+ """
61
+ Fetch PR status via gh pr view.
62
+ Returns dict with 'mergeable' and 'statusCheckRollup' keys.
63
+ Returns None if gh is missing.
64
+ """
65
+ data = run_gh_command([
66
+ "gh", "pr", "view", str(pr_number),
67
+ "--json", "mergeable,statusCheckRollup"
68
+ ])
69
+ return data
70
+
71
+
72
+ def check_ci_status(status_rollup):
73
+ """
74
+ Analyze status check rollup.
75
+ Returns: ("pending", None), ("success", None), or ("failure", check_name)
76
+ """
77
+ if not status_rollup:
78
+ # No checks (unusual but treat as success)
79
+ return ("success", None)
80
+
81
+ # Collect statuses
82
+ pending_checks = []
83
+ failed_checks = []
84
+
85
+ for check in status_rollup:
86
+ status = check.get("status", "").upper()
87
+ if status == "PENDING":
88
+ pending_checks.append(check.get("name", "unknown"))
89
+ elif status == "FAILURE":
90
+ failed_checks.append(check.get("name", "unknown"))
91
+
92
+ # Determine overall status
93
+ if failed_checks:
94
+ return ("failure", failed_checks[0])
95
+ elif pending_checks:
96
+ return ("pending", None)
97
+ else:
98
+ return ("success", None)
99
+
100
+
101
+ def merge_pr(pr_number, merge_method):
102
+ """
103
+ Merge the PR using gh pr merge.
104
+ This call is STRUCTURALLY UNREACHABLE unless CI is SUCCESS.
105
+ Returns True on success, False on error.
106
+ """
107
+ result = subprocess.run(
108
+ ["gh", "pr", "merge", str(pr_number), f"--{merge_method}"],
109
+ capture_output=True,
110
+ text=True,
111
+ timeout=30,
112
+ )
113
+ return result.returncode == 0
114
+
115
+
116
+ def main():
117
+ parser = argparse.ArgumentParser(
118
+ description=__doc__,
119
+ formatter_class=argparse.RawDescriptionHelpFormatter
120
+ )
121
+ parser.add_argument("pr_number", type=int, help="GitHub PR number")
122
+ parser.add_argument(
123
+ "--timeout",
124
+ type=int,
125
+ default=3600,
126
+ help="Max seconds to wait for CI (default: 3600)"
127
+ )
128
+ parser.add_argument(
129
+ "--poll",
130
+ type=int,
131
+ default=10,
132
+ help="Poll interval in seconds (default: 10)"
133
+ )
134
+ parser.add_argument(
135
+ "--merge-method",
136
+ choices=["merge", "squash", "rebase"],
137
+ default="merge",
138
+ help="Merge strategy (default: merge)"
139
+ )
140
+
141
+ args = parser.parse_args()
142
+
143
+ # Validate inputs
144
+ if args.pr_number <= 0:
145
+ print("ERROR: PR number must be positive")
146
+ sys.exit(1)
147
+
148
+ if args.timeout <= 0 or args.poll <= 0:
149
+ print("ERROR: timeout and poll must be positive")
150
+ sys.exit(1)
151
+
152
+ # Fetch initial PR status
153
+ print(f"Checking PR #{args.pr_number} status...")
154
+ status = get_pr_status(args.pr_number)
155
+ if status is None:
156
+ print("ERROR: gh CLI not available or PR not found")
157
+ sys.exit(1)
158
+
159
+ mergeable = status.get("mergeable", "").upper()
160
+ if mergeable == "CONFLICTED":
161
+ print(f"MERGE CONFLICT: PR #{args.pr_number} has conflicts")
162
+ sys.exit(4)
163
+ elif mergeable not in ("MERGEABLE", "UNKNOWN"):
164
+ # Also treat UNKNOWN conservatively as non-mergeable until we have clarity
165
+ print(f"NOT MERGEABLE: PR #{args.pr_number} status={mergeable}")
166
+ sys.exit(4)
167
+
168
+ # Poll until CI concludes
169
+ start_time = time.time()
170
+ while True:
171
+ elapsed = time.time() - start_time
172
+ if elapsed > args.timeout:
173
+ print(f"TIMEOUT: CI did not conclude within {args.timeout}s")
174
+ sys.exit(3)
175
+
176
+ status = get_pr_status(args.pr_number)
177
+ if status is None:
178
+ print("ERROR: gh CLI check failed")
179
+ sys.exit(1)
180
+
181
+ status_rollup = status.get("statusCheckRollup", [])
182
+ ci_status, failed_check = check_ci_status(status_rollup)
183
+
184
+ if ci_status == "failure":
185
+ print(f"CI FAILED: {failed_check}")
186
+ sys.exit(2)
187
+ elif ci_status == "success":
188
+ # SUCCESS: CI is green, proceed to merge
189
+ # This merge call is STRUCTURALLY UNREACHABLE unless ci_status == "success"
190
+ print(f"CI GREEN: All checks passed. Merging PR #{args.pr_number}...")
191
+ if merge_pr(args.pr_number, args.merge_method):
192
+ print(f"MERGED: PR #{args.pr_number} merged successfully")
193
+ sys.exit(0)
194
+ else:
195
+ print("ERROR: merge failed (PR state changed?)")
196
+ sys.exit(1)
197
+
198
+ # Still pending: wait and retry
199
+ print(f"CI PENDING ({elapsed:.0f}s elapsed)... waiting {args.poll}s")
200
+ time.sleep(args.poll)
201
+
202
+
203
+ if __name__ == "__main__":
204
+ main()
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Scaffold durable checkpointing directories for project orchestration.
4
+
5
+ Usage: ensure_state.py --state-dir DIR
6
+
7
+ Creates STATE.md and BUILDLOG.md templates in the state directory
8
+ if they do not already exist. Never overwrites existing files.
9
+ """
10
+ # secretscan: allow-pattern-docs
11
+
12
+ import sys
13
+ import os
14
+ import argparse
15
+ import datetime
16
+ from pathlib import Path
17
+
18
+
19
+ STATE_TEMPLATE = """# STATE — authoritative project checkpoint
20
+
21
+ ## Intent
22
+ One-line summary of the project's current phase and goal.
23
+
24
+ ## Stack & locked decisions
25
+ - Key technology choices and constraints.
26
+ - Data model contracts and API signatures.
27
+
28
+ ## Current status
29
+ - Phase summary and completion %.
30
+ - Major blockers or decisions pending.
31
+
32
+ ## Gotchas
33
+ - Known issues, workarounds, environment quirks.
34
+
35
+ ## NEXT STEPS
36
+ - Explicit ordered list of what comes next.
37
+ - Assigned owners if coordinating multiple agents.
38
+ """
39
+
40
+ BUILDLOG_HEADER = "# BUILDLOG — append-only progress log"
41
+
42
+
43
+ def ensure_state_files(state_dir):
44
+ """
45
+ Create state directory with STATE.md and BUILDLOG.md if missing.
46
+ Returns list of (filename, status) tuples: ('STATE.md', 'CREATED'), etc.
47
+ """
48
+ state_path = Path(state_dir)
49
+ state_path.mkdir(parents=True, exist_ok=True)
50
+
51
+ results = []
52
+
53
+ # STATE.md
54
+ state_file = state_path / 'STATE.md'
55
+ if state_file.exists():
56
+ results.append(('STATE.md', 'EXISTS'))
57
+ else:
58
+ with open(state_file, 'w', encoding='utf-8') as f:
59
+ f.write(STATE_TEMPLATE)
60
+ results.append(('STATE.md', 'CREATED'))
61
+
62
+ # BUILDLOG.md
63
+ buildlog_file = state_path / 'BUILDLOG.md'
64
+ if buildlog_file.exists():
65
+ results.append(('BUILDLOG.md', 'EXISTS'))
66
+ else:
67
+ timestamp = datetime.datetime.now().isoformat()
68
+ with open(buildlog_file, 'w', encoding='utf-8') as f:
69
+ f.write(f'{BUILDLOG_HEADER}\n')
70
+ f.write(f'created {timestamp}\n')
71
+ results.append(('BUILDLOG.md', 'CREATED'))
72
+
73
+ return results
74
+
75
+
76
+ def main():
77
+ parser = argparse.ArgumentParser(
78
+ description='Scaffold durable checkpointing directories.'
79
+ )
80
+ parser.add_argument('--state-dir', required=True,
81
+ help='State directory')
82
+
83
+ args = parser.parse_args()
84
+
85
+ state_dir = args.state_dir
86
+
87
+ results = ensure_state_files(state_dir)
88
+
89
+ for filename, status in results:
90
+ print(f'{status} {filename}')
91
+
92
+
93
+ if __name__ == '__main__':
94
+ main()