@sushanglewis/lincoln 1.6.3 → 1.8.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/build-product-prototype/prompts/main.md +6 -1
- package/framework/.claude/skills/clarify-requirements/SKILL.md +1 -0
- package/framework/.claude/skills/clarify-requirements/prompts/main.md +50 -10
- package/framework/.claude/skills/draft-product-design/SKILL.md +1 -0
- package/framework/.claude/skills/draft-product-design/prompts/main.md +26 -3
- package/framework/.claude/skills/lc-workflow/validators/validate.py +1 -0
- package/framework/.claude/stages/clarify.yaml +3 -1
- 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 +2 -0
- package/framework/.claude/workflows/bug-fix.yaml +1 -0
- package/framework/.claude/workflows/design-spike.yaml +1 -0
- package/framework/.claude/workflows/existing-project-iteration.yaml +1 -0
- package/framework/.claude/workflows/framework-self-improvement.yaml +100 -0
- package/framework/.claude/workflows/interview-to-knowledge.yaml +2 -0
- package/framework/.claude/workflows/oss-first-design.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 +43 -6
- package/framework/README.md +43 -6
- package/framework/RELEASE.md +42 -0
- package/framework/SKILL.md +15 -6
- package/framework/manifest.json +103 -47
- 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_index.py +8 -1
- package/framework/scripts/lincoln_mr_mine.py +146 -0
- package/framework/scripts/lincoln_recall.py +142 -0
- 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
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Risk analyzer for Lincoln pre-tool-use security gate.
|
|
3
|
+
|
|
4
|
+
Loads `.claude/policies/security.yaml` and evaluates a tool call against the
|
|
5
|
+
configured policies. Designed to be invoked from `.claude/hooks/pre-tool-use.sh`
|
|
6
|
+
and from generated harness-specific hook scripts.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import yaml
|
|
19
|
+
|
|
20
|
+
DEFAULT_POLICY_PATH = ".claude/policies/security.yaml"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def load_policy(root: Path) -> dict[str, Any]:
|
|
24
|
+
"""Load the risk policy YAML, returning an empty policy if missing."""
|
|
25
|
+
path = root / DEFAULT_POLICY_PATH
|
|
26
|
+
if not path.exists():
|
|
27
|
+
return {"policies": []}
|
|
28
|
+
return yaml.safe_load(path.read_text(encoding="utf-8")) or {"policies": []}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _normalize_command(command: str) -> str:
|
|
32
|
+
"""Strip leading whitespace and collapse internal spaces for matching."""
|
|
33
|
+
return re.sub(r"\s+", " ", command.strip())
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _split_command_segments(command: str) -> list[str]:
|
|
37
|
+
"""Split a shell command on chain operators so each segment is matched.
|
|
38
|
+
|
|
39
|
+
Handles `&&`, `||`, `;`, and `|`.
|
|
40
|
+
"""
|
|
41
|
+
segments = re.split(r"\s*(&&|\|\||;|\|)\s*", command)
|
|
42
|
+
return [_normalize_command(seg) for seg in segments if seg and not re.match(r"^(&&|\|\||;|\|)$", seg.strip())]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _strip_command_prefix(segment: str) -> str:
|
|
46
|
+
"""Remove common leading prefixes that should not hide the real command.
|
|
47
|
+
|
|
48
|
+
Examples: `sudo rm foo`, `env VAR=1 rm foo`, `cd /tmp && rm foo`.
|
|
49
|
+
"""
|
|
50
|
+
segment = _normalize_command(segment)
|
|
51
|
+
while re.match(r"^[A-Za-z_][A-Za-z0-9_]*=\S+\s+", segment):
|
|
52
|
+
segment = _normalize_command(re.sub(r"^[A-Za-z_][A-Za-z0-9_]*=\S+\s+", "", segment))
|
|
53
|
+
if re.match(r"^(sudo|doas)\s+", segment):
|
|
54
|
+
segment = _normalize_command(re.sub(r"^(sudo|doas)\s+", "", segment))
|
|
55
|
+
if re.match(r"^cd\s+\S+\s*(&&|;|\|)", segment):
|
|
56
|
+
segment = _normalize_command(re.sub(r"^cd\s+\S+\s*(&&|;|\|)", "", segment))
|
|
57
|
+
return segment
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _command_matches(condition: str, command: str) -> bool:
|
|
61
|
+
"""Evaluate a human-readable condition against a bash command."""
|
|
62
|
+
lowered = condition.lower()
|
|
63
|
+
segments = _split_command_segments(command)
|
|
64
|
+
|
|
65
|
+
def _any_segment(pattern: str) -> bool:
|
|
66
|
+
return any(re.search(pattern, _strip_command_prefix(seg)) for seg in segments)
|
|
67
|
+
|
|
68
|
+
if "rm -rf" in lowered or 'command matches "rm -rf"' in lowered:
|
|
69
|
+
if _any_segment(r"\brm\s+-rf\b"):
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
if 'command matches "rm -r" or "rm -rf"' in lowered:
|
|
73
|
+
if _any_segment(r"\brm\s+-r(?:f)?\b"):
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
if 'command matches any of ["rm", "rmdir"]' in lowered:
|
|
77
|
+
if _any_segment(r"(?:^|\s)(?:rm\b|rmdir\b)"):
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
if 'command matches "git push"' in lowered:
|
|
81
|
+
if _any_segment(r"\bgit\s+push\b"):
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
if "external http client" in lowered or "curl, wget" in lowered:
|
|
85
|
+
if _any_segment(r"\b(?:curl|wget|httpie)\b"):
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
if 'command matches "rm"' in lowered and "rmdir" not in lowered:
|
|
89
|
+
if _any_segment(r"\brm\b"):
|
|
90
|
+
return True
|
|
91
|
+
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _resolve_target_path(target: str, root: Path) -> Path:
|
|
96
|
+
"""Resolve a Write/Edit target to an absolute, canonical path."""
|
|
97
|
+
path = Path(target)
|
|
98
|
+
if not path.is_absolute():
|
|
99
|
+
path = root / path
|
|
100
|
+
try:
|
|
101
|
+
return path.resolve()
|
|
102
|
+
except (OSError, ValueError):
|
|
103
|
+
return path.absolute()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _target_matches(condition: str, target: str, process_slug: str, root: Path) -> bool:
|
|
107
|
+
"""Evaluate a file-target condition using canonical absolute paths."""
|
|
108
|
+
lowered = condition.lower()
|
|
109
|
+
resolved = _resolve_target_path(target, root)
|
|
110
|
+
try:
|
|
111
|
+
package_root = (root / process_slug).resolve()
|
|
112
|
+
except (OSError, ValueError):
|
|
113
|
+
package_root = (root / process_slug).absolute()
|
|
114
|
+
|
|
115
|
+
if "outside current process_slug" in lowered:
|
|
116
|
+
try:
|
|
117
|
+
resolved.relative_to(package_root)
|
|
118
|
+
return False
|
|
119
|
+
except ValueError:
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
if "inside current process_slug" in lowered:
|
|
123
|
+
try:
|
|
124
|
+
resolved.relative_to(package_root)
|
|
125
|
+
return True
|
|
126
|
+
except ValueError:
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _evaluate_policy(policy: dict[str, Any], tool_name: str, tool_args: dict[str, Any], process_slug: str, root: Path) -> bool:
|
|
133
|
+
"""Return True if the policy applies to this tool call."""
|
|
134
|
+
tools = [t.strip() for t in policy.get("tool", "").split("|")]
|
|
135
|
+
if tool_name not in tools:
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
condition = policy.get("condition", "")
|
|
139
|
+
if tool_name == "Bash":
|
|
140
|
+
command = tool_args.get("command", "")
|
|
141
|
+
return _command_matches(condition, command)
|
|
142
|
+
|
|
143
|
+
if tool_name in ("Write", "Edit"):
|
|
144
|
+
target = tool_args.get("file_path") or tool_args.get("path") or ""
|
|
145
|
+
return _target_matches(condition, target, process_slug, root)
|
|
146
|
+
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _log_security_event(
|
|
151
|
+
log_dir: Path,
|
|
152
|
+
tool_name: str,
|
|
153
|
+
tool_args: dict[str, Any],
|
|
154
|
+
result: dict[str, Any],
|
|
155
|
+
) -> None:
|
|
156
|
+
"""Append a JSON line to issue-<N>/logs/security.log."""
|
|
157
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
158
|
+
log_file = log_dir / "security.log"
|
|
159
|
+
entry = {
|
|
160
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
161
|
+
"tool": tool_name,
|
|
162
|
+
"level": result["level"],
|
|
163
|
+
"policy": result.get("policy", ""),
|
|
164
|
+
"action": "blocked" if result["confirm_required"] else "allowed",
|
|
165
|
+
"args": {k: v for k, v in tool_args.items() if k in ("command", "file_path", "path")},
|
|
166
|
+
}
|
|
167
|
+
with log_file.open("a", encoding="utf-8") as f:
|
|
168
|
+
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def analyze(
|
|
172
|
+
root: Path,
|
|
173
|
+
tool_name: str,
|
|
174
|
+
tool_args: dict[str, Any],
|
|
175
|
+
process_slug: str = "",
|
|
176
|
+
log_dir: str | None = None,
|
|
177
|
+
) -> dict[str, Any]:
|
|
178
|
+
"""Analyze a tool call and return risk assessment."""
|
|
179
|
+
if os.environ.get("LINCOLN_SECURITY_MODE") == "permissive":
|
|
180
|
+
return {
|
|
181
|
+
"level": "low",
|
|
182
|
+
"policy": "",
|
|
183
|
+
"message": "Security analyzer is in permissive mode.",
|
|
184
|
+
"confirm_required": False,
|
|
185
|
+
"log": False,
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
policy_data = load_policy(root)
|
|
189
|
+
policies = policy_data.get("policies", [])
|
|
190
|
+
|
|
191
|
+
for policy in policies:
|
|
192
|
+
if not _evaluate_policy(policy, tool_name, tool_args, process_slug, root):
|
|
193
|
+
continue
|
|
194
|
+
|
|
195
|
+
result = {
|
|
196
|
+
"level": policy.get("level", "unknown"),
|
|
197
|
+
"policy": policy.get("name", ""),
|
|
198
|
+
"message": policy.get("message", ""),
|
|
199
|
+
"confirm_required": bool(policy.get("confirm", False)),
|
|
200
|
+
"log": bool(policy.get("log", False)),
|
|
201
|
+
}
|
|
202
|
+
if log_dir:
|
|
203
|
+
_log_security_event(Path(log_dir), tool_name, tool_args, result)
|
|
204
|
+
return result
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
"level": "low",
|
|
208
|
+
"policy": "",
|
|
209
|
+
"message": "",
|
|
210
|
+
"confirm_required": False,
|
|
211
|
+
"log": False,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
if __name__ == "__main__":
|
|
216
|
+
import argparse
|
|
217
|
+
|
|
218
|
+
parser = argparse.ArgumentParser(description="Analyze tool call risk")
|
|
219
|
+
parser.add_argument("--root", type=Path, default=Path.cwd())
|
|
220
|
+
parser.add_argument("--tool", required=True)
|
|
221
|
+
parser.add_argument("--args", default="{}")
|
|
222
|
+
parser.add_argument("--process-slug", default="")
|
|
223
|
+
parser.add_argument("--log-dir", default=None)
|
|
224
|
+
args = parser.parse_args()
|
|
225
|
+
|
|
226
|
+
tool_args = json.loads(args.args)
|
|
227
|
+
result = analyze(
|
|
228
|
+
args.root,
|
|
229
|
+
args.tool,
|
|
230
|
+
tool_args,
|
|
231
|
+
process_slug=args.process_slug,
|
|
232
|
+
log_dir=args.log_dir,
|
|
233
|
+
)
|
|
234
|
+
print(json.dumps(result, ensure_ascii=False))
|
|
@@ -36,7 +36,7 @@ if str(PROJECT_ROOT) not in sys.path:
|
|
|
36
36
|
|
|
37
37
|
import yaml
|
|
38
38
|
|
|
39
|
-
from scripts import lincoln_trace
|
|
39
|
+
from scripts import lincoln_condenser, lincoln_trace
|
|
40
40
|
from scripts import lincoln_render
|
|
41
41
|
from scripts.lincoln_documents import write_documents_index
|
|
42
42
|
from scripts.lincoln_index import write_package_index
|
|
@@ -84,6 +84,29 @@ def now_iso() -> str:
|
|
|
84
84
|
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
85
85
|
|
|
86
86
|
|
|
87
|
+
def _record_stage_event(
|
|
88
|
+
state_file: Path | None,
|
|
89
|
+
category: str,
|
|
90
|
+
event_type: str,
|
|
91
|
+
payload: dict[str, Any] | None = None,
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Write a lifecycle or gate event to the session trace.
|
|
94
|
+
|
|
95
|
+
Failures are logged but never block the stage action.
|
|
96
|
+
"""
|
|
97
|
+
try:
|
|
98
|
+
merged_payload = {"event_type": event_type}
|
|
99
|
+
if payload:
|
|
100
|
+
merged_payload.update(payload)
|
|
101
|
+
lincoln_trace.append_event_entry(
|
|
102
|
+
state_path=state_file,
|
|
103
|
+
category=category,
|
|
104
|
+
payload=merged_payload,
|
|
105
|
+
)
|
|
106
|
+
except Exception as exc:
|
|
107
|
+
print(f"Warning: could not record {category}/{event_type} trace event: {exc}")
|
|
108
|
+
|
|
109
|
+
|
|
87
110
|
def load_yaml(path: Path) -> Any:
|
|
88
111
|
if not path.exists():
|
|
89
112
|
raise FileNotFoundError(path)
|
|
@@ -483,6 +506,53 @@ def _check_previous_stage_completed(stage_id: str, prev_stage_id: str, state: di
|
|
|
483
506
|
return False
|
|
484
507
|
|
|
485
508
|
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _check_agent_delegation(stage_id: str) -> bool:
|
|
512
|
+
"""Validate parallel specialist agent configuration in stage YAML."""
|
|
513
|
+
stage = load_stage_yaml(stage_id)
|
|
514
|
+
agent = stage.get("agent", {})
|
|
515
|
+
specialists = agent.get("parallel_specialists", [])
|
|
516
|
+
if not specialists:
|
|
517
|
+
return True
|
|
518
|
+
|
|
519
|
+
all_valid = True
|
|
520
|
+
for specialist in specialists:
|
|
521
|
+
agent_file = f"{specialist.lstrip('lc-')}.md"
|
|
522
|
+
agent_path = PROJECT_ROOT / ".claude" / "agents" / agent_file
|
|
523
|
+
if not agent_path.exists():
|
|
524
|
+
print(
|
|
525
|
+
f"FAIL: specialist agent '{specialist}' not found at .claude/agents/{agent_file}",
|
|
526
|
+
file=sys.stderr,
|
|
527
|
+
)
|
|
528
|
+
all_valid = False
|
|
529
|
+
else:
|
|
530
|
+
print(f"PASS: specialist agent '{specialist}' found")
|
|
531
|
+
|
|
532
|
+
output_schema = agent.get("output_schema")
|
|
533
|
+
if output_schema:
|
|
534
|
+
schema_path = PROJECT_ROOT / output_schema
|
|
535
|
+
if not schema_path.exists():
|
|
536
|
+
print(f"FAIL: output_schema not found: {output_schema}", file=sys.stderr)
|
|
537
|
+
all_valid = False
|
|
538
|
+
else:
|
|
539
|
+
try:
|
|
540
|
+
json.loads(schema_path.read_text(encoding="utf-8"))
|
|
541
|
+
print(f"PASS: output_schema is valid JSON: {output_schema}")
|
|
542
|
+
except json.JSONDecodeError as exc:
|
|
543
|
+
print(f"FAIL: output_schema is invalid JSON: {exc}", file=sys.stderr)
|
|
544
|
+
all_valid = False
|
|
545
|
+
|
|
546
|
+
merge_strategy = agent.get("merge_strategy", "priority")
|
|
547
|
+
if merge_strategy not in {"priority", "vote", "consensus"}:
|
|
548
|
+
print(f"FAIL: unsupported merge_strategy '{merge_strategy}'", file=sys.stderr)
|
|
549
|
+
all_valid = False
|
|
550
|
+
else:
|
|
551
|
+
print(f"PASS: merge_strategy '{merge_strategy}' supported")
|
|
552
|
+
|
|
553
|
+
return all_valid
|
|
554
|
+
|
|
555
|
+
|
|
486
556
|
def _run_gate_check(
|
|
487
557
|
phase: str,
|
|
488
558
|
check: dict[str, Any],
|
|
@@ -564,6 +634,10 @@ def action_validate(
|
|
|
564
634
|
if not passed:
|
|
565
635
|
all_passed = False
|
|
566
636
|
|
|
637
|
+
if phase == "entry":
|
|
638
|
+
if not _check_agent_delegation(stage_id):
|
|
639
|
+
all_passed = False
|
|
640
|
+
|
|
567
641
|
if not all_passed:
|
|
568
642
|
if _is_legacy_state(state):
|
|
569
643
|
stage_state["status"] = "validation_failed"
|
|
@@ -638,6 +712,13 @@ def action_record_artifacts(
|
|
|
638
712
|
print(f"Recorded {len(recorded)} artifacts for stage '{stage_id}'")
|
|
639
713
|
for art in recorded:
|
|
640
714
|
print(f" - {art}")
|
|
715
|
+
|
|
716
|
+
_record_stage_event(
|
|
717
|
+
state_file,
|
|
718
|
+
"stage_lifecycle",
|
|
719
|
+
"artifacts_recorded",
|
|
720
|
+
{"stage_id": stage_id, "artifact_count": len(recorded), "artifacts": recorded},
|
|
721
|
+
)
|
|
641
722
|
return recorded
|
|
642
723
|
|
|
643
724
|
|
|
@@ -677,6 +758,13 @@ def action_approve_gate(
|
|
|
677
758
|
|
|
678
759
|
save_state(state, state_file)
|
|
679
760
|
print(f"PASS: gate approved for stage '{stage_id}' by '{approved_by}'")
|
|
761
|
+
|
|
762
|
+
_record_stage_event(
|
|
763
|
+
state_file,
|
|
764
|
+
"gate",
|
|
765
|
+
"gate_approved",
|
|
766
|
+
{"stage_id": stage_id, "approved_by": approved_by},
|
|
767
|
+
)
|
|
680
768
|
return 0
|
|
681
769
|
|
|
682
770
|
|
|
@@ -799,6 +887,27 @@ def action_transition_next(stage_id: str, state: dict[str, Any], state_file: Pat
|
|
|
799
887
|
|
|
800
888
|
save_state(state, state_file)
|
|
801
889
|
print(f"Transitioned from '{stage_id}' to '{next_stage_id}'")
|
|
890
|
+
|
|
891
|
+
_record_stage_event(
|
|
892
|
+
state_file,
|
|
893
|
+
"stage_lifecycle",
|
|
894
|
+
"stage_completed",
|
|
895
|
+
{"stage_id": stage_id, "next_stage_id": next_stage_id},
|
|
896
|
+
)
|
|
897
|
+
if next_stage_id:
|
|
898
|
+
_record_stage_event(
|
|
899
|
+
state_file,
|
|
900
|
+
"stage_lifecycle",
|
|
901
|
+
"stage_started",
|
|
902
|
+
{"stage_id": next_stage_id, "previous_stage_id": stage_id},
|
|
903
|
+
)
|
|
904
|
+
else:
|
|
905
|
+
_record_stage_event(
|
|
906
|
+
state_file,
|
|
907
|
+
"session",
|
|
908
|
+
"workflow_completed",
|
|
909
|
+
{"final_stage_id": stage_id},
|
|
910
|
+
)
|
|
802
911
|
return next_stage_id
|
|
803
912
|
|
|
804
913
|
|
|
@@ -900,6 +1009,20 @@ def action_handoff_report(stage_id: str, state: dict[str, Any], state_file: Path
|
|
|
900
1009
|
else:
|
|
901
1010
|
lines.append(f"- **Status:** {stage_state.get('status', state.get('current_run', {}).get('status', 'unknown'))}")
|
|
902
1011
|
lines.append("")
|
|
1012
|
+
|
|
1013
|
+
# Condense large traces as a handoff sub-step (OpenHands-style context pressure management).
|
|
1014
|
+
try:
|
|
1015
|
+
trace_file = lincoln_trace.get_trace_file(state, state_file)
|
|
1016
|
+
condensation = lincoln_condenser.condense(trace_file)
|
|
1017
|
+
if condensation:
|
|
1018
|
+
lines.append("## Trace Condensation")
|
|
1019
|
+
lines.append(f"- **Events summarized:** {condensation['event_count']}")
|
|
1020
|
+
lines.append(f"- **Threshold:** {condensation.get('threshold', 200)}")
|
|
1021
|
+
lines.append(f"- **Top categories:** {', '.join(condensation.get('categories', {}).keys())}")
|
|
1022
|
+
lines.append("")
|
|
1023
|
+
except Exception:
|
|
1024
|
+
# Condensation is advisory; never break handoff report generation.
|
|
1025
|
+
pass
|
|
903
1026
|
lines.append("## Waiting For")
|
|
904
1027
|
lines.append(f"- **Waiting for:** {'human' if stage_def.get('human_gate') and not stage_state.get('gate_passed') else 'agent'}")
|
|
905
1028
|
lines.append("")
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sushanglewis/lincoln",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@sushanglewis/lincoln",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.8.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"ink": "^5.2.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lincoln-install",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Terminal TUI installer for the Lincoln AI-Native R&D workflow system",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/main.js",
|
|
@@ -49,5 +49,10 @@
|
|
|
49
49
|
"claude-code",
|
|
50
50
|
"ai-workflow"
|
|
51
51
|
],
|
|
52
|
+
"repository": {
|
|
53
|
+
"type": "git",
|
|
54
|
+
"url": "https://github.com/sushanglewis/Lincoln",
|
|
55
|
+
"directory": "tools/lincoln-installer"
|
|
56
|
+
},
|
|
52
57
|
"license": "MIT"
|
|
53
58
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sushanglewis/lincoln",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Global Lincoln agent harness plugin CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -50,5 +50,10 @@
|
|
|
50
50
|
"opencode",
|
|
51
51
|
"plugin"
|
|
52
52
|
],
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "https://github.com/sushanglewis/Lincoln",
|
|
56
|
+
"directory": "packages/lincoln"
|
|
57
|
+
},
|
|
53
58
|
"license": "MIT"
|
|
54
59
|
}
|