claude-dev-env 1.7.0 → 1.8.1

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.
@@ -0,0 +1,170 @@
1
+ ---
2
+ name: deep-research
3
+ description: Use this agent for iterative, multi-source deep research that produces comprehensive Obsidian reports with full citations. Official-docs-first methodology with anti-hallucination constraints. Examples:
4
+
5
+ <example>
6
+ Context: User wants thorough research on a technical topic
7
+ user: "Research the current state of WebSocket authentication best practices"
8
+ assistant: "I'll use the deep-research agent to conduct iterative multi-source research and produce a cited report."
9
+ <commentary>
10
+ Multi-source research requiring iteration and synthesis — exactly what deep-research handles.
11
+ </commentary>
12
+ </example>
13
+
14
+ <example>
15
+ Context: User needs a landscape survey with citations
16
+ user: "Compare the major vector database options for production RAG systems in 2026"
17
+ assistant: "I'll launch the deep-research agent to survey the landscape across multiple sources."
18
+ <commentary>
19
+ Broad survey requiring many sources, comparison, and synthesis — deep-research with exhaustive depth.
20
+ </commentary>
21
+ </example>
22
+
23
+ model: opus
24
+ color: cyan
25
+ ---
26
+
27
+ You are a Deep Research agent. You conduct thorough, iterative research across many sources and produce comprehensive, fully-cited reports saved to Obsidian.
28
+
29
+ You receive a `<research_brief>` from the orchestrating skill. Your job is to execute the research.
30
+
31
+ ## Setup
32
+
33
+ On receiving the research brief, write the state file:
34
+
35
+ `.deep-research-state.md`:
36
+
37
+ ```markdown
38
+ ---
39
+ topic: "[from brief]"
40
+ brief: "[one-line summary from brief]"
41
+ iteration: 0
42
+ max_iterations: [from brief]
43
+ status: researching
44
+ source_count: 0
45
+ official_docs_found: false
46
+ ---
47
+
48
+ ## Sources Found
49
+
50
+ (none yet)
51
+
52
+ ## Key Findings
53
+
54
+ (none yet)
55
+
56
+ ## Gaps Remaining
57
+
58
+ - Initial broad survey needed
59
+
60
+ ## Next Iteration Focus
61
+
62
+ - Locate official vendor/creator documentation for the topic
63
+ - Broad survey searches on the topic
64
+ - Identify major themes and authoritative sources
65
+ ```
66
+
67
+ Then immediately begin the first iteration.
68
+
69
+ ## Anti-Hallucination Constraints (ALWAYS ACTIVE)
70
+
71
+ These three constraints apply to every claim, finding, and recommendation. Violating any invalidates the work.
72
+
73
+ ### 1. Say "I don't know"
74
+ No credible source for a claim? Say so. Don't guess. Don't infer. Record the gap in the state file.
75
+
76
+ ### 2. Cite everything
77
+ Every claim must cite: an external source with URL, a named expert/paper/researcher, or official documentation. If you cannot find a supporting source, retract the claim.
78
+
79
+ ### 3. Direct quotes for factual grounding
80
+ Extract actual text from sources before analyzing. Ground responses in word-for-word quotes, not paraphrased summaries.
81
+
82
+ ## Iteration Protocol
83
+
84
+ Each iteration, follow these steps in order:
85
+
86
+ ### Step 1: Read State
87
+
88
+ Read `.deep-research-state.md`. Understand: sources found, key findings, remaining gaps, next focus.
89
+
90
+ First iteration? State is empty — start with official docs, then broad survey.
91
+
92
+ ### Step 2: Research (Search + Analyze)
93
+
94
+ Use available search and fetch tools aggressively and in parallel.
95
+
96
+ **Official docs first** — In early iterations, your primary objective is to locate and deeply read the official vendor/creator documentation for the topic. This means documentation published by the organization or person who created the tool, library, API, or protocol being researched. Exhaust official sources before broadening to secondary ones.
97
+
98
+ If no official documentation exists for the primary topic, record this explicitly as a gap in the state file. The absence of official docs is itself a finding — do not silently move on.
99
+
100
+ **Strategy by iteration phase:**
101
+ - **Early (1-3)**: Official docs first. Locate vendor/creator documentation. Read it deeply, extract direct quotes. Only after official sources are covered, begin broad survey to identify themes and secondary sources.
102
+ - **Middle (4-8)**: Deep dives into secondary sources. Fill gaps that official docs don't cover. Cross-reference secondary claims against official docs where possible.
103
+ - **Late (9+)**: Synthesis and gap-filling. Target remaining gaps, resolve contradictions between sources. Prefer official docs when sources disagree.
104
+
105
+ **Source classification** — When recording sources in the state file, tag each as:
106
+ - `[official]` — published by the vendor, creator, or maintainer of the tool/technology
107
+ - `[secondary]` — everything else (blog posts, tutorials, community content, third-party analysis)
108
+
109
+ ### Step 3: Update State
110
+
111
+ Update `.deep-research-state.md` with:
112
+ - New sources (title, URL, one-line relevance summary, [official] or [secondary] tag)
113
+ - Key findings with citations
114
+ - Updated gaps list
115
+ - Next iteration focus (specific queries and angles)
116
+ - Increment `iteration` and `source_count` in frontmatter
117
+ - Update `official_docs_found` if official docs were located
118
+
119
+ ### Step 4: Continue or Complete?
120
+
121
+ **Continue** if:
122
+ - Significant gaps remain
123
+ - Key questions from the brief are unanswered
124
+ - Promising leads not yet followed
125
+ - Source count below the brief's target depth
126
+ - Current iteration < max_iterations
127
+
128
+ **Complete** if:
129
+ - All key questions answered with citations
130
+ - Source target met or exceeded
131
+ - Remaining gaps are minor or out of scope
132
+ - Diminishing returns from further searching
133
+
134
+ If continuing, loop back to Step 1 for the next iteration. If complete, proceed to the Completion Process.
135
+
136
+ ### Completion Process
137
+
138
+ 1. Compile findings from state file into a structured report:
139
+ - Executive Summary (2-3 paragraphs, cite everything)
140
+ - Detailed Findings (organized by theme, not source; direct quotes blockquoted; every claim cited)
141
+ - Analysis (cross-cutting synthesis grounded in findings above)
142
+ - Limitations and Gaps (unanswered questions, source biases, whether official docs were available)
143
+ - Sources (numbered bibliography with [official]/[secondary] tags)
144
+ - Research Methodology (iterations, source count, date)
145
+
146
+ 2. The report must note whether official vendor/creator documentation was available for the topic. If it was not, this is a stated limitation — the user needs to know the research rests on secondary sources only.
147
+
148
+ 3. Write to Obsidian via `mcp__obsidian__write_note`:
149
+ - Path: `Research/[topic-slug].md`
150
+ - Include YAML frontmatter: type (deep-research), topic, date, sources count, iterations, official_docs_available (true/false), tags
151
+ - Every factual claim has an inline citation
152
+ - Full numbered bibliography at the end with [official]/[secondary] tags
153
+
154
+ 4. If Obsidian MCP is unavailable, output the full report in the conversation so the user can save it manually.
155
+
156
+ ### If max iterations reached without completion
157
+
158
+ - Compile what you have into a partial report
159
+ - Mark incomplete sections clearly
160
+ - Add "Future Research" section listing remaining gaps
161
+ - Still write to Obsidian
162
+
163
+ ## Output to Parent
164
+
165
+ After completion, your return message to the parent should include:
166
+ - Obsidian note path where the report was saved (or "output inline" if MCP unavailable)
167
+ - Total sources consulted (with official vs secondary breakdown)
168
+ - Total iterations used
169
+ - Whether official vendor documentation was found
170
+ - Any significant gaps or limitations
package/bin/install.mjs CHANGED
@@ -5,7 +5,6 @@ import { join, dirname, resolve, relative } from 'node:path';
5
5
  import { homedir } from 'node:os';
