@sushanglewis/lincoln 1.6.2 → 1.7.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.
- package/framework/.claude/agents/pm.md +2 -2
- package/framework/.claude/agents/security-reviewer.md +57 -0
- package/framework/.claude/harnesses/codex.yaml +31 -2
- package/framework/.claude/harnesses/command-map.yaml +4 -0
- package/framework/.claude/harnesses/opencode.yaml +33 -3
- package/framework/.claude/hooks/hooks.yaml +37 -0
- package/framework/.claude/hooks/on-session-start.sh +20 -5
- package/framework/.claude/hooks/on-stop.sh +22 -0
- package/framework/.claude/hooks/pre-tool-use.sh +22 -0
- package/framework/.claude/mcp/mcp.yaml +34 -0
- package/framework/.claude/policies/friction.yaml +18 -0
- package/framework/.claude/policies/security.yaml +46 -0
- package/framework/.claude/schemas/design-review-feedback.json +32 -0
- package/framework/.claude/schemas/stage-definition.schema.json +22 -0
- package/framework/.claude/skills/clarify-requirements/SKILL.md +1 -0
- package/framework/.claude/skills/clarify-requirements/prompts/main.md +83 -80
- package/framework/.claude/skills/draft-product-design/prompts/main.md +27 -14
- package/framework/.claude/stages/clarify.yaml +7 -5
- package/framework/.claude/stages/phase-1-p0-design.yaml +44 -0
- package/framework/.claude/stages/phase-1-p0-implement.yaml +47 -0
- package/framework/.claude/stages/phase-2-p1-design.yaml +53 -0
- package/framework/.claude/stages/phase-2-p1-implement.yaml +45 -0
- package/framework/.claude/stages/product-design-docs.yaml +6 -6
- package/framework/.claude/workflows/framework-self-improvement.yaml +100 -0
- package/framework/.claude/workflows/interview-to-knowledge.yaml +1 -0
- package/framework/.claude-plugin/marketplace.json +2 -2
- package/framework/.claude-plugin/plugin.json +1 -1
- package/framework/.version-bump.json +1 -1
- package/framework/README.en.md +24 -5
- package/framework/README.md +24 -5
- package/framework/RELEASE.md +41 -0
- package/framework/SKILL.md +15 -6
- package/framework/manifest.json +96 -56
- package/framework/scripts/check-harness-drift.sh +11 -1
- package/framework/scripts/lincoln-setup.py +17 -0
- package/framework/scripts/lincoln_condenser.py +110 -0
- package/framework/scripts/lincoln_friction.py +266 -0
- package/framework/scripts/lincoln_harness_adapter.py +185 -13
- package/framework/scripts/lincoln_mr_mine.py +146 -0
- package/framework/scripts/lincoln_recall.py +142 -0
- package/framework/scripts/lincoln_render.py +209 -6
- package/framework/scripts/lincoln_trace.py +119 -3
- package/framework/scripts/security_analyzer.py +234 -0
- package/framework/scripts/stage_loader.py +124 -1
- package/framework/tools/lincoln/package-lock.json +2 -2
- package/framework/tools/lincoln/package.json +1 -1
- package/framework/tools/lincoln-installer/package.json +6 -1
- package/package.json +6 -1
- package/framework/.claude/agents/external/everything-claude-code/agents/ecc-tdd-guide.md +0 -108
- package/framework/.claude/agents/external/oh-my-claudecode/agents/omc-architect.md +0 -134
- package/framework/.claude/agents/external/oh-my-claudecode/agents/omc-code-reviewer.md +0 -248
- package/framework/.claude/agents/external/oh-my-claudecode/agents/omc-designer.md +0 -122
- package/framework/.claude/agents/external/oh-my-claudecode/agents/omc-qa-tester.md +0 -106
- package/framework/.claude/agents/external/wshobson-agents/agents/wshobson-frontend-developer.md +0 -173
|
@@ -583,6 +583,20 @@ def _add_common_args(p: argparse.ArgumentParser) -> None:
|
|
|
583
583
|
)
|
|
584
584
|
|
|
585
585
|
|
|
586
|
+
|
|
587
|
+
def _load_dotenv(root: Path) -> None:
|
|
588
|
+
"""Load environment variables from project root .env if python-dotenv is available."""
|
|
589
|
+
env_file = root / ".env"
|
|
590
|
+
if not env_file.exists():
|
|
591
|
+
return
|
|
592
|
+
try:
|
|
593
|
+
from dotenv import load_dotenv
|
|
594
|
+
|
|
595
|
+
load_dotenv(dotenv_path=env_file, override=False, verbose=False)
|
|
596
|
+
except ImportError:
|
|
597
|
+
# python-dotenv is optional; callers can export vars manually.
|
|
598
|
+
pass
|
|
599
|
+
|
|
586
600
|
def main(args: list[str] | None = None) -> int:
|
|
587
601
|
parser = argparse.ArgumentParser(
|
|
588
602
|
prog="lc-setup",
|
|
@@ -632,6 +646,9 @@ def main(args: list[str] | None = None) -> int:
|
|
|
632
646
|
|
|
633
647
|
parsed = parser.parse_args(args)
|
|
634
648
|
|
|
649
|
+
# Make .env variables available to install commands and harness adapters.
|
|
650
|
+
_load_dotenv(Path(parsed.root).resolve())
|
|
651
|
+
|
|
635
652
|
handlers = {
|
|
636
653
|
"check": run_check,
|
|
637
654
|
"install-skills": run_install_skills,
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Lincoln trace condenser.
|
|
3
|
+
|
|
4
|
+
Summarizes a window of trace events when the count exceeds a threshold. The
|
|
5
|
+
summary is appended as a `condensation` event to the trace and returned to the
|
|
6
|
+
caller (stage_loader handoff-report) for inclusion in handoff frontmatter.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
from collections import Counter
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
_PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
19
|
+
if str(_PROJECT_ROOT) not in sys.path:
|
|
20
|
+
sys.path.insert(0, str(_PROJECT_ROOT))
|
|
21
|
+
|
|
22
|
+
from scripts import lincoln_trace
|
|
23
|
+
|
|
24
|
+
DEFAULT_THRESHOLD = 200
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def load_events(trace_file: Path, max_events: int = 10000) -> list[dict[str, Any]]:
|
|
28
|
+
if not trace_file.exists():
|
|
29
|
+
return []
|
|
30
|
+
try:
|
|
31
|
+
with open(trace_file, "r", encoding="utf-8") as fh:
|
|
32
|
+
lines = fh.readlines()
|
|
33
|
+
return [json.loads(line) for line in lines[-max_events:] if line.strip()]
|
|
34
|
+
except Exception:
|
|
35
|
+
return []
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def summarize(events: list[dict[str, Any]]) -> dict[str, Any]:
|
|
39
|
+
categories = Counter(e.get("category", "other") for e in events)
|
|
40
|
+
tools = Counter(e.get("tool", "") for e in events if e.get("category") == "tool")
|
|
41
|
+
stages = Counter(e.get("stage", "unknown") for e in events)
|
|
42
|
+
failed = sum(1 for e in events if e.get("exit_code", 0) != 0)
|
|
43
|
+
blocked = sum(
|
|
44
|
+
1 for e in events if e.get("args_summary", {}).get("blocked") is True
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
"event_count": len(events),
|
|
49
|
+
"categories": dict(categories.most_common(10)),
|
|
50
|
+
"top_tools": dict(tools.most_common(10)),
|
|
51
|
+
"stages": dict(stages.most_common(10)),
|
|
52
|
+
"failed_count": failed,
|
|
53
|
+
"blocked_count": blocked,
|
|
54
|
+
"first_timestamp": events[0].get("timestamp") if events else None,
|
|
55
|
+
"last_timestamp": events[-1].get("timestamp") if events else None,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def condense(
|
|
60
|
+
trace_file: Path,
|
|
61
|
+
threshold: int = DEFAULT_THRESHOLD,
|
|
62
|
+
dry_run: bool = False,
|
|
63
|
+
) -> dict[str, Any] | None:
|
|
64
|
+
"""Condense trace events if threshold is exceeded.
|
|
65
|
+
|
|
66
|
+
Returns the condensation summary or None when below threshold.
|
|
67
|
+
"""
|
|
68
|
+
events = load_events(trace_file)
|
|
69
|
+
if len(events) < threshold:
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
summary = summarize(events)
|
|
73
|
+
if not dry_run:
|
|
74
|
+
try:
|
|
75
|
+
lincoln_trace.append_event_to_file(
|
|
76
|
+
trace_file=trace_file,
|
|
77
|
+
category="condensation",
|
|
78
|
+
payload={
|
|
79
|
+
"event_type": "condensation",
|
|
80
|
+
"event_count": summary["event_count"],
|
|
81
|
+
"threshold": threshold,
|
|
82
|
+
"summary": summary,
|
|
83
|
+
},
|
|
84
|
+
run_id=events[-1].get("run_id") if events else None,
|
|
85
|
+
stage=events[-1].get("stage") if events else None,
|
|
86
|
+
node_id=events[-1].get("node_id") if events else None,
|
|
87
|
+
)
|
|
88
|
+
except Exception:
|
|
89
|
+
# Condensation must never block handoff report generation.
|
|
90
|
+
pass
|
|
91
|
+
return summary
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def main(argv: list[str] | None = None) -> int:
|
|
95
|
+
parser = argparse.ArgumentParser(description="Condense Lincoln trace events")
|
|
96
|
+
parser.add_argument("--trace-file", type=Path, required=True)
|
|
97
|
+
parser.add_argument("--threshold", type=int, default=DEFAULT_THRESHOLD)
|
|
98
|
+
parser.add_argument("--dry-run", action="store_true", default=False)
|
|
99
|
+
args = parser.parse_args(argv)
|
|
100
|
+
|
|
101
|
+
result = condense(args.trace_file, threshold=args.threshold, dry_run=args.dry_run)
|
|
102
|
+
if result is None:
|
|
103
|
+
print(json.dumps({"condensed": False}, ensure_ascii=False))
|
|
104
|
+
else:
|
|
105
|
+
print(json.dumps({"condensed": True, "summary": result}, ensure_ascii=False, indent=2))
|
|
106
|
+
return 0
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if __name__ == "__main__":
|
|
110
|
+
sys.exit(main())
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Lincoln friction scorer.
|
|
3
|
+
|
|
4
|
+
Reads the session trace for the current node, computes a friction score based
|
|
5
|
+
on configured signals, and writes a suggestion/prompt file when thresholds are
|
|
6
|
+
met. Designed to be invoked asynchronously from on-stop.sh.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
import yaml
|
|
20
|
+
|
|
21
|
+
DEFAULT_POLICY_PATH = ".claude/policies/friction.yaml"
|
|
22
|
+
DEFAULT_TRACE_TAIL_LINES = 500
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def load_policy(root: Path) -> dict[str, Any]:
|
|
26
|
+
"""Load the friction policy YAML, returning defaults if missing."""
|
|
27
|
+
path = root / DEFAULT_POLICY_PATH
|
|
28
|
+
if not path.exists():
|
|
29
|
+
return _default_policy()
|
|
30
|
+
return yaml.safe_load(path.read_text(encoding="utf-8")) or _default_policy()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _default_policy() -> dict[str, Any]:
|
|
34
|
+
return {
|
|
35
|
+
"schema_version": "1.0.0",
|
|
36
|
+
"thresholds": {"prompt_user": 5, "auto_suggest": 3},
|
|
37
|
+
"weights": {
|
|
38
|
+
"tool_failed": 2,
|
|
39
|
+
"tool_rejected": 3,
|
|
40
|
+
"retry_spike": 3,
|
|
41
|
+
"human_override": 5,
|
|
42
|
+
"long_session": 1,
|
|
43
|
+
},
|
|
44
|
+
"long_session_minutes": 30,
|
|
45
|
+
"retry_count_threshold": 3,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _tail_jsonl(trace_file: Path, lines: int) -> list[dict[str, Any]]:
|
|
50
|
+
if not trace_file.exists():
|
|
51
|
+
return []
|
|
52
|
+
try:
|
|
53
|
+
with open(trace_file, "r", encoding="utf-8") as fh:
|
|
54
|
+
all_lines = fh.readlines()
|
|
55
|
+
return [json.loads(line) for line in all_lines[-lines:] if line.strip()]
|
|
56
|
+
except Exception:
|
|
57
|
+
return []
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _parse_timestamp(ts: str | None) -> datetime | None:
|
|
61
|
+
if not ts:
|
|
62
|
+
return None
|
|
63
|
+
try:
|
|
64
|
+
return datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
|
65
|
+
except ValueError:
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _compute_long_session_minutes(events: list[dict[str, Any]], threshold_minutes: int) -> float:
|
|
70
|
+
if len(events) < 2 or threshold_minutes <= 0:
|
|
71
|
+
return 0.0
|
|
72
|
+
timestamps = [t for t in (_parse_timestamp(e.get("timestamp")) for e in events) if t]
|
|
73
|
+
if len(timestamps) < 2:
|
|
74
|
+
return 0.0
|
|
75
|
+
duration = (max(timestamps) - min(timestamps)).total_seconds() / 60.0
|
|
76
|
+
return duration if duration >= threshold_minutes else 0.0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _count_tool_failures(events: list[dict[str, Any]]) -> int:
|
|
80
|
+
return sum(
|
|
81
|
+
1
|
|
82
|
+
for e in events
|
|
83
|
+
if e.get("category") == "tool" and e.get("exit_code", 0) != 0
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _count_tool_rejected(events: list[dict[str, Any]]) -> int:
|
|
88
|
+
return sum(
|
|
89
|
+
1
|
|
90
|
+
for e in events
|
|
91
|
+
if e.get("category") == "tool" and e.get("args_summary", {}).get("blocked") is True
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _count_retry_spikes(events: list[dict[str, Any]], threshold: int) -> int:
|
|
96
|
+
"""Count tools that consecutively failed >= threshold times in this window."""
|
|
97
|
+
if threshold <= 0:
|
|
98
|
+
return 0
|
|
99
|
+
counts: dict[str, int] = {}
|
|
100
|
+
spikes = 0
|
|
101
|
+
for e in events:
|
|
102
|
+
if e.get("category") != "tool":
|
|
103
|
+
continue
|
|
104
|
+
tool = e.get("tool", "")
|
|
105
|
+
failed = e.get("exit_code", 0) != 0 or e.get("args_summary", {}).get("blocked") is True
|
|
106
|
+
if failed:
|
|
107
|
+
counts[tool] = counts.get(tool, 0) + 1
|
|
108
|
+
if counts[tool] == threshold:
|
|
109
|
+
spikes += 1
|
|
110
|
+
else:
|
|
111
|
+
counts[tool] = 0
|
|
112
|
+
return spikes
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def compute_friction(
|
|
116
|
+
events: list[dict[str, Any]],
|
|
117
|
+
policy: dict[str, Any],
|
|
118
|
+
) -> dict[str, Any]:
|
|
119
|
+
"""Compute friction score and signals from a window of trace events."""
|
|
120
|
+
weights = policy.get("weights", {})
|
|
121
|
+
retry_threshold = policy.get("retry_count_threshold", 3)
|
|
122
|
+
long_session_minutes = policy.get("long_session_minutes", 30)
|
|
123
|
+
|
|
124
|
+
tool_failed = _count_tool_failures(events)
|
|
125
|
+
tool_rejected = _count_tool_rejected(events)
|
|
126
|
+
retry_spike = _count_retry_spikes(events, retry_threshold)
|
|
127
|
+
long_session = _compute_long_session_minutes(events, long_session_minutes)
|
|
128
|
+
|
|
129
|
+
signals: dict[str, Any] = {
|
|
130
|
+
"tool_failed": {"count": tool_failed, "weight": weights.get("tool_failed", 0)},
|
|
131
|
+
"tool_rejected": {"count": tool_rejected, "weight": weights.get("tool_rejected", 0)},
|
|
132
|
+
"retry_spike": {"count": retry_spike, "weight": weights.get("retry_spike", 0)},
|
|
133
|
+
"human_override": {"count": 0, "weight": weights.get("human_override", 0)},
|
|
134
|
+
"long_session": {
|
|
135
|
+
"minutes": round(long_session, 2),
|
|
136
|
+
"weight": weights.get("long_session", 0),
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
score = (
|
|
141
|
+
tool_failed * signals["tool_failed"]["weight"]
|
|
142
|
+
+ tool_rejected * signals["tool_rejected"]["weight"]
|
|
143
|
+
+ retry_spike * signals["retry_spike"]["weight"]
|
|
144
|
+
+ signals["human_override"]["count"] * signals["human_override"]["weight"]
|
|
145
|
+
+ (1 if long_session >= long_session_minutes else 0) * signals["long_session"]["weight"]
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
"score": score,
|
|
150
|
+
"signals": signals,
|
|
151
|
+
"event_count": len(events),
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def resolve_trace_file(state_path: Path | None) -> Path | None:
|
|
156
|
+
"""Resolve the trace file from workflow state or environment override."""
|
|
157
|
+
env_file = os.environ.get("LINCOLN_TRACE_FILE")
|
|
158
|
+
if env_file:
|
|
159
|
+
return Path(env_file)
|
|
160
|
+
|
|
161
|
+
if state_path is None:
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
sys.path.insert(0, str(state_path.resolve().parents[1]))
|
|
166
|
+
from scripts.lincoln_paths import get_process_slug, process_package_root
|
|
167
|
+
from scripts.lincoln_trace import _load_state
|
|
168
|
+
|
|
169
|
+
state = _load_state(state_path)
|
|
170
|
+
if state is None:
|
|
171
|
+
return state_path.parent / ".trace" / "lc-trace.jsonl"
|
|
172
|
+
slug = get_process_slug(state, state_path)
|
|
173
|
+
root = process_package_root(slug, state, state_path)
|
|
174
|
+
return root / ".trace" / "lc-trace.jsonl"
|
|
175
|
+
except Exception:
|
|
176
|
+
return state_path.parent / ".trace" / "lc-trace.jsonl"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def write_friction_report(
|
|
180
|
+
trace_dir: Path,
|
|
181
|
+
score: int,
|
|
182
|
+
signals: dict[str, Any],
|
|
183
|
+
policy: dict[str, Any],
|
|
184
|
+
) -> Path | None:
|
|
185
|
+
"""Write a friction suggestion or prompt file when thresholds are met."""
|
|
186
|
+
thresholds = policy.get("thresholds", {})
|
|
187
|
+
prompt_threshold = thresholds.get("prompt_user", 5)
|
|
188
|
+
suggest_threshold = thresholds.get("auto_suggest", 3)
|
|
189
|
+
|
|
190
|
+
if score >= prompt_threshold:
|
|
191
|
+
path = trace_dir / "friction-prompt.md"
|
|
192
|
+
kind = "prompt"
|
|
193
|
+
elif score >= suggest_threshold:
|
|
194
|
+
path = trace_dir / "friction-suggestion.md"
|
|
195
|
+
kind = "suggestion"
|
|
196
|
+
else:
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
active_signals = [
|
|
200
|
+
f"- **{name}**: {info.get('count', info.get('minutes', 0))} (weight {info['weight']})"
|
|
201
|
+
for name, info in signals.items()
|
|
202
|
+
if (info.get("count", 0) > 0 or info.get("minutes", 0) > 0)
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
content = f"""# Friction Report ({kind})
|
|
206
|
+
|
|
207
|
+
Generated at: {datetime.now(timezone.utc).isoformat()}
|
|
208
|
+
Score: **{score}**
|
|
209
|
+
|
|
210
|
+
## Detected signals
|
|
211
|
+
|
|
212
|
+
{chr(10).join(active_signals) if active_signals else "_No strong friction signals detected._"}
|
|
213
|
+
|
|
214
|
+
## Suggested action
|
|
215
|
+
|
|
216
|
+
Consider capturing a lesson learned in `knowledge/05-learnings/` or running:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
python3 scripts/lincoln-knowledge.py suggest --stage <stage_id>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
This is a suggestion only; the human PM decides whether to persist anything.
|
|
223
|
+
"""
|
|
224
|
+
path.write_text(content, encoding="utf-8")
|
|
225
|
+
return path
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def score_friction(
|
|
229
|
+
root: Path,
|
|
230
|
+
state_path: Path | None = None,
|
|
231
|
+
trace_file: Path | None = None,
|
|
232
|
+
tail_lines: int = DEFAULT_TRACE_TAIL_LINES,
|
|
233
|
+
) -> dict[str, Any]:
|
|
234
|
+
"""Score friction and write a report if thresholds are met."""
|
|
235
|
+
policy = load_policy(root)
|
|
236
|
+
target_trace = trace_file or resolve_trace_file(state_path)
|
|
237
|
+
if target_trace is None:
|
|
238
|
+
return {"score": 0, "signals": {}, "report": None, "error": "no trace file"}
|
|
239
|
+
|
|
240
|
+
events = _tail_jsonl(target_trace, tail_lines)
|
|
241
|
+
result = compute_friction(events, policy)
|
|
242
|
+
report = write_friction_report(target_trace.parent, result["score"], result["signals"], policy)
|
|
243
|
+
result["report"] = str(report) if report else None
|
|
244
|
+
return result
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def main(argv: list[str] | None = None) -> int:
|
|
248
|
+
parser = argparse.ArgumentParser(description="Score session friction from trace events")
|
|
249
|
+
parser.add_argument("--root", type=Path, default=Path.cwd())
|
|
250
|
+
parser.add_argument("--state-file", type=Path, default=None)
|
|
251
|
+
parser.add_argument("--trace-file", type=Path, default=None)
|
|
252
|
+
parser.add_argument("--tail-lines", type=int, default=DEFAULT_TRACE_TAIL_LINES)
|
|
253
|
+
args = parser.parse_args(argv)
|
|
254
|
+
|
|
255
|
+
result = score_friction(
|
|
256
|
+
root=args.root,
|
|
257
|
+
state_path=args.state_file,
|
|
258
|
+
trace_file=args.trace_file,
|
|
259
|
+
tail_lines=args.tail_lines,
|
|
260
|
+
)
|
|
261
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
262
|
+
return 0
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
if __name__ == "__main__":
|
|
266
|
+
sys.exit(main())
|