code-as-plan 2.0.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 (188) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja-JP.md +834 -0
  3. package/README.ko-KR.md +823 -0
  4. package/README.md +1006 -0
  5. package/README.pt-BR.md +452 -0
  6. package/README.zh-CN.md +800 -0
  7. package/agents/cap-brainstormer.md +154 -0
  8. package/agents/cap-debugger.md +221 -0
  9. package/agents/cap-prototyper.md +170 -0
  10. package/agents/cap-reviewer.md +230 -0
  11. package/agents/cap-tester.md +193 -0
  12. package/bin/install.js +5002 -0
  13. package/cap/bin/gsd-tools.cjs +1141 -0
  14. package/cap/bin/lib/arc-scanner.cjs +341 -0
  15. package/cap/bin/lib/cap-feature-map.cjs +506 -0
  16. package/cap/bin/lib/cap-session.cjs +191 -0
  17. package/cap/bin/lib/cap-stack-docs.cjs +598 -0
  18. package/cap/bin/lib/cap-tag-scanner.cjs +458 -0
  19. package/cap/bin/lib/commands.cjs +959 -0
  20. package/cap/bin/lib/config.cjs +466 -0
  21. package/cap/bin/lib/convention-reader.cjs +180 -0
  22. package/cap/bin/lib/core.cjs +1230 -0
  23. package/cap/bin/lib/feature-aggregator.cjs +422 -0
  24. package/cap/bin/lib/frontmatter.cjs +336 -0
  25. package/cap/bin/lib/init.cjs +1442 -0
  26. package/cap/bin/lib/manifest-generator.cjs +381 -0
  27. package/cap/bin/lib/milestone.cjs +252 -0
  28. package/cap/bin/lib/model-profiles.cjs +68 -0
  29. package/cap/bin/lib/monorepo-context.cjs +224 -0
  30. package/cap/bin/lib/monorepo-migrator.cjs +507 -0
  31. package/cap/bin/lib/phase.cjs +888 -0
  32. package/cap/bin/lib/profile-output.cjs +952 -0
  33. package/cap/bin/lib/profile-pipeline.cjs +539 -0
  34. package/cap/bin/lib/roadmap.cjs +329 -0
  35. package/cap/bin/lib/security.cjs +382 -0
  36. package/cap/bin/lib/session-manager.cjs +290 -0
  37. package/cap/bin/lib/skeleton-generator.cjs +177 -0
  38. package/cap/bin/lib/state.cjs +1031 -0
  39. package/cap/bin/lib/template.cjs +222 -0
  40. package/cap/bin/lib/test-detector.cjs +61 -0
  41. package/cap/bin/lib/uat.cjs +282 -0
  42. package/cap/bin/lib/verify.cjs +888 -0
  43. package/cap/bin/lib/workspace-detector.cjs +369 -0
  44. package/cap/bin/lib/workstream.cjs +491 -0
  45. package/cap/commands/gsd/workstreams.md +63 -0
  46. package/cap/references/arc-standard.md +315 -0
  47. package/cap/references/cap-agent-architecture.md +102 -0
  48. package/cap/references/cap-gitignore-template +9 -0
  49. package/cap/references/cap-zero-deps.md +158 -0
  50. package/cap/references/checkpoints.md +778 -0
  51. package/cap/references/continuation-format.md +249 -0
  52. package/cap/references/decimal-phase-calculation.md +64 -0
  53. package/cap/references/feature-map-template.md +25 -0
  54. package/cap/references/git-integration.md +295 -0
  55. package/cap/references/git-planning-commit.md +38 -0
  56. package/cap/references/model-profile-resolution.md +36 -0
  57. package/cap/references/model-profiles.md +139 -0
  58. package/cap/references/phase-argument-parsing.md +61 -0
  59. package/cap/references/planning-config.md +202 -0
  60. package/cap/references/questioning.md +162 -0
  61. package/cap/references/session-template.json +8 -0
  62. package/cap/references/tdd.md +263 -0
  63. package/cap/references/ui-brand.md +160 -0
  64. package/cap/references/user-profiling.md +681 -0
  65. package/cap/references/verification-patterns.md +612 -0
  66. package/cap/references/workstream-flag.md +58 -0
  67. package/cap/templates/DEBUG.md +164 -0
  68. package/cap/templates/UAT.md +265 -0
  69. package/cap/templates/UI-SPEC.md +100 -0
  70. package/cap/templates/VALIDATION.md +76 -0
  71. package/cap/templates/claude-md.md +122 -0
  72. package/cap/templates/codebase/architecture.md +255 -0
  73. package/cap/templates/codebase/concerns.md +310 -0
  74. package/cap/templates/codebase/conventions.md +307 -0
  75. package/cap/templates/codebase/integrations.md +280 -0
  76. package/cap/templates/codebase/stack.md +186 -0
  77. package/cap/templates/codebase/structure.md +285 -0
  78. package/cap/templates/codebase/testing.md +480 -0
  79. package/cap/templates/config.json +44 -0
  80. package/cap/templates/context.md +352 -0
  81. package/cap/templates/continue-here.md +78 -0
  82. package/cap/templates/copilot-instructions.md +7 -0
  83. package/cap/templates/debug-subagent-prompt.md +91 -0
  84. package/cap/templates/dev-preferences.md +21 -0
  85. package/cap/templates/discovery.md +146 -0
  86. package/cap/templates/discussion-log.md +63 -0
  87. package/cap/templates/milestone-archive.md +123 -0
  88. package/cap/templates/milestone.md +115 -0
  89. package/cap/templates/phase-prompt.md +610 -0
  90. package/cap/templates/planner-subagent-prompt.md +117 -0
  91. package/cap/templates/project.md +186 -0
  92. package/cap/templates/requirements.md +231 -0
  93. package/cap/templates/research-project/ARCHITECTURE.md +204 -0
  94. package/cap/templates/research-project/FEATURES.md +147 -0
  95. package/cap/templates/research-project/PITFALLS.md +200 -0
  96. package/cap/templates/research-project/STACK.md +120 -0
  97. package/cap/templates/research-project/SUMMARY.md +170 -0
  98. package/cap/templates/research.md +552 -0
  99. package/cap/templates/retrospective.md +54 -0
  100. package/cap/templates/roadmap.md +202 -0
  101. package/cap/templates/state.md +176 -0
  102. package/cap/templates/summary-complex.md +59 -0
  103. package/cap/templates/summary-minimal.md +41 -0
  104. package/cap/templates/summary-standard.md +48 -0
  105. package/cap/templates/summary.md +248 -0
  106. package/cap/templates/user-profile.md +146 -0
  107. package/cap/templates/user-setup.md +311 -0
  108. package/cap/templates/verification-report.md +322 -0
  109. package/cap/workflows/add-phase.md +112 -0
  110. package/cap/workflows/add-tests.md +351 -0
  111. package/cap/workflows/add-todo.md +158 -0
  112. package/cap/workflows/audit-milestone.md +340 -0
  113. package/cap/workflows/audit-uat.md +109 -0
  114. package/cap/workflows/autonomous.md +891 -0
  115. package/cap/workflows/check-todos.md +177 -0
  116. package/cap/workflows/cleanup.md +152 -0
  117. package/cap/workflows/complete-milestone.md +767 -0
  118. package/cap/workflows/diagnose-issues.md +231 -0
  119. package/cap/workflows/discovery-phase.md +289 -0
  120. package/cap/workflows/discuss-phase-assumptions.md +653 -0
  121. package/cap/workflows/discuss-phase.md +1049 -0
  122. package/cap/workflows/do.md +104 -0
  123. package/cap/workflows/execute-phase.md +846 -0
  124. package/cap/workflows/execute-plan.md +514 -0
  125. package/cap/workflows/fast.md +105 -0
  126. package/cap/workflows/forensics.md +265 -0
  127. package/cap/workflows/health.md +181 -0
  128. package/cap/workflows/help.md +660 -0
  129. package/cap/workflows/insert-phase.md +130 -0
  130. package/cap/workflows/list-phase-assumptions.md +178 -0
  131. package/cap/workflows/list-workspaces.md +56 -0
  132. package/cap/workflows/manager.md +362 -0
  133. package/cap/workflows/map-codebase.md +377 -0
  134. package/cap/workflows/milestone-summary.md +223 -0
  135. package/cap/workflows/new-milestone.md +486 -0
  136. package/cap/workflows/new-project.md +1250 -0
  137. package/cap/workflows/new-workspace.md +237 -0
  138. package/cap/workflows/next.md +97 -0
  139. package/cap/workflows/node-repair.md +92 -0
  140. package/cap/workflows/note.md +156 -0
  141. package/cap/workflows/pause-work.md +176 -0
  142. package/cap/workflows/plan-milestone-gaps.md +273 -0
  143. package/cap/workflows/plan-phase.md +859 -0
  144. package/cap/workflows/plant-seed.md +169 -0
  145. package/cap/workflows/pr-branch.md +129 -0
  146. package/cap/workflows/profile-user.md +450 -0
  147. package/cap/workflows/progress.md +507 -0
  148. package/cap/workflows/quick.md +757 -0
  149. package/cap/workflows/remove-phase.md +155 -0
  150. package/cap/workflows/remove-workspace.md +90 -0
  151. package/cap/workflows/research-phase.md +82 -0
  152. package/cap/workflows/resume-project.md +326 -0
  153. package/cap/workflows/review.md +228 -0
  154. package/cap/workflows/session-report.md +146 -0
  155. package/cap/workflows/settings.md +283 -0
  156. package/cap/workflows/ship.md +228 -0
  157. package/cap/workflows/stats.md +60 -0
  158. package/cap/workflows/transition.md +671 -0
  159. package/cap/workflows/ui-phase.md +302 -0
  160. package/cap/workflows/ui-review.md +165 -0
  161. package/cap/workflows/update.md +323 -0
  162. package/cap/workflows/validate-phase.md +174 -0
  163. package/cap/workflows/verify-phase.md +254 -0
  164. package/cap/workflows/verify-work.md +637 -0
  165. package/commands/cap/annotate.md +165 -0
  166. package/commands/cap/brainstorm.md +238 -0
  167. package/commands/cap/debug.md +297 -0
  168. package/commands/cap/init.md +262 -0
  169. package/commands/cap/iterate.md +234 -0
  170. package/commands/cap/prototype.md +281 -0
  171. package/commands/cap/refresh-docs.md +37 -0
  172. package/commands/cap/review.md +272 -0
  173. package/commands/cap/scan.md +249 -0
  174. package/commands/cap/start.md +234 -0
  175. package/commands/cap/status.md +189 -0
  176. package/commands/cap/test.md +250 -0
  177. package/hooks/dist/gsd-check-update.js +114 -0
  178. package/hooks/dist/gsd-context-monitor.js +156 -0
  179. package/hooks/dist/gsd-prompt-guard.js +96 -0
  180. package/hooks/dist/gsd-statusline.js +119 -0
  181. package/hooks/dist/gsd-workflow-guard.js +94 -0
  182. package/package.json +51 -0
  183. package/scripts/base64-scan.sh +262 -0
  184. package/scripts/build-hooks.js +82 -0
  185. package/scripts/cap-removal-checklist.md +202 -0
  186. package/scripts/prompt-injection-scan.sh +198 -0
  187. package/scripts/run-tests.cjs +29 -0
  188. package/scripts/secret-scan.sh +227 -0
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+ // gsd-hook-version: {{GSD_VERSION}}
3
+ // Claude Code Statusline - GSD Edition
4
+ // Shows: model | current task | directory | context usage
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const os = require('os');
9
+
10
+ // Read JSON from stdin
11
+ let input = '';
12
+ // Timeout guard: if stdin doesn't close within 3s (e.g. pipe issues on
13
+ // Windows/Git Bash), exit silently instead of hanging. See #775.
14
+ const stdinTimeout = setTimeout(() => process.exit(0), 3000);
15
+ process.stdin.setEncoding('utf8');
16
+ process.stdin.on('data', chunk => input += chunk);
17
+ process.stdin.on('end', () => {
18
+ clearTimeout(stdinTimeout);
19
+ try {
20
+ const data = JSON.parse(input);
21
+ const model = data.model?.display_name || 'Claude';
22
+ const dir = data.workspace?.current_dir || process.cwd();
23
+ const session = data.session_id || '';
24
+ const remaining = data.context_window?.remaining_percentage;
25
+
26
+ // Context window display (shows USED percentage scaled to usable context)
27
+ // Claude Code reserves ~16.5% for autocompact buffer, so usable context
28
+ // is 83.5% of the total window. We normalize to show 100% at that point.
29
+ const AUTO_COMPACT_BUFFER_PCT = 16.5;
30
+ let ctx = '';
31
+ if (remaining != null) {
32
+ // Normalize: subtract buffer from remaining, scale to usable range
33
+ const usableRemaining = Math.max(0, ((remaining - AUTO_COMPACT_BUFFER_PCT) / (100 - AUTO_COMPACT_BUFFER_PCT)) * 100);
34
+ const used = Math.max(0, Math.min(100, Math.round(100 - usableRemaining)));
35
+
36
+ // Write context metrics to bridge file for the context-monitor PostToolUse hook.
37
+ // The monitor reads this file to inject agent-facing warnings when context is low.
38
+ if (session) {
39
+ try {
40
+ const bridgePath = path.join(os.tmpdir(), `claude-ctx-${session}.json`);
41
+ const bridgeData = JSON.stringify({
42
+ session_id: session,
43
+ remaining_percentage: remaining,
44
+ used_pct: used,
45
+ timestamp: Math.floor(Date.now() / 1000)
46
+ });
47
+ fs.writeFileSync(bridgePath, bridgeData);
48
+ } catch (e) {
49
+ // Silent fail -- bridge is best-effort, don't break statusline
50
+ }
51
+ }
52
+
53
+ // Build progress bar (10 segments)
54
+ const filled = Math.floor(used / 10);
55
+ const bar = '█'.repeat(filled) + '░'.repeat(10 - filled);
56
+
57
+ // Color based on usable context thresholds
58
+ if (used < 50) {
59
+ ctx = ` \x1b[32m${bar} ${used}%\x1b[0m`;
60
+ } else if (used < 65) {
61
+ ctx = ` \x1b[33m${bar} ${used}%\x1b[0m`;
62
+ } else if (used < 80) {
63
+ ctx = ` \x1b[38;5;208m${bar} ${used}%\x1b[0m`;
64
+ } else {
65
+ ctx = ` \x1b[5;31m💀 ${bar} ${used}%\x1b[0m`;
66
+ }
67
+ }
68
+
69
+ // Current task from todos
70
+ let task = '';
71
+ const homeDir = os.homedir();
72
+ // Respect CLAUDE_CONFIG_DIR for custom config directory setups (#870)
73
+ const claudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(homeDir, '.claude');
74
+ const todosDir = path.join(claudeDir, 'todos');
75
+ if (session && fs.existsSync(todosDir)) {
76
+ try {
77
+ const files = fs.readdirSync(todosDir)
78
+ .filter(f => f.startsWith(session) && f.includes('-agent-') && f.endsWith('.json'))
79
+ .map(f => ({ name: f, mtime: fs.statSync(path.join(todosDir, f)).mtime }))
80
+ .sort((a, b) => b.mtime - a.mtime);
81
+
82
+ if (files.length > 0) {
83
+ try {
84
+ const todos = JSON.parse(fs.readFileSync(path.join(todosDir, files[0].name), 'utf8'));
85
+ const inProgress = todos.find(t => t.status === 'in_progress');
86
+ if (inProgress) task = inProgress.activeForm || '';
87
+ } catch (e) {}
88
+ }
89
+ } catch (e) {
90
+ // Silently fail on file system errors - don't break statusline
91
+ }
92
+ }
93
+
94
+ // GSD update available?
95
+ let gsdUpdate = '';
96
+ const cacheFile = path.join(claudeDir, 'cache', 'gsd-update-check.json');
97
+ if (fs.existsSync(cacheFile)) {
98
+ try {
99
+ const cache = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
100
+ if (cache.update_available) {
101
+ gsdUpdate = '\x1b[33m⬆ /gsd:update\x1b[0m │ ';
102
+ }
103
+ if (cache.stale_hooks && cache.stale_hooks.length > 0) {
104
+ gsdUpdate += '\x1b[31m⚠ stale hooks — run /gsd:update\x1b[0m │ ';
105
+ }
106
+ } catch (e) {}
107
+ }
108
+
109
+ // Output
110
+ const dirname = path.basename(dir);
111
+ if (task) {
112
+ process.stdout.write(`${gsdUpdate}\x1b[2m${model}\x1b[0m │ \x1b[1m${task}\x1b[0m │ \x1b[2m${dirname}\x1b[0m${ctx}`);
113
+ } else {
114
+ process.stdout.write(`${gsdUpdate}\x1b[2m${model}\x1b[0m │ \x1b[2m${dirname}\x1b[0m${ctx}`);
115
+ }
116
+ } catch (e) {
117
+ // Silent fail - don't break statusline on parse errors
118
+ }
119
+ });
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+ // gsd-hook-version: {{GSD_VERSION}}
3
+ // GSD Workflow Guard — PreToolUse hook
4
+ // Detects when Claude attempts file edits outside a GSD workflow context
5
+ // (no active /gsd: command or Task subagent) and injects an advisory warning.
6
+ //
7
+ // This is a SOFT guard — it advises, not blocks. The edit still proceeds.
8
+ // The warning nudges Claude to use /gsd:quick or /gsd:fast instead of
9
+ // making direct edits that bypass state tracking.
10
+ //
11
+ // Enable via config: hooks.workflow_guard: true (default: false)
12
+ // Only triggers on Write/Edit tool calls to non-.planning/ files.
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+
17
+ let input = '';
18
+ const stdinTimeout = setTimeout(() => process.exit(0), 3000);
19
+ process.stdin.setEncoding('utf8');
20
+ process.stdin.on('data', chunk => input += chunk);
21
+ process.stdin.on('end', () => {
22
+ clearTimeout(stdinTimeout);
23
+ try {
24
+ const data = JSON.parse(input);
25
+ const toolName = data.tool_name;
26
+
27
+ // Only guard Write and Edit tool calls
28
+ if (toolName !== 'Write' && toolName !== 'Edit') {
29
+ process.exit(0);
30
+ }
31
+
32
+ // Check if we're inside a GSD workflow (Task subagent or /gsd: command)
33
+ // Subagents have a session_id that differs from the parent
34
+ // and typically have a description field set by the orchestrator
35
+ if (data.tool_input?.is_subagent || data.session_type === 'task') {
36
+ process.exit(0);
37
+ }
38
+
39
+ // Check the file being edited
40
+ const filePath = data.tool_input?.file_path || data.tool_input?.path || '';
41
+
42
+ // Allow edits to .planning/ files (GSD state management)
43
+ if (filePath.includes('.planning/') || filePath.includes('.planning\\')) {
44
+ process.exit(0);
45
+ }
46
+
47
+ // Allow edits to common config/docs files that don't need GSD tracking
48
+ const allowedPatterns = [
49
+ /\.gitignore$/,
50
+ /\.env/,
51
+ /CLAUDE\.md$/,
52
+ /AGENTS\.md$/,
53
+ /GEMINI\.md$/,
54
+ /settings\.json$/,
55
+ ];
56
+ if (allowedPatterns.some(p => p.test(filePath))) {
57
+ process.exit(0);
58
+ }
59
+
60
+ // Check if workflow guard is enabled
61
+ const cwd = data.cwd || process.cwd();
62
+ const configPath = path.join(cwd, '.planning', 'config.json');
63
+ if (fs.existsSync(configPath)) {
64
+ try {
65
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
66
+ if (!config.hooks?.workflow_guard) {
67
+ process.exit(0); // Guard disabled (default)
68
+ }
69
+ } catch (e) {
70
+ process.exit(0);
71
+ }
72
+ } else {
73
+ process.exit(0); // No GSD project — don't guard
74
+ }
75
+
76
+ // If we get here: GSD project, guard enabled, file edit outside .planning/,
77
+ // not in a subagent context. Inject advisory warning.
78
+ const output = {
79
+ hookSpecificOutput: {
80
+ hookEventName: "PreToolUse",
81
+ additionalContext: `⚠️ WORKFLOW ADVISORY: You're editing ${path.basename(filePath)} directly without a GSD command. ` +
82
+ 'This edit will not be tracked in STATE.md or produce a SUMMARY.md. ' +
83
+ 'Consider using /gsd:fast for trivial fixes or /gsd:quick for larger changes ' +
84
+ 'to maintain project state tracking. ' +
85
+ 'If this is intentional (e.g., user explicitly asked for a direct edit), proceed normally.'
86
+ }
87
+ };
88
+
89
+ process.stdout.write(JSON.stringify(output));
90
+ } catch (e) {
91
+ // Silent fail — never block tool execution
92
+ process.exit(0);
93
+ }
94
+ });
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "code-as-plan",
3
+ "version": "2.0.0",
4
+ "description": "CAP — Code as Plan. Build first, plan from code. Farley-aligned engineering framework for Claude Code.",
5
+ "bin": {
6
+ "cap": "bin/install.js"
7
+ },
8
+ "files": [
9
+ "bin",
10
+ "commands",
11
+ "cap",
12
+ "agents",
13
+ "hooks/dist",
14
+ "scripts"
15
+ ],
16
+ "keywords": [
17
+ "claude",
18
+ "claude-code",
19
+ "ai",
20
+ "code-first",
21
+ "code-as-plan",
22
+ "engineering-framework",
23
+ "prototype-driven",
24
+ "annotation-driven",
25
+ "farley"
26
+ ],
27
+ "author": "TÂCHES",
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/dwall-sys/code-as-plan.git"
32
+ },
33
+ "homepage": "https://github.com/dwall-sys/code-as-plan",
34
+ "bugs": {
35
+ "url": "https://github.com/dwall-sys/code-as-plan/issues"
36
+ },
37
+ "engines": {
38
+ "node": ">=20.0.0"
39
+ },
40
+ "devDependencies": {
41
+ "c8": "^11.0.0",
42
+ "esbuild": "^0.24.0",
43
+ "vitest": "^4.1.2"
44
+ },
45
+ "scripts": {
46
+ "build:hooks": "node scripts/build-hooks.js",
47
+ "prepublishOnly": "npm run build:hooks",
48
+ "test": "node scripts/run-tests.cjs",
49
+ "test:coverage": "c8 --check-coverage --lines 70 --reporter text --include 'cap/bin/lib/*.cjs' --exclude 'tests/**' --all node scripts/run-tests.cjs"
50
+ }
51
+ }
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env bash
2
+ # base64-scan.sh — Detect base64-obfuscated prompt injection in source files
3
+ #
4
+ # Extracts base64 blobs >= 40 chars, decodes them, and checks decoded content
5
+ # against the same injection patterns used by prompt-injection-scan.sh.
6
+ #
7
+ # Usage:
8
+ # scripts/base64-scan.sh --diff origin/main # CI mode: scan changed files
9
+ # scripts/base64-scan.sh --file path/to/file # Scan a single file
10
+ # scripts/base64-scan.sh --dir agents/ # Scan all files in a directory
11
+ #
12
+ # Exit codes:
13
+ # 0 = clean
14
+ # 1 = findings detected
15
+ # 2 = usage error
16
+ set -euo pipefail
17
+
18
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19
+ MIN_BLOB_LENGTH=40
20
+
21
+ # ─── Injection Patterns (decoded content) ────────────────────────────────────
22
+ # Subset of patterns — if someone base64-encoded something, check for the
23
+ # most common injection indicators.
24
+ DECODED_PATTERNS=(
25
+ 'ignore[[:space:]]+(all[[:space:]]+)?previous[[:space:]]+instructions'
26
+ 'you[[:space:]]+are[[:space:]]+now[[:space:]]+'
27
+ 'system[[:space:]]+prompt'
28
+ '</?system>'
29
+ '</?assistant>'
30
+ '\[SYSTEM\]'
31
+ '\[INST\]'
32
+ '<<SYS>>'
33
+ 'override[[:space:]]+(system|safety|security)'
34
+ 'pretend[[:space:]]+(you|to)[[:space:]]'
35
+ 'act[[:space:]]+as[[:space:]]+(a|an|if)'
36
+ 'jailbreak'
37
+ 'bypass[[:space:]]+(safety|content|security)'
38
+ 'eval[[:space:]]*\('
39
+ 'exec[[:space:]]*\('
40
+ 'rm[[:space:]]+-rf'
41
+ 'curl[[:space:]].*\|[[:space:]]*sh'
42
+ 'wget[[:space:]].*\|[[:space:]]*sh'
43
+ )
44
+
45
+ # ─── Ignorelist ──────────────────────────────────────────────────────────────
46
+
47
+ IGNOREFILE=".base64scanignore"
48
+ IGNORED_PATTERNS=()
49
+
50
+ load_ignorelist() {
51
+ if [[ -f "$IGNOREFILE" ]]; then
52
+ while IFS= read -r line; do
53
+ # Skip comments and empty lines
54
+ [[ "$line" =~ ^[[:space:]]*# ]] && continue
55
+ [[ -z "${line// }" ]] && continue
56
+ IGNORED_PATTERNS+=("$line")
57
+ done < "$IGNOREFILE"
58
+ fi
59
+ }
60
+
61
+ is_ignored() {
62
+ local blob="$1"
63
+ if [[ ${#IGNORED_PATTERNS[@]} -eq 0 ]]; then
64
+ return 1
65
+ fi
66
+ for pattern in "${IGNORED_PATTERNS[@]}"; do
67
+ if [[ "$blob" == "$pattern" ]]; then
68
+ return 0
69
+ fi
70
+ done
71
+ return 1
72
+ }
73
+
74
+ # ─── Skip Rules ──────────────────────────────────────────────────────────────
75
+
76
+ should_skip_file() {
77
+ local file="$1"
78
+ # Skip binary files
79
+ case "$file" in
80
+ *.png|*.jpg|*.jpeg|*.gif|*.ico|*.woff|*.woff2|*.ttf|*.eot|*.otf) return 0 ;;
81
+ *.zip|*.tar|*.gz|*.bz2|*.xz|*.7z) return 0 ;;
82
+ *.pdf|*.doc|*.docx|*.xls|*.xlsx) return 0 ;;
83
+ esac
84
+ # Skip lockfiles and node_modules
85
+ case "$file" in
86
+ */node_modules/*) return 0 ;;
87
+ */package-lock.json) return 0 ;;
88
+ */yarn.lock) return 0 ;;
89
+ */pnpm-lock.yaml) return 0 ;;
90
+ esac
91
+ # Skip the scan scripts themselves and test files
92
+ case "$file" in
93
+ */base64-scan.sh) return 0 ;;
94
+ */security-scan.test.cjs) return 0 ;;
95
+ esac
96
+ return 1
97
+ }
98
+
99
+ is_data_uri() {
100
+ local context="$1"
101
+ # data:image/png;base64,... or data:application/font-woff;base64,...
102
+ echo "$context" | grep -qE 'data:[a-zA-Z]+/[a-zA-Z0-9.+-]+;base64,' 2>/dev/null
103
+ }
104
+
105
+ # ─── File Collection ─────────────────────────────────────────────────────────
106
+
107
+ collect_files() {
108
+ local mode="$1"
109
+ shift
110
+
111
+ case "$mode" in
112
+ --diff)
113
+ local base="${1:-origin/main}"
114
+ git diff --name-only --diff-filter=ACMR "$base"...HEAD 2>/dev/null \
115
+ | grep -vE '\.(png|jpg|jpeg|gif|ico|woff|woff2|ttf|eot|otf|zip|tar|gz|pdf)$' || true
116
+ ;;
117
+ --file)
118
+ if [[ -f "$1" ]]; then
119
+ echo "$1"
120
+ else
121
+ echo "Error: file not found: $1" >&2
122
+ exit 2
123
+ fi
124
+ ;;
125
+ --dir)
126
+ local dir="$1"
127
+ if [[ ! -d "$dir" ]]; then
128
+ echo "Error: directory not found: $dir" >&2
129
+ exit 2
130
+ fi
131
+ find "$dir" -type f ! -path '*/node_modules/*' ! -path '*/.git/*' ! -path '*/dist/*' \
132
+ ! -name '*.png' ! -name '*.jpg' ! -name '*.gif' ! -name '*.woff*' 2>/dev/null || true
133
+ ;;
134
+ --stdin)
135
+ cat
136
+ ;;
137
+ *)
138
+ echo "Usage: $0 --diff [base] | --file <path> | --dir <path> | --stdin" >&2
139
+ exit 2
140
+ ;;
141
+ esac
142
+ }
143
+
144
+ # ─── Scanner ─────────────────────────────────────────────────────────────────
145
+
146
+ extract_and_check_blobs() {
147
+ local file="$1"
148
+ local found=0
149
+ local line_num=0
150
+
151
+ while IFS= read -r line; do
152
+ line_num=$((line_num + 1))
153
+
154
+ # Skip data URIs — legitimate base64 usage
155
+ if is_data_uri "$line"; then
156
+ continue
157
+ fi
158
+
159
+ # Extract base64-like blobs (alphanumeric + / + = padding, >= MIN_BLOB_LENGTH)
160
+ local blobs
161
+ blobs=$(echo "$line" | grep -oE '[A-Za-z0-9+/]{'"$MIN_BLOB_LENGTH"',}={0,3}' 2>/dev/null || true)
162
+
163
+ if [[ -z "$blobs" ]]; then
164
+ continue
165
+ fi
166
+
167
+ while IFS= read -r blob; do
168
+ [[ -z "$blob" ]] && continue
169
+
170
+ # Check ignorelist
171
+ if [[ ${#IGNORED_PATTERNS[@]} -gt 0 ]] && is_ignored "$blob"; then
172
+ continue
173
+ fi
174
+
175
+ # Try to decode — if it fails, not valid base64
176
+ local decoded
177
+ decoded=$(echo "$blob" | base64 -d 2>/dev/null || echo "")
178
+
179
+ if [[ -z "$decoded" ]]; then
180
+ continue
181
+ fi
182
+
183
+ # Check if decoded content is mostly printable text (not random binary)
184
+ local printable_ratio
185
+ local total_chars=${#decoded}
186
+ if [[ $total_chars -eq 0 ]]; then
187
+ continue
188
+ fi
189
+
190
+ # Count printable ASCII characters
191
+ local printable_count
192
+ printable_count=$(echo -n "$decoded" | tr -cd '[:print:]' | wc -c | tr -d ' ')
193
+ # Skip if less than 70% printable (likely binary data, not obfuscated text)
194
+ if [[ $((printable_count * 100 / total_chars)) -lt 70 ]]; then
195
+ continue
196
+ fi
197
+
198
+ # Scan decoded content against injection patterns
199
+ for pattern in "${DECODED_PATTERNS[@]}"; do
200
+ if echo "$decoded" | grep -iqE "$pattern" 2>/dev/null; then
201
+ if [[ $found -eq 0 ]]; then
202
+ echo "FAIL: $file"
203
+ found=1
204
+ fi
205
+ echo " line $line_num: base64 blob decodes to suspicious content"
206
+ echo " blob: ${blob:0:60}..."
207
+ echo " decoded: ${decoded:0:120}"
208
+ echo " matched: $pattern"
209
+ break
210
+ fi
211
+ done
212
+ done <<< "$blobs"
213
+ done < "$file"
214
+
215
+ return $found
216
+ }
217
+
218
+ # ─── Main ────────────────────────────────────────────────────────────────────
219
+
220
+ main() {
221
+ if [[ $# -eq 0 ]]; then
222
+ echo "Usage: $0 --diff [base] | --file <path> | --dir <path>" >&2
223
+ exit 2
224
+ fi
225
+
226
+ load_ignorelist
227
+
228
+ local mode="$1"
229
+ shift
230
+
231
+ local files
232
+ files=$(collect_files "$mode" "$@")
233
+
234
+ if [[ -z "$files" ]]; then
235
+ echo "base64-scan: no files to scan"
236
+ exit 0
237
+ fi
238
+
239
+ local total=0
240
+ local failed=0
241
+
242
+ while IFS= read -r file; do
243
+ [[ -z "$file" ]] && continue
244
+ if should_skip_file "$file"; then
245
+ continue
246
+ fi
247
+ total=$((total + 1))
248
+ if ! extract_and_check_blobs "$file"; then
249
+ failed=$((failed + 1))
250
+ fi
251
+ done <<< "$files"
252
+
253
+ echo ""
254
+ echo "base64-scan: scanned $total files, $failed with findings"
255
+
256
+ if [[ $failed -gt 0 ]]; then
257
+ exit 1
258
+ fi
259
+ exit 0
260
+ }
261
+
262
+ main "$@"
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Copy GSD hooks to dist for installation.
4
+ * Validates JavaScript syntax before copying to prevent shipping broken hooks.
5
+ * See #1107, #1109, #1125, #1161 — a duplicate const declaration shipped
6
+ * in dist and caused PostToolUse hook errors for all users.
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const vm = require('vm');
12
+
13
+ const HOOKS_DIR = path.join(__dirname, '..', 'hooks');
14
+ const DIST_DIR = path.join(HOOKS_DIR, 'dist');
15
+
16
+ // Hooks to copy (pure Node.js, no bundling needed)
17
+ const HOOKS_TO_COPY = [
18
+ 'gsd-check-update.js',
19
+ 'gsd-context-monitor.js',
20
+ 'gsd-prompt-guard.js',
21
+ 'gsd-statusline.js',
22
+ 'gsd-workflow-guard.js'
23
+ ];
24
+
25
+ /**
26
+ * Validate JavaScript syntax without executing the file.
27
+ * Catches SyntaxError (duplicate const, missing brackets, etc.)
28
+ * before the hook gets shipped to users.
29
+ */
30
+ function validateSyntax(filePath) {
31
+ const content = fs.readFileSync(filePath, 'utf8');
32
+ try {
33
+ // Use vm.compileFunction to check syntax without executing
34
+ new vm.Script(content, { filename: path.basename(filePath) });
35
+ return null; // No error
36
+ } catch (e) {
37
+ if (e instanceof SyntaxError) {
38
+ return e.message;
39
+ }
40
+ throw e;
41
+ }
42
+ }
43
+
44
+ function build() {
45
+ // Ensure dist directory exists
46
+ if (!fs.existsSync(DIST_DIR)) {
47
+ fs.mkdirSync(DIST_DIR, { recursive: true });
48
+ }
49
+
50
+ let hasErrors = false;
51
+
52
+ // Copy hooks to dist with syntax validation
53
+ for (const hook of HOOKS_TO_COPY) {
54
+ const src = path.join(HOOKS_DIR, hook);
55
+ const dest = path.join(DIST_DIR, hook);
56
+
57
+ if (!fs.existsSync(src)) {
58
+ console.warn(`Warning: ${hook} not found, skipping`);
59
+ continue;
60
+ }
61
+
62
+ // Validate syntax before copying
63
+ const syntaxError = validateSyntax(src);
64
+ if (syntaxError) {
65
+ console.error(`\x1b[31m✗ ${hook}: SyntaxError — ${syntaxError}\x1b[0m`);
66
+ hasErrors = true;
67
+ continue;
68
+ }
69
+
70
+ console.log(`\x1b[32m✓\x1b[0m Copying ${hook}...`);
71
+ fs.copyFileSync(src, dest);
72
+ }
73
+
74
+ if (hasErrors) {
75
+ console.error('\n\x1b[31mBuild failed: fix syntax errors above before publishing.\x1b[0m');
76
+ process.exit(1);
77
+ }
78
+
79
+ console.log('\nBuild complete.');
80
+ }
81
+
82
+ build();