6
6
  import { execSync } from 'node:child_process';
7
7
  import { fileURLToPath } from 'node:url';
8
- import { createRequire } from 'node:module';
9
8
 
10
9
  const CLAUDE_HOME = join(homedir(), '.claude');
11
10
  const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
@@ -13,7 +12,39 @@ const MANIFEST_FILE = join(CLAUDE_HOME, '.claude-dev-env-manifest.json');
13
12
  const PACKAGE_NAME = 'claude-dev-env';
14
13
 
15
14
  const CONTENT_DIRECTORIES = ['rules', 'docs', 'commands', 'agents'];
16
- const WORKSPACE_SIBLINGS = ['claude-journal', 'claude-deep-research', 'claude-prompt-tools'];
15
+
16
+ const INSTALL_GROUPS = {
17
+ core: {
18
+ description: 'Development standards, hooks, agents, commands',
19
+ skills: [
20
+ 'anthropic-plan', 'everything-search', 'ingest',
21
+ 'npm-creator', 'pr-review-responder', 'readability-review',
22
+ 'recall', 'remember', 'rule-audit', 'rule-creator',
23
+ 'skill-writer', 'tdd-team'
24
+ ],
25
+ includeDirectories: ['rules', 'docs', 'commands', 'agents'],
26
+ includeAllHooks: true,
27
+ },
28
+ prompts: {
29
+ description: 'Prompt engineering tools',
30
+ skills: ['prompt-generator', 'agent-prompt'],
31
+ includeHookFiles: [
32
+ 'blocking/agent-execution-intent-gate.py',
33
+ 'blocking/prompt_workflow_gate_core.py',
34
+ 'blocking/prompt-workflow-stop-guard.py',
35
+ 'HOOK_SPECS_PROMPT_WORKFLOW.md',
36
+ ],
37
+ includeRules: ['prompt-workflow-context-controls.md'],
38
+ },
39
+ journal: {
40
+ description: 'Session logging and memory',
41
+ skills: ['dream', 'session-log', 'session-tidy'],
42
+ },
43
+ research: {
44
+ description: 'Deep research and citation tools',
45
+ skills: ['deep-research', 'research-mode'],
46
+ },
47
+ };
17
48
 
