From 0f57484431bbfb3c8df234f518b54c752b54389d Mon Sep 17 00:00:00 2001 From: sgaabdu4 Date: Tue, 7 Apr 2026 21:07:11 +0100 Subject: [PATCH 1/5] feat: add Copilot hooks enforcement (Stop + PreCompact) with auto-setup - Add hookContent.ts as SSOT for hook messages - Stop hook blocks agent from ending without calling ask_user - PreCompact hook reminds agent to preserve session_id after compaction - Auto-create hooks on extension activation when missing - Add TaskSync: Setup Copilot Hooks command for manual setup - Update CHANGELOG, README, AGENTS.md, copilot-instructions.md --- .github/copilot-instructions.md | 3 + .github/hooks/tasksync-stop.json | 16 ++++ AGENTS.md | 4 + CHANGELOG.md | 3 + README.md | 9 +++ tasksync-chat/package.json | 4 + tasksync-chat/src/constants/hookContent.ts | 34 +++++++++ tasksync-chat/src/extension.ts | 86 ++++++++++++++++++++++ 8 files changed, 159 insertions(+) create mode 100644 .github/hooks/tasksync-stop.json create mode 100644 tasksync-chat/src/constants/hookContent.ts diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2bf3bae..172b490 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -20,6 +20,8 @@ TaskSync is a human-in-the-loop workflow toolkit for AI-assisted development. Th - `tasksync-chat/biome.json` — Biome linter/formatter config - `tasksync-chat/vitest.config.ts` — Test config - `tasksync-chat/web/` — Remote access PWA (login, service worker) +- `tasksync-chat/src/constants/hookContent.ts` — Copilot hook message SSOT +- `.github/hooks/tasksync-stop.json` — Copilot hooks (Stop + PreCompact) for ask_user enforcement - `Prompt/` — Standalone prompt/protocol markdown files (not actively developed) ## Build, Test, and Validate @@ -98,3 +100,4 @@ Follow OWASP Top 10 principles. Specific patterns enforced in this codebase: - Debounced history saves (2 s) for disk I/O performance. - Remote server uses WebSocket over HTTP with PIN-based auth and session tokens. - `esbuild.js` watch mode monitors `remoteConstants.ts` changes and rebuilds shared constants with 100ms debounce. +- **Copilot Hooks:** Hook messages are defined in `src/constants/hookContent.ts` (SSOT). `.github/hooks/tasksync-stop.json` is the committed copy for this repo. The `TaskSync: Setup Copilot Hooks` command generates these files in any workspace from the SSOT. Agent-scoped hooks can also be set in `.agent.md` frontmatter. diff --git a/.github/hooks/tasksync-stop.json b/.github/hooks/tasksync-stop.json new file mode 100644 index 0000000..a748ba8 --- /dev/null +++ b/.github/hooks/tasksync-stop.json @@ -0,0 +1,16 @@ +{ + "hooks": { + "Stop": [ + { + "type": "command", + "command": "echo '{\"hookSpecificOutput\":{\"hookEventName\":\"Stop\",\"decision\":\"block\",\"reason\":\"MANDATORY: Call ask_user with session_id before ending. The user CANNOT see your response without ask_user. Reuse the session_id from your previous ask_user result. NEVER end without calling ask_user.\"}}'" + } + ], + "PreCompact": [ + { + "type": "command", + "command": "echo '{\"systemMessage\":\"CRITICAL: After compaction, continue calling ask_user with session_id on every turn. Reuse the session_id from your previous ask_user result. NEVER end without calling ask_user.\"}'" + } + ] + } +} diff --git a/AGENTS.md b/AGENTS.md index 5e11a35..2db4513 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,6 +23,9 @@ TaskSync/ ├── CHANGELOG.md # Release history ├── README.md # Project overview ├── LICENSE +├── .github/ +│ └── hooks/ +│ └── tasksync-stop.json # Copilot hooks (Stop + PreCompact) ├── Prompt/ # Standalone prompt/protocol markdown files │ ├── tasksync-v5.2.md │ ├── tasksync-v5.1.md @@ -137,6 +140,7 @@ These principles are mandatory for all changes: - Session state uses a boolean `sessionTerminated` flag — do not use string matching for termination detection. - Debounced history saves (2 s) are used for disk I/O performance. - New Session now supports a modal-first flow and starts a fresh Copilot chat session via `startFreshCopilotChatWithQuery`. +- **Copilot Hooks:** Hook messages are defined in `src/constants/hookContent.ts` (SSOT). `.github/hooks/tasksync-stop.json` is the committed copy for this repo. The `TaskSync: Setup Copilot Hooks` command generates these files in any workspace from the SSOT. Agent-scoped hooks can also be defined in `.agent.md` frontmatter. - Remote Code Review is read-only by design: - Diff browsing is available (`/api/changes`, `/api/diff`) - Write operations (stage/unstage/discard/commit/push) are blocked remotely diff --git a/CHANGELOG.md b/CHANGELOG.md index e5e1008..c5a8ce1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to this project will be documented in this file. +## TaskSync v3.0.11 (07-09-25) +- feat: add Copilot hooks enforcement (Stop + PreCompact) and `TaskSync: Setup Copilot Hooks` command + ## TaskSync v3.0.10 (04-03-26) - feat: add agent orchestration toggle, single-session routing mode, and always-returned session_id tool payloads - fix: tighten the gap below the view toolbar, focus dialogs on open, and let TaskSync dialogs close on `Escape` diff --git a/README.md b/README.md index f110dc9..78a2fa3 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,15 @@ Recommended settings for agent mode: **Enable "Auto Approve" in settings for uninterrupted agent operation. Sessions beyond 2 hours may produce lower-quality results — TaskSync will warn you when it's time to consider starting a fresh session.** +### Copilot Hooks (Preview) + +TaskSync includes [Copilot hooks](https://code.visualstudio.com/docs/copilot/copilot-extensibility-overview#_hooks) that prevent the agent from ending without calling `ask_user`. Run **`TaskSync: Setup Copilot Hooks`** from the command palette to generate `.github/hooks/tasksync-stop.json` in your workspace. This adds: + +- **Stop hook** — blocks the agent from stopping until it calls `ask_user` +- **PreCompact hook** — reminds the agent to preserve `session_id` after context compaction + +Requires VS Code 1.109.3+ and the `chat.agent.hooks` setting enabled. + ## Discussions The TaskSync community can be found on [GitHub Discussions](https://github.com/4regab/TaskSync/discussions) where you can ask questions, voice ideas, and share your prompts with other people. Contributions to TaskSync are welcome and highly appreciated. diff --git a/tasksync-chat/package.json b/tasksync-chat/package.json index d754c59..bab3495 100644 --- a/tasksync-chat/package.json +++ b/tasksync-chat/package.json @@ -345,6 +345,10 @@ "command": "tasksync.goRemote", "title": "Remote", "icon": "$(broadcast)" + }, + { + "command": "tasksync.setupHooks", + "title": "TaskSync: Setup Copilot Hooks" } ], "menus": { diff --git a/tasksync-chat/src/constants/hookContent.ts b/tasksync-chat/src/constants/hookContent.ts new file mode 100644 index 0000000..05183c9 --- /dev/null +++ b/tasksync-chat/src/constants/hookContent.ts @@ -0,0 +1,34 @@ +/** + * Copilot hook content constants (SSOT) + * + * Used by: + * - extension.ts → setupHooksCmd (generates .github/hooks/tasksync-stop.json) + * - .github/hooks/tasksync-stop.json (committed copy for this repo) + * - tasksync.agent.md (agent-scoped hooks — wording should match) + */ + +export const HOOK_STOP_REASON = + "MANDATORY: Call ask_user with session_id before ending. The user CANNOT see your response without ask_user. Reuse the session_id from your previous ask_user result. NEVER end without calling ask_user."; + +export const HOOK_PRECOMPACT_MESSAGE = + "CRITICAL: After compaction, continue calling ask_user with session_id on every turn. Reuse the session_id from your previous ask_user result. NEVER end without calling ask_user."; + +/** Build the full hook file JSON object for writing to disk. */ +export function buildHookFileContent(): object { + return { + hooks: { + Stop: [ + { + type: "command", + command: `echo '{"hookSpecificOutput":{"hookEventName":"Stop","decision":"block","reason":"${HOOK_STOP_REASON}"}}'`, + }, + ], + PreCompact: [ + { + type: "command", + command: `echo '{"systemMessage":"${HOOK_PRECOMPACT_MESSAGE}"}'`, + }, + ], + }, + }; +} diff --git a/tasksync-chat/src/extension.ts b/tasksync-chat/src/extension.ts index 3f955e9..69fa683 100644 --- a/tasksync-chat/src/extension.ts +++ b/tasksync-chat/src/extension.ts @@ -1,4 +1,6 @@ +import * as path from "node:path"; import * as vscode from "vscode"; +import { buildHookFileContent } from "./constants/hookContent"; import { CONFIG_SECTION, DEFAULT_REMOTE_PORT, @@ -14,6 +16,38 @@ let webviewProvider: TaskSyncWebviewProvider | undefined; let contextManager: ContextManager | undefined; let remoteServer: RemoteServer | undefined; +/** Auto-create .github/hooks/tasksync-stop.json if workspace exists and file is missing. */ +async function ensureCopilotHooks(): Promise { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + if (!workspaceFolder) return; + + const hookFile = vscode.Uri.file( + path.join( + workspaceFolder.uri.fsPath, + ".github", + "hooks", + "tasksync-stop.json", + ), + ); + + try { + await vscode.workspace.fs.stat(hookFile); + return; // File already exists — nothing to do + } catch { + // File doesn't exist — create it + } + + const hooksDir = vscode.Uri.file( + path.join(workspaceFolder.uri.fsPath, ".github", "hooks"), + ); + const content = JSON.stringify(buildHookFileContent(), null, 4); + await vscode.workspace.fs.createDirectory(hooksDir); + await vscode.workspace.fs.writeFile( + hookFile, + Buffer.from(`${content}\n`, "utf-8"), + ); +} + export function activate(context: vscode.ExtensionContext): void { // Initialize context manager for #terminal, #problems features contextManager = new ContextManager(); @@ -40,6 +74,11 @@ export function activate(context: vscode.ExtensionContext): void { /* fallback to sync read */ }); + // Auto-setup Copilot hooks if workspace exists and hooks file is missing + ensureCopilotHooks().catch(() => { + /* best-effort — no user-facing error */ + }); + // Register VS Code LM Tools (always available for Copilot) registerTools(context, provider); @@ -308,6 +347,52 @@ export function activate(context: vscode.ExtensionContext): void { }, ); + // Setup Copilot hooks command — writes .github/hooks/ to workspace + const setupHooksCmd = vscode.commands.registerCommand( + "tasksync.setupHooks", + async () => { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + if (!workspaceFolder) { + vscode.window.showErrorMessage( + "No workspace folder open. Open a folder first.", + ); + return; + } + + const hooksDir = vscode.Uri.file( + path.join(workspaceFolder.uri.fsPath, ".github", "hooks"), + ); + const hookFile = vscode.Uri.file( + path.join(hooksDir.fsPath, "tasksync-stop.json"), + ); + + // Check if file already exists + try { + await vscode.workspace.fs.stat(hookFile); + const overwrite = await vscode.window.showWarningMessage( + "tasksync-stop.json already exists. Overwrite?", + { modal: true }, + "Overwrite", + ); + if (overwrite !== "Overwrite") return; + } catch { + // File doesn't exist — proceed + } + + const hookContent = JSON.stringify(buildHookFileContent(), null, 4); + + await vscode.workspace.fs.createDirectory(hooksDir); + await vscode.workspace.fs.writeFile( + hookFile, + Buffer.from(hookContent + "\n", "utf-8"), + ); + + vscode.window.showInformationMessage( + "TaskSync hooks created at .github/hooks/tasksync-stop.json", + ); + }, + ); + context.subscriptions.push( sendMessageCmd, openHistoryCmd, @@ -318,6 +403,7 @@ export function activate(context: vscode.ExtensionContext): void { startRemoteLanCmd, stopRemoteCmd, goRemoteCmd, + setupHooksCmd, ); } From 727355823dd62e5dd953a96c316eff20e546e221 Mon Sep 17 00:00:00 2001 From: sgaabdu4 Date: Tue, 7 Apr 2026 21:14:03 +0100 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20address=20PR=20review=20=E2=80=94=20?= =?UTF-8?q?Uri.joinPath=20for=20remote=20workspaces,=20error=20handling,?= =?UTF-8?q?=20date/docs=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 +- README.md | 2 +- tasksync-chat/src/extension.ts | 47 +++++++++++++++------------------- 3 files changed, 23 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5a8ce1..dcf77f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file. -## TaskSync v3.0.11 (07-09-25) +## TaskSync v3.0.11 (04-07-26) - feat: add Copilot hooks enforcement (Stop + PreCompact) and `TaskSync: Setup Copilot Hooks` command ## TaskSync v3.0.10 (04-03-26) diff --git a/README.md b/README.md index 78a2fa3..4edc098 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ TaskSync includes [Copilot hooks](https://code.visualstudio.com/docs/copilot/cop - **Stop hook** — blocks the agent from stopping until it calls `ask_user` - **PreCompact hook** — reminds the agent to preserve `session_id` after context compaction -Requires VS Code 1.109.3+ and the `chat.agent.hooks` setting enabled. +Copilot Hooks require VS Code 1.109.3+ and the `chat.agent.hooks` setting enabled. The extension itself runs on older supported VS Code versions without hooks. ## Discussions diff --git a/tasksync-chat/src/extension.ts b/tasksync-chat/src/extension.ts index 69fa683..e7f4db9 100644 --- a/tasksync-chat/src/extension.ts +++ b/tasksync-chat/src/extension.ts @@ -1,4 +1,3 @@ -import * as path from "node:path"; import * as vscode from "vscode"; import { buildHookFileContent } from "./constants/hookContent"; import { @@ -21,14 +20,8 @@ async function ensureCopilotHooks(): Promise { const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; if (!workspaceFolder) return; - const hookFile = vscode.Uri.file( - path.join( - workspaceFolder.uri.fsPath, - ".github", - "hooks", - "tasksync-stop.json", - ), - ); + const hooksDir = vscode.Uri.joinPath(workspaceFolder.uri, ".github", "hooks"); + const hookFile = vscode.Uri.joinPath(hooksDir, "tasksync-stop.json"); try { await vscode.workspace.fs.stat(hookFile); @@ -36,10 +29,6 @@ async function ensureCopilotHooks(): Promise { } catch { // File doesn't exist — create it } - - const hooksDir = vscode.Uri.file( - path.join(workspaceFolder.uri.fsPath, ".github", "hooks"), - ); const content = JSON.stringify(buildHookFileContent(), null, 4); await vscode.workspace.fs.createDirectory(hooksDir); await vscode.workspace.fs.writeFile( @@ -359,12 +348,12 @@ export function activate(context: vscode.ExtensionContext): void { return; } - const hooksDir = vscode.Uri.file( - path.join(workspaceFolder.uri.fsPath, ".github", "hooks"), - ); - const hookFile = vscode.Uri.file( - path.join(hooksDir.fsPath, "tasksync-stop.json"), + const hooksDir = vscode.Uri.joinPath( + workspaceFolder.uri, + ".github", + "hooks", ); + const hookFile = vscode.Uri.joinPath(hooksDir, "tasksync-stop.json"); // Check if file already exists try { @@ -381,15 +370,21 @@ export function activate(context: vscode.ExtensionContext): void { const hookContent = JSON.stringify(buildHookFileContent(), null, 4); - await vscode.workspace.fs.createDirectory(hooksDir); - await vscode.workspace.fs.writeFile( - hookFile, - Buffer.from(hookContent + "\n", "utf-8"), - ); + try { + await vscode.workspace.fs.createDirectory(hooksDir); + await vscode.workspace.fs.writeFile( + hookFile, + Buffer.from(hookContent + "\n", "utf-8"), + ); - vscode.window.showInformationMessage( - "TaskSync hooks created at .github/hooks/tasksync-stop.json", - ); + vscode.window.showInformationMessage( + "TaskSync hooks created at .github/hooks/tasksync-stop.json", + ); + } catch (err) { + vscode.window.showErrorMessage( + `Failed to create TaskSync hooks: ${getSafeErrorMessage(err)}`, + ); + } }, ); From 4cf6ba24c09d5ad755c4f637881d51774ba17bb5 Mon Sep 17 00:00:00 2001 From: sgaabdu4 Date: Tue, 7 Apr 2026 21:15:48 +0100 Subject: [PATCH 3/5] chore: bump version to 3.0.12 --- CHANGELOG.md | 2 +- tasksync-chat/package-lock.json | 4 ++-- tasksync-chat/package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcf77f2..fcacadd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file. -## TaskSync v3.0.11 (04-07-26) +## TaskSync v3.0.12 (04-07-26) - feat: add Copilot hooks enforcement (Stop + PreCompact) and `TaskSync: Setup Copilot Hooks` command ## TaskSync v3.0.10 (04-03-26) diff --git a/tasksync-chat/package-lock.json b/tasksync-chat/package-lock.json index d4277c6..56769af 100644 --- a/tasksync-chat/package-lock.json +++ b/tasksync-chat/package-lock.json @@ -1,12 +1,12 @@ { "name": "tasksync-chat", - "version": "3.0.11", + "version": "3.0.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tasksync-chat", - "version": "3.0.11", + "version": "3.0.12", "license": "MIT", "dependencies": { "@vscode/codicons": "^0.0.36", diff --git a/tasksync-chat/package.json b/tasksync-chat/package.json index bab3495..59d3bac 100644 --- a/tasksync-chat/package.json +++ b/tasksync-chat/package.json @@ -4,7 +4,7 @@ "displayName": "TaskSync", "description": "Queue your prompts or tasks. Work uninterrupted.", "icon": "media/Tasksync-logo.png", - "version": "3.0.11", + "version": "3.0.12", "engines": { "vscode": "^1.90.0" }, From 5b73a78a86336ad965ec32c4c915d2eb81176298 Mon Sep 17 00:00:00 2001 From: sgaabdu4 Date: Tue, 7 Apr 2026 21:34:34 +0100 Subject: [PATCH 4/5] feat: add SubagentStart hook to prevent subagents from calling ask_user --- .github/hooks/tasksync-stop.json | 6 ++++++ tasksync-chat/src/constants/hookContent.ts | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/.github/hooks/tasksync-stop.json b/.github/hooks/tasksync-stop.json index a748ba8..56109c2 100644 --- a/.github/hooks/tasksync-stop.json +++ b/.github/hooks/tasksync-stop.json @@ -11,6 +11,12 @@ "type": "command", "command": "echo '{\"systemMessage\":\"CRITICAL: After compaction, continue calling ask_user with session_id on every turn. Reuse the session_id from your previous ask_user result. NEVER end without calling ask_user.\"}'" } + ], + "SubagentStart": [ + { + "type": "command", + "command": "echo '{\"systemMessage\":\"You are a subagent. DO NOT call ask_user. Complete your task and return results. STOP after returning.\"}'" + } ] } } diff --git a/tasksync-chat/src/constants/hookContent.ts b/tasksync-chat/src/constants/hookContent.ts index 05183c9..e4cf1c2 100644 --- a/tasksync-chat/src/constants/hookContent.ts +++ b/tasksync-chat/src/constants/hookContent.ts @@ -13,6 +13,9 @@ export const HOOK_STOP_REASON = export const HOOK_PRECOMPACT_MESSAGE = "CRITICAL: After compaction, continue calling ask_user with session_id on every turn. Reuse the session_id from your previous ask_user result. NEVER end without calling ask_user."; +export const HOOK_SUBAGENT_START_MESSAGE = + "You are a subagent. DO NOT call ask_user. Complete your task and return results. STOP after returning."; + /** Build the full hook file JSON object for writing to disk. */ export function buildHookFileContent(): object { return { @@ -29,6 +32,12 @@ export function buildHookFileContent(): object { command: `echo '{"systemMessage":"${HOOK_PRECOMPACT_MESSAGE}"}'`, }, ], + SubagentStart: [ + { + type: "command", + command: `echo '{"systemMessage":"${HOOK_SUBAGENT_START_MESSAGE}"}'`, + }, + ], }, }; } From f2eacd3bee1cdbbbbab422da5bf455c39c1a75c1 Mon Sep 17 00:00:00 2001 From: sgaabdu4 Date: Fri, 10 Apr 2026 01:29:31 +0100 Subject: [PATCH 5/5] feat: add global non-blocking Copilot hooks setup --- .github/copilot-instructions.md | 3 +- .github/hooks/tasksync-stop.json | 22 ---------- .github/hooks/tasksync.json | 23 +++++++++++ AGENTS.md | 5 +-- CHANGELOG.md | 2 +- README.md | 9 ++++- tasksync-chat/package.json | 4 +- tasksync-chat/src/constants/hookContent.ts | 38 ++++++++++++----- tasksync-chat/src/extension.ts | 47 +++++++++++----------- 9 files changed, 87 insertions(+), 66 deletions(-) delete mode 100644 .github/hooks/tasksync-stop.json create mode 100644 .github/hooks/tasksync.json diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 172b490..f36bb76 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -21,7 +21,6 @@ TaskSync is a human-in-the-loop workflow toolkit for AI-assisted development. Th - `tasksync-chat/vitest.config.ts` — Test config - `tasksync-chat/web/` — Remote access PWA (login, service worker) - `tasksync-chat/src/constants/hookContent.ts` — Copilot hook message SSOT -- `.github/hooks/tasksync-stop.json` — Copilot hooks (Stop + PreCompact) for ask_user enforcement - `Prompt/` — Standalone prompt/protocol markdown files (not actively developed) ## Build, Test, and Validate @@ -100,4 +99,4 @@ Follow OWASP Top 10 principles. Specific patterns enforced in this codebase: - Debounced history saves (2 s) for disk I/O performance. - Remote server uses WebSocket over HTTP with PIN-based auth and session tokens. - `esbuild.js` watch mode monitors `remoteConstants.ts` changes and rebuilds shared constants with 100ms debounce. -- **Copilot Hooks:** Hook messages are defined in `src/constants/hookContent.ts` (SSOT). `.github/hooks/tasksync-stop.json` is the committed copy for this repo. The `TaskSync: Setup Copilot Hooks` command generates these files in any workspace from the SSOT. Agent-scoped hooks can also be set in `.agent.md` frontmatter. +- **Copilot Hooks:** Hook messages are defined in `src/constants/hookContent.ts` (SSOT). The default hook file is generated to `~/.copilot/hooks/tasksync.json` and adds non-blocking `SessionStart`, `PreCompact`, and `SubagentStart` guidance. This repository also keeps a matching workspace copy at `.github/hooks/tasksync.json`. The `TaskSync: Setup Global Copilot Hooks` command generates the user-scoped hook file from the SSOT. Agent-scoped hooks can also be set in `.agent.md` frontmatter. diff --git a/.github/hooks/tasksync-stop.json b/.github/hooks/tasksync-stop.json deleted file mode 100644 index 56109c2..0000000 --- a/.github/hooks/tasksync-stop.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "hooks": { - "Stop": [ - { - "type": "command", - "command": "echo '{\"hookSpecificOutput\":{\"hookEventName\":\"Stop\",\"decision\":\"block\",\"reason\":\"MANDATORY: Call ask_user with session_id before ending. The user CANNOT see your response without ask_user. Reuse the session_id from your previous ask_user result. NEVER end without calling ask_user.\"}}'" - } - ], - "PreCompact": [ - { - "type": "command", - "command": "echo '{\"systemMessage\":\"CRITICAL: After compaction, continue calling ask_user with session_id on every turn. Reuse the session_id from your previous ask_user result. NEVER end without calling ask_user.\"}'" - } - ], - "SubagentStart": [ - { - "type": "command", - "command": "echo '{\"systemMessage\":\"You are a subagent. DO NOT call ask_user. Complete your task and return results. STOP after returning.\"}'" - } - ] - } -} diff --git a/.github/hooks/tasksync.json b/.github/hooks/tasksync.json new file mode 100644 index 0000000..cb2592c --- /dev/null +++ b/.github/hooks/tasksync.json @@ -0,0 +1,23 @@ +{ + "hooks": { + "SessionStart": [ + { + "type": "command", + "command": "echo '{\"hookSpecificOutput\":{\"hookEventName\":\"SessionStart\",\"additionalContext\":\"MANDATORY: You MUST call ask_user with session_id after EVERY completed task and whenever you need user input. Plain chat is invisible to the user. Reuse the previous session_id. NEVER finish silently.\"}}'" + } + ], + "PreCompact": [ + { + "type": "command", + "command": "echo '{\"systemMessage\":\"CRITICAL: You MUST keep calling ask_user with session_id on every turn after compaction. Reuse the previous session_id. NEVER end without ask_user.\"}'" + } + ], + "SubagentStart": [ + { + "type": "command", + "command": "echo '{\"hookSpecificOutput\":{\"hookEventName\":\"SubagentStart\",\"additionalContext\":\"MANDATORY: You are a subagent. NEVER call ask_user. Return results to the main agent, then STOP.\"}}'" + } + ] + }, + "version": 1 +} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 2db4513..f9b231f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,9 +23,6 @@ TaskSync/ ├── CHANGELOG.md # Release history ├── README.md # Project overview ├── LICENSE -├── .github/ -│ └── hooks/ -│ └── tasksync-stop.json # Copilot hooks (Stop + PreCompact) ├── Prompt/ # Standalone prompt/protocol markdown files │ ├── tasksync-v5.2.md │ ├── tasksync-v5.1.md @@ -140,7 +137,7 @@ These principles are mandatory for all changes: - Session state uses a boolean `sessionTerminated` flag — do not use string matching for termination detection. - Debounced history saves (2 s) are used for disk I/O performance. - New Session now supports a modal-first flow and starts a fresh Copilot chat session via `startFreshCopilotChatWithQuery`. -- **Copilot Hooks:** Hook messages are defined in `src/constants/hookContent.ts` (SSOT). `.github/hooks/tasksync-stop.json` is the committed copy for this repo. The `TaskSync: Setup Copilot Hooks` command generates these files in any workspace from the SSOT. Agent-scoped hooks can also be defined in `.agent.md` frontmatter. +- **Copilot Hooks:** Hook messages are defined in `src/constants/hookContent.ts` (SSOT). The default hook file is generated to `~/.copilot/hooks/tasksync.json` and adds non-blocking `SessionStart`, `PreCompact`, and `SubagentStart` guidance. This repository also keeps a matching workspace copy at `.github/hooks/tasksync.json`. The `TaskSync: Setup Global Copilot Hooks` command generates the user-scoped hook file from the SSOT. Agent-scoped hooks can also be defined in `.agent.md` frontmatter. - Remote Code Review is read-only by design: - Diff browsing is available (`/api/changes`, `/api/diff`) - Write operations (stage/unstage/discard/commit/push) are blocked remotely diff --git a/CHANGELOG.md b/CHANGELOG.md index fcacadd..83d3dd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ All notable changes to this project will be documented in this file. ## TaskSync v3.0.12 (04-07-26) -- feat: add Copilot hooks enforcement (Stop + PreCompact) and `TaskSync: Setup Copilot Hooks` command +- feat: add global Copilot hooks setup with non-blocking SessionStart, PreCompact, and SubagentStart guidance for `ask_user` ## TaskSync v3.0.10 (04-03-26) - feat: add agent orchestration toggle, single-session routing mode, and always-returned session_id tool payloads diff --git a/README.md b/README.md index 4edc098..713b277 100644 --- a/README.md +++ b/README.md @@ -82,10 +82,15 @@ Recommended settings for agent mode: ### Copilot Hooks (Preview) -TaskSync includes [Copilot hooks](https://code.visualstudio.com/docs/copilot/copilot-extensibility-overview#_hooks) that prevent the agent from ending without calling `ask_user`. Run **`TaskSync: Setup Copilot Hooks`** from the command palette to generate `.github/hooks/tasksync-stop.json` in your workspace. This adds: +TaskSync includes [Copilot hooks](https://code.visualstudio.com/docs/copilot/customization/hooks) that inject the `ask_user` contract at session start and preserve it through context compaction. Run **`TaskSync: Setup Global Copilot Hooks`** from the command palette to generate `~/.copilot/hooks/tasksync.json` in your user profile. This repo also keeps a matching workspace hook file at `.github/hooks/tasksync.json`. This adds: -- **Stop hook** — blocks the agent from stopping until it calls `ask_user` +- **SessionStart hook** — injects the `ask_user` contract when a session begins - **PreCompact hook** — reminds the agent to preserve `session_id` after context compaction +- **SubagentStart hook** — tells subagents not to call `ask_user` + +The default hook set is non-blocking, so it does not force extra turns at stop time. In this repository, the committed workspace hook file mirrors the same hook content, so workspace scope wins without changing behavior. + +This is a user-scoped setup by default, so it applies across workspaces. If a workspace hook exists for the same event, VS Code will prefer the workspace hook. Copilot Hooks require VS Code 1.109.3+ and the `chat.agent.hooks` setting enabled. The extension itself runs on older supported VS Code versions without hooks. diff --git a/tasksync-chat/package.json b/tasksync-chat/package.json index 59d3bac..a42a9b3 100644 --- a/tasksync-chat/package.json +++ b/tasksync-chat/package.json @@ -348,7 +348,7 @@ }, { "command": "tasksync.setupHooks", - "title": "TaskSync: Setup Copilot Hooks" + "title": "TaskSync: Setup Global Copilot Hooks" } ], "menus": { @@ -415,4 +415,4 @@ "typescript": "^5.3.3", "vitest": "^4.0.18" } -} +} \ No newline at end of file diff --git a/tasksync-chat/src/constants/hookContent.ts b/tasksync-chat/src/constants/hookContent.ts index e4cf1c2..6310c64 100644 --- a/tasksync-chat/src/constants/hookContent.ts +++ b/tasksync-chat/src/constants/hookContent.ts @@ -2,28 +2,43 @@ * Copilot hook content constants (SSOT) * * Used by: - * - extension.ts → setupHooksCmd (generates .github/hooks/tasksync-stop.json) - * - .github/hooks/tasksync-stop.json (committed copy for this repo) - * - tasksync.agent.md (agent-scoped hooks — wording should match) + * - extension.ts → setupHooksCmd (generates ~/.copilot/hooks/tasksync.json) */ -export const HOOK_STOP_REASON = - "MANDATORY: Call ask_user with session_id before ending. The user CANNOT see your response without ask_user. Reuse the session_id from your previous ask_user result. NEVER end without calling ask_user."; +export const HOOK_SESSION_START_MESSAGE = + "MANDATORY: You MUST call ask_user with session_id after EVERY completed task and whenever you need user input. Plain chat is invisible to the user. Reuse the previous session_id. NEVER finish silently."; export const HOOK_PRECOMPACT_MESSAGE = - "CRITICAL: After compaction, continue calling ask_user with session_id on every turn. Reuse the session_id from your previous ask_user result. NEVER end without calling ask_user."; + "CRITICAL: You MUST keep calling ask_user with session_id on every turn after compaction. Reuse the previous session_id. NEVER end without ask_user."; export const HOOK_SUBAGENT_START_MESSAGE = - "You are a subagent. DO NOT call ask_user. Complete your task and return results. STOP after returning."; + "MANDATORY: You are a subagent. NEVER call ask_user. Return results to the main agent, then STOP."; + +function buildAdditionalContextCommand( + hookEventName: "SessionStart" | "SubagentStart", + additionalContext: string, +): string { + const payload = JSON.stringify({ + hookSpecificOutput: { + hookEventName, + additionalContext, + }, + }); + + return `echo '${payload}'`; +} /** Build the full hook file JSON object for writing to disk. */ export function buildHookFileContent(): object { return { hooks: { - Stop: [ + SessionStart: [ { type: "command", - command: `echo '{"hookSpecificOutput":{"hookEventName":"Stop","decision":"block","reason":"${HOOK_STOP_REASON}"}}'`, + command: buildAdditionalContextCommand( + "SessionStart", + HOOK_SESSION_START_MESSAGE, + ), }, ], PreCompact: [ @@ -35,7 +50,10 @@ export function buildHookFileContent(): object { SubagentStart: [ { type: "command", - command: `echo '{"systemMessage":"${HOOK_SUBAGENT_START_MESSAGE}"}'`, + command: buildAdditionalContextCommand( + "SubagentStart", + HOOK_SUBAGENT_START_MESSAGE, + ), }, ], }, diff --git a/tasksync-chat/src/extension.ts b/tasksync-chat/src/extension.ts index e7f4db9..4e1593c 100644 --- a/tasksync-chat/src/extension.ts +++ b/tasksync-chat/src/extension.ts @@ -1,3 +1,5 @@ +import * as os from "node:os"; +import * as path from "node:path"; import * as vscode from "vscode"; import { buildHookFileContent } from "./constants/hookContent"; import { @@ -15,13 +17,24 @@ let webviewProvider: TaskSyncWebviewProvider | undefined; let contextManager: ContextManager | undefined; let remoteServer: RemoteServer | undefined; -/** Auto-create .github/hooks/tasksync-stop.json if workspace exists and file is missing. */ -async function ensureCopilotHooks(): Promise { - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; - if (!workspaceFolder) return; +const GLOBAL_HOOKS_DIR_PATH = path.join(os.homedir(), ".copilot", "hooks"); +const GLOBAL_HOOK_FILE_NAME = "tasksync.json"; +const GLOBAL_HOOK_FILE_DISPLAY_PATH = `~/.copilot/hooks/${GLOBAL_HOOK_FILE_NAME}`; + +function getGlobalHooksDirUri(): vscode.Uri { + return vscode.Uri.file(GLOBAL_HOOKS_DIR_PATH); +} - const hooksDir = vscode.Uri.joinPath(workspaceFolder.uri, ".github", "hooks"); - const hookFile = vscode.Uri.joinPath(hooksDir, "tasksync-stop.json"); +function getGlobalHookFileUri(): vscode.Uri { + return vscode.Uri.file( + path.join(GLOBAL_HOOKS_DIR_PATH, GLOBAL_HOOK_FILE_NAME), + ); +} + +/** Auto-create ~/.copilot/hooks/tasksync.json if it is missing. */ +async function ensureCopilotHooks(): Promise { + const hooksDir = getGlobalHooksDirUri(); + const hookFile = getGlobalHookFileUri(); try { await vscode.workspace.fs.stat(hookFile); @@ -336,30 +349,18 @@ export function activate(context: vscode.ExtensionContext): void { }, ); - // Setup Copilot hooks command — writes .github/hooks/ to workspace + // Setup Copilot hooks command — writes ~/.copilot/hooks/tasksync.json to user profile const setupHooksCmd = vscode.commands.registerCommand( "tasksync.setupHooks", async () => { - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; - if (!workspaceFolder) { - vscode.window.showErrorMessage( - "No workspace folder open. Open a folder first.", - ); - return; - } - - const hooksDir = vscode.Uri.joinPath( - workspaceFolder.uri, - ".github", - "hooks", - ); - const hookFile = vscode.Uri.joinPath(hooksDir, "tasksync-stop.json"); + const hooksDir = getGlobalHooksDirUri(); + const hookFile = getGlobalHookFileUri(); // Check if file already exists try { await vscode.workspace.fs.stat(hookFile); const overwrite = await vscode.window.showWarningMessage( - "tasksync-stop.json already exists. Overwrite?", + `${GLOBAL_HOOK_FILE_DISPLAY_PATH} already exists. Overwrite?`, { modal: true }, "Overwrite", ); @@ -378,7 +379,7 @@ export function activate(context: vscode.ExtensionContext): void { ); vscode.window.showInformationMessage( - "TaskSync hooks created at .github/hooks/tasksync-stop.json", + `TaskSync hooks created at ${GLOBAL_HOOK_FILE_DISPLAY_PATH}`, ); } catch (err) { vscode.window.showErrorMessage(