@research-copilot/plugin 1.1.15 → 1.1.17
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,188 @@
|
|
|
1
|
+
"""SessionStart hook: inject .copilot/ __HANDOFF__ summaries into context."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
HANDOFF_HEADER = "## __HANDOFF__"
|
|
9
|
+
COPILOT_FILES = ["state.md", "literature.md", "ideas.md",
|
|
10
|
+
"experiments.md", "decisions.md", "handoff.md"]
|
|
11
|
+
MAX_TOTAL_LINES = 400
|
|
12
|
+
PIPELINES_TAIL_LINES = 20
|
|
13
|
+
RECENT_PIPELINES = 3
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def conductor_protocol_path() -> Path:
|
|
17
|
+
"""Locate CONDUCTOR-PROTOCOL.md in both dev (self/) and installed
|
|
18
|
+
(${CLAUDE_PLUGIN_ROOT}/) layouts. The script lives at
|
|
19
|
+
<root>/hooks/scripts/session_start_memory_injector.py, so the protocol is
|
|
20
|
+
two levels up.
|
|
21
|
+
"""
|
|
22
|
+
return Path(__file__).resolve().parent.parent.parent / "CONDUCTOR-PROTOCOL.md"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def extract_handoff_block(text: str) -> str | None:
|
|
26
|
+
"""Return the body of the trailing ## __HANDOFF__ section, or None."""
|
|
27
|
+
idx = text.rfind(HANDOFF_HEADER)
|
|
28
|
+
if idx < 0:
|
|
29
|
+
return None
|
|
30
|
+
body = text[idx + len(HANDOFF_HEADER):].strip()
|
|
31
|
+
end = body.find("\n## ")
|
|
32
|
+
if end >= 0:
|
|
33
|
+
body = body[:end].rstrip()
|
|
34
|
+
return body or None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def extract_last_n_lines(text: str, n: int) -> str:
|
|
38
|
+
lines = text.rstrip("\n").split("\n")
|
|
39
|
+
return "\n".join(lines[-n:])
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def main() -> int:
|
|
43
|
+
workspace = Path.cwd()
|
|
44
|
+
copilot = workspace / ".copilot"
|
|
45
|
+
if not copilot.exists():
|
|
46
|
+
sys.stdout.write("[memory-injector] .copilot/ not initialized — skipping.\n")
|
|
47
|
+
sys.stdout.flush()
|
|
48
|
+
return 0
|
|
49
|
+
|
|
50
|
+
blocks: list[str] = []
|
|
51
|
+
total_lines = 0
|
|
52
|
+
|
|
53
|
+
for fname in COPILOT_FILES:
|
|
54
|
+
f = copilot / fname
|
|
55
|
+
if not f.is_file():
|
|
56
|
+
continue
|
|
57
|
+
text = f.read_text(encoding="utf-8", errors="replace")
|
|
58
|
+
block = extract_handoff_block(text)
|
|
59
|
+
if block is None:
|
|
60
|
+
block = extract_last_n_lines(text, n=PIPELINES_TAIL_LINES)
|
|
61
|
+
if not block.strip():
|
|
62
|
+
continue
|
|
63
|
+
header = f"### {fname} (no __HANDOFF__; last {PIPELINES_TAIL_LINES} lines)"
|
|
64
|
+
else:
|
|
65
|
+
header = f"### {fname}"
|
|
66
|
+
blocks.append(f"{header}\n{block}")
|
|
67
|
+
total_lines += block.count("\n") + 2
|
|
68
|
+
if total_lines >= MAX_TOTAL_LINES:
|
|
69
|
+
blocks.append(f"[memory-injector] truncated at {MAX_TOTAL_LINES} lines budget")
|
|
70
|
+
break
|
|
71
|
+
|
|
72
|
+
# ---- Write last_updated snapshot for SubagentStop hook ----
|
|
73
|
+
snapshot: dict[str, str | None] = {}
|
|
74
|
+
for fname in COPILOT_FILES:
|
|
75
|
+
f = copilot / fname
|
|
76
|
+
if not f.is_file():
|
|
77
|
+
continue
|
|
78
|
+
try:
|
|
79
|
+
text = f.read_text(encoding="utf-8", errors="replace")
|
|
80
|
+
except OSError:
|
|
81
|
+
continue
|
|
82
|
+
idx = text.rfind(HANDOFF_HEADER)
|
|
83
|
+
if idx < 0:
|
|
84
|
+
snapshot[fname] = None
|
|
85
|
+
continue
|
|
86
|
+
body = text[idx + len(HANDOFF_HEADER):]
|
|
87
|
+
last_updated: str | None = None
|
|
88
|
+
for line in body.splitlines():
|
|
89
|
+
s = line.strip()
|
|
90
|
+
if s.startswith("- last_updated:"):
|
|
91
|
+
last_updated = s.split(":", 1)[1].strip() or None
|
|
92
|
+
break
|
|
93
|
+
snapshot[fname] = last_updated
|
|
94
|
+
try:
|
|
95
|
+
(copilot / ".session_snapshot.json").write_text(
|
|
96
|
+
json.dumps(snapshot, indent=2, ensure_ascii=False) + "\n",
|
|
97
|
+
encoding="utf-8",
|
|
98
|
+
)
|
|
99
|
+
except OSError:
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
pipelines_dir = copilot / "pipelines"
|
|
103
|
+
if pipelines_dir.is_dir() and total_lines < MAX_TOTAL_LINES:
|
|
104
|
+
recent = sorted(pipelines_dir.glob("*.md"))[-RECENT_PIPELINES:]
|
|
105
|
+
for p in recent:
|
|
106
|
+
text = p.read_text(encoding="utf-8", errors="replace")
|
|
107
|
+
block = extract_handoff_block(text) or extract_last_n_lines(text, n=PIPELINES_TAIL_LINES)
|
|
108
|
+
if not block.strip():
|
|
109
|
+
continue
|
|
110
|
+
blocks.append(f"### pipelines/{p.stem}\n{block}")
|
|
111
|
+
total_lines += block.count("\n") + 2
|
|
112
|
+
if total_lines >= MAX_TOTAL_LINES:
|
|
113
|
+
break
|
|
114
|
+
|
|
115
|
+
if not blocks:
|
|
116
|
+
sys.stdout.write(
|
|
117
|
+
"[memory-injector] .copilot/ exists but no __HANDOFF__ blocks found — "
|
|
118
|
+
"sub-agents likely not following PIPELINE-OS §9.\n"
|
|
119
|
+
)
|
|
120
|
+
sys.stdout.flush()
|
|
121
|
+
return 0
|
|
122
|
+
|
|
123
|
+
sys.stdout.write("[memory-injector] Loaded research state from .copilot/:\n\n")
|
|
124
|
+
sys.stdout.write("\n\n".join(blocks))
|
|
125
|
+
sys.stdout.write(
|
|
126
|
+
"\n\n[memory-injector] Constraints: do NOT propose ideas already in ideas.md; "
|
|
127
|
+
"do NOT re-run experiments already in experiments.md unless explicitly asked.\n"
|
|
128
|
+
)
|
|
129
|
+
sys.stdout.flush()
|
|
130
|
+
|
|
131
|
+
# ---- Summarize last 24h of violations log ----
|
|
132
|
+
vlog = copilot / "__violations.log"
|
|
133
|
+
if vlog.is_file():
|
|
134
|
+
try:
|
|
135
|
+
import datetime as _dt
|
|
136
|
+
now = _dt.datetime.now(_dt.timezone.utc)
|
|
137
|
+
cutoff = now - _dt.timedelta(hours=24)
|
|
138
|
+
hard_blocks = 0
|
|
139
|
+
releases = 0
|
|
140
|
+
soft_warns = 0
|
|
141
|
+
for line in vlog.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
142
|
+
if not line.strip():
|
|
143
|
+
continue
|
|
144
|
+
try:
|
|
145
|
+
rec = json.loads(line)
|
|
146
|
+
except json.JSONDecodeError:
|
|
147
|
+
continue
|
|
148
|
+
try:
|
|
149
|
+
t = _dt.datetime.fromisoformat(rec.get("ts", "").replace("Z", "+00:00"))
|
|
150
|
+
except ValueError:
|
|
151
|
+
continue
|
|
152
|
+
if t.tzinfo is None:
|
|
153
|
+
t = t.replace(tzinfo=_dt.timezone.utc)
|
|
154
|
+
if t < cutoff:
|
|
155
|
+
continue
|
|
156
|
+
sev, kind = rec.get("sev"), rec.get("kind")
|
|
157
|
+
if sev == "HARD" and kind == "BLOCK":
|
|
158
|
+
hard_blocks += 1
|
|
159
|
+
elif sev == "HARD" and kind == "RELEASE":
|
|
160
|
+
releases += 1
|
|
161
|
+
elif sev == "SOFT" and kind == "WARN":
|
|
162
|
+
soft_warns += 1
|
|
163
|
+
if hard_blocks or releases or soft_warns:
|
|
164
|
+
sys.stdout.write(
|
|
165
|
+
f"\n[memory-injector] Last 24h: {hard_blocks} HARD blocks "
|
|
166
|
+
f"({releases} 3-strike releases), {soft_warns} SOFT warns. "
|
|
167
|
+
f"See .copilot/__violations.log.\n"
|
|
168
|
+
)
|
|
169
|
+
sys.stdout.flush()
|
|
170
|
+
except OSError:
|
|
171
|
+
pass
|
|
172
|
+
|
|
173
|
+
proto = conductor_protocol_path()
|
|
174
|
+
if proto.is_file():
|
|
175
|
+
try:
|
|
176
|
+
sys.stdout.write(
|
|
177
|
+
"\n\n[conductor] Active protocol (you ARE the conductor; "
|
|
178
|
+
"delegate execution to copilot-*, own the task list):\n\n"
|
|
179
|
+
+ proto.read_text(encoding="utf-8", errors="replace") + "\n"
|
|
180
|
+
)
|
|
181
|
+
sys.stdout.flush()
|
|
182
|
+
except OSError:
|
|
183
|
+
pass
|
|
184
|
+
return 0
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
if __name__ == "__main__":
|
|
188
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""UserPromptSubmit hook: re-assert the main-session conductor's standing orders
|
|
2
|
+
on EVERY turn. No suppression — the constraint must apply from every interaction
|
|
3
|
+
onward, including 'next step' / slash / @ prompts. Honors a .disabled flag."""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
STANDING_ORDERS = (
|
|
10
|
+
"[conductor] You are the research-pipeline conductor (main session). Standing orders:\n"
|
|
11
|
+
" 1. Do NOT execute domain work inline. For any execution-class request, FIRST\n"
|
|
12
|
+
" publish a TaskCreate plan list (one task per planned dispatch), THEN dispatch:\n"
|
|
13
|
+
" - literature / paper search -> Agent(subagent_type='copilot-literature')\n"
|
|
14
|
+
" - innovation / brainstorm -> Agent(subagent_type='copilot-ideation')\n"
|
|
15
|
+
" - experiment / training -> Agent(subagent_type='copilot-experiment')\n"
|
|
16
|
+
" - drafting / writing -> Agent(subagent_type='copilot-writer')\n"
|
|
17
|
+
" - polish / de-AI -> Agent(subagent_type='copilot-polisher')\n"
|
|
18
|
+
" - review / sanity -> Agent(subagent_type='copilot-reviewer')\n"
|
|
19
|
+
" - rebuttal -> Agent(subagent_type='copilot-rebuttal')\n"
|
|
20
|
+
" 2. You OWN the plan and the task list — never let the first sub-agent's closing\n"
|
|
21
|
+
" recommendation decide the next step. Audit each return, then advance the plan.\n"
|
|
22
|
+
" 3. You may write .copilot/state.md and .copilot/decisions.md; refresh their\n"
|
|
23
|
+
" __HANDOFF__ blocks on every stage transition (PIPELINE-OS §9).\n"
|
|
24
|
+
" 4. Read .copilot/state.md before diagnosing where the pipeline stands.\n"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main() -> int:
|
|
29
|
+
if (Path.cwd() / ".copilot" / "dispatch-reminder.disabled").exists():
|
|
30
|
+
return 0
|
|
31
|
+
# Drain stdin (the prompt) so the hook doesn't block; content is not inspected —
|
|
32
|
+
# standing orders fire unconditionally.
|
|
33
|
+
_ = sys.stdin.read()
|
|
34
|
+
sys.stdout.write(STANDING_ORDERS)
|
|
35
|
+
sys.stdout.flush()
|
|
36
|
+
return 0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if __name__ == "__main__":
|
|
40
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "SessionStart hook: inject .copilot/ __HANDOFF__ summaries so the new session knows where the pipeline stands.",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"SessionStart": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": "*",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "python self/hooks/scripts/session_start_memory_injector.py",
|
|
11
|
+
"timeout": 10
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Shared pytest fixtures for copilot hook tests."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
SCRIPTS_DIR = Path(__file__).resolve().parent.parent / "scripts"
|
|
11
|
+
sys.path.insert(0, str(SCRIPTS_DIR))
|
|
12
|
+
|
|
13
|
+
FIXTURES = Path(__file__).resolve().parent / "fixtures"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def workspace(tmp_path: Path) -> Path:
|
|
18
|
+
(tmp_path / ".copilot").mkdir()
|
|
19
|
+
return tmp_path
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture
|
|
23
|
+
def fixtures_dir() -> Path:
|
|
24
|
+
return FIXTURES
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def make_payload(tool_name: str, tool_input: dict, transcript_path: str,
|
|
28
|
+
stop_hook_active: bool = False) -> dict:
|
|
29
|
+
return {"tool_name": tool_name, "tool_input": tool_input,
|
|
30
|
+
"transcript_path": transcript_path,
|
|
31
|
+
"stop_hook_active": stop_hook_active}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.fixture
|
|
35
|
+
def payload_builder():
|
|
36
|
+
return make_payload
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def write_handoff_block(file: Path, last_updated: str,
|
|
40
|
+
written_by: str = "copilot-test",
|
|
41
|
+
key_facts: list[str] | None = None) -> None:
|
|
42
|
+
facts = key_facts or ["(placeholder)"]
|
|
43
|
+
body = "\n".join([
|
|
44
|
+
"",
|
|
45
|
+
"## __HANDOFF__",
|
|
46
|
+
f"- last_updated: {last_updated}",
|
|
47
|
+
f"- written_by: {written_by}",
|
|
48
|
+
"- key_facts:",
|
|
49
|
+
*(f" - {f}" for f in facts),
|
|
50
|
+
"- next_owner: (none)",
|
|
51
|
+
"",
|
|
52
|
+
])
|
|
53
|
+
if file.exists():
|
|
54
|
+
file.write_text(file.read_text() + body, encoding="utf-8")
|
|
55
|
+
else:
|
|
56
|
+
file.write_text(body, encoding="utf-8")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.fixture
|
|
60
|
+
def handoff_writer():
|
|
61
|
+
return write_handoff_block
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
{"role":"user","content":"run experiment"}
|
|
2
|
+
{"role":"assistant","metadata":{"subagent_type":"copilot-experiment"},"content":[{"type":"text","text":"[STATE_OUTPUT]\nPrevious: JUDGED\nCurrent: END\nAction completed: Wrote handoff\nCapability gate: passed\nEvidence: experiments.md:42\nNext allowed: []\nTransition reason: goal met\n[/STATE_OUTPUT]"}]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
{"role":"user","content":"run experiment"}
|
|
2
|
+
{"role":"assistant","metadata":{"subagent_type":"copilot-experiment"},"content":[{"type":"text","text":"[STATE_OUTPUT]\nPrevious: UNINITIALIZED\nCurrent: END\nAction completed: skipped\nCapability gate: passed\nEvidence: none\nNext allowed: []\nTransition reason: shortcut\n[/STATE_OUTPUT]"}]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
{"role":"user","content":"scan papers"}
|
|
2
|
+
{"role":"assistant","metadata":{"subagent_type":"copilot-literature"},"content":[{"type":"text","text":"[STATE_OUTPUT]\nPrevious: UNINITIALIZED\nCurrent: SCANNING\nAction completed: Loaded literature.md\nCapability gate: passed\nEvidence: literature.md:1\nNext allowed: [BASELINE_LOCKED, RELATED_WORK_AUGMENTED]\nTransition reason: memory-gate passed\n[/STATE_OUTPUT]"}]}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Manual integration smoke test for copilot guard hooks.
|
|
2
|
+
# Usage: pwsh -File self/hooks/tests/integration_run.ps1
|
|
3
|
+
#
|
|
4
|
+
# Exercises each hook with fake stdin payloads and asserts the decision JSON.
|
|
5
|
+
# Does NOT register the hooks in .claude/settings.json (Task 20 does that).
|
|
6
|
+
|
|
7
|
+
$ErrorActionPreference = "Stop"
|
|
8
|
+
$python = "D:/article/.venv/Scripts/python.exe"
|
|
9
|
+
$repo = "D:/article"
|
|
10
|
+
|
|
11
|
+
$tmpRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("copilot-smoke-" + [Guid]::NewGuid().ToString("N"))
|
|
12
|
+
New-Item -ItemType Directory -Path $tmpRoot | Out-Null
|
|
13
|
+
$workspace = $tmpRoot
|
|
14
|
+
New-Item -ItemType Directory -Path "$workspace/.copilot" | Out-Null
|
|
15
|
+
|
|
16
|
+
function Run-Hook ($script, $payload) {
|
|
17
|
+
$json = $payload | ConvertTo-Json -Compress -Depth 10
|
|
18
|
+
$stdinFile = Join-Path $tmpRoot "stdin.txt"
|
|
19
|
+
Set-Content -Path $stdinFile -Value $json -NoNewline -Encoding UTF8
|
|
20
|
+
$stdoutFile = Join-Path $tmpRoot "stdout.txt"
|
|
21
|
+
$stderrFile = Join-Path $tmpRoot "stderr.txt"
|
|
22
|
+
$proc = Start-Process -FilePath $python `
|
|
23
|
+
-ArgumentList "$repo/self/hooks/scripts/$script" `
|
|
24
|
+
-WorkingDirectory $workspace `
|
|
25
|
+
-RedirectStandardInput $stdinFile `
|
|
26
|
+
-RedirectStandardOutput $stdoutFile `
|
|
27
|
+
-RedirectStandardError $stderrFile `
|
|
28
|
+
-NoNewWindow -PassThru -Wait
|
|
29
|
+
return (Get-Content $stdoutFile -Raw)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function Assert ($cond, $msg) {
|
|
33
|
+
if (-not $cond) {
|
|
34
|
+
Write-Host "FAIL: $msg" -ForegroundColor Red
|
|
35
|
+
exit 1
|
|
36
|
+
}
|
|
37
|
+
Write-Host "PASS: $msg" -ForegroundColor Green
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Sanity: scripts import cleanly
|
|
41
|
+
& $python -c "import sys; sys.path.insert(0, '$repo/self/hooks/scripts'); import _copilot_hook_lib, copilot_write_guard, copilot_subagent_stop"
|
|
42
|
+
Assert ($LASTEXITCODE -eq 0) "scripts import cleanly"
|
|
43
|
+
|
|
44
|
+
# Test 1: PreToolUse — literature writing ideas.md → deny
|
|
45
|
+
$lit = Join-Path $tmpRoot "lit.jsonl"
|
|
46
|
+
'{"role":"assistant","metadata":{"subagent_type":"copilot-literature"}}' | Set-Content -Path $lit -Encoding UTF8
|
|
47
|
+
$payload = @{
|
|
48
|
+
tool_name = "Write"
|
|
49
|
+
tool_input = @{ file_path = (Join-Path $workspace ".copilot/ideas.md") }
|
|
50
|
+
transcript_path = $lit
|
|
51
|
+
}
|
|
52
|
+
$out = Run-Hook -script "copilot_write_guard.py" -payload $payload
|
|
53
|
+
Assert ($out -match '"deny"') "PreToolUse denies non-owned write (copilot-literature -> ideas.md)"
|
|
54
|
+
|
|
55
|
+
# Test 2: SubagentStop — first boot, no snapshot, no handoff → allow + NO-SNAPSHOT log
|
|
56
|
+
$payload = @{ transcript_path = $lit; stop_hook_active = $false }
|
|
57
|
+
$out = Run-Hook -script "copilot_subagent_stop.py" -payload $payload
|
|
58
|
+
Assert ($out -match '"allow"') "SubagentStop allows on first boot (SOFT degrade)"
|
|
59
|
+
|
|
60
|
+
$log = Get-Content "$workspace/.copilot/__violations.log" -Raw -ErrorAction SilentlyContinue
|
|
61
|
+
Assert ($log -and $log -match "NO-SNAPSHOT") "violations.log records NO-SNAPSHOT entry"
|
|
62
|
+
|
|
63
|
+
Write-Host "ALL INTEGRATION CHECKS PASSED" -ForegroundColor Green
|
|
64
|
+
Remove-Item -Recurse -Force $tmpRoot
|
|
65
|
+
exit 0
|