@relipa/ai-flow-kit 0.0.5 → 0.0.6-beta.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/README.md CHANGED
@@ -23,81 +23,49 @@ Developers only need a single command to load ticket context → AI automaticall
23
23
  > [!TIP]
24
24
  > After running `aiflow init`, documentation files are automatically copied to `.aiflow/docs/` in your project for easy access.
25
25
 
26
- - **[Quick Start Guide](./docs/common/QUICK_START.md)** — Step-by-step instructions for developers.
27
- - **[Full Workflow (5 Gates)](./docs/common/AIFLOW.md)** — In-depth look at the Gate process.
28
- - **[Troubleshooting](./docs/common/troubleshooting.md)** — Common issues and fixes.
29
- - **[Integration Guide](./docs/common/ai-integration.md)** — Advanced tool setup (Claude, Cursor, Gemini).
30
- - **[Changelog](./docs/common/CHANGELOG.md)** — Version history and release notes.
26
+ - **[Quick Start Guide](https://gitlab.relipa.vn/ai/ai-flow-kit/-/blob/main/docs/common/QUICK_START.md)** — Step-by-step instructions for developers.
27
+ - **[Full Workflow (5 Gates)](https://gitlab.relipa.vn/ai/ai-flow-kit/-/blob/main/docs/common/AIFLOW.md)** — In-depth look at the Gate process.
28
+ - **[Troubleshooting](https://gitlab.relipa.vn/ai/ai-flow-kit/-/blob/main/docs/common/troubleshooting.md)** — Common issues and fixes.
29
+ - **[Integration Guide](https://gitlab.relipa.vn/ai/ai-flow-kit/-/blob/main/docs/common/ai-integration.md)** — Advanced tool setup (Claude, Cursor, Gemini).
30
+ - **[Changelog](https://gitlab.relipa.vn/ai/ai-flow-kit/-/blob/main/docs/common/CHANGELOG.md)** — Version history and release notes.
31
31
 
32
32
  ---
33
33
 
34
34
  ## Overview Flow
35
35
 
36
36
  ```
37
- ┌─────────────────────────────────────────────────────────────────┐
38
- TECH LEAD │
39
- │ npm publish @relipa/ai-flow-kit → developer: npm install -g │
40
- └─────────────────────────────────┬───────────────────────────────┘
41
-
42
- ┌─────────────▼─────────────┐
43
- │ aiflow init │
44
- --framework spring-boot │
45
- --adapter backlog │
46
- └─────────────┬─────────────┘
47
- │ setup
48
- ┌─────────────▼──────────────────────────────┐
49
- │ Project │
50
- .claude/ │
51
- ├── skills/ ← superpowers + custom│
52
- │ ├── hooks/ ← SessionStart hook │
53
- │ └── settings.json ← hook config │
54
- .aiflow/ │
55
- ├── context/ ← ticket context │
56
- └── versions/ ← versioned skills │
57
- │ .rules/ ← team rules (java/) │
58
- │ CLAUDE.md ← framework template │
59
- .mcp.json ← MCP adapter config │
60
- └─────────────┬──────────────────────────────┘
61
-
62
- ┌──────────────────▼──────────────────┐
63
- │ Developer workflow │
64
- │ │
65
- 1. aiflow use PROJ-33 │
66
- └─ Pull title, description, │
67
- and comments from Backlog │
68
- │ └─ Save → .claude/context/
69
- │ │
70
- │ 2. claude (open Claude terminal) │
71
- │ │
72
- │ 3. "Fix bug PROJ-33" (1 line) │
73
- └──────────────────┬──────────────────┘
74
-
75
- ┌──────────────────▼──────────────────┐
76
- │ Claude AI │
77
- │ │
78
- │ SessionStart hook injected │
79
- │ using-superpowers skill │
80
- │ ↓ │
81
- │ Read CLAUDE.md (spring-boot rules) │
82
- │ ↓ │
83
- │ Read context/current.json (PROJ-33) │
84
- │ ↓ │
85
- │ Auto-detect: bug-fix task │
86
- │ ↓ │
87
- │ Invoke investigate-bug skill │
88
- │ → inherits systematic-debugging │
89
- │ → trace Controller→Service→Repo │
90
- │ → git blame to understand history │
91
- │ → propose minimal fix │
92
- │ ↓ │
93
- │ Invoke impact-analysis skill │
94
- │ → check breaking changes │
95
- └──────────────────┬──────────────────┘
96
-
97
- ┌──────────────────▼──────────────────┐
98
- │ Developer review │
99
- │ git diff → test → commit │
100
- └──────────────────────────────────────┘
37
+ PM writes ticket on Backlog/Jira
38
+
39
+
40
+ ⛩️ GATE 1 — AI Analyze Requirement [AI + DEV]
41
+ aiflow use PROJ-33 → claude
42
+ AI loads ticket + reads source code
43
+ AI asks clarifying questions (max 1 in Fast mode)
44
+ AI outputs: plan/PROJ-33/requirement.md (Lite in Fast mode)
45
+ DEV reviews → "APPROVED"
46
+ │ APPROVED
47
+
48
+ ⛩️ GATE 2 — Implementation Plan [AI + DEV]
49
+ AI creates detailed coding plan (TDD steps)
50
+ DEV reviews → "APPROVED"
51
+ APPROVED
52
+
53
+ ⛩️ GATE 3 — Code Generation (TDD) [AI]
54
+ AI writes tests first → implement → tests pass
55
+ Commit each small step
56
+ Code done
57
+
58
+ ⛩️ GATE 4 — AI Self-Review [AI + DEV]
59
+ AI runs: verification + impact-analysis + checklist
60
+ AI generates: plan/PROJ-33/summary.md
61
+ DEV reviews → "APPROVED" or "BUG: [description]"
62
+ │ APPROVED
63
+
64
+ ⛩️ GATE 5 — Peer Review & PR [DEV + Peer]
65
+ Create Pull Request → peer review → merge
66
+
67
+
68
+ DONE
101
69
  ```
102
70
 
103
71
  ---
@@ -123,6 +91,7 @@ npm uninstall -g @relipa/ai-flow-kit
123
91
  |---------|-------------|
124
92
  | `aiflow init` | Setup framework, adapters, and multi-AI rules. |
125
93
  | `aiflow use <ticket>` | Load ticket context (Backlog/Jira). |
94
+ | `aiflow task` | Manage multiple tasks — pause, switch, resume. |
126
95
  | `aiflow prompt` | Generate tool-optimized prompts (`--env cursor`). |
127
96
  | `aiflow telemetry` | Manage telemetry tracking (enable/disable/status). |
128
97
  | `aiflow guide` | View integrated multi-AI integration guide. |
@@ -135,6 +104,9 @@ Initial setup for the project. Run once per project.
135
104
  # Single framework + adapter
136
105
  aiflow init --framework spring-boot --adapter backlog
137
106
 
107
+ # Force enable RTK token compression
108
+ aiflow init --with-rtk
109
+
138
110
  # Multiple frameworks and adapters
139
111
  aiflow init --framework spring-boot,reactjs --adapter backlog,jira
140
112
  ```
@@ -181,12 +153,52 @@ aiflow use PROJ-33 --comments-from 3
181
153
  # Manual input
182
154
  aiflow use --manual
183
155
 
156
+ # Load from local file (JSON or plain text)
157
+ aiflow use --file task.md
158
+
159
+ # Modes (Fast is default)
160
+ aiflow use PROJ-33 # Fast Mode: skip Q&A, target < 5 min
161
+ aiflow use PROJ-33 --full # Full Mode: deep analysis with Q&A
162
+
184
163
  # Save with custom name
185
164
  aiflow use PROJ-33 --save sprint-42-bug
186
165
  ```
187
166
 
188
167
  ---
189
168
 
169
+ ### `aiflow task`
170
+
171
+ Manage multiple tasks — pause a task, switch to another, and resume later while keeping full progress.
172
+
173
+ ```bash
174
+ aiflow task status # show active task and pending tasks
175
+ aiflow task list # list all saved tasks
176
+ aiflow task pause # pause current task (saves context + gate progress)
177
+ aiflow task pause --note "waiting for API spec" # pause with a note
178
+ aiflow task switch PROJ-99 # pause current + switch to another task
179
+ aiflow task resume PROJ-33 # resume a paused task (restores context + gate state)
180
+ aiflow task reset PROJ-33 # reset to Gate 1 (keeps ticket context, clears progress)
181
+ aiflow task remove PROJ-33 # permanently delete all saved data for the task
182
+ ```
183
+
184
+ **Use case — switching tasks mid-flow:**
185
+
186
+ ```bash
187
+ # Working on PROJ-33 at Gate 2...
188
+ aiflow task pause # pauses and saves PROJ-33 progress
189
+
190
+ aiflow use PROJ-99 # loads urgent task (auto-pauses current)
191
+ claude # AI starts Gate 1 for PROJ-99
192
+
193
+ # Done with PROJ-99, resuming PROJ-33
194
+ aiflow task resume PROJ-33 # restores context
195
+ claude # AI resumes from Gate 2 automatically
196
+ ```
197
+
198
+ Task state (context + gate progress) is saved in `.aiflow/tasks/<taskId>/`.
199
+
200
+ ---
201
+
190
202
  ### `aiflow doctor`
191
203
 
192
204
  Check if the setup is complete.
package/bin/aiflow.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  const { program } = require('commander');
4
4
  const chalk = require('chalk');
@@ -15,6 +15,7 @@ const promptCommand = require('../scripts/prompt');
15
15
  const removeCommand = require('../scripts/remove');
16
16
  const guideCommand = require('../scripts/guide');
17
17
  const telemetryCommand= require('../scripts/telemetry/cli');
18
+ const taskCommand = require('../scripts/task');
18
19
  const { record } = require('../scripts/telemetry/record');
19
20
 
20
21
  program
@@ -60,6 +61,8 @@ program
60
61
  .option('-f, --framework <types>', 'framework(s), comma-separated (e.g. spring-boot,reactjs)')
61
62
  .option('-a, --adapter <types>', 'adapter(s), comma-separated (e.g. backlog,jira)')
62
63
  .option('-e, --env <types>', 'AI environment(s)/tool(s), comma-separated (e.g. cursor,gemini,copilot)')
64
+ .option('--with-rtk', 'force enable RTK token compression hook')
65
+ .option('--no-rtk', 'skip RTK setup even if RTK is detected')
63
66
  .action((options) => {
64
67
  options.frameworks = options.framework
65
68
  ? options.framework.split(',').map(s => s.trim()).filter(Boolean)
@@ -69,7 +72,7 @@ program
69
72
  : [];
70
73
  options.aiTools = options.env
71
74
  ? options.env.split(',').map(s => s.trim()).filter(Boolean)
72
- : Object.keys(require('../scripts/init').AI_TOOL_FILES || {});
75
+ : Object.keys(require('../scripts/init').AI_TOOL_FILES || {}).filter(t => t !== 'generic');
73
76
 
74
77
  record('command.invoked', { command: 'init' });
75
78
  initCommand(options);
@@ -85,6 +88,8 @@ program
85
88
  .option('--with-comments', 'load all comments (default: description only)')
86
89
  .option('--comments-last <n>', 'load only the last N comments', parseInt)
87
90
  .option('--comments-from <n>', 'load comments from index N onward (0-based)', parseInt)
91
+ .option('--fast', 'fast mode: minimal Q&A, concise requirement doc (Default)')
92
+ .option('--full', 'full mode: force complete analysis with Q&A (default)')
88
93
  .action((target, options) => {
89
94
  record('command.invoked', { command: 'use' });
90
95
  useCommand(target, options);
@@ -118,6 +123,45 @@ program
118
123
  console.log(detector.formatOutput(result, options.verbose));
119
124
  });
120
125
 
126
+ // ── task ─────────────────────────────────────────────────────
127
+ const taskCmd = program.command('task').description('Manage multiple tasks — pause, switch, resume');
128
+
129
+ taskCmd
130
+ .command('status')
131
+ .description('Show active task and pending tasks')
132
+ .action(() => { record('command.invoked', { command: 'task.status' }); taskCommand('status'); });
133
+
134
+ taskCmd
135
+ .command('list')
136
+ .description('List all saved tasks')
137
+ .action(() => { record('command.invoked', { command: 'task.list' }); taskCommand('list'); });
138
+
139
+ taskCmd
140
+ .command('pause')
141
+ .description('Pause current task and save progress')
142
+ .option('-n, --note <text>', 'note about why you are pausing')
143
+ .action((opts) => { record('command.invoked', { command: 'task.pause' }); taskCommand('pause', { note: opts.note }); });
144
+
145
+ taskCmd
146
+ .command('switch <ticket-id>')
147
+ .description('Pause current task and switch to another')
148
+ .action((ticketId) => { record('command.invoked', { command: 'task.switch' }); taskCommand('switch', { taskId: ticketId }); });
149
+
150
+ taskCmd
151
+ .command('resume <ticket-id>')
152
+ .description('Resume a paused task')
153
+ .action((ticketId) => { record('command.invoked', { command: 'task.resume' }); taskCommand('resume', { taskId: ticketId }); });
154
+
155
+ taskCmd
156
+ .command('reset <ticket-id>')
157
+ .description('Reset task to Gate 1 (keeps ticket context, clears gate progress)')
158
+ .action((ticketId) => { record('command.invoked', { command: 'task.reset' }); taskCommand('reset', { taskId: ticketId }); });
159
+
160
+ taskCmd
161
+ .command('remove <ticket-id>')
162
+ .description('Permanently delete all saved data for a task')
163
+ .action((ticketId) => { record('command.invoked', { command: 'task.remove' }); taskCommand('remove', { taskId: ticketId }); });
164
+
121
165
  // ── context ───────────────────────────────────────────────────
122
166
  program
123
167
  .command('context [action]')
@@ -0,0 +1,329 @@
1
+ # Spring Boot Code Examples
2
+
3
+ Reference examples for each layer. Read the relevant section when generating code for that layer.
4
+
5
+ ---
6
+
7
+ ## Controller
8
+
9
+ ```java
10
+ // ✅ Good
11
+ \@PostMapping
12
+ public ResponseEntity<UserResponse> createUser(
13
+ \@Valid \@RequestBody CreateUserRequest request) {
14
+ UserResponse response = userService.create(request);
15
+ return ResponseEntity.status(HttpStatus.CREATED).body(response);
16
+ }
17
+
18
+ // ❌ Bad — business logic in controller
19
+ \@PostMapping
20
+ public ResponseEntity<User> createUser(\@RequestBody User user) {
21
+ if (userRepository.existsByEmail(user.getEmail())) {
22
+ throw new RuntimeException("Email exists");
23
+ }
24
+ return ResponseEntity.ok(userRepository.save(user));
25
+ }
26
+ ```
27
+
28
+ ---
29
+
30
+ ## Service
31
+
32
+ ```java
33
+ // Interface
34
+ public interface UserService {
35
+ UserResponse create(CreateUserRequest request);
36
+ UserResponse findById(Long id);
37
+ }
38
+
39
+ // Implementation
40
+ \@Service
41
+ \@RequiredArgsConstructor
42
+ public class UserServiceImpl implements UserService {
43
+
44
+ private final UserRepository userRepository;
45
+ private final UserMapper userMapper;
46
+
47
+ \@Override
48
+ \@Transactional
49
+ public UserResponse create(CreateUserRequest request) {
50
+ if (userRepository.existsByEmail(request.getEmail())) {
51
+ throw new DuplicateEmailException(request.getEmail());
52
+ }
53
+ User user = userMapper.toEntity(request);
54
+ User saved = userRepository.save(user);
55
+ return userMapper.toResponse(saved);
56
+ }
57
+
58
+ \@Override
59
+ \@Transactional(readOnly = true)
60
+ public UserResponse findById(Long id) {
61
+ log.info("Finding user by id: {}", id);
62
+ return userRepository.findById(id)
63
+ .map(userMapper::toResponse)
64
+ .orElseThrow(() -> {
65
+ log.warn("User not found: id={}", id);
66
+ return new ResourceNotFoundException("User", id);
67
+ });
68
+ }
69
+
70
+ \@Transactional(readOnly = true)
71
+ public Page<UserResponse> findAll(Pageable pageable) {
72
+ return userRepository.findAll(pageable).map(userMapper::toResponse);
73
+ }
74
+ }
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Repository
80
+
81
+ ```java
82
+ // ✅ Good
83
+ public interface UserRepository extends JpaRepository<User, Long> {
84
+
85
+ boolean existsByEmail(String email);
86
+
87
+ Optional<User> findByEmailAndDeletedFalse(String email);
88
+
89
+ \@Query("SELECT u FROM User u JOIN FETCH u.roles WHERE u.id = :id")
90
+ Optional<User> findByIdWithRoles(\@Param("id") Long id);
91
+ }
92
+
93
+ // ❌ Bad — N+1 problem
94
+ List<Order> findAll(); // If Order has \@ManyToOne User, this causes N+1
95
+ ```
96
+
97
+ ---
98
+
99
+ ## Entity
100
+
101
+ ```java
102
+ \@Entity
103
+ \@Table(name = "users")
104
+ \@Getter
105
+ \@Setter
106
+ \@NoArgsConstructor
107
+ \@AllArgsConstructor
108
+ \@Builder
109
+ public class User extends BaseEntity {
110
+
111
+ \@Id
112
+ \@GeneratedValue(strategy = GenerationType.IDENTITY)
113
+ private Long id;
114
+
115
+ \@Column(name = "email", nullable = false, unique = true, length = 255)
116
+ private String email;
117
+
118
+ \@Column(name = "full_name", nullable = false, length = 100)
119
+ private String fullName;
120
+
121
+ \@Column(name = "deleted", nullable = false)
122
+ \@Builder.Default
123
+ private boolean deleted = false;
124
+
125
+ \@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
126
+ private List<Order> orders = new ArrayList<>();
127
+ }
128
+ ```
129
+
130
+ ---
131
+
132
+ ## DTO
133
+
134
+ ```java
135
+ // Request DTO
136
+ \@Getter
137
+ \@NoArgsConstructor
138
+ \@AllArgsConstructor
139
+ \@Builder
140
+ public class CreateUserRequest {
141
+
142
+ \@NotBlank(message = "Email is required")
143
+ \@Email(message = "Invalid email format")
144
+ private String email;
145
+
146
+ \@NotBlank(message = "Full name is required")
147
+ \@Size(min = 2, max = 100, message = "Full name must be 2-100 characters")
148
+ private String fullName;
149
+
150
+ \@NotBlank(message = "Password is required")
151
+ \@Size(min = 8, message = "Password must be at least 8 characters")
152
+ private String password;
153
+ }
154
+
155
+ // Response DTO
156
+ \@Getter
157
+ \@NoArgsConstructor
158
+ \@AllArgsConstructor
159
+ \@Builder
160
+ public class UserResponse {
161
+ private Long id;
162
+ private String email;
163
+ private String fullName;
164
+ private LocalDateTime createdAt;
165
+ }
166
+ ```
167
+
168
+ ---
169
+
170
+ ## MapStruct Mapper
171
+
172
+ ```java
173
+ \@Mapper(componentModel = "spring")
174
+ public interface UserMapper {
175
+
176
+ \@Mapping(target = "createdAt", source = "createdAt")
177
+ UserResponse toResponse(User user);
178
+
179
+ \@Mapping(target = "id", ignore = true)
180
+ \@Mapping(target = "deleted", ignore = true)
181
+ \@Mapping(target = "createdAt", ignore = true)
182
+ \@Mapping(target = "updatedAt", ignore = true)
183
+ User toEntity(CreateUserRequest request);
184
+
185
+ List<UserResponse> toResponseList(List<User> users);
186
+ }
187
+ ```
188
+
189
+ ---
190
+
191
+ ## Exception Handling
192
+
193
+ ```java
194
+ // Custom exception
195
+ public class ResourceNotFoundException extends RuntimeException {
196
+ public ResourceNotFoundException(String resource, Long id) {
197
+ super(String.format("%s with id %d not found", resource, id));
198
+ }
199
+ }
200
+
201
+ // Global handler
202
+ \@RestControllerAdvice
203
+ \@Slf4j
204
+ public class GlobalExceptionHandler {
205
+
206
+ \@ExceptionHandler(ResourceNotFoundException.class)
207
+ public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
208
+ log.warn("Resource not found: {}", ex.getMessage());
209
+ return ResponseEntity.status(HttpStatus.NOT_FOUND)
210
+ .body(ErrorResponse.of("NOT_FOUND", ex.getMessage()));
211
+ }
212
+
213
+ \@ExceptionHandler(MethodArgumentNotValidException.class)
214
+ public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
215
+ List<String> errors = ex.getBindingResult()
216
+ .getFieldErrors()
217
+ .stream()
218
+ .map(e -> e.getField() + ": " + e.getDefaultMessage())
219
+ .toList();
220
+ return ResponseEntity.status(HttpStatus.BAD_REQUEST)
221
+ .body(ErrorResponse.of("VALIDATION_FAILED", errors.toString()));
222
+ }
223
+
224
+ \@ExceptionHandler(Exception.class)
225
+ public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
226
+ log.error("Unexpected error", ex);
227
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
228
+ .body(ErrorResponse.of("INTERNAL_ERROR", "An unexpected error occurred"));
229
+ }
230
+ }
231
+ ```
232
+
233
+ ---
234
+
235
+ ## Unit Tests (Service layer)
236
+
237
+ ```java
238
+ \@ExtendWith(MockitoExtension.class)
239
+ class UserServiceImplTest {
240
+
241
+ \@Mock
242
+ private UserRepository userRepository;
243
+
244
+ \@Mock
245
+ private UserMapper userMapper;
246
+
247
+ \@InjectMocks
248
+ private UserServiceImpl userService;
249
+
250
+ \@Test
251
+ void create_shouldReturnUserResponse_whenEmailIsUnique() {
252
+ CreateUserRequest request = CreateUserRequest.builder()
253
+ .email("test\@example.com")
254
+ .fullName("Test User")
255
+ .build();
256
+ User user = User.builder().id(1L).email("test\@example.com").build();
257
+ UserResponse expected = UserResponse.builder().id(1L).build();
258
+
259
+ given(userRepository.existsByEmail(request.getEmail())).willReturn(false);
260
+ given(userMapper.toEntity(request)).willReturn(user);
261
+ given(userRepository.save(user)).willReturn(user);
262
+ given(userMapper.toResponse(user)).willReturn(expected);
263
+
264
+ UserResponse result = userService.create(request);
265
+
266
+ assertThat(result).isEqualTo(expected);
267
+ verify(userRepository).save(user);
268
+ }
269
+
270
+ \@Test
271
+ void create_shouldThrowDuplicateEmailException_whenEmailAlreadyExists() {
272
+ CreateUserRequest request = CreateUserRequest.builder()
273
+ .email("existing\@example.com").build();
274
+ given(userRepository.existsByEmail(request.getEmail())).willReturn(true);
275
+
276
+ assertThatThrownBy(() -> userService.create(request))
277
+ .isInstanceOf(DuplicateEmailException.class);
278
+ verify(userRepository, never()).save(any());
279
+ }
280
+ }
281
+ ```
282
+
283
+ ---
284
+
285
+ ## Integration Tests (Controller layer)
286
+
287
+ ```java
288
+ \@SpringBootTest
289
+ \@AutoConfigureMockMvc
290
+ class UserControllerIntegrationTest {
291
+
292
+ \@Autowired
293
+ private MockMvc mockMvc;
294
+
295
+ \@Autowired
296
+ private ObjectMapper objectMapper;
297
+
298
+ \@Test
299
+ void createUser_shouldReturn201_whenRequestIsValid() throws Exception {
300
+ CreateUserRequest request = CreateUserRequest.builder()
301
+ .email("test\@example.com")
302
+ .fullName("Test User")
303
+ .password("password123")
304
+ .build();
305
+
306
+ mockMvc.perform(post("/api/v1/users")
307
+ .contentType(MediaType.APPLICATION_JSON)
308
+ .content(objectMapper.writeValueAsString(request)))
309
+ .andExpect(status().isCreated())
310
+ .andExpect(jsonPath("$.email").value("test\@example.com"))
311
+ .andExpect(jsonPath("$.id").isNumber());
312
+ }
313
+
314
+ \@Test
315
+ void createUser_shouldReturn400_whenEmailIsInvalid() throws Exception {
316
+ CreateUserRequest request = CreateUserRequest.builder()
317
+ .email("not-an-email")
318
+ .fullName("Test")
319
+ .password("password123")
320
+ .build();
321
+
322
+ mockMvc.perform(post("/api/v1/users")
323
+ .contentType(MediaType.APPLICATION_JSON)
324
+ .content(objectMapper.writeValueAsString(request)))
325
+ .andExpect(status().isBadRequest())
326
+ .andExpect(jsonPath("$.code").value("VALIDATION_FAILED"));
327
+ }
328
+ }
329
+ ```
@@ -12,6 +12,60 @@ keywords: ticket, requirement, study, read, context, backlog, jira, analyze, und
12
12
 
13
13
  ---
14
14
 
15
+ ## Mode Selection
16
+
17
+ Check `mode` in `.aiflow/context/current.json` before starting:
18
+
19
+ | Mode | Trigger | Behaviour |
20
+ |------|---------|-----------|
21
+ | `fast` | `aiflow use TICKET --fast` | Jump to **[Fast Track](#fast-track-mode-fast)** below |
22
+ | `full` | `aiflow use TICKET --full` | Use standard steps — full Q&A + all 6 sections |
23
+ | `auto` (default) | `aiflow use TICKET` | Standard steps |
24
+
25
+ ---
26
+
27
+ ## Fast Track (mode: fast)
28
+
29
+ > Target: Gate 1 complete in **< 5 minutes**, minimum token usage.
30
+
31
+ 1. Read `.aiflow/context/current.json` — ticket title, description, acceptance criteria
32
+ 2. Identify **directly affected files** from ticket text and file names only.
33
+ DO NOT trace data flow or read unrelated source files.
34
+ 3. Q&A: Ask at most **ONE** question if there is a blocking ambiguity.
35
+ If the ticket + source file names make intent clear → skip Q&A entirely.
36
+ Wait for answer before writing the requirement doc.
37
+ 4. Output `plan/[ticket-id]/requirement.md` with **3 sections only**:
38
+
39
+ ```markdown
40
+ # Requirement: [Ticket ID] — [Title]
41
+
42
+ **Date:** [YYYY-MM-DD] **Mode:** fast **Status:** ⏸️ Waiting for DEV Approval
43
+
44
+ ## 1. Requirements Summary
45
+ [2–4 sentence restatement of what needs to change and why]
46
+
47
+ **Acceptance Criteria:**
48
+ - [ ] [each AC from ticket]
49
+
50
+ ## 3. Files to Create / Modify
51
+ | Action | File | Change |
52
+ |--------|------|--------|
53
+ | Modify | [path] | [what changes] |
54
+
55
+ ## 5. Effort Estimate
56
+ | Task | Size |
57
+ |------|------|
58
+ | Tests | S/M/L |
59
+ | Implementation | S/M/L |
60
+ | **Total** | **S/M/L** |
61
+ ```
62
+
63
+ 5. Display Gate 1 prompt and wait for `APPROVED`.
64
+
65
+ > After APPROVED, proceed to Gate 2. The lean doc is intentional — Gate 2 plan will fill in the details.
66
+
67
+ ---
68
+
15
69
  ## Steps
16
70
 
17
71
  ### Step 1: Load Ticket & Read Source Code