forge-dev-framework 1.0.1 → 1.2.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/.claude/commands/forge/README.md +281 -0
- package/.claude/commands/forge/add-phase.md +90 -0
- package/.claude/commands/forge/complete-milestone.md +130 -0
- package/.claude/commands/forge/config.md +115 -0
- package/.claude/commands/forge/convert.md +31 -0
- package/.claude/commands/forge/debug.md +31 -0
- package/.claude/commands/forge/discuss.md +78 -0
- package/.claude/commands/forge/execute.md +85 -0
- package/.claude/commands/forge/generate.md +21 -0
- package/.claude/commands/forge/help.md +18 -0
- package/.claude/commands/forge/init.md +21 -0
- package/.claude/commands/forge/insert-phase.md +99 -0
- package/.claude/commands/forge/new-milestone.md +114 -0
- package/.claude/commands/forge/new-project.md +24 -0
- package/.claude/commands/forge/pause-work.md +111 -0
- package/.claude/commands/forge/plan.md +129 -0
- package/.claude/commands/forge/quick.md +41 -0
- package/.claude/commands/forge/remove-phase.md +92 -0
- package/.claude/commands/forge/resume.md +22 -0
- package/.claude/commands/forge/status.md +87 -0
- package/.claude/commands/forge/team-add.md +24 -0
- package/.claude/commands/forge/team-create.md +22 -0
- package/.claude/commands/forge/team-remove.md +24 -0
- package/.claude/commands/forge/team-start.md +22 -0
- package/.claude/commands/forge/team-view.md +18 -0
- package/.claude/commands/forge/verify.md +95 -0
- package/.claude/hooks/forge-context-cleanup.cjs +79 -0
- package/.claude/hooks/forge-event-guard.cjs +36 -0
- package/.claude/hooks/forge-size-guard.cjs +55 -0
- package/.claude/rules/api-patterns.md +13 -98
- package/.claude/rules/context-efficiency.md +10 -0
- package/.claude/rules/security-baseline.md +18 -204
- package/.claude/rules/testing-standards.md +16 -177
- package/.claude/rules/ui-conventions.md +17 -142
- package/README.md +1 -0
- package/bin/forge.js +5 -3
- package/dist/bin/forge.js +5 -3
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +15 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/convert.d.ts +6 -0
- package/dist/commands/convert.d.ts.map +1 -0
- package/dist/commands/convert.js +132 -0
- package/dist/commands/convert.js.map +1 -0
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +3 -2
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/index.d.ts +4 -4
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +4 -4
- package/dist/commands/index.js.map +1 -1
- package/dist/generators/gsd-converter.d.ts +100 -0
- package/dist/generators/gsd-converter.d.ts.map +1 -0
- package/dist/generators/gsd-converter.js +335 -0
- package/dist/generators/gsd-converter.js.map +1 -0
- package/dist/templates/.claude/rules/api-patterns.md.template +212 -0
- package/dist/templates/.claude/rules/security-baseline.md.template +322 -0
- package/dist/templates/.claude/rules/testing-standards.md.template +280 -0
- package/dist/templates/.claude/rules/ui-conventions.md.template +264 -0
- package/dist/templates/.planning/forge.config.json.template +75 -0
- package/dist/templates/CLAUDE.md.template +161 -0
- package/dist/templates/PLAN.md.template +177 -0
- package/dist/templates/PROJECT.md.template +156 -0
- package/dist/templates/REQUIREMENTS.md.template +221 -0
- package/dist/templates/ROADMAP.md.template +130 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +2 -2
- package/dist/types/index.js.map +1 -1
- package/dist/utils/index.d.ts +5 -5
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +5 -5
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/template-client.d.ts.map +1 -1
- package/dist/utils/template-client.js +3 -2
- package/dist/utils/template-client.js.map +1 -1
- package/package.json +6 -4
- package/dist/git/__tests__/worktree.test.d.ts +0 -5
- package/dist/git/__tests__/worktree.test.d.ts.map +0 -1
- package/dist/git/__tests__/worktree.test.js +0 -121
- package/dist/git/__tests__/worktree.test.js.map +0 -1
- package/dist/git/codeowners.d.ts +0 -101
- package/dist/git/codeowners.d.ts.map +0 -1
- package/dist/git/codeowners.js +0 -216
- package/dist/git/codeowners.js.map +0 -1
- package/dist/git/commit.d.ts +0 -135
- package/dist/git/commit.d.ts.map +0 -1
- package/dist/git/commit.js +0 -223
- package/dist/git/commit.js.map +0 -1
- package/dist/git/hooks/commit-msg.d.ts +0 -8
- package/dist/git/hooks/commit-msg.d.ts.map +0 -1
- package/dist/git/hooks/commit-msg.js +0 -34
- package/dist/git/hooks/commit-msg.js.map +0 -1
- package/dist/git/hooks/pre-commit.d.ts +0 -8
- package/dist/git/hooks/pre-commit.d.ts.map +0 -1
- package/dist/git/hooks/pre-commit.js +0 -34
- package/dist/git/hooks/pre-commit.js.map +0 -1
- package/dist/git/pre-commit-hooks.d.ts +0 -117
- package/dist/git/pre-commit-hooks.d.ts.map +0 -1
- package/dist/git/pre-commit-hooks.js +0 -270
- package/dist/git/pre-commit-hooks.js.map +0 -1
- package/dist/git/wipe-protocol.d.ts +0 -281
- package/dist/git/wipe-protocol.d.ts.map +0 -1
- package/dist/git/wipe-protocol.js +0 -237
- package/dist/git/wipe-protocol.js.map +0 -1
- package/dist/git/worktree.d.ts +0 -69
- package/dist/git/worktree.d.ts.map +0 -1
- package/dist/git/worktree.js +0 -202
- package/dist/git/worktree.js.map +0 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// FORGE Hook: Event Immutability Guard
|
|
3
|
+
// PreToolUse on Edit — prevents modifying existing event files in state/events/
|
|
4
|
+
// Events are append-only per FORGE architecture
|
|
5
|
+
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
let input = '';
|
|
9
|
+
process.stdin.setEncoding('utf8');
|
|
10
|
+
process.stdin.on('data', chunk => input += chunk);
|
|
11
|
+
process.stdin.on('end', () => {
|
|
12
|
+
try {
|
|
13
|
+
const data = JSON.parse(input);
|
|
14
|
+
const toolName = data.tool_name || '';
|
|
15
|
+
const filePath = data.tool_input?.file_path || '';
|
|
16
|
+
|
|
17
|
+
// Only guard Edit operations on event files
|
|
18
|
+
if (toolName === 'Edit' && filePath) {
|
|
19
|
+
const resolved = path.resolve(filePath);
|
|
20
|
+
const eventsDir = path.resolve(process.cwd(), 'state', 'events');
|
|
21
|
+
|
|
22
|
+
if (resolved.startsWith(eventsDir) && resolved.endsWith('.json')) {
|
|
23
|
+
process.stdout.write(JSON.stringify({
|
|
24
|
+
decision: 'block',
|
|
25
|
+
reason: 'FORGE: Events are append-only. Cannot edit files in state/events/. Write new events instead.'
|
|
26
|
+
}));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
process.stdout.write(JSON.stringify({ decision: 'approve' }));
|
|
32
|
+
} catch (e) {
|
|
33
|
+
// Don't block on parse errors
|
|
34
|
+
process.stdout.write(JSON.stringify({ decision: 'approve' }));
|
|
35
|
+
}
|
|
36
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// FORGE Hook: File Size Guard
|
|
3
|
+
// PostToolUse on Write/Edit — warns when artifacts exceed token limits
|
|
4
|
+
// Limits: CLAUDE.md ~2000t, rules ~1000t, REQUIREMENTS.md ~4000t, PLAN.md ~6000t
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
// Approximate tokens as chars/4
|
|
10
|
+
const LIMITS = {
|
|
11
|
+
'CLAUDE.md': { tokens: 2000, chars: 8000 },
|
|
12
|
+
'REQUIREMENTS.md': { tokens: 4000, chars: 16000 },
|
|
13
|
+
'PLAN.md': { tokens: 6000, chars: 24000 },
|
|
14
|
+
};
|
|
15
|
+
const RULES_LIMIT = { tokens: 1000, chars: 4000 };
|
|
16
|
+
|
|
17
|
+
let input = '';
|
|
18
|
+
process.stdin.setEncoding('utf8');
|
|
19
|
+
process.stdin.on('data', chunk => input += chunk);
|
|
20
|
+
process.stdin.on('end', () => {
|
|
21
|
+
try {
|
|
22
|
+
const data = JSON.parse(input);
|
|
23
|
+
const filePath = data.tool_input?.file_path || data.tool_result?.file_path || '';
|
|
24
|
+
|
|
25
|
+
if (!filePath) {
|
|
26
|
+
process.stdout.write(JSON.stringify({ decision: 'approve' }));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const basename = path.basename(filePath);
|
|
31
|
+
const resolved = path.resolve(filePath);
|
|
32
|
+
|
|
33
|
+
// Check specific file limits
|
|
34
|
+
let limit = LIMITS[basename];
|
|
35
|
+
|
|
36
|
+
// Check rules directory
|
|
37
|
+
if (!limit && resolved.includes('.claude/rules/') && resolved.endsWith('.md')) {
|
|
38
|
+
limit = RULES_LIMIT;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (limit && fs.existsSync(resolved)) {
|
|
42
|
+
const size = fs.statSync(resolved).size;
|
|
43
|
+
if (size > limit.chars) {
|
|
44
|
+
const approxTokens = Math.round(size / 4);
|
|
45
|
+
process.stderr.write(
|
|
46
|
+
`\x1b[33m⚠ FORGE: ${basename} is ~${approxTokens} tokens (limit: ~${limit.tokens}). Consider trimming.\x1b[0m\n`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
process.stdout.write(JSON.stringify({ decision: 'approve' }));
|
|
52
|
+
} catch (e) {
|
|
53
|
+
process.stdout.write(JSON.stringify({ decision: 'approve' }));
|
|
54
|
+
}
|
|
55
|
+
});
|
|
@@ -1,98 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
1. Types/interfaces first
|
|
15
|
-
2. Functions/operations
|
|
16
|
-
3. Constants at the end
|
|
17
|
-
|
|
18
|
-
```typescript
|
|
19
|
-
// src/state/event-types.ts
|
|
20
|
-
export interface StateEvent { ... }
|
|
21
|
-
export interface TaskStartedEvent extends StateEvent { ... }
|
|
22
|
-
|
|
23
|
-
export function createEvent(type: EventType, payload: unknown): StateEvent { ... }
|
|
24
|
-
|
|
25
|
-
export const EVENT_TYPES = ['TASK_STARTED', 'TASK_COMPLETED', ...] as const;
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## Error Handling
|
|
29
|
-
|
|
30
|
-
- Define custom error types extending `Error`
|
|
31
|
-
- Include error codes for programmatic handling
|
|
32
|
-
- Provide context in error messages
|
|
33
|
-
|
|
34
|
-
```typescript
|
|
35
|
-
export class StateValidationError extends Error {
|
|
36
|
-
constructor(
|
|
37
|
-
public schema: string,
|
|
38
|
-
public errors: z.ZodError,
|
|
39
|
-
) {
|
|
40
|
-
super(`State validation failed for ${schema}: ${errors.message}`);
|
|
41
|
-
this.name = 'StateValidationError';
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
## Async Operations
|
|
47
|
-
|
|
48
|
-
- Use `async/await`, not Promises directly
|
|
49
|
-
- Return typed promises: `Promise<T>`
|
|
50
|
-
- Handle rejections with try/catch
|
|
51
|
-
|
|
52
|
-
```typescript
|
|
53
|
-
export async function loadState(path: string): Promise<State> {
|
|
54
|
-
try {
|
|
55
|
-
const content = await fs.readFile(path, 'utf-8');
|
|
56
|
-
return JSON.parse(content);
|
|
57
|
-
} catch (error) {
|
|
58
|
-
throw new StateLoadError(path, error);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
## Input Validation
|
|
64
|
-
|
|
65
|
-
- Use Zod schemas for all public APIs
|
|
66
|
-
- Validate at module boundaries
|
|
67
|
-
- Return typed results, never `any`
|
|
68
|
-
|
|
69
|
-
```typescript
|
|
70
|
-
import { z } from 'zod';
|
|
71
|
-
|
|
72
|
-
const EventSchema = z.object({
|
|
73
|
-
eventId: z.string(),
|
|
74
|
-
timestamp: z.string().datetime(),
|
|
75
|
-
taskId: z.string(),
|
|
76
|
-
actor: z.string(),
|
|
77
|
-
type: z.enum(EVENT_TYPES),
|
|
78
|
-
payload: z.unknown(),
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
export function parseEvent(data: unknown): StateEvent {
|
|
82
|
-
return EventSchema.parse(data);
|
|
83
|
-
}
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
## File I/O
|
|
87
|
-
|
|
88
|
-
- Use absolute paths from project root
|
|
89
|
-
- Check file existence before reading
|
|
90
|
-
- Use atomic writes (write to temp, then rename)
|
|
91
|
-
|
|
92
|
-
```typescript
|
|
93
|
-
export async function writeState(path: string, state: State): Promise<void> {
|
|
94
|
-
const tmpPath = `${path}.tmp`;
|
|
95
|
-
await fs.writeFile(tmpPath, JSON.stringify(state, null, 2));
|
|
96
|
-
await fs.rename(tmpPath, path);
|
|
97
|
-
}
|
|
98
|
-
```
|
|
1
|
+
---
|
|
2
|
+
globs:
|
|
3
|
+
- "src/**/*.ts"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# API Design Patterns
|
|
7
|
+
|
|
8
|
+
- **Files:** `kebab-case.ts` | **Types:** `PascalCase` | **Functions:** `camelCase`
|
|
9
|
+
- **Module exports order:** types/interfaces → functions → constants
|
|
10
|
+
- **Errors:** Custom error classes extending `Error` with error codes and context
|
|
11
|
+
- **Async:** Use `async/await` with typed `Promise<T>` returns, try/catch for rejections
|
|
12
|
+
- **Validation:** Zod schemas at all public API boundaries — never return `any`
|
|
13
|
+
- **File I/O:** Absolute paths from project root, check existence, atomic writes (write to temp → rename)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Context Efficiency
|
|
2
|
+
|
|
3
|
+
- **Read selectively:** Use `offset`/`limit` for large files — don't read entire files when only a section matters
|
|
4
|
+
- **Minimize re-reads:** Cache key info in your working memory instead of re-reading the same file multiple times
|
|
5
|
+
- **Short outputs:** Prefer concise tool output — pipe through `head`/`tail` when full output isn't needed
|
|
6
|
+
- **Atomic tasks:** One task = one focused context. Don't accumulate unrelated work in a single session
|
|
7
|
+
- **Summarize early:** After reading large files, extract what you need and move on — don't keep raw content in context
|
|
8
|
+
- **Event hygiene:** Old events auto-archive after 7 days (SessionStart hook). Don't read archived events unless replaying state
|
|
9
|
+
- **File size limits:** CLAUDE.md ~2000t, rules ~1000t each, REQUIREMENTS.md ~4000t, PLAN.md ~6000t (enforced by PostToolUse hook)
|
|
10
|
+
- **Team messages:** Keep inter-agent messages concise — include only actionable information
|
|
@@ -1,204 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
### Path Validation
|
|
23
|
-
|
|
24
|
-
Prevent directory traversal:
|
|
25
|
-
|
|
26
|
-
```typescript
|
|
27
|
-
import { resolve, normalize } from 'path';
|
|
28
|
-
|
|
29
|
-
export function safeJoin(base: string, userPath: string): string {
|
|
30
|
-
const resolved = resolve(base, userPath);
|
|
31
|
-
if (!resolved.startsWith(base)) {
|
|
32
|
-
throw new Error('Invalid path: outside base directory');
|
|
33
|
-
}
|
|
34
|
-
return resolved;
|
|
35
|
-
}
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
## Secrets Management
|
|
39
|
-
|
|
40
|
-
**NEVER hardcode secrets.** Use environment variables:
|
|
41
|
-
|
|
42
|
-
```typescript
|
|
43
|
-
export function getApiKey(): string {
|
|
44
|
-
const key = process.env.FORGE_API_KEY;
|
|
45
|
-
if (!key) {
|
|
46
|
-
throw new Error('FORGE_API_KEY environment variable not set');
|
|
47
|
-
}
|
|
48
|
-
return key;
|
|
49
|
-
}
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
### Git Ignore
|
|
53
|
-
|
|
54
|
-
Add to `.gitignore`:
|
|
55
|
-
|
|
56
|
-
```
|
|
57
|
-
# FORGE
|
|
58
|
-
state/events/*.json # Optional: don't commit event logs
|
|
59
|
-
.planning/local/ # User-specific config
|
|
60
|
-
.env # Environment variables
|
|
61
|
-
*.tmp # Temp files
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
## Command Injection Prevention
|
|
65
|
-
|
|
66
|
-
**NEVER pass user input directly to shell commands.** Use array arguments:
|
|
67
|
-
|
|
68
|
-
```typescript
|
|
69
|
-
// BAD
|
|
70
|
-
await execa(`git checkout ${userBranch}`);
|
|
71
|
-
|
|
72
|
-
// GOOD
|
|
73
|
-
await execa('git', ['checkout', userBranch]);
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
## File Permissions
|
|
77
|
-
|
|
78
|
-
Respect user's umask, don't over-permission:
|
|
79
|
-
|
|
80
|
-
```typescript
|
|
81
|
-
import { constants } from 'fs';
|
|
82
|
-
|
|
83
|
-
await fs.writeFile(path, content, {
|
|
84
|
-
mode: 0o644, // rw-r--r--
|
|
85
|
-
});
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
For sensitive files:
|
|
89
|
-
|
|
90
|
-
```typescript
|
|
91
|
-
await fs.writeFile(path, secrets, {
|
|
92
|
-
mode: 0o600, // rw-------
|
|
93
|
-
});
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
## Dependency Security
|
|
97
|
-
|
|
98
|
-
Run `npm audit` in CI:
|
|
99
|
-
|
|
100
|
-
```yaml
|
|
101
|
-
# .github/workflows/ci.yml
|
|
102
|
-
- name: Audit dependencies
|
|
103
|
-
run: npm audit --audit-level=moderate
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
### Locked Dependencies
|
|
107
|
-
|
|
108
|
-
Commit `package-lock.json`. Always install with exact versions:
|
|
109
|
-
|
|
110
|
-
```bash
|
|
111
|
-
npm ci # Use lock file, don't update
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
## Code Execution Safety
|
|
115
|
-
|
|
116
|
-
**NO `eval()`, no dynamic imports from untrusted sources:**
|
|
117
|
-
|
|
118
|
-
```typescript
|
|
119
|
-
// BAD
|
|
120
|
-
const module = await import(userPath);
|
|
121
|
-
|
|
122
|
-
// GOOD — validate against whitelist
|
|
123
|
-
const ALLOWED = ['state', 'cli', 'templates'];
|
|
124
|
-
if (ALLOWED.includes(userPath)) {
|
|
125
|
-
const module = await import(`./${userPath}`);
|
|
126
|
-
}
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
## Event Log Integrity
|
|
130
|
-
|
|
131
|
-
Events should be **append-only**. Never modify existing events:
|
|
132
|
-
|
|
133
|
-
```typescript
|
|
134
|
-
export async function writeEvent(path: string, event: StateEvent): Promise<void> {
|
|
135
|
-
if (await fs.pathExists(path)) {
|
|
136
|
-
throw new Error(`Event file already exists: ${path}`);
|
|
137
|
-
}
|
|
138
|
-
await fs.writeFile(path, JSON.stringify(event, null, 2), { mode: 0o444 }); // read-only
|
|
139
|
-
}
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
## State Validation
|
|
143
|
-
|
|
144
|
-
Validate `STATE.json` against schema on every read:
|
|
145
|
-
|
|
146
|
-
```typescript
|
|
147
|
-
export async function loadState(path: string): Promise<State> {
|
|
148
|
-
const content = await fs.readFile(path, 'utf-8');
|
|
149
|
-
const raw = JSON.parse(content);
|
|
150
|
-
return StateSchema.parse(raw); // Zod validation
|
|
151
|
-
}
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
## Git Worktree Isolation
|
|
155
|
-
|
|
156
|
-
Ensure worktrees can't access each other's files:
|
|
157
|
-
|
|
158
|
-
```typescript
|
|
159
|
-
export function validateWorktreePath(worktreePath: string): void {
|
|
160
|
-
if (!worktreePath.startsWith('.worktrees/')) {
|
|
161
|
-
throw new Error('Invalid worktree: must be under .worktrees/');
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
## Error Messages
|
|
167
|
-
|
|
168
|
-
**Don't leak sensitive info in errors:**
|
|
169
|
-
|
|
170
|
-
```typescript
|
|
171
|
-
// BAD — leaks file path
|
|
172
|
-
throw new Error(`Failed to read ${filePath}`);
|
|
173
|
-
|
|
174
|
-
// GOOD — generic error
|
|
175
|
-
throw new Error('Failed to read configuration file');
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
## CI/CD Security
|
|
179
|
-
|
|
180
|
-
```yaml
|
|
181
|
-
# .github/workflows/ci.yml
|
|
182
|
-
permissions:
|
|
183
|
-
contents: read # No write access
|
|
184
|
-
issues: read
|
|
185
|
-
pull-requests: read
|
|
186
|
-
|
|
187
|
-
- name: Run tests
|
|
188
|
-
run: npm test
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
## Third-Party Code
|
|
192
|
-
|
|
193
|
-
Review all dependencies before adding:
|
|
194
|
-
|
|
195
|
-
```bash
|
|
196
|
-
npm audit
|
|
197
|
-
npm ls <package>
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
Prefer well-maintained packages with:
|
|
201
|
-
- Active development
|
|
202
|
-
- Security policy
|
|
203
|
-
- TypeScript types
|
|
204
|
-
- Low dependency count
|
|
1
|
+
---
|
|
2
|
+
globs:
|
|
3
|
+
- "src/**/*.ts"
|
|
4
|
+
- "*.json"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Security Baseline
|
|
8
|
+
|
|
9
|
+
- **Input validation:** All user input validated with Zod schemas
|
|
10
|
+
- **Path validation:** Prevent directory traversal — resolve and check `startsWith(base)`
|
|
11
|
+
- **Secrets:** Never hardcode — use environment variables only
|
|
12
|
+
- **Command injection:** Never pass user input to shell strings — use array args: `execa('git', ['checkout', branch])`
|
|
13
|
+
- **File permissions:** `0o644` default, `0o600` for sensitive files, respect user umask
|
|
14
|
+
- **No `eval()`** or dynamic imports from untrusted sources
|
|
15
|
+
- **Events are append-only** — never modify, write as read-only (`0o444`)
|
|
16
|
+
- **Validate STATE.json** against schema on every read
|
|
17
|
+
- **Error messages:** Don't leak file paths or sensitive info
|
|
18
|
+
- **Dependencies:** `npm audit` in CI, commit `package-lock.json`, use `npm ci`
|
|
@@ -1,177 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
## Unit Tests
|
|
19
|
-
|
|
20
|
-
Test **pure functions** and **business logic**:
|
|
21
|
-
|
|
22
|
-
```typescript
|
|
23
|
-
// event-types.test.ts
|
|
24
|
-
import { describe, it, expect } from 'jest';
|
|
25
|
-
import { parseEvent, createEvent } from './event-types';
|
|
26
|
-
|
|
27
|
-
describe('parseEvent', () => {
|
|
28
|
-
it('parses valid TASK_STARTED event', () => {
|
|
29
|
-
const event = parseEvent({
|
|
30
|
-
eventId: 'evt-001',
|
|
31
|
-
timestamp: '2026-02-11T21:10:00Z',
|
|
32
|
-
taskId: 'api-003',
|
|
33
|
-
actor: 'backend',
|
|
34
|
-
type: 'TASK_STARTED',
|
|
35
|
-
payload: {},
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
expect(event.type).toBe('TASK_STARTED');
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('throws on missing eventId', () => {
|
|
42
|
-
expect(() => parseEvent({})).toThrow('eventId');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('throws on invalid timestamp format', () => {
|
|
46
|
-
expect(() => parseEvent({
|
|
47
|
-
eventId: 'evt-001',
|
|
48
|
-
timestamp: 'invalid',
|
|
49
|
-
taskId: 'api-003',
|
|
50
|
-
actor: 'backend',
|
|
51
|
-
type: 'TASK_STARTED',
|
|
52
|
-
payload: {},
|
|
53
|
-
})).toThrow('timestamp');
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## Integration Tests
|
|
59
|
-
|
|
60
|
-
Test **workflows end-to-end**:
|
|
61
|
-
|
|
62
|
-
```typescript
|
|
63
|
-
// state-merge.integration.test.ts
|
|
64
|
-
describe('State Steward merge', () => {
|
|
65
|
-
const testDir = tmpdir();
|
|
66
|
-
|
|
67
|
-
beforeEach(async () => {
|
|
68
|
-
await fs.mkdir(`${testDir}/state/events`, { recursive: true });
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('merges events in timestamp order', async () => {
|
|
72
|
-
await writeEvent(`${testDir}/state/events/evt-002.json`, {
|
|
73
|
-
timestamp: '2026-02-11T21:11:00Z',
|
|
74
|
-
type: 'TASK_COMPLETED',
|
|
75
|
-
});
|
|
76
|
-
await writeEvent(`${testDir}/state/events/evt-001.json`, {
|
|
77
|
-
timestamp: '2026-02-11T21:10:00Z',
|
|
78
|
-
type: 'TASK_STARTED',
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
const state = await mergeEvents(testDir);
|
|
82
|
-
|
|
83
|
-
expect(state.tasks[0].status).toBe('completed');
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
## Coverage Targets
|
|
89
|
-
|
|
90
|
-
- **Core logic** (state, events, templates): >90%
|
|
91
|
-
- **CLI commands**: >70%
|
|
92
|
-
- **Git operations**: >80%
|
|
93
|
-
|
|
94
|
-
Run: `npm run test:coverage`
|
|
95
|
-
|
|
96
|
-
## Test Data
|
|
97
|
-
|
|
98
|
-
Create fixtures in `test/fixtures/`:
|
|
99
|
-
|
|
100
|
-
```
|
|
101
|
-
test/fixtures/
|
|
102
|
-
events/
|
|
103
|
-
task-started.json
|
|
104
|
-
task-completed.json
|
|
105
|
-
state/
|
|
106
|
-
initial-state.json
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
## Mocking
|
|
110
|
-
|
|
111
|
-
Mock external dependencies (git, fs, network):
|
|
112
|
-
|
|
113
|
-
```typescript
|
|
114
|
-
import { jest } from '@jest/globals';
|
|
115
|
-
|
|
116
|
-
jest.mock('execa', () => ({
|
|
117
|
-
execa: jest.fn(),
|
|
118
|
-
}));
|
|
119
|
-
|
|
120
|
-
import { execa } from 'execa';
|
|
121
|
-
|
|
122
|
-
it('creates git worktree', async () => {
|
|
123
|
-
(execa as jest.Mock).mockResolvedValue({ stdout: '' });
|
|
124
|
-
|
|
125
|
-
await createWorktree('api-003', 'forge/api-003');
|
|
126
|
-
|
|
127
|
-
expect(execa).toHaveBeenCalledWith('git', [
|
|
128
|
-
'worktree', 'add', '.worktrees/api-003', '-b', 'forge/api-003',
|
|
129
|
-
]);
|
|
130
|
-
});
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
## Test Organization
|
|
134
|
-
|
|
135
|
-
Use `describe` for logical groups, `it` for individual tests:
|
|
136
|
-
|
|
137
|
-
```typescript
|
|
138
|
-
describe('State Steward', () => {
|
|
139
|
-
describe('mergeEvents', () => {
|
|
140
|
-
it('handles empty event directory');
|
|
141
|
-
it('sorts events by timestamp');
|
|
142
|
-
it('handles duplicate eventIds');
|
|
143
|
-
it('skips malformed events');
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
## Async Tests
|
|
149
|
-
|
|
150
|
-
Always `await` async operations:
|
|
151
|
-
|
|
152
|
-
```typescript
|
|
153
|
-
it('merges events', async () => {
|
|
154
|
-
const state = await mergeEvents('/path/to/events');
|
|
155
|
-
expect(state).toBeDefined();
|
|
156
|
-
});
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
## Clean Up
|
|
160
|
-
|
|
161
|
-
Use `beforeEach` / `afterEach` for test isolation:
|
|
162
|
-
|
|
163
|
-
```typescript
|
|
164
|
-
import { tmpdir } from 'os';
|
|
165
|
-
import { rimraf } from 'rimraf';
|
|
166
|
-
|
|
167
|
-
let testDir: string;
|
|
168
|
-
|
|
169
|
-
beforeEach(async () => {
|
|
170
|
-
testDir = path.join(tmpdir(), `forge-test-${Date.now()}`);
|
|
171
|
-
await fs.mkdir(testDir, { recursive: true });
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
afterEach(async () => {
|
|
175
|
-
await rimraf(testDir);
|
|
176
|
-
});
|
|
177
|
-
```
|
|
1
|
+
---
|
|
2
|
+
globs:
|
|
3
|
+
- "**/*.test.ts"
|
|
4
|
+
- "**/*.spec.ts"
|
|
5
|
+
- "src/**/*.ts"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Testing Standards
|
|
9
|
+
|
|
10
|
+
- **Placement:** Tests next to source files with `.test.ts` suffix
|
|
11
|
+
- **Coverage:** Core logic >90%, CLI >70%, Git ops >80%
|
|
12
|
+
- **Fixtures:** In `test/fixtures/` (events, state samples)
|
|
13
|
+
- **Mocking:** Mock externals (git, fs, network) with `jest.mock()`
|
|
14
|
+
- **Structure:** `describe` for groups, `it` for cases, always `await` async ops
|
|
15
|
+
- **Cleanup:** `beforeEach`/`afterEach` with temp dirs, `rimraf` after
|
|
16
|
+
- **Run:** `npm test` / `npm run test:coverage`
|