@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.
Files changed (117) hide show
  1. package/dist/.claude-plugin/plugin.json +3 -2
  2. package/dist/.codex-plugin/plugin.toml +2 -1
  3. package/dist/.cursor-plugin/plugin.json +3 -2
  4. package/dist/.gemini-plugin/plugin.json +3 -2
  5. package/dist/.opencode-plugin/plugin.json +3 -2
  6. package/dist/.windsurf-plugin/plugin.json +3 -2
  7. package/dist/agents/copilot-conductor.agent.md +60 -0
  8. package/dist/agents/copilot-experiment.agent.md +56 -0
  9. package/dist/agents/copilot-ideation.agent.md +45 -0
  10. package/dist/agents/copilot-literature.agent.md +34 -0
  11. package/dist/agents/copilot-polisher.agent.md +30 -0
  12. package/dist/agents/copilot-rebuttal.agent.md +35 -0
  13. package/dist/agents/copilot-reviewer.agent.md +35 -0
  14. package/dist/agents/copilot-writer.agent.md +39 -0
  15. package/dist/hooks/dispatch-reminder.json +17 -0
  16. package/dist/hooks/loop-armer.json +17 -0
  17. package/dist/hooks/research-copilot-guard.hook.md +51 -0
  18. package/dist/hooks/scientist-guardrails.json +17 -0
  19. package/dist/hooks/scripts/__tests__/__init__.py +0 -0
  20. package/dist/hooks/scripts/__tests__/test_post_tool_loop_armer.py +88 -0
  21. package/dist/hooks/scripts/__tests__/test_research_copilot_guard_main_session.py +150 -0
  22. package/dist/hooks/scripts/__tests__/test_session_start_memory_injector.py +66 -0
  23. package/dist/hooks/scripts/__tests__/test_user_prompt_dispatch_reminder.py +37 -0
  24. package/dist/hooks/scripts/_copilot_hook_lib.py +564 -0
  25. package/dist/hooks/scripts/copilot_subagent_stop.py +203 -0
  26. package/dist/hooks/scripts/copilot_write_guard.py +96 -0
  27. package/dist/hooks/scripts/post_tool_loop_armer.py +61 -0
  28. package/dist/hooks/scripts/research_copilot_guard.py +208 -0
  29. package/dist/hooks/scripts/scientist_guardrails.py +29 -0
  30. package/dist/hooks/scripts/session_start_memory_injector.py +188 -0
  31. package/dist/hooks/scripts/user_prompt_dispatch_reminder.py +40 -0
  32. package/dist/hooks/session-memory-injector.json +17 -0
  33. package/dist/hooks/tests/__init__.py +0 -0
  34. package/dist/hooks/tests/conftest.py +61 -0
  35. package/dist/hooks/tests/fixtures/transcript_copilot_experiment_complete.jsonl +2 -0
  36. package/dist/hooks/tests/fixtures/transcript_copilot_experiment_state_jump.jsonl +2 -0
  37. package/dist/hooks/tests/fixtures/transcript_copilot_literature.jsonl +2 -0
  38. package/dist/hooks/tests/fixtures/transcript_main_only.jsonl +2 -0
  39. package/dist/hooks/tests/fixtures/transcript_malformed_state_output.jsonl +2 -0
  40. package/dist/hooks/tests/integration_run.ps1 +65 -0
  41. package/dist/hooks/tests/test_copilot_hook_lib.py +398 -0
  42. package/dist/hooks/tests/test_copilot_subagent_stop.py +186 -0
  43. package/dist/hooks/tests/test_copilot_write_guard.py +137 -0
  44. package/dist/hooks/tests/test_session_start_snapshot.py +116 -0
  45. package/dist/hooks/tests/test_state_machine_consistency.py +75 -0
  46. package/dist/skills/arxivsub-skill/SKILL.md +98 -0
  47. package/dist/skills/arxivsub-skill/skill.json +5 -0
  48. package/dist/skills/de-ai-checker/SKILL.md +110 -0
  49. package/dist/skills/de-ai-checker/skill.json +5 -0
  50. package/dist/skills/deep-interview/SKILL.md +91 -0
  51. package/dist/skills/deep-interview/skill.json +5 -0
  52. package/dist/skills/grill-with-docs/SKILL.md +120 -0
  53. package/dist/skills/grill-with-docs/skill.json +5 -0
  54. package/dist/skills/init-mcp/SKILL.md +83 -0
  55. package/dist/skills/init-mcp/skill.json +5 -0
  56. package/dist/skills/model-escalation/SKILL.md +93 -0
  57. package/dist/skills/model-escalation/skill.json +5 -0
  58. package/dist/skills/paper-architecture-web-drawing/SKILL.md +282 -0
  59. package/dist/skills/paper-architecture-web-drawing/skill.json +5 -0
  60. package/dist/skills/paper-deai/SKILL.md +53 -0
  61. package/dist/skills/paper-deai/skill.json +5 -0
  62. package/dist/skills/paper-en2zh/SKILL.md +29 -0
  63. package/dist/skills/paper-en2zh/skill.json +5 -0
  64. package/dist/skills/paper-expand/SKILL.md +43 -0
  65. package/dist/skills/paper-expand/skill.json +5 -0
  66. package/dist/skills/paper-experiment-analysis/SKILL.md +38 -0
  67. package/dist/skills/paper-experiment-analysis/skill.json +5 -0
  68. package/dist/skills/paper-figure-caption/SKILL.md +29 -0
  69. package/dist/skills/paper-figure-caption/skill.json +5 -0
  70. package/dist/skills/paper-logic-check/SKILL.md +30 -0
  71. package/dist/skills/paper-logic-check/skill.json +5 -0
  72. package/dist/skills/paper-polish/SKILL.md +34 -305
  73. package/dist/skills/paper-polish/skill.json +5 -0
  74. package/dist/skills/paper-review/SKILL.md +49 -0
  75. package/dist/skills/paper-review/skill.json +5 -0
  76. package/dist/skills/paper-sanity-check/SKILL.md +122 -0
  77. package/dist/skills/paper-sanity-check/skill.json +5 -0
  78. package/dist/skills/paper-shorten/SKILL.md +42 -0
  79. package/dist/skills/paper-shorten/skill.json +5 -0
  80. package/dist/skills/paper-table-caption/SKILL.md +29 -0
  81. package/dist/skills/paper-table-caption/skill.json +5 -0
  82. package/dist/skills/paper-translate/SKILL.md +48 -0
  83. package/dist/skills/paper-translate/skill.json +5 -0
  84. package/dist/skills/plugin-dev-agent-development/SKILL.md +95 -0
  85. package/dist/skills/plugin-dev-agent-development/skill.json +5 -0
  86. package/dist/skills/research-workflow/SKILL.md +116 -0
  87. package/dist/skills/research-workflow/skill.json +5 -0
  88. package/dist/skills/scientist-experiment-runner/SKILL.md +76 -0
  89. package/dist/skills/scientist-experiment-runner/skill.json +5 -0
  90. package/dist/skills/scientist-ideation/SKILL.md +52 -0
  91. package/dist/skills/scientist-ideation/skill.json +5 -0
  92. package/dist/skills/scientist-plotting/SKILL.md +49 -0
  93. package/dist/skills/scientist-plotting/skill.json +5 -0
  94. package/dist/skills/scientist-review/SKILL.md +40 -0
  95. package/dist/skills/scientist-review/skill.json +5 -0
  96. package/dist/skills/scientist-runtime-init/SKILL.md +46 -0
  97. package/dist/skills/scientist-runtime-init/skill.json +5 -0
  98. package/dist/skills/scientist-writeup/SKILL.md +60 -0
  99. package/dist/skills/scientist-writeup/skill.json +5 -0
  100. package/dist/skills/talk-normal/SKILL.md +73 -0
  101. package/dist/skills/talk-normal/skill.json +5 -0
  102. package/package.json +1 -1
  103. package/dist/agents/rc-experiment.md +0 -203
  104. package/dist/agents/rc-ideation.md +0 -224
  105. package/dist/agents/rc-literature.md +0 -228
  106. package/dist/agents/rc-plan.md +0 -189
  107. package/dist/agents/rc-polisher.md +0 -166
  108. package/dist/agents/rc-rebuttal.md +0 -194
  109. package/dist/agents/rc-reviewer.md +0 -187
  110. package/dist/agents/rc-update-spec.md +0 -231
  111. package/dist/agents/rc-verify.md +0 -234
  112. package/dist/agents/rc-writer.md +0 -161
  113. package/dist/skills/experiment-design/SKILL.md +0 -331
  114. package/dist/skills/full-research-workflow/SKILL.md +0 -363
  115. package/dist/skills/literature-search/SKILL.md +0 -244
  116. package/dist/skills/sanity-check/SKILL.md +0 -449
  117. 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 == ""