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.
- package/LICENSE +21 -0
- package/README.ja-JP.md +834 -0
- package/README.ko-KR.md +823 -0
- package/README.md +1006 -0
- package/README.pt-BR.md +452 -0
- package/README.zh-CN.md +800 -0
- package/agents/cap-brainstormer.md +154 -0
- package/agents/cap-debugger.md +221 -0
- package/agents/cap-prototyper.md +170 -0
- package/agents/cap-reviewer.md +230 -0
- package/agents/cap-tester.md +193 -0
- package/bin/install.js +5002 -0
- package/cap/bin/gsd-tools.cjs +1141 -0
- package/cap/bin/lib/arc-scanner.cjs +341 -0
- package/cap/bin/lib/cap-feature-map.cjs +506 -0
- package/cap/bin/lib/cap-session.cjs +191 -0
- package/cap/bin/lib/cap-stack-docs.cjs +598 -0
- package/cap/bin/lib/cap-tag-scanner.cjs +458 -0
- package/cap/bin/lib/commands.cjs +959 -0
- package/cap/bin/lib/config.cjs +466 -0
- package/cap/bin/lib/convention-reader.cjs +180 -0
- package/cap/bin/lib/core.cjs +1230 -0
- package/cap/bin/lib/feature-aggregator.cjs +422 -0
- package/cap/bin/lib/frontmatter.cjs +336 -0
- package/cap/bin/lib/init.cjs +1442 -0
- package/cap/bin/lib/manifest-generator.cjs +381 -0
- package/cap/bin/lib/milestone.cjs +252 -0
- package/cap/bin/lib/model-profiles.cjs +68 -0
- package/cap/bin/lib/monorepo-context.cjs +224 -0
- package/cap/bin/lib/monorepo-migrator.cjs +507 -0
- package/cap/bin/lib/phase.cjs +888 -0
- package/cap/bin/lib/profile-output.cjs +952 -0
- package/cap/bin/lib/profile-pipeline.cjs +539 -0
- package/cap/bin/lib/roadmap.cjs +329 -0
- package/cap/bin/lib/security.cjs +382 -0
- package/cap/bin/lib/session-manager.cjs +290 -0
- package/cap/bin/lib/skeleton-generator.cjs +177 -0
- package/cap/bin/lib/state.cjs +1031 -0
- package/cap/bin/lib/template.cjs +222 -0
- package/cap/bin/lib/test-detector.cjs +61 -0
- package/cap/bin/lib/uat.cjs +282 -0
- package/cap/bin/lib/verify.cjs +888 -0
- package/cap/bin/lib/workspace-detector.cjs +369 -0
- package/cap/bin/lib/workstream.cjs +491 -0
- package/cap/commands/gsd/workstreams.md +63 -0
- package/cap/references/arc-standard.md +315 -0
- package/cap/references/cap-agent-architecture.md +102 -0
- package/cap/references/cap-gitignore-template +9 -0
- package/cap/references/cap-zero-deps.md +158 -0
- package/cap/references/checkpoints.md +778 -0
- package/cap/references/continuation-format.md +249 -0
- package/cap/references/decimal-phase-calculation.md +64 -0
- package/cap/references/feature-map-template.md +25 -0
- package/cap/references/git-integration.md +295 -0
- package/cap/references/git-planning-commit.md +38 -0
- package/cap/references/model-profile-resolution.md +36 -0
- package/cap/references/model-profiles.md +139 -0
- package/cap/references/phase-argument-parsing.md +61 -0
- package/cap/references/planning-config.md +202 -0
- package/cap/references/questioning.md +162 -0
- package/cap/references/session-template.json +8 -0
- package/cap/references/tdd.md +263 -0
- package/cap/references/ui-brand.md +160 -0
- package/cap/references/user-profiling.md +681 -0
- package/cap/references/verification-patterns.md +612 -0
- package/cap/references/workstream-flag.md +58 -0
- package/cap/templates/DEBUG.md +164 -0
- package/cap/templates/UAT.md +265 -0
- package/cap/templates/UI-SPEC.md +100 -0
- package/cap/templates/VALIDATION.md +76 -0
- package/cap/templates/claude-md.md +122 -0
- package/cap/templates/codebase/architecture.md +255 -0
- package/cap/templates/codebase/concerns.md +310 -0
- package/cap/templates/codebase/conventions.md +307 -0
- package/cap/templates/codebase/integrations.md +280 -0
- package/cap/templates/codebase/stack.md +186 -0
- package/cap/templates/codebase/structure.md +285 -0
- package/cap/templates/codebase/testing.md +480 -0
- package/cap/templates/config.json +44 -0
- package/cap/templates/context.md +352 -0
- package/cap/templates/continue-here.md +78 -0
- package/cap/templates/copilot-instructions.md +7 -0
- package/cap/templates/debug-subagent-prompt.md +91 -0
- package/cap/templates/dev-preferences.md +21 -0
- package/cap/templates/discovery.md +146 -0
- package/cap/templates/discussion-log.md +63 -0
- package/cap/templates/milestone-archive.md +123 -0
- package/cap/templates/milestone.md +115 -0
- package/cap/templates/phase-prompt.md +610 -0
- package/cap/templates/planner-subagent-prompt.md +117 -0
- package/cap/templates/project.md +186 -0
- package/cap/templates/requirements.md +231 -0
- package/cap/templates/research-project/ARCHITECTURE.md +204 -0
- package/cap/templates/research-project/FEATURES.md +147 -0
- package/cap/templates/research-project/PITFALLS.md +200 -0
- package/cap/templates/research-project/STACK.md +120 -0
- package/cap/templates/research-project/SUMMARY.md +170 -0
- package/cap/templates/research.md +552 -0
- package/cap/templates/retrospective.md +54 -0
- package/cap/templates/roadmap.md +202 -0
- package/cap/templates/state.md +176 -0
- package/cap/templates/summary-complex.md +59 -0
- package/cap/templates/summary-minimal.md +41 -0
- package/cap/templates/summary-standard.md +48 -0
- package/cap/templates/summary.md +248 -0
- package/cap/templates/user-profile.md +146 -0
- package/cap/templates/user-setup.md +311 -0
- package/cap/templates/verification-report.md +322 -0
- package/cap/workflows/add-phase.md +112 -0
- package/cap/workflows/add-tests.md +351 -0
- package/cap/workflows/add-todo.md +158 -0
- package/cap/workflows/audit-milestone.md +340 -0
- package/cap/workflows/audit-uat.md +109 -0
- package/cap/workflows/autonomous.md +891 -0
- package/cap/workflows/check-todos.md +177 -0
- package/cap/workflows/cleanup.md +152 -0
- package/cap/workflows/complete-milestone.md +767 -0
- package/cap/workflows/diagnose-issues.md +231 -0
- package/cap/workflows/discovery-phase.md +289 -0
- package/cap/workflows/discuss-phase-assumptions.md +653 -0
- package/cap/workflows/discuss-phase.md +1049 -0
- package/cap/workflows/do.md +104 -0
- package/cap/workflows/execute-phase.md +846 -0
- package/cap/workflows/execute-plan.md +514 -0
- package/cap/workflows/fast.md +105 -0
- package/cap/workflows/forensics.md +265 -0
- package/cap/workflows/health.md +181 -0
- package/cap/workflows/help.md +660 -0
- package/cap/workflows/insert-phase.md +130 -0
- package/cap/workflows/list-phase-assumptions.md +178 -0
- package/cap/workflows/list-workspaces.md +56 -0
- package/cap/workflows/manager.md +362 -0
- package/cap/workflows/map-codebase.md +377 -0
- package/cap/workflows/milestone-summary.md +223 -0
- package/cap/workflows/new-milestone.md +486 -0
- package/cap/workflows/new-project.md +1250 -0
- package/cap/workflows/new-workspace.md +237 -0
- package/cap/workflows/next.md +97 -0
- package/cap/workflows/node-repair.md +92 -0
- package/cap/workflows/note.md +156 -0
- package/cap/workflows/pause-work.md +176 -0
- package/cap/workflows/plan-milestone-gaps.md +273 -0
- package/cap/workflows/plan-phase.md +859 -0
- package/cap/workflows/plant-seed.md +169 -0
- package/cap/workflows/pr-branch.md +129 -0
- package/cap/workflows/profile-user.md +450 -0
- package/cap/workflows/progress.md +507 -0
- package/cap/workflows/quick.md +757 -0
- package/cap/workflows/remove-phase.md +155 -0
- package/cap/workflows/remove-workspace.md +90 -0
- package/cap/workflows/research-phase.md +82 -0
- package/cap/workflows/resume-project.md +326 -0
- package/cap/workflows/review.md +228 -0
- package/cap/workflows/session-report.md +146 -0
- package/cap/workflows/settings.md +283 -0
- package/cap/workflows/ship.md +228 -0
- package/cap/workflows/stats.md +60 -0
- package/cap/workflows/transition.md +671 -0
- package/cap/workflows/ui-phase.md +302 -0
- package/cap/workflows/ui-review.md +165 -0
- package/cap/workflows/update.md +323 -0
- package/cap/workflows/validate-phase.md +174 -0
- package/cap/workflows/verify-phase.md +254 -0
- package/cap/workflows/verify-work.md +637 -0
- package/commands/cap/annotate.md +165 -0
- package/commands/cap/brainstorm.md +238 -0
- package/commands/cap/debug.md +297 -0
- package/commands/cap/init.md +262 -0
- package/commands/cap/iterate.md +234 -0
- package/commands/cap/prototype.md +281 -0
- package/commands/cap/refresh-docs.md +37 -0
- package/commands/cap/review.md +272 -0
- package/commands/cap/scan.md +249 -0
- package/commands/cap/start.md +234 -0
- package/commands/cap/status.md +189 -0
- package/commands/cap/test.md +250 -0
- package/hooks/dist/gsd-check-update.js +114 -0
- package/hooks/dist/gsd-context-monitor.js +156 -0
- package/hooks/dist/gsd-prompt-guard.js +96 -0
- package/hooks/dist/gsd-statusline.js +119 -0
- package/hooks/dist/gsd-workflow-guard.js +94 -0
- package/package.json +51 -0
- package/scripts/base64-scan.sh +262 -0
- package/scripts/build-hooks.js +82 -0
- package/scripts/cap-removal-checklist.md +202 -0
- package/scripts/prompt-injection-scan.sh +198 -0
- package/scripts/run-tests.cjs +29 -0
- 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();
|