@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,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,2 @@
1
+ {"role":"user","content":"hello"}
2
+ {"role":"assistant","content":[{"type":"text","text":"hi"}]}
@@ -0,0 +1,2 @@
1
+ {"role":"user","content":"go"}
2
+ {"role":"assistant","metadata":{"subagent_type":"copilot-literature"},"content":[{"type":"text","text":"[STATE_OUTPUT]\nPrevious: SCANNING\nCurrent: END\nTransition reason: done\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