@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.
- package/CHANGELOG.md +37 -94
- package/CLAUDE-TEMPLATE.md +115 -0
- package/README.md +89 -264
- package/aesop.config.example.json +27 -26
- package/bin/CLAUDE.md +39 -0
- package/bin/cli.js +368 -24
- package/daemons/CLAUDE.md +33 -0
- package/daemons/backup-fleet.sh +92 -21
- package/daemons/run-watchdog.sh +90 -10
- package/dash/CLAUDE.md +32 -0
- package/dash/dash-extra.mjs +194 -21
- package/dash/watchdog-gui.sh +23 -6
- package/docs/BEHAVIORAL-PR-REVIEW.md +86 -0
- package/docs/CHECKPOINTING.md +100 -0
- package/docs/DISPATCH-MODEL.md +11 -7
- package/docs/FORENSICS.md +204 -0
- package/docs/GOVERNANCE.md +4 -2
- package/docs/HOOK-INSTALL.md +307 -0
- package/docs/HOW-THE-LOOP-WORKS.md +175 -0
- package/docs/MEMORY-TEMPLATE.md +61 -0
- package/docs/README.md +44 -0
- package/docs/RELIABILITY.md +61 -0
- package/docs/RESTORE.md +397 -0
- package/docs/SCRIPTS-POLICY.md +97 -0
- package/docs/spikes/tiered-cognition/ACTIVATION.md +125 -0
- package/docs/spikes/tiered-cognition/DESIGN.md +287 -0
- package/docs/spikes/tiered-cognition/FINDINGS.md +113 -0
- package/docs/spikes/tiered-cognition/README.md +29 -0
- package/docs/spikes/tiered-cognition/aesop-cognition.example.md +32 -0
- package/docs/spikes/tiered-cognition/force-model-policy.merged.mjs +673 -0
- package/docs/spikes/tiered-cognition/strip-tools-hook.mjs +434 -0
- package/hooks/CLAUDE.md +38 -0
- package/hooks/claude/force-model-policy.mjs +159 -0
- package/hooks/pre-push-policy.sh +753 -0
- package/monitor/.signal-state.json +3 -0
- package/monitor/ACTIONS.log +1 -0
- package/monitor/BRIEF.md +24 -0
- package/monitor/CHARTER.md +29 -8
- package/monitor/CLAUDE.md +40 -0
- package/monitor/SIGNALS.json +54 -0
- package/monitor/collect-signals.mjs +529 -62
- package/package.json +12 -3
- package/skills/CLAUDE.md +3 -0
- package/skills/power/SKILL.md +161 -0
- package/tools/CLAUDE.md +191 -0
- package/tools/__pycache__/buildlog.cpython-314.pyc +0 -0
- package/tools/__pycache__/ci_merge_wait.cpython-314.pyc +0 -0
- package/tools/__pycache__/ensure_state.cpython-314.pyc +0 -0
- package/tools/__pycache__/eod_sweep.cpython-314.pyc +0 -0
- package/tools/__pycache__/fleet_ledger.cpython-314.pyc +0 -0
- package/tools/__pycache__/heartbeat.cpython-314.pyc +0 -0
- package/tools/__pycache__/inbox_drain.cpython-314.pyc +0 -0
- package/tools/__pycache__/launch_tui.cpython-314.pyc +0 -0
- package/tools/__pycache__/orchestrator_status.cpython-314.pyc +0 -0
- package/tools/__pycache__/power_selftest.cpython-314.pyc +0 -0
- package/tools/__pycache__/prepublish_scan.cpython-314.pyc +0 -0
- package/tools/__pycache__/rotate_logs.cpython-314.pyc +0 -0
- package/tools/__pycache__/scanner_selftest.cpython-314.pyc +0 -0
- package/tools/__pycache__/secret_scan.cpython-314.pyc +0 -0
- package/tools/__pycache__/stall_check.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_dash.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_submit_encoding.cpython-314.pyc +0 -0
- package/tools/agent-forensics.sh +186 -0
- package/tools/buildlog.py +182 -0
- package/tools/ci_merge_wait.py +204 -0
- package/tools/ensure_state.py +94 -0
- package/tools/eod_sweep.py +239 -0
- package/tools/fleet_ledger.py +272 -0
- package/tools/heartbeat.py +131 -0
- package/tools/inbox_drain.py +249 -0
- package/tools/launch_tui.py +51 -3
- package/tools/lock.mjs +211 -0
- package/tools/metrics_gate.py +205 -0
- package/tools/orchestrator_status.py +99 -0
- package/tools/power_selftest.py +386 -0
- package/tools/prepublish_scan.py +84 -0
- package/tools/proposals.mjs +248 -0
- package/tools/reconstitute.sh +467 -0
- package/tools/rotate_logs.py +228 -0
- package/tools/scanner_selftest.py +303 -0
- package/tools/secret_scan.py +131 -31
- package/tools/stall_check.py +172 -0
- package/tools/verify_dash.py +694 -0
- package/tools/verify_submit_encoding.py +194 -0
- package/ui/CLAUDE.md +109 -0
- package/ui/__pycache__/agents.cpython-314.pyc +0 -0
- package/ui/__pycache__/collectors.cpython-314.pyc +0 -0
- package/ui/__pycache__/config.cpython-314.pyc +0 -0
- package/ui/__pycache__/csrf.cpython-314.pyc +0 -0
- package/ui/__pycache__/handler.cpython-314.pyc +0 -0
- package/ui/__pycache__/render.cpython-314.pyc +0 -0
- package/ui/__pycache__/serve.cpython-314.pyc +0 -0
- package/ui/__pycache__/sse.cpython-314.pyc +0 -0
- package/ui/agents.py +179 -0
- package/ui/api/__init__.py +84 -0
- package/ui/api/__pycache__/__init__.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/submit.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/tracker.cpython-314.pyc +0 -0
- package/ui/api/submit.py +58 -0
- package/ui/api/tracker.py +116 -0
- package/ui/collectors.py +586 -0
- package/ui/config.py +114 -0
- package/ui/csrf.py +140 -0
- package/ui/handler.py +426 -0
- package/ui/render.py +29 -0
- package/ui/serve.py +68 -796
- package/ui/sse.py +168 -0
- 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()
|