18
49
  function detectPython() {
19
50
  const candidates = [
@@ -116,74 +147,14 @@ function mergeHooks(pythonCommand) {
116
147
  return groupCount;
117
148
  }
118
149
 
119
- function findSiblingPackage(packageName) {
120
- const workspaceRoot = resolve(PACKAGE_ROOT, '..', '..');
121
- const workspacePath = join(workspaceRoot, 'packages', packageName);
122
- if (existsSync(join(workspacePath, 'package.json'))) {
123
- return workspacePath;
124
- }
125
- const require = createRequire(import.meta.url);
126
- try {
127
- const packageJsonPath = require.resolve(join(packageName, 'package.json'));
128
- return dirname(packageJsonPath);
129
- } catch { /* not found */ }
130
- return null;
131
- }
132
-
133
- function installSiblingContent(siblingRoot, allInstalledFiles) {
134
- const siblingName = JSON.parse(readFileSync(join(siblingRoot, 'package.json'), 'utf8')).name;
135
- console.log(` Installing sibling: ${siblingName}`);
136
- let totalFiles = 0;
137
- const skillsSource = join(siblingRoot, 'skills');
138
- if (existsSync(skillsSource)) {
139
- const skillDirs = readdirSync(skillsSource, { withFileTypes: true }).filter(entry => entry.isDirectory());
140
- for (const skillDir of skillDirs) {
141
- const stats = copyTree(join(skillsSource, skillDir.name), join(CLAUDE_HOME, 'skills', skillDir.name));
142
- allInstalledFiles.push(...stats.paths);
143
- totalFiles += stats.created + stats.updated;
144
- }
145
- }
146
- const agentsSource = join(siblingRoot, 'agents');
147
- if (existsSync(agentsSource)) {
148
- const stats = copyTree(agentsSource, join(CLAUDE_HOME, 'agents'));
149
- allInstalledFiles.push(...stats.paths);
150
- totalFiles += stats.created + stats.updated;
151
- }
152
- const rulesSource = join(siblingRoot, 'rules');
153
- if (existsSync(rulesSource)) {
154
- const stats = copyTree(rulesSource, join(CLAUDE_HOME, 'rules'));
155
- allInstalledFiles.push(...stats.paths);
156
- totalFiles += stats.created + stats.updated;
157
- }
158
- const hooksSource = join(siblingRoot, 'hooks');
159
- if (existsSync(hooksSource)) {
160
- const filesToCopy = collectFiles(hooksSource).filter(file => !file.endsWith('hooks.json'));
161
- const hooksDestination = join(CLAUDE_HOME, 'hooks');
162
- for (const sourceFile of filesToCopy) {
163
- const relativePath = relative(hooksSource, sourceFile);
164
- const destFile = join(hooksDestination, relativePath);
165
- mkdirSync(dirname(destFile), { recursive: true });
166
- const existed = existsSync(destFile);
167
- copyFileSync(sourceFile, destFile);
168
- allInstalledFiles.push(destFile);
169
- totalFiles++;
170
- if (existed) {
171
- console.log(` ↻ ${join('hooks', relativePath)} (updated)`);
172
- } else {
173
- console.log(` ✓ ${join('hooks', relativePath)} (new)`);
174
- }
175
- }
176
- }
177
- return totalFiles;
178
- }
179
-
180
150
  function writeManifest(installedFiles) {
181
151
  const manifest = { package: PACKAGE_NAME, version: '1.0.0', installedAt: new Date().toISOString(), files: installedFiles };
182
152
  writeFileSync(MANIFEST_FILE, JSON.stringify(manifest, null, 2) + '\n');
183
153
  }
184
154
 
185
- function install() {
186
- console.log(`\nInstalling ${PACKAGE_NAME}...\n`);
155
+ function install(selectedGroups) {
156
+ const groupLabel = selectedGroups ? `groups: ${selectedGroups.join(', ')}` : 'all';
157
+ console.log(`\nInstalling ${PACKAGE_NAME} (${groupLabel})...\n`);
187
158
  const pythonCommand = detectPython();
188
159
  if (!pythonCommand) {
189
160
  console.error('ERROR: Python 3 not found. Install Python 3.8+ and ensure python3, python, or py is on PATH.');
@@ -191,15 +162,52 @@ function install() {
191
162
  }
192
163
  console.log(` Python: ${pythonCommand}`);
193
164
  mkdirSync(CLAUDE_HOME, { recursive: true });
165
+
166
+ const allowedSkills = selectedGroups
167
+ ? new Set(selectedGroups.flatMap(groupName => INSTALL_GROUPS[groupName].skills || []))
168
+ : null;
169
+ const allowedDirectories = selectedGroups
170
+ ? new Set(selectedGroups.flatMap(groupName => INSTALL_GROUPS[groupName].includeDirectories || []))
171
+ : null;
172
+ const shouldInstallAllHooks = selectedGroups
173
+ ? selectedGroups.some(groupName => INSTALL_GROUPS[groupName].includeAllHooks)
174
+ : true;
175
+ const allowedHookFiles = selectedGroups
176
+ ? new Set(selectedGroups.flatMap(groupName => INSTALL_GROUPS[groupName].includeHookFiles || []))
177
+ : null;
178
+ const allowedRules = selectedGroups
179
+ ? new Set(selectedGroups.flatMap(groupName => INSTALL_GROUPS[groupName].includeRules || []))
180
+ : null;
181
+
194
182
  const allInstalledFiles = [];
195
183
  const summary = {};
196
184
  for (const directory of CONTENT_DIRECTORIES) {
185
+ const hasFullAccess = !allowedDirectories || allowedDirectories.has(directory);
186
+ const hasPartialRules = directory === 'rules' && allowedRules && allowedRules.size > 0;
187
+ if (!hasFullAccess && !hasPartialRules) continue;
197
188
  const sourceDir = join(PACKAGE_ROOT, directory);
198
189
  if (!existsSync(sourceDir)) continue;
199
190
  const destDir = join(CLAUDE_HOME, directory);
200
- const stats = copyTree(sourceDir, destDir);
201
- summary[directory] = stats;
202
- allInstalledFiles.push(...stats.paths);
191
+ if (hasFullAccess) {
192
+ const stats = copyTree(sourceDir, destDir);
193
+ summary[directory] = stats;
194
+ allInstalledFiles.push(...stats.paths);
195
+ } else if (hasPartialRules) {
196
+ let rulesCreated = 0;
197
+ let rulesUpdated = 0;
198
+ for (const ruleFile of allowedRules) {
199
+ const sourcePath = join(sourceDir, ruleFile);
200
+ if (!existsSync(sourcePath)) continue;
201
+ const destPath = join(destDir, ruleFile);
202
+ mkdirSync(dirname(destPath), { recursive: true });
203
+ const existed = existsSync(destPath);
204
+ copyFileSync(sourcePath, destPath);
205
+ allInstalledFiles.push(destPath);
206
+ if (existed) { rulesUpdated++; } else { rulesCreated++; }
207
+ console.log(` ${existed ? '\u21bb' : '\u2713'} ${join(directory, ruleFile)} (${existed ? 'updated' : 'new'})`);
208
+ }
209
+ summary[directory] = { created: rulesCreated, updated: rulesUpdated, paths: [] };
210
+ }
203
211
  }
204
212
  const skillsSource = join(PACKAGE_ROOT, 'skills');
205
213
  if (existsSync(skillsSource)) {
@@ -208,6 +216,7 @@ function install() {
208
216
  let skillsUpdated = 0;
209
217
  const skillPaths = [];
210
218
  for (const skillDir of skillDirs) {
219
+ if (allowedSkills && !allowedSkills.has(skillDir.name)) continue;
211
220
  const stats = copyTree(join(skillsSource, skillDir.name), join(CLAUDE_HOME, 'skills', skillDir.name));
212
221
  skillsCreated += stats.created;
213
222
  skillsUpdated += stats.updated;
@@ -216,36 +225,35 @@ function install() {
216
225
  summary.skills = { created: skillsCreated, updated: skillsUpdated, paths: skillPaths };
217
226
  allInstalledFiles.push(...skillPaths);
218
227
  }
219
- let siblingCount = 0;
220
- for (const siblingName of WORKSPACE_SIBLINGS) {
221
- const siblingRoot = findSiblingPackage(siblingName);
222
- if (siblingRoot) {
223
- siblingCount += installSiblingContent(siblingRoot, allInstalledFiles);
224
- }
225
- }
226
- if (siblingCount > 0) {
227
- summary.siblings = siblingCount;
228
- }
229
- const hooksSource = join(PACKAGE_ROOT, 'hooks');
230
- if (existsSync(hooksSource)) {
231
- const hooksDestination = join(CLAUDE_HOME, 'hooks');
232
- const filesToCopy = collectFiles(hooksSource).filter(file => !file.endsWith('hooks.json'));
233
- let hooksCreated = 0;
234
- let hooksUpdated = 0;
235
- for (const sourceFile of filesToCopy) {
236
- const relativePath = relative(hooksSource, sourceFile);
237
- const destFile = join(hooksDestination, relativePath);
238
- mkdirSync(dirname(destFile), { recursive: true });
239
- const existed = existsSync(destFile);
240
- copyFileSync(sourceFile, destFile);
241
- allInstalledFiles.push(destFile);
242
- if (existed) { hooksUpdated++; } else { hooksCreated++; }
228
+ const shouldInstallAnyHooks = shouldInstallAllHooks || (allowedHookFiles && allowedHookFiles.size > 0);
229
+ if (shouldInstallAnyHooks) {
230
+ const hooksSource = join(PACKAGE_ROOT, 'hooks');
231
+ if (existsSync(hooksSource)) {
232
+ const hooksDestination = join(CLAUDE_HOME, 'hooks');
233
+ const filesToCopy = collectFiles(hooksSource)
234
+ .filter(file => !file.endsWith('hooks.json'))
235
+ .filter(file => {
236
+ if (shouldInstallAllHooks) return true;
237
+ const relativePath = relative(hooksSource, file).replace(/\\/g, '/');
238
+ return allowedHookFiles.has(relativePath);
239
+ });
240
+ let hooksCreated = 0;
241
+ let hooksUpdated = 0;
242
+ for (const sourceFile of filesToCopy) {
243
+ const relativePath = relative(hooksSource, sourceFile);
244
+ const destFile = join(hooksDestination, relativePath);
245
+ mkdirSync(dirname(destFile), { recursive: true });
246
+ const existed = existsSync(destFile);
247
+ copyFileSync(sourceFile, destFile);
248
+ allInstalledFiles.push(destFile);
249
+ if (existed) { hooksUpdated++; } else { hooksCreated++; }
250
+ }
251
+ summary.hookFiles = { created: hooksCreated, updated: hooksUpdated };
252
+ console.log(` Hook files: ${hooksCreated} new, ${hooksUpdated} updated`);
253
+ const groupCount = mergeHooks(pythonCommand);
254
+ summary.hookGroups = groupCount;
255
+ console.log(` Hook groups: ${groupCount} merged into settings.json`);
243
256
  }
244
- summary.hookFiles = { created: hooksCreated, updated: hooksUpdated };
245
- console.log(` Hook files: ${hooksCreated} new, ${hooksUpdated} updated`);
246
- const groupCount = mergeHooks(pythonCommand);
247
- summary.hookGroups = groupCount;
248
- console.log(` Hook groups: ${groupCount} merged into settings.json`);
249
257
  }
250
258
  writeManifest(allInstalledFiles);
251
259
  console.log(`\nInstalled ${PACKAGE_NAME}:`);
@@ -259,9 +267,6 @@ function install() {
259
267
  const { created, updated } = summary.skills;
260
268
  console.log(` skills: ${created + updated} files (${created} new, ${updated} updated)`);
261
269
  }
262
- if (summary.siblings) {
263
- console.log(` workspace siblings: ${summary.siblings} files`);
264
- }
265
270
  if (summary.hookFiles) {
266
271
  console.log(` hooks: ${summary.hookFiles.created + summary.hookFiles.updated} files, ${summary.hookGroups} groups in settings.json`);
267
272
  }
@@ -319,15 +324,45 @@ function printHelp() {
319
324
  ${PACKAGE_NAME} - Claude Code development standards installer
320
325
 
321
326
  Usage:
322
- npx ${PACKAGE_NAME} Install rules, hooks, agents, commands, skills
323
- npx ${PACKAGE_NAME} --uninstall Remove installed files and hooks
327
+ npx ${PACKAGE_NAME} Install everything
328
+ npx ${PACKAGE_NAME} --only X Install specific groups
329
+ npx ${PACKAGE_NAME} --uninstall Remove installed files
324
330
  npx ${PACKAGE_NAME} --help Show this help
325
331
 
332
+ Groups:
333
+ core Development standards, hooks, agents, commands
334
+ prompts Prompt engineering tools
335
+ journal Session logging and memory
336
+ research Deep research and citation tools
337
+
338
+ Examples:
339
+ npx ${PACKAGE_NAME} --only prompts
340
+ npx ${PACKAGE_NAME} --only prompts,research
341
+
326
342
  Install location: ~/.claude/
327
343
  `);
328
344
  }
329
345
 
330
346
  const args = process.argv.slice(2);
331
- if (args.includes('--help') || args.includes('-h')) printHelp();
332
- else if (args.includes('--uninstall')) uninstall();
333
- else install();
347
+ if (args.includes('--help') || args.includes('-h')) {
348
+ printHelp();
349
+ } else if (args.includes('--uninstall')) {
350
+ uninstall();
351
+ } else {
352
+ const onlyIndex = args.indexOf('--only');
353
+ let selectedGroups = null;
354
+ if (onlyIndex !== -1) {
355
+ const onlyValue = args[onlyIndex + 1];
356
+ if (!onlyValue || onlyValue.startsWith('--')) {
357
+ console.error(`ERROR: --only requires a comma-separated list of groups.\nAvailable groups: ${Object.keys(INSTALL_GROUPS).join(', ')}`);
358
+ process.exit(1);
359
+ }
360
+ selectedGroups = onlyValue.split(',').map(name => name.trim());
361
+ const invalidGroups = selectedGroups.filter(name => !INSTALL_GROUPS[name]);
362
+ if (invalidGroups.length > 0) {
363
+ console.error(`ERROR: Unknown group(s): ${invalidGroups.join(', ')}\nAvailable groups: ${Object.keys(INSTALL_GROUPS).join(', ')}`);
364
+ process.exit(1);
365
+ }
366
+ }
367
+ install(selectedGroups);
368
+ }
@@ -0,0 +1,68 @@
1
+ # Prompt Workflow Hook Specs
2
+
3
+ Deterministic runtime gates for prompt workflows.
4
+
5
+ ## Gate: Execution Intent (PreToolUse Task/Agent)
6
+
7
+ - Hook: `hooks/blocking/agent-execution-intent-gate.py`
8
+ - Event: `PreToolUse`
9
+ - Matcher: `Task|Agent`
10
+ - Fail condition:
11
+ - Missing structured execution intent contract field:
12
+ - `tool_input.execution_intent: explicit|execute|delegate`, or
13
+ - `tool_input.execution_intent_explicit: true`, or
14
+ - `tool_input.metadata.execution_intent: explicit|execute|delegate`
15
+ - Missing required scope anchors in launch payload (always enforced when execution launch is evaluated)
16
+ - Compatibility fallback:
17
+ - Text markers are only accepted when `PROMPT_WORKFLOW_ALLOW_TEXT_INTENT_FALLBACK=1` is set.
18
+ - Fallback usage is logged to stderr.
19
+ - Action: `deny` with concrete missing requirement list.
20
+
21
+ ## Gate: Leakage + Checklist + Scope (Stop)
22
+
23
+ - Hook: `hooks/blocking/prompt-workflow-stop-guard.py`
24
+ - Event: `Stop`
25
+ - Fail condition:
26
+ - Raw internal refinement object appears in assistant output without explicit debug intent
27
+ - Prompt-workflow response detected but deterministic checklist container is missing
28
+ - Prompt-workflow response detected and required deterministic checklist rows are missing
29
+ - Prompt-workflow response detected and required scope anchors are missing
30
+ - Prompt-workflow response detected and runtime context-control signals are missing
31
+ - Scope-bound text uses banned ambiguous scope terms
32
+ - Action: `block` with correction reason.
33
+
34
+ ## Required Scope Anchors
35
+
36
+ - `target_local_roots`
37
+ - `target_canonical_roots`
38
+ - `target_file_globs`
39
+ - `comparison_basis`
40
+ - `completion_boundary`
41
+
42
+ ## Required Deterministic Checklist Rows
43
+
44
+ - `structured_scoped_instructions`
45
+ - `sequential_steps_present`
46
+ - `positive_framing`
47
+ - `acceptance_criteria_defined`
48
+ - `safety_reversibility_language`
49
+ - `no_destructive_shortcuts_guidance`
50
+ - `concrete_output_contract`
51
+ - `scope_boundary_present`
52
+ - `explicit_scope_anchors_present`
53
+ - `all_instructions_artifact_bound`
54
+ - `no_ambiguous_scope_terms`
55
+ - `completion_boundary_measurable`
56
+ - `citation_grounding_policy_present`
57
+ - `source_priority_rules_present`
58
+
59
+ ## Runtime Context-Control Signals
60
+
61
+ - `base_minimal_instruction_layer: true`
62
+ - `on_demand_skill_loading: true`
63
+
64
+ These two signals are runtime-checked by the Stop guard whenever a prompt-workflow response is detected.
65
+
66
+ ## Deterministic Boundary
67
+
68
+ These hooks enforce only structural/runtime checks. Semantic quality remains in auditor layer.
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env python3
2
+ """PreToolUse gate for Task/Agent execution intent and scope anchors."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import os
8
+ import sys
9
+
10
+ from prompt_workflow_gate_core import (
11
+ has_explicit_execution_intent,
12
+ has_structured_execution_intent,
13
+ missing_scope_anchors,
14
+ )
15
+
16
+
17
+ def _deny(reason: str) -> None:
18
+ response = {
19
+ "hookSpecificOutput": {
20
+ "hookEventName": "PreToolUse",
21
+ "permissionDecision": "deny",
22
+ "permissionDecisionReason": reason,
23
+ }
24
+ }
25
+ print(json.dumps(response))
26
+
27
+
28
+ def main() -> None:
29
+ try:
30
+ hook_input = json.load(sys.stdin)
31
+ except json.JSONDecodeError:
32
+ sys.exit(0)
33
+
34
+ tool_name = str(hook_input.get("tool_name", ""))
35
+ if tool_name not in {"Task", "Agent"}:
36
+ sys.exit(0)
37
+
38
+ tool_input = hook_input.get("tool_input", {})
39
+ prompt_text = str(tool_input.get("prompt", ""))
40
+ description = str(tool_input.get("description", ""))
41
+ combined_text = f"{description}\n{prompt_text}"
42
+
43
+ if not has_structured_execution_intent(tool_input):
44
+ allow_text_fallback = os.getenv(
45
+ "PROMPT_WORKFLOW_ALLOW_TEXT_INTENT_FALLBACK", ""
46
+ ).strip().lower() in {"1", "true", "yes"}
47
+ text_intent_detected = has_explicit_execution_intent(combined_text)
48
+ if allow_text_fallback and text_intent_detected:
49
+ print(
50
+ "PROMPT-WORKFLOW GATE: compatibility text-intent fallback used; "
51
+ "structured execution intent contract should be provided.",
52
+ file=sys.stderr,
53
+ )
54
+ else:
55
+ fallback_note = ""
56
+ if text_intent_detected:
57
+ print(
58
+ "PROMPT-WORKFLOW GATE: text intent marker detected without structured "
59
+ "execution intent contract.",
60
+ file=sys.stderr,
61
+ )
62
+ fallback_note = " Legacy text marker was detected but is not sufficient."
63
+ _deny(
64
+ "BLOCKED: Missing structured execution intent signal for Agent/Task launch. "
65
+ "Provide `tool_input.execution_intent: explicit` or "
66
+ "`tool_input.execution_intent_explicit: true`."
67
+ + fallback_note
68
+ )
69
+ sys.exit(0)
70
+
71
+ missing_anchors = missing_scope_anchors(combined_text)
72
+ if missing_anchors:
73
+ _deny(
74
+ "BLOCKED: Scope anchors missing for prompt workflow execution: "
75
+ + ", ".join(missing_anchors)
76
+ )
77
+ sys.exit(0)
78
+
79
+ sys.exit(0)
80
+
81
+
82
+ if __name__ == "__main__":
83
+ main()