diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2bf3bae..f36bb76 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -20,6 +20,7 @@ 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 - `Prompt/` — Standalone prompt/protocol markdown files (not actively developed) ## Build, Test, and Validate @@ -98,3 +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). 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.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 5e11a35..f9b231f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -137,6 +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). 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 e5e1008..83d3dd5 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.12 (04-07-26) +- 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 - 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..713b277 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,20 @@ 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/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: + +- **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. + ## 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-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 d754c59..a42a9b3 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" }, @@ -345,6 +345,10 @@ "command": "tasksync.goRemote", "title": "Remote", "icon": "$(broadcast)" + }, + { + "command": "tasksync.setupHooks", + "title": "TaskSync: Setup Global Copilot Hooks" } ], "menus": { @@ -411,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 new file mode 100644 index 0000000..6310c64 --- /dev/null +++ b/tasksync-chat/src/constants/hookContent.ts @@ -0,0 +1,61 @@ +/** + * Copilot hook content constants (SSOT) + * + * Used by: + * - extension.ts → setupHooksCmd (generates ~/.copilot/hooks/tasksync.json) + */ + +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: 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 = + "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: { + SessionStart: [ + { + type: "command", + command: buildAdditionalContextCommand( + "SessionStart", + HOOK_SESSION_START_MESSAGE, + ), + }, + ], + PreCompact: [ + { + type: "command", + command: `echo '{"systemMessage":"${HOOK_PRECOMPACT_MESSAGE}"}'`, + }, + ], + SubagentStart: [ + { + type: "command", + command: buildAdditionalContextCommand( + "SubagentStart", + HOOK_SUBAGENT_START_MESSAGE, + ), + }, + ], + }, + }; +} diff --git a/tasksync-chat/src/extension.ts b/tasksync-chat/src/extension.ts index 3f955e9..4e1593c 100644 --- a/tasksync-chat/src/extension.ts +++ b/tasksync-chat/src/extension.ts @@ -1,4 +1,7 @@ +import * as os from "node:os"; +import * as path from "node:path"; import * as vscode from "vscode"; +import { buildHookFileContent } from "./constants/hookContent"; import { CONFIG_SECTION, DEFAULT_REMOTE_PORT, @@ -14,6 +17,39 @@ let webviewProvider: TaskSyncWebviewProvider | undefined; let contextManager: ContextManager | undefined; let remoteServer: RemoteServer | undefined; +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); +} + +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); + return; // File already exists — nothing to do + } catch { + // File doesn't exist — create it + } + 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 +76,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 +349,46 @@ export function activate(context: vscode.ExtensionContext): void { }, ); + // Setup Copilot hooks command — writes ~/.copilot/hooks/tasksync.json to user profile + const setupHooksCmd = vscode.commands.registerCommand( + "tasksync.setupHooks", + async () => { + const hooksDir = getGlobalHooksDirUri(); + const hookFile = getGlobalHookFileUri(); + + // Check if file already exists + try { + await vscode.workspace.fs.stat(hookFile); + const overwrite = await vscode.window.showWarningMessage( + `${GLOBAL_HOOK_FILE_DISPLAY_PATH} already exists. Overwrite?`, + { modal: true }, + "Overwrite", + ); + if (overwrite !== "Overwrite") return; + } catch { + // File doesn't exist — proceed + } + + const hookContent = JSON.stringify(buildHookFileContent(), null, 4); + + 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 ${GLOBAL_HOOK_FILE_DISPLAY_PATH}`, + ); + } catch (err) { + vscode.window.showErrorMessage( + `Failed to create TaskSync hooks: ${getSafeErrorMessage(err)}`, + ); + } + }, + ); + context.subscriptions.push( sendMessageCmd, openHistoryCmd, @@ -318,6 +399,7 @@ export function activate(context: vscode.ExtensionContext): void { startRemoteLanCmd, stopRemoteCmd, goRemoteCmd, + setupHooksCmd, ); }