@research-copilot/plugin 1.1.15 → 1.1.16
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/dist/.claude-plugin/plugin.json +3 -2
- package/dist/.codex-plugin/plugin.toml +2 -1
- package/dist/.cursor-plugin/plugin.json +3 -2
- package/dist/.gemini-plugin/plugin.json +3 -2
- package/dist/.opencode-plugin/plugin.json +3 -2
- package/dist/.windsurf-plugin/plugin.json +3 -2
- package/dist/agents/copilot-conductor.agent.md +60 -0
- package/dist/agents/copilot-experiment.agent.md +56 -0
- package/dist/agents/copilot-ideation.agent.md +45 -0
- package/dist/agents/copilot-literature.agent.md +34 -0
- package/dist/agents/copilot-polisher.agent.md +30 -0
- package/dist/agents/copilot-rebuttal.agent.md +35 -0
- package/dist/agents/copilot-reviewer.agent.md +35 -0
- package/dist/agents/copilot-writer.agent.md +39 -0
- package/dist/hooks/dispatch-reminder.json +17 -0
- package/dist/hooks/loop-armer.json +17 -0
- package/dist/hooks/research-copilot-guard.hook.md +51 -0
- package/dist/hooks/scientist-guardrails.json +17 -0
- package/dist/hooks/scripts/__tests__/__init__.py +0 -0
- package/dist/hooks/scripts/__tests__/test_post_tool_loop_armer.py +88 -0
- package/dist/hooks/scripts/__tests__/test_research_copilot_guard_main_session.py +150 -0
- package/dist/hooks/scripts/__tests__/test_session_start_memory_injector.py +66 -0
- package/dist/hooks/scripts/__tests__/test_user_prompt_dispatch_reminder.py +37 -0
- package/dist/hooks/scripts/_copilot_hook_lib.py +564 -0
- package/dist/hooks/scripts/copilot_subagent_stop.py +203 -0
- package/dist/hooks/scripts/copilot_write_guard.py +96 -0
- package/dist/hooks/scripts/post_tool_loop_armer.py +61 -0
- package/dist/hooks/scripts/research_copilot_guard.py +208 -0
- package/dist/hooks/scripts/scientist_guardrails.py +29 -0
- package/dist/hooks/scripts/session_start_memory_injector.py +188 -0
- package/dist/hooks/scripts/user_prompt_dispatch_reminder.py +40 -0
- package/dist/hooks/session-memory-injector.json +17 -0
- package/dist/hooks/tests/__init__.py +0 -0
- package/dist/hooks/tests/conftest.py +61 -0
- package/dist/hooks/tests/fixtures/transcript_copilot_experiment_complete.jsonl +2 -0
- package/dist/hooks/tests/fixtures/transcript_copilot_experiment_state_jump.jsonl +2 -0
- package/dist/hooks/tests/fixtures/transcript_copilot_literature.jsonl +2 -0
- package/dist/hooks/tests/fixtures/transcript_main_only.jsonl +2 -0
- package/dist/hooks/tests/fixtures/transcript_malformed_state_output.jsonl +2 -0
- package/dist/hooks/tests/integration_run.ps1 +65 -0
- package/dist/hooks/tests/test_copilot_hook_lib.py +398 -0
- package/dist/hooks/tests/test_copilot_subagent_stop.py +186 -0
- package/dist/hooks/tests/test_copilot_write_guard.py +137 -0
- package/dist/hooks/tests/test_session_start_snapshot.py +116 -0
- package/dist/hooks/tests/test_state_machine_consistency.py +75 -0
- package/dist/skills/arxivsub-skill/SKILL.md +98 -0
- package/dist/skills/arxivsub-skill/skill.json +5 -0
- package/dist/skills/de-ai-checker/SKILL.md +110 -0
- package/dist/skills/de-ai-checker/skill.json +5 -0
- package/dist/skills/deep-interview/SKILL.md +91 -0
- package/dist/skills/deep-interview/skill.json +5 -0
- package/dist/skills/grill-with-docs/SKILL.md +120 -0
- package/dist/skills/grill-with-docs/skill.json +5 -0
- package/dist/skills/init-mcp/SKILL.md +83 -0
- package/dist/skills/init-mcp/skill.json +5 -0
- package/dist/skills/model-escalation/SKILL.md +93 -0
- package/dist/skills/model-escalation/skill.json +5 -0
- package/dist/skills/paper-architecture-web-drawing/SKILL.md +282 -0
- package/dist/skills/paper-architecture-web-drawing/skill.json +5 -0
- package/dist/skills/paper-deai/SKILL.md +53 -0
- package/dist/skills/paper-deai/skill.json +5 -0
- package/dist/skills/paper-en2zh/SKILL.md +29 -0
- package/dist/skills/paper-en2zh/skill.json +5 -0
- package/dist/skills/paper-expand/SKILL.md +43 -0
- package/dist/skills/paper-expand/skill.json +5 -0
- package/dist/skills/paper-experiment-analysis/SKILL.md +38 -0
- package/dist/skills/paper-experiment-analysis/skill.json +5 -0
- package/dist/skills/paper-figure-caption/SKILL.md +29 -0
- package/dist/skills/paper-figure-caption/skill.json +5 -0
- package/dist/skills/paper-logic-check/SKILL.md +30 -0
- package/dist/skills/paper-logic-check/skill.json +5 -0
- package/dist/skills/paper-polish/SKILL.md +34 -305
- package/dist/skills/paper-polish/skill.json +5 -0
- package/dist/skills/paper-review/SKILL.md +49 -0
- package/dist/skills/paper-review/skill.json +5 -0
- package/dist/skills/paper-sanity-check/SKILL.md +122 -0
- package/dist/skills/paper-sanity-check/skill.json +5 -0
- package/dist/skills/paper-shorten/SKILL.md +42 -0
- package/dist/skills/paper-shorten/skill.json +5 -0
- package/dist/skills/paper-table-caption/SKILL.md +29 -0
- package/dist/skills/paper-table-caption/skill.json +5 -0
- package/dist/skills/paper-translate/SKILL.md +48 -0
- package/dist/skills/paper-translate/skill.json +5 -0
- package/dist/skills/plugin-dev-agent-development/SKILL.md +95 -0
- package/dist/skills/plugin-dev-agent-development/skill.json +5 -0
- package/dist/skills/research-workflow/SKILL.md +116 -0
- package/dist/skills/research-workflow/skill.json +5 -0
- package/dist/skills/scientist-experiment-runner/SKILL.md +76 -0
- package/dist/skills/scientist-experiment-runner/skill.json +5 -0
- package/dist/skills/scientist-ideation/SKILL.md +52 -0
- package/dist/skills/scientist-ideation/skill.json +5 -0
- package/dist/skills/scientist-plotting/SKILL.md +49 -0
- package/dist/skills/scientist-plotting/skill.json +5 -0
- package/dist/skills/scientist-review/SKILL.md +40 -0
- package/dist/skills/scientist-review/skill.json +5 -0
- package/dist/skills/scientist-runtime-init/SKILL.md +46 -0
- package/dist/skills/scientist-runtime-init/skill.json +5 -0
- package/dist/skills/scientist-writeup/SKILL.md +60 -0
- package/dist/skills/scientist-writeup/skill.json +5 -0
- package/dist/skills/talk-normal/SKILL.md +73 -0
- package/dist/skills/talk-normal/skill.json +5 -0
- package/package.json +1 -1
- package/dist/agents/rc-experiment.md +0 -203
- package/dist/agents/rc-ideation.md +0 -224
- package/dist/agents/rc-literature.md +0 -228
- package/dist/agents/rc-plan.md +0 -189
- package/dist/agents/rc-polisher.md +0 -166
- package/dist/agents/rc-rebuttal.md +0 -194
- package/dist/agents/rc-reviewer.md +0 -187
- package/dist/agents/rc-update-spec.md +0 -231
- package/dist/agents/rc-verify.md +0 -234
- package/dist/agents/rc-writer.md +0 -161
- package/dist/skills/experiment-design/SKILL.md +0 -331
- package/dist/skills/full-research-workflow/SKILL.md +0 -363
- package/dist/skills/literature-search/SKILL.md +0 -244
- package/dist/skills/sanity-check/SKILL.md +0 -449
- package/dist/skills/submission-sprint/SKILL.md +0 -361
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _write_transcript(path: Path, entries: list[dict]) -> None:
|
|
9
|
+
path.write_text("\n".join(json.dumps(e) for e in entries) + "\n", encoding="utf-8")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# ---- M1 delegation gate (main session) ----
|
|
13
|
+
|
|
14
|
+
def test_m1_blocks_main_session_experiment_bash(tmp_path):
|
|
15
|
+
from research_copilot_guard import check_m1_delegation
|
|
16
|
+
msg = check_m1_delegation("Bash", {"command": "python train.py --epochs 3"})
|
|
17
|
+
assert msg is not None and "delegate" in msg.lower()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_m1_allows_read_only_bash(tmp_path):
|
|
21
|
+
from research_copilot_guard import check_m1_delegation
|
|
22
|
+
assert check_m1_delegation("Bash", {"command": "cat results.txt"}) is None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_m1_blocks_main_session_research_mcp(tmp_path):
|
|
26
|
+
from research_copilot_guard import check_m1_delegation
|
|
27
|
+
msg = check_m1_delegation("mcp__arxiv-search__search_arxiv", {"query": "ssms"})
|
|
28
|
+
assert msg is not None and "delegate" in msg.lower()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_m1_blocks_write_to_tex(tmp_path):
|
|
32
|
+
from research_copilot_guard import check_m1_delegation
|
|
33
|
+
msg = check_m1_delegation("Write", {"file_path": "sections/intro.tex"})
|
|
34
|
+
assert msg is not None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_m1_blocks_write_to_ideas(tmp_path):
|
|
38
|
+
from research_copilot_guard import check_m1_delegation
|
|
39
|
+
msg = check_m1_delegation("Edit", {"file_path": ".copilot/ideas.md"})
|
|
40
|
+
assert msg is not None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_m1_allows_write_to_state_md(tmp_path):
|
|
44
|
+
"""Conductor owns state.md / decisions.md — never denied."""
|
|
45
|
+
from research_copilot_guard import check_m1_delegation
|
|
46
|
+
assert check_m1_delegation("Write", {"file_path": ".copilot/state.md"}) is None
|
|
47
|
+
assert check_m1_delegation("Edit", {"file_path": ".copilot/decisions.md"}) is None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_m1_allows_unrelated_write(tmp_path):
|
|
51
|
+
from research_copilot_guard import check_m1_delegation
|
|
52
|
+
assert check_m1_delegation("Write", {"file_path": "notes/scratch.md"}) is None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_m1_allows_lookalike_sections_dir(tmp_path):
|
|
56
|
+
"""A dir merely containing 'sections' as a substring is not delegated."""
|
|
57
|
+
from research_copilot_guard import check_m1_delegation
|
|
58
|
+
assert check_m1_delegation("Write", {"file_path": "my-sections/notes.md"}) is None
|
|
59
|
+
assert check_m1_delegation("Write", {"file_path": "docs/subsections/a.tex"}) is None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_m1_allows_lookalike_references_bib(tmp_path):
|
|
63
|
+
"""'references.bib' must match by segment, not substring."""
|
|
64
|
+
from research_copilot_guard import check_m1_delegation
|
|
65
|
+
assert check_m1_delegation("Write", {"file_path": "old_references.bib"}) is None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_m1_blocks_powershell_experiment(tmp_path):
|
|
69
|
+
from research_copilot_guard import check_m1_delegation
|
|
70
|
+
msg = check_m1_delegation("PowerShell", {"command": "python train.py"})
|
|
71
|
+
assert msg is not None and "delegate" in msg.lower()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_m1_allows_powershell_read_only(tmp_path):
|
|
75
|
+
from research_copilot_guard import check_m1_delegation
|
|
76
|
+
assert check_m1_delegation("PowerShell", {"command": "Get-Content results.txt"}) is None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ---- M2 task-list gate (main session) ----
|
|
80
|
+
|
|
81
|
+
def test_m2_blocks_dispatch_without_taskcreate(tmp_path):
|
|
82
|
+
from research_copilot_guard import check_m2_task_list
|
|
83
|
+
t = tmp_path / "s.jsonl"
|
|
84
|
+
_write_transcript(t, [{"type": "tool_use", "name": "Read",
|
|
85
|
+
"input": {"file_path": ".copilot/state.md"}}])
|
|
86
|
+
msg = check_m2_task_list("Agent", {"subagent_type": "copilot-literature"}, str(t))
|
|
87
|
+
assert msg is not None and "taskcreate" in msg.lower()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_m2_allows_dispatch_with_taskcreate(tmp_path):
|
|
91
|
+
from research_copilot_guard import check_m2_task_list
|
|
92
|
+
t = tmp_path / "s.jsonl"
|
|
93
|
+
_write_transcript(t, [{"type": "tool_use", "name": "TaskCreate",
|
|
94
|
+
"input": {"subject": "S1"}}])
|
|
95
|
+
assert check_m2_task_list("Agent", {"subagent_type": "copilot-literature"}, str(t)) is None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_m2_skips_non_copilot_dispatch(tmp_path):
|
|
99
|
+
from research_copilot_guard import check_m2_task_list
|
|
100
|
+
t = tmp_path / "s.jsonl"
|
|
101
|
+
_write_transcript(t, [])
|
|
102
|
+
assert check_m2_task_list("Agent", {"subagent_type": "general-purpose"}, str(t)) is None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_m2_fail_open_no_transcript(tmp_path):
|
|
106
|
+
"""No transcript_path => cannot inspect => fail-open (allow)."""
|
|
107
|
+
from research_copilot_guard import check_m2_task_list
|
|
108
|
+
assert check_m2_task_list("Agent", {"subagent_type": "copilot-writer"}, "") is None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ---- main() integration: attribution via agent_id ----
|
|
112
|
+
|
|
113
|
+
def _run_main(monkeypatch, capsys, payload: dict) -> dict:
|
|
114
|
+
monkeypatch.setattr("sys.stdin", type("S", (), {"read": lambda self: json.dumps(payload)})())
|
|
115
|
+
import importlib, research_copilot_guard
|
|
116
|
+
importlib.reload(research_copilot_guard)
|
|
117
|
+
research_copilot_guard.main()
|
|
118
|
+
return json.loads(capsys.readouterr().out)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_main_polices_main_session_train(tmp_path, monkeypatch, capsys):
|
|
122
|
+
"""No agent_id => main session => Bash train.py denied."""
|
|
123
|
+
out = _run_main(monkeypatch, capsys, {
|
|
124
|
+
"tool_name": "Bash", "tool_input": {"command": "python train.py"},
|
|
125
|
+
"transcript_path": str(tmp_path / "x.jsonl"),
|
|
126
|
+
})
|
|
127
|
+
assert out["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_main_exempts_copilot_subagent_train(tmp_path, monkeypatch, capsys):
|
|
131
|
+
"""agent_id present + copilot-experiment => exempt => allowed."""
|
|
132
|
+
out = _run_main(monkeypatch, capsys, {
|
|
133
|
+
"tool_name": "Bash", "tool_input": {"command": "python train.py"},
|
|
134
|
+
"transcript_path": str(tmp_path / "x.jsonl"),
|
|
135
|
+
"agent_id": "sa_01", "agent_type": "copilot-experiment",
|
|
136
|
+
})
|
|
137
|
+
assert out["hookSpecificOutput"]["permissionDecision"] == "allow"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_main_fails_open_on_internal_exception(monkeypatch, capsys):
|
|
141
|
+
"""An exception inside the decision path must yield allow, never crash."""
|
|
142
|
+
import importlib, research_copilot_guard
|
|
143
|
+
importlib.reload(research_copilot_guard)
|
|
144
|
+
monkeypatch.setattr(research_copilot_guard, "_decide",
|
|
145
|
+
lambda payload: (_ for _ in ()).throw(RuntimeError("boom")))
|
|
146
|
+
monkeypatch.setattr("sys.stdin",
|
|
147
|
+
type("S", (), {"read": lambda self: json.dumps({"tool_name": "Bash"})})())
|
|
148
|
+
research_copilot_guard.main()
|
|
149
|
+
out = json.loads(capsys.readouterr().out)
|
|
150
|
+
assert out["hookSpecificOutput"]["permissionDecision"] == "allow"
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_extract_handoff_block_returns_block_text_when_present():
|
|
8
|
+
from session_start_memory_injector import extract_handoff_block
|
|
9
|
+
text = (
|
|
10
|
+
"# heading\n"
|
|
11
|
+
"some content\n"
|
|
12
|
+
"\n"
|
|
13
|
+
"## __HANDOFF__\n"
|
|
14
|
+
"- last_updated: 2026-05-23T00:00:00Z\n"
|
|
15
|
+
"- written_by: research-copilot\n"
|
|
16
|
+
"- key_facts:\n"
|
|
17
|
+
" - locked baseline = Foo\n"
|
|
18
|
+
"- next_owner: copilot-ideation\n"
|
|
19
|
+
)
|
|
20
|
+
block = extract_handoff_block(text)
|
|
21
|
+
assert block is not None
|
|
22
|
+
assert "last_updated" in block
|
|
23
|
+
assert "key_facts" in block
|
|
24
|
+
assert "locked baseline = Foo" in block
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_extract_handoff_block_returns_none_when_absent():
|
|
28
|
+
from session_start_memory_injector import extract_handoff_block
|
|
29
|
+
assert extract_handoff_block("just a body\nno trailer\n") is None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_extract_last_n_lines_returns_tail():
|
|
33
|
+
from session_start_memory_injector import extract_last_n_lines
|
|
34
|
+
text = "\n".join(f"line {i}" for i in range(1, 31)) + "\n"
|
|
35
|
+
result = extract_last_n_lines(text, n=5)
|
|
36
|
+
assert result == "line 26\nline 27\nline 28\nline 29\nline 30"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_main_prints_summary_when_copilot_dir_has_handoff_blocks(tmp_path, monkeypatch, capsys):
|
|
40
|
+
copilot = tmp_path / ".copilot"
|
|
41
|
+
copilot.mkdir()
|
|
42
|
+
(copilot / "state.md").write_text(
|
|
43
|
+
"# state\n\n## __HANDOFF__\n"
|
|
44
|
+
"- last_updated: 2026-05-23T00:00:00Z\n"
|
|
45
|
+
"- written_by: research-copilot\n"
|
|
46
|
+
"- key_facts:\n"
|
|
47
|
+
" - stage cursor at S2\n"
|
|
48
|
+
"- next_owner: copilot-ideation\n",
|
|
49
|
+
encoding="utf-8",
|
|
50
|
+
)
|
|
51
|
+
monkeypatch.chdir(tmp_path)
|
|
52
|
+
from session_start_memory_injector import main
|
|
53
|
+
rc = main()
|
|
54
|
+
assert rc == 0
|
|
55
|
+
captured = capsys.readouterr().out
|
|
56
|
+
assert "[memory-injector]" in captured
|
|
57
|
+
assert "stage cursor at S2" in captured
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_main_skips_when_no_copilot_dir(tmp_path, monkeypatch, capsys):
|
|
61
|
+
monkeypatch.chdir(tmp_path)
|
|
62
|
+
from session_start_memory_injector import main
|
|
63
|
+
rc = main()
|
|
64
|
+
assert rc == 0
|
|
65
|
+
captured = capsys.readouterr().out
|
|
66
|
+
assert "not initialized" in captured.lower() or "skipping" in captured.lower()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class _StringIO:
|
|
8
|
+
def __init__(self, s): self._s = s
|
|
9
|
+
def read(self): return self._s
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_no_suppression_for_status_query(tmp_path, monkeypatch, capsys):
|
|
13
|
+
"""Standing orders fire even on 'what's next' / 下一步 (no suppression)."""
|
|
14
|
+
monkeypatch.chdir(tmp_path)
|
|
15
|
+
monkeypatch.setattr("sys.stdin", _StringIO("下一步"))
|
|
16
|
+
from user_prompt_dispatch_reminder import main
|
|
17
|
+
assert main() == 0
|
|
18
|
+
assert "conductor" in capsys.readouterr().out.lower()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_no_suppression_for_slash_or_at(tmp_path, monkeypatch, capsys):
|
|
22
|
+
monkeypatch.chdir(tmp_path)
|
|
23
|
+
monkeypatch.setattr("sys.stdin", _StringIO("/loop 1m check"))
|
|
24
|
+
from user_prompt_dispatch_reminder import main
|
|
25
|
+
assert main() == 0
|
|
26
|
+
assert capsys.readouterr().out.strip() != ""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_main_respects_disabled_flag(tmp_path, monkeypatch, capsys):
|
|
30
|
+
(tmp_path / ".copilot").mkdir()
|
|
31
|
+
(tmp_path / ".copilot" / "dispatch-reminder.disabled").write_text("", encoding="utf-8")
|
|
32
|
+
monkeypatch.chdir(tmp_path)
|
|
33
|
+
monkeypatch.setattr("sys.stdin", _StringIO("anything at all"))
|
|
34
|
+
from user_prompt_dispatch_reminder import main
|
|
35
|
+
rc = main()
|
|
36
|
+
assert rc == 0
|
|
37
|
+
assert capsys.readouterr().out == ""
|