@shirlytaylor73/superharness 1.5.0

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 (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +202 -0
  3. package/bin/lib/codex-installer.js +228 -0
  4. package/bin/lib/interactive-select.js +96 -0
  5. package/bin/superharness.js +67 -0
  6. package/package.json +52 -0
  7. package/plugins/superharness/.claude-plugin/plugin.json +19 -0
  8. package/plugins/superharness/.codex-plugin/plugin.json +31 -0
  9. package/plugins/superharness/.mcp.json +9 -0
  10. package/plugins/superharness/CODE_OF_CONDUCT.md +79 -0
  11. package/plugins/superharness/LICENSE +21 -0
  12. package/plugins/superharness/README.md +57 -0
  13. package/plugins/superharness/agents/code-reviewer.md +48 -0
  14. package/plugins/superharness/archived-skills/using-superpowers/SKILL.md +140 -0
  15. package/plugins/superharness/archived-skills/using-superpowers/references/codex-tools.md +25 -0
  16. package/plugins/superharness/archived-skills/using-superpowers/references/copilot-tools.md +52 -0
  17. package/plugins/superharness/archived-skills/using-superpowers/references/gemini-tools.md +33 -0
  18. package/plugins/superharness/archived-skills/using-superpowers/references/hermes-tools.md +44 -0
  19. package/plugins/superharness/commands/free.md +6 -0
  20. package/plugins/superharness/commands/rollback.md +30 -0
  21. package/plugins/superharness/commands-codex/free.md +29 -0
  22. package/plugins/superharness/commands-codex/rollback.md +33 -0
  23. package/plugins/superharness/hooks/hooks-codex.json +50 -0
  24. package/plugins/superharness/hooks/hooks.json +50 -0
  25. package/plugins/superharness/hooks/lib/free-mode-check.mjs +27 -0
  26. package/plugins/superharness/hooks/run-hook.cmd +58 -0
  27. package/plugins/superharness/hooks/workflow-context +4 -0
  28. package/plugins/superharness/hooks/workflow-context.mjs +184 -0
  29. package/plugins/superharness/hooks/workflow-post-transition +4 -0
  30. package/plugins/superharness/hooks/workflow-post-transition.mjs +89 -0
  31. package/plugins/superharness/hooks/workflow-pre-tool-use +4 -0
  32. package/plugins/superharness/hooks/workflow-pre-tool-use.mjs +97 -0
  33. package/plugins/superharness/hooks/workflow-stop +4 -0
  34. package/plugins/superharness/hooks/workflow-stop.mjs +136 -0
  35. package/plugins/superharness/scripts/rollback.mjs +86 -0
  36. package/plugins/superharness/scripts/set-free-mode.mjs +77 -0
  37. package/plugins/superharness/skills/brainstorming/SKILL.md +182 -0
  38. package/plugins/superharness/skills/brainstorming/scripts/frame-template.html +214 -0
  39. package/plugins/superharness/skills/brainstorming/scripts/helper.js +88 -0
  40. package/plugins/superharness/skills/brainstorming/scripts/server.cjs +338 -0
  41. package/plugins/superharness/skills/brainstorming/scripts/start-server.sh +153 -0
  42. package/plugins/superharness/skills/brainstorming/scripts/stop-server.sh +55 -0
  43. package/plugins/superharness/skills/brainstorming/spec-document-reviewer-prompt.md +49 -0
  44. package/plugins/superharness/skills/brainstorming/visual-companion.md +286 -0
  45. package/plugins/superharness/skills/chinese-code-review/SKILL.md +277 -0
  46. package/plugins/superharness/skills/chinese-commit-conventions/SKILL.md +364 -0
  47. package/plugins/superharness/skills/chinese-documentation/SKILL.md +448 -0
  48. package/plugins/superharness/skills/chinese-git-workflow/SKILL.md +547 -0
  49. package/plugins/superharness/skills/dispatching-parallel-agents/SKILL.md +186 -0
  50. package/plugins/superharness/skills/exploration/SKILL.md +197 -0
  51. package/plugins/superharness/skills/finishing/SKILL.md +200 -0
  52. package/plugins/superharness/skills/intake/SKILL.md +134 -0
  53. package/plugins/superharness/skills/mcp-builder/SKILL.md +255 -0
  54. package/plugins/superharness/skills/parallel-execution/SKILL.md +368 -0
  55. package/plugins/superharness/skills/parallel-execution/implementer-prompt.md +144 -0
  56. package/plugins/superharness/skills/parallel-execution/spec-reviewer-prompt.md +84 -0
  57. package/plugins/superharness/skills/parallel-execution/wave-final-manual-qa-prompt.md +61 -0
  58. package/plugins/superharness/skills/parallel-execution/wave-final-quality-prompt.md +59 -0
  59. package/plugins/superharness/skills/parallel-execution/wave-final-scope-fidelity-prompt.md +69 -0
  60. package/plugins/superharness/skills/parallel-execution/wave-final-spec-prompt.md +56 -0
  61. package/plugins/superharness/skills/planning/SKILL.md +265 -0
  62. package/plugins/superharness/skills/planning/plan-document-reviewer-prompt.md +80 -0
  63. package/plugins/superharness/skills/receiving-code-review/SKILL.md +213 -0
  64. package/plugins/superharness/skills/requesting-code-review/SKILL.md +107 -0
  65. package/plugins/superharness/skills/requesting-code-review/code-reviewer.md +146 -0
  66. package/plugins/superharness/skills/serial-execution/SKILL.md +183 -0
  67. package/plugins/superharness/skills/systematic-debugging/CREATION-LOG.md +119 -0
  68. package/plugins/superharness/skills/systematic-debugging/SKILL.md +320 -0
  69. package/plugins/superharness/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
  70. package/plugins/superharness/skills/systematic-debugging/condition-based-waiting.md +115 -0
  71. package/plugins/superharness/skills/systematic-debugging/defense-in-depth.md +122 -0
  72. package/plugins/superharness/skills/systematic-debugging/find-polluter.sh +63 -0
  73. package/plugins/superharness/skills/systematic-debugging/root-cause-tracing.md +169 -0
  74. package/plugins/superharness/skills/systematic-debugging/test-academic.md +14 -0
  75. package/plugins/superharness/skills/systematic-debugging/test-pressure-1.md +58 -0
  76. package/plugins/superharness/skills/systematic-debugging/test-pressure-2.md +68 -0
  77. package/plugins/superharness/skills/systematic-debugging/test-pressure-3.md +69 -0
  78. package/plugins/superharness/skills/test-driven-development/SKILL.md +371 -0
  79. package/plugins/superharness/skills/test-driven-development/testing-anti-patterns.md +299 -0
  80. package/plugins/superharness/skills/trivial/SKILL.md +118 -0
  81. package/plugins/superharness/skills/using-git-worktrees/SKILL.md +218 -0
  82. package/plugins/superharness/skills/verification/SKILL.md +139 -0
  83. package/plugins/superharness/skills/workflow-runner/SKILL.md +172 -0
  84. package/plugins/superharness/skills/writing-skills/SKILL.md +655 -0
  85. package/plugins/superharness/skills/writing-skills/anthropic-best-practices.md +1149 -0
  86. package/plugins/superharness/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
  87. package/plugins/superharness/skills/writing-skills/graphviz-conventions.dot +172 -0
  88. package/plugins/superharness/skills/writing-skills/persuasion-principles.md +187 -0
  89. package/plugins/superharness/skills/writing-skills/render-graphs.js +168 -0
  90. package/plugins/superharness/skills/writing-skills/testing-skills-with-subagents.md +385 -0
  91. package/plugins/superharness/workflow/default-workflow.yaml +84 -0
  92. package/plugins/superharness/workflow-state-server/bootstrap.js +44 -0
  93. package/plugins/superharness/workflow-state-server/package-lock.json +2853 -0
  94. package/plugins/superharness/workflow-state-server/package.json +22 -0
  95. package/plugins/superharness/workflow-state-server/render-context.js +124 -0
  96. package/plugins/superharness/workflow-state-server/schema.sql +39 -0
  97. package/plugins/superharness/workflow-state-server/server.js +290 -0
  98. package/plugins/superharness/workflow-state-server/state.js +424 -0
  99. package/plugins/superharness/workflow-state-server/validate-workflow.js +165 -0
@@ -0,0 +1,50 @@
1
+ {
2
+ "hooks": {
3
+ "UserPromptSubmit": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "\"${PLUGIN_ROOT}/hooks/run-hook.cmd\" workflow-context",
9
+ "async": false
10
+ }
11
+ ]
12
+ }
13
+ ],
14
+ "PreToolUse": [
15
+ {
16
+ "matcher": "Bash|apply_patch|Write|Edit",
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "\"${PLUGIN_ROOT}/hooks/run-hook.cmd\" workflow-pre-tool-use",
21
+ "async": false
22
+ }
23
+ ]
24
+ }
25
+ ],
26
+ "PostToolUse": [
27
+ {
28
+ "matcher": "mcp__plugin_superharness_superharness-workflow-state__transition_state",
29
+ "hooks": [
30
+ {
31
+ "type": "command",
32
+ "command": "\"${PLUGIN_ROOT}/hooks/run-hook.cmd\" workflow-post-transition",
33
+ "async": false
34
+ }
35
+ ]
36
+ }
37
+ ],
38
+ "Stop": [
39
+ {
40
+ "hooks": [
41
+ {
42
+ "type": "command",
43
+ "command": "\"${PLUGIN_ROOT}/hooks/run-hook.cmd\" workflow-stop",
44
+ "async": false
45
+ }
46
+ ]
47
+ }
48
+ ]
49
+ }
50
+ }
@@ -0,0 +1,50 @@
1
+ {
2
+ "hooks": {
3
+ "UserPromptSubmit": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" workflow-context",
9
+ "async": false
10
+ }
11
+ ]
12
+ }
13
+ ],
14
+ "PreToolUse": [
15
+ {
16
+ "matcher": "Write|Edit|MultiEdit|Bash",
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" workflow-pre-tool-use",
21
+ "async": false
22
+ }
23
+ ]
24
+ }
25
+ ],
26
+ "PostToolUse": [
27
+ {
28
+ "matcher": "mcp__plugin_superharness_superharness-workflow-state__transition_state",
29
+ "hooks": [
30
+ {
31
+ "type": "command",
32
+ "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" workflow-post-transition",
33
+ "async": false
34
+ }
35
+ ]
36
+ }
37
+ ],
38
+ "Stop": [
39
+ {
40
+ "hooks": [
41
+ {
42
+ "type": "command",
43
+ "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" workflow-stop",
44
+ "async": false
45
+ }
46
+ ]
47
+ }
48
+ ]
49
+ }
50
+ }
@@ -0,0 +1,27 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+ import { pathToFileURL } from 'node:url';
4
+
5
+ /**
6
+ * Sync-ish check of free_mode flag without depending on state.js statically.
7
+ * Opens DB, reads free_mode, returns false on any error (fail-open).
8
+ */
9
+ export async function isFreeMode({ pluginRoot, workspaceRoot }) {
10
+ const dbPath = process.env.SUPERHARNESS_WORKFLOW_STATE_DB
11
+ || path.join(path.resolve(workspaceRoot), '.superharness', 'workflow-state.db');
12
+ if (!fs.existsSync(dbPath)) return false;
13
+
14
+ try {
15
+ const { openWorkflowStateStore, readFreeMode } = await import(
16
+ pathToFileURL(path.join(pluginRoot, 'workflow-state-server', 'state.js')).href
17
+ );
18
+ const store = openWorkflowStateStore({ dbPath });
19
+ try {
20
+ return readFreeMode(store, workspaceRoot);
21
+ } finally {
22
+ store.close?.();
23
+ }
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
@@ -0,0 +1,58 @@
1
+ : << 'CMDBLOCK'
2
+ @echo off
3
+ REM Cross-platform polyglot wrapper for hook scripts.
4
+ REM On Windows: cmd.exe runs the batch portion, which finds and calls bash.
5
+ REM On Unix: the shell interprets this as a script (: is a no-op in bash).
6
+ REM
7
+ REM Hook scripts use extensionless filenames so Claude Code's Windows auto-detection -- which
8
+ REM prepends "bash" to any command containing .sh -- doesn't interfere.
9
+ REM
10
+ REM Usage: run-hook.cmd <script-name> [args...]
11
+
12
+ if "%~1"=="" (
13
+ echo run-hook.cmd: missing script name >&2
14
+ exit /b 1
15
+ )
16
+
17
+ set "HOOK_DIR=%~dp0"
18
+
19
+ REM Try Git for Windows bash in standard locations
20
+ if exist "C:\Program Files\Git\bin\bash.exe" (
21
+ "C:\Program Files\Git\bin\bash.exe" "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
22
+ exit /b %ERRORLEVEL%
23
+ )
24
+ if exist "C:\Program Files (x86)\Git\bin\bash.exe" (
25
+ "C:\Program Files (x86)\Git\bin\bash.exe" "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
26
+ exit /b %ERRORLEVEL%
27
+ )
28
+
29
+ REM Try bash on PATH, but skip Windows' WSL shim because it cannot run
30
+ REM Windows-style hook paths like C:\...\hooks\<script-name>.
31
+ for /f "delims=" %%B in ('where bash 2^>nul') do (
32
+ echo %%B | findstr /I "\\Windows\\System32\\bash.exe \\WindowsApps\\bash.exe" >nul
33
+ if errorlevel 1 (
34
+ "%%B" "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
35
+ exit /b %ERRORLEVEL%
36
+ )
37
+ )
38
+
39
+ REM Try common MSYS2 locations.
40
+ if exist "C:\msys64\usr\bin\bash.exe" (
41
+ "C:\msys64\usr\bin\bash.exe" "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
42
+ exit /b %ERRORLEVEL%
43
+ )
44
+ if exist "C:\cygwin64\bin\bash.exe" (
45
+ "C:\cygwin64\bin\bash.exe" "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
46
+ exit /b %ERRORLEVEL%
47
+ )
48
+
49
+ REM No bash found - exit silently rather than error
50
+ REM (plugin still works, just without optional context injection)
51
+ exit /b 0
52
+ CMDBLOCK
53
+
54
+ # Unix: run the named script directly
55
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
56
+ SCRIPT_NAME="$1"
57
+ shift
58
+ exec bash "${SCRIPT_DIR}/${SCRIPT_NAME}" "$@"
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
4
+ exec node "${SCRIPT_DIR}/workflow-context.mjs"
@@ -0,0 +1,184 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { fileURLToPath, pathToFileURL } from 'node:url';
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const STATE_DIR_LABEL = '.' + 'superharness/';
9
+
10
+ async function readStdinJson() {
11
+ const chunks = [];
12
+ for await (const chunk of process.stdin) {
13
+ chunks.push(chunk);
14
+ }
15
+ const raw = Buffer.concat(chunks).toString('utf8').trim();
16
+ if (!raw) return {};
17
+ return JSON.parse(raw);
18
+ }
19
+
20
+ function resolvePluginRoot() {
21
+ return path.resolve(
22
+ process.env.CLAUDE_PLUGIN_ROOT
23
+ || process.env.CODEX_PLUGIN_ROOT
24
+ || path.join(__dirname, '..'),
25
+ );
26
+ }
27
+
28
+ function workflowStateRoot(pluginRoot) {
29
+ return path.join(pluginRoot, 'workflow-state-server');
30
+ }
31
+
32
+ function hasWorkflowStateDeps(workflowStateDir) {
33
+ const nativeSqlite = path.join(
34
+ workflowStateDir,
35
+ 'node_modules',
36
+ 'better-sqlite3',
37
+ 'build',
38
+ 'Release',
39
+ 'better_sqlite3.node',
40
+ );
41
+ return fs.existsSync(nativeSqlite)
42
+ && fs.existsSync(path.join(workflowStateDir, 'node_modules', 'yaml'))
43
+ && fs.existsSync(path.join(workflowStateDir, 'node_modules', '@modelcontextprotocol', 'sdk'));
44
+ }
45
+
46
+ function ensureWorkflowStateDeps(workflowStateDir) {
47
+ if (hasWorkflowStateDeps(workflowStateDir)) return { ok: true };
48
+
49
+ // Node 22+ on Windows refuses to spawn .cmd / .bat without shell: true
50
+ // (CVE-2024-27980 mitigation). To avoid DEP0190 warnings from
51
+ // shell: true + args[], pass the full command as a single string and
52
+ // omit the args array on Windows. Arguments here are hard-coded constants,
53
+ // so there is no injection surface.
54
+ const isWin = process.platform === 'win32';
55
+ const result = isWin
56
+ ? spawnSync('npm.cmd install --omit=dev --no-audit --no-fund', {
57
+ cwd: workflowStateDir,
58
+ encoding: 'utf8',
59
+ stdio: 'pipe',
60
+ shell: true,
61
+ })
62
+ : spawnSync('npm', ['install', '--omit=dev', '--no-audit', '--no-fund'], {
63
+ cwd: workflowStateDir,
64
+ encoding: 'utf8',
65
+ stdio: 'pipe',
66
+ });
67
+
68
+ if (result.status === 0 && hasWorkflowStateDeps(workflowStateDir)) {
69
+ return { ok: true };
70
+ }
71
+ return {
72
+ ok: false,
73
+ error: result.error?.message
74
+ || result.stderr
75
+ || result.stdout
76
+ || `npm install exited with ${result.status}`,
77
+ };
78
+ }
79
+
80
+ function importFrom(workflowStateDir, file) {
81
+ return import(pathToFileURL(path.join(workflowStateDir, file)).href);
82
+ }
83
+
84
+ function loadInstalledSkills(pluginRoot) {
85
+ const skillsDir = path.join(pluginRoot, 'skills');
86
+ if (!fs.existsSync(skillsDir)) return new Set();
87
+ return new Set(
88
+ fs.readdirSync(skillsDir, { withFileTypes: true })
89
+ .filter((entry) => entry.isDirectory())
90
+ .filter((entry) => fs.existsSync(path.join(skillsDir, entry.name, 'SKILL.md')))
91
+ .map((entry) => entry.name),
92
+ );
93
+ }
94
+
95
+ function hookOutput(additionalContext) {
96
+ return {
97
+ hookSpecificOutput: {
98
+ hookEventName: 'UserPromptSubmit',
99
+ additionalContext,
100
+ },
101
+ };
102
+ }
103
+
104
+ const fallbackStopWorkContext = (reason) => [
105
+ '<SUPERHARNESS_WORKFLOW_STATE>',
106
+ 'Runtime status: unavailable',
107
+ `Reason: ${reason || 'workflow state context could not be loaded'}`,
108
+ '',
109
+ 'Rules:',
110
+ '- Stop business work.',
111
+ '- Report the workflow error to the user.',
112
+ '- Do not invent workflow state.',
113
+ `- Do not edit ${STATE_DIR_LABEL} directly.`,
114
+ '</SUPERHARNESS_WORKFLOW_STATE>',
115
+ ].join('\n');
116
+
117
+ export async function main() {
118
+ let store;
119
+ const pluginRoot = resolvePluginRoot();
120
+ const workflowStateDir = workflowStateRoot(pluginRoot);
121
+
122
+ const deps = ensureWorkflowStateDeps(workflowStateDir);
123
+ if (!deps.ok) {
124
+ process.stdout.write(`${JSON.stringify(hookOutput(fallbackStopWorkContext(`workflow-state-server dependencies are unavailable: ${deps.error}`)))}\n`);
125
+ return;
126
+ }
127
+
128
+ try {
129
+ const input = await readStdinJson();
130
+ const workspaceRoot = path.resolve(input.cwd || process.cwd());
131
+
132
+ // Free-mode check: skip injection entirely
133
+ const { isFreeMode } = await import(pathToFileURL(path.join(pluginRoot, 'hooks', 'lib', 'free-mode-check.mjs')).href);
134
+ if (await isFreeMode({ pluginRoot, workspaceRoot })) {
135
+ process.stdout.write(JSON.stringify({}) + '\n');
136
+ return;
137
+ }
138
+
139
+ const [
140
+ { loadWorkflowConfig, buildWorkflowGraph },
141
+ { openWorkflowStateStore, getWorkflowState, resolveWorkflowDbPath, createTurn },
142
+ { renderWorkflowContext },
143
+ ] = await Promise.all([
144
+ importFrom(workflowStateDir, 'validate-workflow.js'),
145
+ importFrom(workflowStateDir, 'state.js'),
146
+ importFrom(workflowStateDir, 'render-context.js'),
147
+ ]);
148
+ const config = loadWorkflowConfig({ pluginRoot, workspaceRoot });
149
+ const workflowGraph = buildWorkflowGraph(config, {
150
+ installedSkills: loadInstalledSkills(pluginRoot),
151
+ });
152
+ store = openWorkflowStateStore({
153
+ mode: process.env.SUPERHARNESS_WORKFLOW_STATE_MODE,
154
+ dbPath: process.env.SUPERHARNESS_WORKFLOW_STATE_DB
155
+ || resolveWorkflowDbPath({ workspaceRoot }),
156
+ });
157
+ const turnId = crypto.randomUUID();
158
+ createTurn(store, { workspaceRoot, turnId });
159
+ const stateInfo = getWorkflowState(store, {
160
+ workspaceRoot,
161
+ workflowGraph,
162
+ });
163
+ const context = renderWorkflowContext({
164
+ stateInfo,
165
+ workflowGraph,
166
+ skillsDir: path.join(pluginRoot, 'skills'),
167
+ });
168
+ process.stdout.write(`${JSON.stringify(hookOutput(context))}\n`);
169
+ } catch (error) {
170
+ const reason = error instanceof Error ? error.message : String(error);
171
+ try {
172
+ const { renderStopWorkContext } = await importFrom(workflowStateDir, 'render-context.js');
173
+ process.stdout.write(`${JSON.stringify(hookOutput(renderStopWorkContext({ reason })))}\n`);
174
+ } catch {
175
+ process.stdout.write(`${JSON.stringify(hookOutput(fallbackStopWorkContext(reason)))}\n`);
176
+ }
177
+ } finally {
178
+ store?.close();
179
+ }
180
+ }
181
+
182
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
183
+ await main();
184
+ }
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
4
+ exec node "${SCRIPT_DIR}/workflow-post-transition.mjs"
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath, pathToFileURL } from 'node:url';
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+
8
+ async function readStdinJson() {
9
+ const chunks = [];
10
+ for await (const chunk of process.stdin) chunks.push(chunk);
11
+ const raw = Buffer.concat(chunks).toString('utf8').trim();
12
+ return raw ? JSON.parse(raw) : {};
13
+ }
14
+
15
+ function resolvePluginRoot() {
16
+ return path.resolve(
17
+ process.env.CLAUDE_PLUGIN_ROOT
18
+ || process.env.CODEX_PLUGIN_ROOT
19
+ || process.env.PLUGIN_ROOT
20
+ || path.join(__dirname, '..'),
21
+ );
22
+ }
23
+
24
+ function hookOutput(additionalContext) {
25
+ return additionalContext
26
+ ? { hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext } }
27
+ : {};
28
+ }
29
+
30
+ export async function main() {
31
+ try {
32
+ const input = await readStdinJson();
33
+ const toState = input?.tool_input?.to_state;
34
+ if (!toState) {
35
+ process.stdout.write(JSON.stringify({}) + '\n');
36
+ return;
37
+ }
38
+ const pluginRoot = resolvePluginRoot();
39
+ const workflowStateDir = path.join(pluginRoot, 'workflow-state-server');
40
+
41
+ // Free-mode check: skip skill injection entirely
42
+ const workspaceRoot = path.resolve(input.cwd || process.cwd());
43
+ const { isFreeMode } = await import(pathToFileURL(path.join(pluginRoot, 'hooks', 'lib', 'free-mode-check.mjs')).href);
44
+ if (await isFreeMode({ pluginRoot, workspaceRoot })) {
45
+ process.stdout.write(JSON.stringify({}) + '\n');
46
+ return;
47
+ }
48
+ const [{ renderActiveSkill }, { loadWorkflowConfig, buildWorkflowGraph }] = await Promise.all([
49
+ import(pathToFileURL(path.join(workflowStateDir, 'render-context.js')).href),
50
+ import(pathToFileURL(path.join(workflowStateDir, 'validate-workflow.js')).href),
51
+ ]);
52
+
53
+ const skillsDir = path.join(pluginRoot, 'skills');
54
+
55
+ // State 名 (e.g. serial_execution) → skill 目录名 (serial-execution) 由 YAML
56
+ // 的 state.skill 字段映射。从 graph 拿,不要直接用 stateName 当 skill name。
57
+ let skillName = toState;
58
+ try {
59
+ const config = loadWorkflowConfig({ pluginRoot });
60
+ const installedSkills = new Set();
61
+ if (fs.existsSync(skillsDir)) {
62
+ for (const e of fs.readdirSync(skillsDir, { withFileTypes: true })) {
63
+ if (e.isDirectory() && fs.existsSync(path.join(skillsDir, e.name, 'SKILL.md'))) {
64
+ installedSkills.add(e.name);
65
+ }
66
+ }
67
+ }
68
+ const graph = buildWorkflowGraph(config, { installedSkills });
69
+ skillName = graph.states.get(toState)?.skill ?? toState;
70
+ } catch {
71
+ // graph 加载失败 → 回退用 stateName 当 skill name;renderActiveSkill 再 fail-open
72
+ }
73
+
74
+ try {
75
+ const skillContext = renderActiveSkill({ stateName: toState, skillsDir, skillName });
76
+ process.stdout.write(JSON.stringify(hookOutput(skillContext)) + '\n');
77
+ } catch {
78
+ // unknown state 或 SKILL 文件不存在 → fail-open
79
+ process.stdout.write(JSON.stringify({}) + '\n');
80
+ }
81
+ } catch {
82
+ // fail-open: hook 错误绝不阻塞 agent
83
+ process.stdout.write(JSON.stringify({}) + '\n');
84
+ }
85
+ }
86
+
87
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
88
+ await main();
89
+ }
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
4
+ exec node "${SCRIPT_DIR}/workflow-pre-tool-use.mjs"
@@ -0,0 +1,97 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+
4
+ async function readStdinJson() {
5
+ const chunks = [];
6
+ for await (const chunk of process.stdin) {
7
+ chunks.push(chunk);
8
+ }
9
+ const raw = Buffer.concat(chunks).toString('utf8').trim();
10
+ if (!raw) return {};
11
+ return JSON.parse(raw);
12
+ }
13
+
14
+ function normalizePathText(value) {
15
+ return value.replaceAll('\\', '/').toLowerCase();
16
+ }
17
+
18
+ function pathTouchesWorkflowState(value) {
19
+ const normalized = normalizePathText(value);
20
+ return normalized.includes('/.superharness/')
21
+ || normalized.startsWith('.superharness/')
22
+ || normalized.includes('.superharness/');
23
+ }
24
+
25
+ function collectStrings(value, output = []) {
26
+ if (typeof value === 'string') {
27
+ output.push(value);
28
+ } else if (Array.isArray(value)) {
29
+ for (const item of value) collectStrings(item, output);
30
+ } else if (value && typeof value === 'object') {
31
+ for (const item of Object.values(value)) collectStrings(item, output);
32
+ }
33
+ return output;
34
+ }
35
+
36
+ function isWriteCommand(command) {
37
+ return />>?|(^|\s)(rm|del|move|mv|copy|cp|set-content|add-content|out-file|sqlite3)(\s|$)/i
38
+ .test(command);
39
+ }
40
+
41
+ function firstString(...candidates) {
42
+ for (const value of candidates) {
43
+ if (typeof value === 'string') return value;
44
+ }
45
+ return '';
46
+ }
47
+
48
+ function shouldDeny(input) {
49
+ const toolName = String(input.tool_name || input.toolName || '');
50
+ const toolInput = input.tool_input || input.toolInput || {};
51
+ const lowered = toolName.toLowerCase();
52
+
53
+ if (lowered === 'bash' || lowered === 'shell') {
54
+ const command = firstString(toolInput.command, ...collectStrings(toolInput));
55
+ return pathTouchesWorkflowState(command) && isWriteCommand(command);
56
+ }
57
+
58
+ if (lowered === 'write' || lowered === 'edit' || lowered === 'multiedit') {
59
+ const filePath = firstString(toolInput.file_path, toolInput.filePath);
60
+ return pathTouchesWorkflowState(filePath);
61
+ }
62
+
63
+ if (lowered === 'notebookedit') {
64
+ const notebookPath = firstString(toolInput.notebook_path, toolInput.notebookPath);
65
+ return pathTouchesWorkflowState(notebookPath);
66
+ }
67
+
68
+ if (lowered === 'apply_patch') {
69
+ return collectStrings(toolInput).some(pathTouchesWorkflowState);
70
+ }
71
+
72
+ return collectStrings(toolInput).some(pathTouchesWorkflowState)
73
+ && /write|edit|patch/i.test(toolName);
74
+ }
75
+
76
+ function denyOutput() {
77
+ return {
78
+ hookSpecificOutput: {
79
+ hookEventName: 'PreToolUse',
80
+ permissionDecision: 'deny',
81
+ permissionDecisionReason: 'Workflow state is managed by superharness workflow tools; do not edit .superharness/ directly.',
82
+ },
83
+ };
84
+ }
85
+
86
+ export async function main() {
87
+ const input = await readStdinJson();
88
+ if (shouldDeny(input)) {
89
+ process.stdout.write(`${JSON.stringify(denyOutput())}\n`);
90
+ return;
91
+ }
92
+ process.stdout.write('{}\n');
93
+ }
94
+
95
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
96
+ await main();
97
+ }
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
4
+ exec node "${SCRIPT_DIR}/workflow-stop.mjs"