@matt82198/aesop 0.1.0-beta.3 → 0.1.0-beta.5
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 +79 -95
- package/CLAUDE-TEMPLATE.md +115 -0
- package/README.md +126 -245
- package/aesop.config.example.json +34 -26
- package/bin/CLAUDE.md +67 -0
- package/bin/cli.js +593 -36
- package/daemons/CLAUDE.md +36 -0
- package/daemons/backup-fleet.sh +92 -21
- package/daemons/run-watchdog.sh +95 -12
- 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/RELEASING.md +159 -0
- package/docs/RELIABILITY.md +61 -0
- package/docs/RESTORE.md +397 -0
- package/docs/SCRIPTS-POLICY.md +97 -0
- package/docs/archive/README.md +3 -0
- package/docs/archive/spikes/tiered-cognition/ACTIVATION.md +125 -0
- package/docs/archive/spikes/tiered-cognition/DESIGN.md +287 -0
- package/docs/archive/spikes/tiered-cognition/FINDINGS.md +113 -0
- package/docs/archive/spikes/tiered-cognition/README.md +27 -0
- package/docs/archive/spikes/tiered-cognition/aesop-cognition.example.md +32 -0
- package/docs/archive/spikes/tiered-cognition/force-model-policy.merged.mjs +673 -0
- package/docs/archive/spikes/tiered-cognition/strip-tools-hook.mjs +434 -0
- package/docs/case-study-portfolio.md +61 -0
- package/docs/self-stats-data.json +11 -0
- package/docs/templates/FLEET-OPS-ANALYSIS.example.md +27 -0
- package/docs/templates/FLEET-OPS-RECOMMENDATIONS.example.md +24 -0
- package/docs/templates/PROPOSALS-LOG.example.md +64 -0
- package/hooks/CLAUDE.md +61 -0
- package/hooks/claude/force-model-policy.mjs +159 -0
- package/hooks/pre-push-policy.sh +753 -0
- package/mcp/CLAUDE.md +213 -0
- package/mcp/package.json +26 -0
- package/mcp/server.mjs +543 -0
- package/monitor/.signal-state.json +3 -0
- package/monitor/ACTIONS.log +1 -0
- package/monitor/BRIEF.md +24 -0
- package/monitor/CHARTER.md +75 -88
- package/monitor/CLAUDE.md +40 -0
- package/monitor/SIGNALS.json +54 -0
- package/monitor/collect-signals.mjs +613 -61
- package/package.json +15 -3
- package/scan/fleet-scan.example.mjs +292 -0
- package/skills/CLAUDE.md +3 -0
- package/skills/healthcheck/SKILL.md +44 -0
- package/skills/power/SKILL.md +161 -0
- package/state_store/CLAUDE.md +39 -0
- package/state_store/__init__.py +24 -0
- package/state_store/__pycache__/__init__.cpython-314.pyc +0 -0
- package/state_store/__pycache__/api.cpython-314.pyc +0 -0
- package/state_store/__pycache__/export.cpython-314.pyc +0 -0
- package/state_store/__pycache__/ingest.cpython-314.pyc +0 -0
- package/state_store/__pycache__/projections.cpython-314.pyc +0 -0
- package/state_store/__pycache__/store.cpython-314.pyc +0 -0
- package/state_store/api.py +35 -0
- package/state_store/export.py +19 -0
- package/state_store/ingest.py +24 -0
- package/state_store/projections.py +52 -0
- package/state_store/store.py +102 -0
- package/tools/CLAUDE.md +72 -0
- package/tools/__pycache__/alert_bridge.cpython-314.pyc +0 -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__/fleet_prompt_extractor.cpython-314.pyc +0 -0
- package/tools/__pycache__/healthcheck.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__/metrics_gate.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__/self_stats.cpython-314.pyc +0 -0
- package/tools/__pycache__/session_usage_summary.cpython-314.pyc +0 -0
- package/tools/__pycache__/stall_check.cpython-314.pyc +0 -0
- package/tools/__pycache__/transcript_replay.cpython-314.pyc +0 -0
- package/tools/__pycache__/transcript_timeline.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/alert_bridge.py +449 -0
- package/tools/buildlog.py +182 -0
- package/tools/ci_merge_wait.py +317 -0
- package/tools/ensure_state.py +94 -0
- package/tools/eod_sweep.py +239 -0
- package/tools/fleet_ledger.py +272 -0
- package/tools/fleet_prompt_extractor.py +134 -0
- package/tools/healthcheck.py +296 -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 +138 -31
- package/tools/self_stats.py +509 -0
- package/tools/session_usage_summary.py +198 -0
- package/tools/stall_check.py +172 -0
- package/tools/svg_to_png.mjs +50 -0
- package/tools/transcript_replay.py +236 -0
- package/tools/transcript_timeline.py +184 -0
- package/tools/verify_dash.py +513 -0
- package/tools/verify_submit_encoding.py +202 -0
- package/ui/CLAUDE.md +125 -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__/cost.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 +183 -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 +97 -0
- package/ui/api/tracker.py +116 -0
- package/ui/collectors.py +735 -0
- package/ui/config.py +123 -0
- package/ui/cost.py +260 -0
- package/ui/csrf.py +144 -0
- package/ui/handler.py +676 -0
- package/ui/render.py +49 -0
- package/ui/serve.py +68 -796
- package/ui/sse.py +183 -0
- package/ui/web/.gitattributes +13 -0
- package/ui/web/dist/assets/index-2LZDQirC.js +9 -0
- package/ui/web/dist/assets/index-D4M1qyOv.css +1 -0
- package/ui/web/dist/index.html +14 -0
- package/ui/web/index.html +13 -0
- package/ui/web/package-lock.json +2225 -0
- package/ui/web/package.json +26 -0
- package/ui/web/src/App.test.tsx +74 -0
- package/ui/web/src/App.tsx +142 -0
- package/ui/web/src/CONTRIBUTING-UI.md +49 -0
- package/ui/web/src/components/AgentRow.css +187 -0
- package/ui/web/src/components/AgentRow.test.tsx +209 -0
- package/ui/web/src/components/AgentRow.tsx +207 -0
- package/ui/web/src/components/AgentsPanel.css +108 -0
- package/ui/web/src/components/AgentsPanel.test.tsx +41 -0
- package/ui/web/src/components/AgentsPanel.tsx +58 -0
- package/ui/web/src/components/AlertsPanel.css +88 -0
- package/ui/web/src/components/AlertsPanel.test.tsx +51 -0
- package/ui/web/src/components/AlertsPanel.tsx +67 -0
- package/ui/web/src/components/BacklogPanel.test.tsx +126 -0
- package/ui/web/src/components/BacklogPanel.tsx +122 -0
- package/ui/web/src/components/CostChart.css +110 -0
- package/ui/web/src/components/CostChart.test.tsx +144 -0
- package/ui/web/src/components/CostChart.tsx +152 -0
- package/ui/web/src/components/CostTable.css +93 -0
- package/ui/web/src/components/CostTable.test.tsx +165 -0
- package/ui/web/src/components/CostTable.tsx +94 -0
- package/ui/web/src/components/EventsFeed.css +68 -0
- package/ui/web/src/components/EventsFeed.test.tsx +36 -0
- package/ui/web/src/components/EventsFeed.tsx +31 -0
- package/ui/web/src/components/HealthHeader.css +137 -0
- package/ui/web/src/components/HealthHeader.test.tsx +278 -0
- package/ui/web/src/components/HealthHeader.tsx +281 -0
- package/ui/web/src/components/InboxForm.css +135 -0
- package/ui/web/src/components/InboxForm.test.tsx +208 -0
- package/ui/web/src/components/InboxForm.tsx +116 -0
- package/ui/web/src/components/MessagesTail.module.css +144 -0
- package/ui/web/src/components/MessagesTail.test.tsx +176 -0
- package/ui/web/src/components/MessagesTail.tsx +94 -0
- package/ui/web/src/components/ReposPanel.css +90 -0
- package/ui/web/src/components/ReposPanel.test.tsx +45 -0
- package/ui/web/src/components/ReposPanel.tsx +67 -0
- package/ui/web/src/components/Scorecard.css +106 -0
- package/ui/web/src/components/Scorecard.test.tsx +117 -0
- package/ui/web/src/components/Scorecard.tsx +85 -0
- package/ui/web/src/components/Timeline.module.css +151 -0
- package/ui/web/src/components/Timeline.test.tsx +215 -0
- package/ui/web/src/components/Timeline.tsx +99 -0
- package/ui/web/src/components/TrackerBoard.test.tsx +121 -0
- package/ui/web/src/components/TrackerBoard.tsx +107 -0
- package/ui/web/src/components/TrackerCard.test.tsx +180 -0
- package/ui/web/src/components/TrackerCard.tsx +160 -0
- package/ui/web/src/components/TrackerForm.test.tsx +189 -0
- package/ui/web/src/components/TrackerForm.tsx +144 -0
- package/ui/web/src/lib/api.ts +218 -0
- package/ui/web/src/lib/format.test.ts +89 -0
- package/ui/web/src/lib/format.ts +103 -0
- package/ui/web/src/lib/sanitizeUrl.test.ts +84 -0
- package/ui/web/src/lib/sanitizeUrl.ts +38 -0
- package/ui/web/src/lib/types.ts +230 -0
- package/ui/web/src/lib/useHashRoute.test.ts +60 -0
- package/ui/web/src/lib/useHashRoute.ts +23 -0
- package/ui/web/src/lib/useSSE.ts +175 -0
- package/ui/web/src/main.tsx +10 -0
- package/ui/web/src/styles/global.css +179 -0
- package/ui/web/src/styles/theme.css +184 -0
- package/ui/web/src/styles/work.css +572 -0
- package/ui/web/src/test/fixtures.ts +385 -0
- package/ui/web/src/test/setup.ts +49 -0
- package/ui/web/src/views/Activity.module.css +43 -0
- package/ui/web/src/views/Activity.test.tsx +89 -0
- package/ui/web/src/views/Activity.tsx +31 -0
- package/ui/web/src/views/Cost.css +87 -0
- package/ui/web/src/views/Cost.test.tsx +142 -0
- package/ui/web/src/views/Cost.tsx +54 -0
- package/ui/web/src/views/Overview.css +51 -0
- package/ui/web/src/views/Overview.test.tsx +76 -0
- package/ui/web/src/views/Overview.tsx +46 -0
- package/ui/web/src/views/Work.test.tsx +82 -0
- package/ui/web/src/views/Work.tsx +79 -0
- package/ui/web/src/vite-env.d.ts +10 -0
- package/ui/web/tsconfig.json +22 -0
- package/ui/web/vite.config.ts +25 -0
- package/ui/web/vitest.config.ts +12 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
prepublish_scan.py — Pre-publish gate for public repos: scans full git history + staged changes.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
prepublish_scan.py [--repo PATH] Scan full history and staged changes before publishing
|
|
7
|
+
|
|
8
|
+
Exit codes: 0=clean, 1=findings, 2=usage error
|
|
9
|
+
Output: CLEAR-TO-PUBLISH or STOP with offending commits/files
|
|
10
|
+
|
|
11
|
+
This gate runs TWO scans (both must pass for CLEAR-TO-PUBLISH):
|
|
12
|
+
1. Full git history (--history mode): detects secrets in any prior commit
|
|
13
|
+
2. Staged changes (--staged mode): detects secrets in uncommitted changes
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
import subprocess
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def run_secret_scan(mode, repo_path):
|
|
24
|
+
"""Run secret_scan.py in the specified mode; return (exit_code, output, stderr)."""
|
|
25
|
+
scripts_dir = Path(__file__).parent
|
|
26
|
+
secret_scan_path = scripts_dir / "secret_scan.py"
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
cmd = [sys.executable, str(secret_scan_path), mode, "--repo", repo_path]
|
|
30
|
+
result = subprocess.run(
|
|
31
|
+
cmd,
|
|
32
|
+
capture_output=True,
|
|
33
|
+
text=True,
|
|
34
|
+
timeout=120
|
|
35
|
+
)
|
|
36
|
+
return result.returncode, result.stdout, result.stderr
|
|
37
|
+
except subprocess.TimeoutExpired:
|
|
38
|
+
return 1, "", "secret_scan.py timed out"
|
|
39
|
+
except Exception as e:
|
|
40
|
+
return 1, "", f"Error running secret_scan.py: {e}"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def main():
|
|
44
|
+
parser = argparse.ArgumentParser(
|
|
45
|
+
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
|
|
46
|
+
)
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"--repo",
|
|
49
|
+
default=os.getcwd(),
|
|
50
|
+
help="Git repo path (default: current directory)",
|
|
51
|
+
)
|
|
52
|
+
args = parser.parse_args()
|
|
53
|
+
|
|
54
|
+
repo_path = args.repo
|
|
55
|
+
|
|
56
|
+
print("[1/2] Scanning full git history for secrets...")
|
|
57
|
+
history_exit, history_out, history_err = run_secret_scan("--history", repo_path)
|
|
58
|
+
|
|
59
|
+
print("[2/2] Scanning staged changes for secrets...")
|
|
60
|
+
staged_exit, staged_out, staged_err = run_secret_scan("--staged", repo_path)
|
|
61
|
+
|
|
62
|
+
# Combine outputs
|
|
63
|
+
print("\n=== History Scan ===")
|
|
64
|
+
print(history_out)
|
|
65
|
+
if history_err:
|
|
66
|
+
print(history_err, file=sys.stderr)
|
|
67
|
+
|
|
68
|
+
print("\n=== Staged Scan ===")
|
|
69
|
+
print(staged_out)
|
|
70
|
+
if staged_err:
|
|
71
|
+
print(staged_err, file=sys.stderr)
|
|
72
|
+
|
|
73
|
+
# Verdict
|
|
74
|
+
print("\n" + "=" * 50)
|
|
75
|
+
if history_exit == 0 and staged_exit == 0:
|
|
76
|
+
print("CLEAR-TO-PUBLISH: Full history and staged changes are clean")
|
|
77
|
+
sys.exit(0)
|
|
78
|
+
else:
|
|
79
|
+
print("STOP: Secrets found in history or staged changes")
|
|
80
|
+
sys.exit(1)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
main()
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* proposals.mjs — Proposal lifecycle management tool
|
|
4
|
+
*
|
|
5
|
+
* Commands:
|
|
6
|
+
* list [--file PATH] List all pending proposals (signal key + status)
|
|
7
|
+
* accept <signal-key> [--file PATH] Move proposal to PROPOSALS-LOG.md as ACCEPTED
|
|
8
|
+
* reject <signal-key> [--file PATH] Move proposal to PROPOSALS-LOG.md as REJECTED
|
|
9
|
+
*
|
|
10
|
+
* Default file: monitor/PROPOSALS.md
|
|
11
|
+
* Log file (auto): same directory as PROPOSALS.md, named PROPOSALS-LOG.md
|
|
12
|
+
*
|
|
13
|
+
* Lock behavior (P0 wave-8 fix): fail-closed with exponential backoff + stale lock breaking.
|
|
14
|
+
* On lock timeout (default 30s), throws error instead of proceeding unlocked.
|
|
15
|
+
* Stale locks (>10min) are detected and broken with warning.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { acquireLock, releaseLock } from './lock.mjs';
|
|
21
|
+
|
|
22
|
+
// === Arg parsing ===
|
|
23
|
+
const args = process.argv.slice(2);
|
|
24
|
+
let command = '';
|
|
25
|
+
let signalKey = '';
|
|
26
|
+
let proposalsFile = 'monitor/PROPOSALS.md';
|
|
27
|
+
|
|
28
|
+
// Parse args: command [arg] [--file path]
|
|
29
|
+
if (args.length === 0) {
|
|
30
|
+
usage();
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
command = args[0];
|
|
35
|
+
|
|
36
|
+
// Find --file flag
|
|
37
|
+
let fileIdx = args.indexOf('--file');
|
|
38
|
+
if (fileIdx !== -1 && fileIdx + 1 < args.length) {
|
|
39
|
+
proposalsFile = args[fileIdx + 1];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Extract signal key for accept/reject
|
|
43
|
+
if (command === 'accept' || command === 'reject') {
|
|
44
|
+
// Find first non-flag arg after command
|
|
45
|
+
for (let i = 1; i < args.length; i++) {
|
|
46
|
+
if (!args[i].startsWith('--')) {
|
|
47
|
+
signalKey = args[i];
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (!signalKey) {
|
|
52
|
+
console.error(`Error: ${command} requires a signal-key argument`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (command !== 'list' && command !== 'accept' && command !== 'reject') {
|
|
58
|
+
console.error(`Error: unknown command '${command}'`);
|
|
59
|
+
usage();
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// === Core functions ===
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Parse PROPOSALS.md into proposal objects
|
|
67
|
+
* Format per monitor/collect-signals.mjs:
|
|
68
|
+
* ## <signal-key> — <timestamp>
|
|
69
|
+
* **Signal:** <signal-key>
|
|
70
|
+
* **Problem:** <problem>
|
|
71
|
+
* **Suggested change:** <change>
|
|
72
|
+
* ---
|
|
73
|
+
*/
|
|
74
|
+
function parseProposals(content) {
|
|
75
|
+
const proposals = [];
|
|
76
|
+
// Split on line containing only "---" (handle both LF and CRLF)
|
|
77
|
+
const blocks = content.split(/\r?\n---\r?\n/);
|
|
78
|
+
|
|
79
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
80
|
+
const block = blocks[i];
|
|
81
|
+
const trimmed = block.trim();
|
|
82
|
+
if (!trimmed) continue;
|
|
83
|
+
|
|
84
|
+
// Extract signal key from "**Signal:** <key>" line
|
|
85
|
+
const signalMatch = trimmed.match(/\*\*Signal:\*\*\s+(\S+)/);
|
|
86
|
+
if (!signalMatch) continue;
|
|
87
|
+
|
|
88
|
+
const key = signalMatch[1];
|
|
89
|
+
const firstLine = trimmed.split('\n')[0]; // e.g., "## signal-key — timestamp"
|
|
90
|
+
|
|
91
|
+
proposals.push({
|
|
92
|
+
key,
|
|
93
|
+
firstLine,
|
|
94
|
+
block: trimmed, // Store trimmed block (without leading/trailing whitespace)
|
|
95
|
+
originalBlock: block, // Store original block with original whitespace
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return proposals;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* List proposals
|
|
104
|
+
*/
|
|
105
|
+
function listProposals() {
|
|
106
|
+
let content = '';
|
|
107
|
+
try {
|
|
108
|
+
content = fs.readFileSync(proposalsFile, 'utf8');
|
|
109
|
+
} catch {
|
|
110
|
+
console.log('No proposals file found.');
|
|
111
|
+
process.exit(0);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const proposals = parseProposals(content);
|
|
115
|
+
if (proposals.length === 0) {
|
|
116
|
+
console.log('No proposals.');
|
|
117
|
+
process.exit(0);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log(`Found ${proposals.length} proposal(s):\n`);
|
|
121
|
+
for (const p of proposals) {
|
|
122
|
+
console.log(` ${p.key}`);
|
|
123
|
+
console.log(` ${p.firstLine}`);
|
|
124
|
+
console.log(` Status: PENDING`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Move proposal from PROPOSALS.md to PROPOSALS-LOG.md (with atomic locking for multi-writer safety)
|
|
130
|
+
* P0 wave-8 fix: fail-closed lock acquisition with exponential backoff.
|
|
131
|
+
* On timeout, throws error (does not fall through to unlocked write).
|
|
132
|
+
*/
|
|
133
|
+
function moveProposal(status) {
|
|
134
|
+
// Acquire lock with fail-closed behavior (throws on timeout)
|
|
135
|
+
let lockDir;
|
|
136
|
+
try {
|
|
137
|
+
lockDir = acquireLock(proposalsFile);
|
|
138
|
+
} catch (e) {
|
|
139
|
+
console.error(`Error: ${e.message}`);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
// ATOMIC READ: re-read to ensure we have latest content (guard against concurrent appends)
|
|
145
|
+
let content = '';
|
|
146
|
+
try {
|
|
147
|
+
content = fs.readFileSync(proposalsFile, 'utf8');
|
|
148
|
+
} catch {
|
|
149
|
+
console.error(`Error: Could not read ${proposalsFile}`);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Check if already in log (idempotency check first)
|
|
154
|
+
const logFile = path.join(path.dirname(proposalsFile), 'PROPOSALS-LOG.md');
|
|
155
|
+
let logContent = '';
|
|
156
|
+
if (fs.existsSync(logFile)) {
|
|
157
|
+
try {
|
|
158
|
+
logContent = fs.readFileSync(logFile, 'utf8');
|
|
159
|
+
} catch {
|
|
160
|
+
// Log file not readable; continue
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (logContent.includes(`**Signal:** ${signalKey}`)) {
|
|
165
|
+
console.log(`Notice: Signal key '${signalKey}' already moved to log; no-op.`);
|
|
166
|
+
process.exit(0);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const proposals = parseProposals(content);
|
|
170
|
+
const proposal = proposals.find(p => p.key === signalKey);
|
|
171
|
+
|
|
172
|
+
if (!proposal) {
|
|
173
|
+
console.error(`Error: Signal key '${signalKey}' not found in ${proposalsFile}`);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Remove proposal from source by rebuilding without this proposal
|
|
178
|
+
// Split on separators and filter out the matching proposal (handle both LF and CRLF)
|
|
179
|
+
const blocks = content.split(/\r?\n---\r?\n/);
|
|
180
|
+
const filteredBlocks = blocks.filter(block => {
|
|
181
|
+
const trimmed = block.trim();
|
|
182
|
+
if (!trimmed) return true; // Keep empty blocks
|
|
183
|
+
const signalMatch = trimmed.match(/\*\*Signal:\*\*\s+(\S+)/);
|
|
184
|
+
if (!signalMatch) return true; // Keep non-proposal blocks
|
|
185
|
+
return signalMatch[1] !== signalKey; // Filter out matching proposal
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Rebuild content with separators
|
|
189
|
+
const updatedContent = filteredBlocks.map((b, i) => {
|
|
190
|
+
if (i < filteredBlocks.length - 1 && b.trim()) {
|
|
191
|
+
return b.trim();
|
|
192
|
+
}
|
|
193
|
+
return b.trim();
|
|
194
|
+
}).filter(b => b).join('\n\n---\n\n');
|
|
195
|
+
|
|
196
|
+
// ATOMIC WRITE: write to temp file, then rename (atomic on all platforms)
|
|
197
|
+
const tmpFile = proposalsFile + '.tmp';
|
|
198
|
+
try {
|
|
199
|
+
fs.writeFileSync(tmpFile, updatedContent.trim() ? updatedContent + '\n' : '', 'utf8');
|
|
200
|
+
fs.renameSync(tmpFile, proposalsFile);
|
|
201
|
+
} catch (e) {
|
|
202
|
+
// Clean up temp file if it exists
|
|
203
|
+
try { fs.unlinkSync(tmpFile); } catch { }
|
|
204
|
+
console.error(`Error: Could not write ${proposalsFile}: ${e.message}`);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Append to log with status heading
|
|
209
|
+
const timestamp = new Date().toISOString();
|
|
210
|
+
const logEntry = `## ${status} ${timestamp}\n\n${proposal.block}\n\n---\n`;
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
if (!logContent) {
|
|
214
|
+
fs.writeFileSync(logFile, logEntry, 'utf8');
|
|
215
|
+
} else {
|
|
216
|
+
fs.appendFileSync(logFile, logEntry, 'utf8');
|
|
217
|
+
}
|
|
218
|
+
} catch (e) {
|
|
219
|
+
console.error(`Error: Could not write ${logFile}: ${e.message}`);
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
console.log(`✓ Moved signal '${signalKey}' to ${status} in ${path.basename(logFile)}`);
|
|
224
|
+
} finally {
|
|
225
|
+
releaseLock(lockDir);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// === Main ===
|
|
230
|
+
if (command === 'list') {
|
|
231
|
+
listProposals();
|
|
232
|
+
} else if (command === 'accept') {
|
|
233
|
+
moveProposal('ACCEPTED');
|
|
234
|
+
} else if (command === 'reject') {
|
|
235
|
+
moveProposal('REJECTED');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function usage() {
|
|
239
|
+
console.error(`
|
|
240
|
+
Usage:
|
|
241
|
+
node proposals.mjs list [--file <path>]
|
|
242
|
+
node proposals.mjs accept <signal-key> [--file <path>]
|
|
243
|
+
node proposals.mjs reject <signal-key> [--file <path>]
|
|
244
|
+
|
|
245
|
+
Default file: monitor/PROPOSALS.md
|
|
246
|
+
Log file: same directory as PROPOSALS.md, named PROPOSALS-LOG.md
|
|
247
|
+
`);
|
|
248
|
+
}
|