@atlashub/smartstack-cli 3.30.0 → 3.32.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 (52) hide show
  1. package/.documentation/installation.html +7 -2
  2. package/README.md +7 -1
  3. package/dist/index.js +33 -37
  4. package/dist/index.js.map +1 -1
  5. package/dist/mcp-entry.mjs +547 -97
  6. package/dist/mcp-entry.mjs.map +1 -1
  7. package/package.json +1 -1
  8. package/scripts/health-check.sh +2 -1
  9. package/templates/mcp-scaffolding/controller.cs.hbs +10 -7
  10. package/templates/mcp-scaffolding/entity-extension.cs.hbs +132 -124
  11. package/templates/mcp-scaffolding/frontend/api-client.ts.hbs +4 -4
  12. package/templates/mcp-scaffolding/tests/controller.test.cs.hbs +38 -15
  13. package/templates/mcp-scaffolding/tests/service.test.cs.hbs +20 -8
  14. package/templates/skills/apex/SKILL.md +7 -9
  15. package/templates/skills/apex/_shared.md +9 -2
  16. package/templates/skills/apex/references/code-generation.md +412 -0
  17. package/templates/skills/apex/references/post-checks.md +377 -37
  18. package/templates/skills/apex/references/smartstack-api.md +229 -5
  19. package/templates/skills/apex/references/smartstack-frontend.md +368 -11
  20. package/templates/skills/apex/references/smartstack-layers.md +54 -7
  21. package/templates/skills/apex/steps/step-00-init.md +1 -2
  22. package/templates/skills/apex/steps/step-01-analyze.md +45 -2
  23. package/templates/skills/apex/steps/step-02-plan.md +23 -2
  24. package/templates/skills/apex/steps/step-03-execute.md +195 -5
  25. package/templates/skills/apex/steps/step-04-examine.md +18 -5
  26. package/templates/skills/apex/steps/step-05-deep-review.md +9 -11
  27. package/templates/skills/apex/steps/step-06-resolve.md +5 -9
  28. package/templates/skills/apex/steps/step-07-tests.md +66 -1
  29. package/templates/skills/apex/steps/step-08-run-tests.md +12 -3
  30. package/templates/skills/application/references/provider-template.md +62 -39
  31. package/templates/skills/application/templates-backend.md +3 -3
  32. package/templates/skills/application/templates-frontend.md +12 -12
  33. package/templates/skills/application/templates-seed.md +14 -4
  34. package/templates/skills/business-analyse/SKILL.md +10 -7
  35. package/templates/skills/business-analyse/questionnaire/04-data.md +8 -0
  36. package/templates/skills/business-analyse/references/agent-module-prompt.md +84 -5
  37. package/templates/skills/business-analyse/references/agent-pooling-best-practices.md +83 -19
  38. package/templates/skills/business-analyse/references/consolidation-structural-checks.md +6 -2
  39. package/templates/skills/business-analyse/references/team-orchestration.md +470 -113
  40. package/templates/skills/business-analyse/references/validation-checklist.md +5 -4
  41. package/templates/skills/business-analyse/schemas/sections/analysis-schema.json +44 -0
  42. package/templates/skills/business-analyse/steps/step-03a2-analysis.md +72 -1
  43. package/templates/skills/business-analyse/steps/step-03c-compile.md +93 -7
  44. package/templates/skills/business-analyse/steps/step-03d-validate.md +34 -2
  45. package/templates/skills/business-analyse/steps/step-04b-analyze.md +40 -0
  46. package/templates/skills/controller/references/controller-code-templates.md +2 -2
  47. package/templates/skills/controller/templates.md +12 -12
  48. package/templates/skills/feature-full/steps/step-01-implementation.md +4 -4
  49. package/templates/skills/ralph-loop/references/category-rules.md +44 -2
  50. package/templates/skills/ralph-loop/references/compact-loop.md +37 -0
  51. package/templates/skills/ralph-loop/references/core-seed-data.md +51 -20
  52. package/templates/skills/review-code/references/owasp-api-top10.md +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlashub/smartstack-cli",
3
- "version": "3.30.0",
3
+ "version": "3.32.0",
4
4
  "description": "SmartStack Claude Code automation toolkit - GitFlow, EF Core migrations, prompts and more",
5
5
  "author": {
6
6
  "name": "SmartStack",
@@ -107,7 +107,8 @@ if command -v claude &> /dev/null; then
107
107
  fi
108
108
  else
109
109
  log_error "Claude Code CLI not found"
110
- log_info " Install: npm install -g @anthropic-ai/claude-code"
110
+ log_info " Install: curl -fsSL https://claude.ai/install.sh | bash"
111
+ log_info " More info: https://code.claude.com/docs/en/setup"
111
112
  fi
112
113
 
113
114
  echo ""
@@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authorization;
6
6
  using Microsoft.AspNetCore.Mvc;
7
7
  using Microsoft.Extensions.Logging;
8
8
  using SmartStack.Api.Authorization;
9
+ using SmartStack.Application.Common.Models;
9
10
 
10
11
  namespace {{namespace}}.Controllers;
11
12
 
@@ -34,24 +35,26 @@ public class {{name}}Controller : ControllerBase
34
35
  }
35
36
 
36
37
  /// <summary>
37
- /// Get all {{namePlural}}
38
+ /// Get all {{namePlural}} with pagination
38
39
  /// </summary>
39
- /// <param name="cancellationToken">Cancellation token</param>
40
- /// <returns>List of {{namePlural}}</returns>
41
40
  [HttpGet]
42
41
  {{#if navRoute}}
43
42
  [RequirePermission("{{navRoute}}.read")]
44
43
  {{/if}}
45
- [ProducesResponseType(typeof(IEnumerable<{{name}}Dto>), 200)]
46
- public async Task<ActionResult<IEnumerable<{{name}}Dto>>> GetAll(CancellationToken cancellationToken = default)
44
+ [ProducesResponseType(typeof(PaginatedResult<{{name}}Dto>), 200)]
45
+ public async Task<ActionResult<PaginatedResult<{{name}}Dto>>> GetAll(
46
+ [FromQuery] string? search = null,
47
+ [FromQuery] int page = 1,
48
+ [FromQuery] int pageSize = 20,
49
+ CancellationToken cancellationToken = default)
47
50
  {
48
51
  _logger.LogInformation("Getting all {{namePlural}}");
49
52
 
50
53
  // TODO: Implement using service
51
- // var result = await _{{nameCamel}}Service.GetAllAsync(cancellationToken);
54
+ // var result = await _{{nameCamel}}Service.GetAllAsync(search, page, pageSize, cancellationToken);
52
55
  // return Ok(result);
53
56
 
54
- return Ok(Array.Empty<{{name}}Dto>());
57
+ return Ok(PaginatedResult<{{name}}Dto>.Empty(page, pageSize));
55
58
  }
56
59
 
57
60
  /// <summary>
@@ -1,20 +1,64 @@
1
1
  using System;
2
- using System.ComponentModel.DataAnnotations;
3
2
  using SmartStack.Domain.Common;
4
- using SmartStack.Domain.Common.Interfaces;
5
3
 
6
4
  namespace {{namespace}};
7
5
 
8
6
  /// <summary>
9
7
  /// {{name}} entity{{#if baseEntity}} extending {{baseEntity}}{{/if}}
8
+ {{#if isStrict}}
9
+ /// Tenant-scoped: data is isolated per tenant (ITenantEntity)
10
+ {{else if isOptional}}
11
+ /// Cross-tenant: data can be shared across tenants or tenant-specific (IOptionalTenantEntity)
12
+ {{else if isScoped}}
13
+ /// Scoped: data visibility controlled by EntityScope (IScopedTenantEntity)
14
+ {{else}}
15
+ /// Platform-level entity: no tenant isolation
16
+ {{/if}}
10
17
  /// </summary>
11
- {{#if isSystemEntity}}
12
- public class {{name}} : SystemEntity
18
+ {{#if isStrict}}
19
+ public class {{name}} : BaseEntity, ITenantEntity, IAuditableEntity
20
+ {{else if isOptional}}
21
+ public class {{name}} : BaseEntity, IOptionalTenantEntity, IAuditableEntity
22
+ {{else if isScoped}}
23
+ public class {{name}} : BaseEntity, IScopedTenantEntity, IAuditableEntity
13
24
  {{else}}
14
- public class {{name}} : BaseEntity
25
+ public class {{name}} : BaseEntity, IAuditableEntity
15
26
  {{/if}}
16
27
  {
28
+ {{#if hasTenantId}}
29
+ // === MULTI-TENANT ===
30
+
31
+ {{#if isNullableTenant}}
32
+ /// <summary>
33
+ /// Tenant identifier — null means shared across all tenants
34
+ /// </summary>
35
+ public Guid? TenantId { get; private set; }
36
+ {{else}}
37
+ /// <summary>
38
+ /// Tenant identifier for multi-tenant isolation (required)
39
+ /// </summary>
40
+ public Guid TenantId { get; private set; }
41
+ {{/if}}
42
+
43
+ {{/if}}
44
+ {{#if hasScope}}
45
+ /// <summary>
46
+ /// Visibility scope: Tenant (tenant-specific), Shared (all tenants), Platform (admin only)
47
+ /// </summary>
48
+ public EntityScope Scope { get; private set; }
49
+
50
+ {{/if}}
51
+ // === AUDIT ===
52
+
53
+ /// <summary>User who created this entity</summary>
54
+ public string? CreatedBy { get; set; }
55
+
56
+ /// <summary>User who last updated this entity</summary>
57
+ public string? UpdatedBy { get; set; }
58
+
17
59
  {{#if baseEntity}}
60
+ // === RELATIONSHIPS ===
61
+
18
62
  /// <summary>
19
63
  /// Foreign key to {{baseEntity}}
20
64
  /// </summary>
@@ -23,7 +67,7 @@ public class {{name}} : BaseEntity
23
67
  /// <summary>
24
68
  /// Navigation property to {{baseEntity}}
25
69
  /// </summary>
26
- public virtual {{baseEntity}}? {{baseEntity}} { get; private set; }
70
+ public {{baseEntity}}? {{baseEntity}} { get; private set; }
27
71
 
28
72
  {{/if}}
29
73
  // === BUSINESS PROPERTIES ===
@@ -37,130 +81,95 @@ public class {{name}} : BaseEntity
37
81
  /// <summary>
38
82
  /// Factory method to create a new {{name}}
39
83
  /// </summary>
40
- {{#if isSystemEntity}}
84
+ {{#if isStrict}}
85
+ /// <param name="tenantId">Required tenant identifier</param>
41
86
  public static {{name}} Create(
42
- string code,
87
+ Guid tenantId,
43
88
  string? createdBy = null)
44
89
  {
90
+ if (tenantId == Guid.Empty)
91
+ throw new ArgumentException("TenantId is required", nameof(tenantId));
92
+
45
93
  return new {{name}}
46
94
  {
47
95
  Id = Guid.NewGuid(),
48
- Code = code.ToLowerInvariant(),
96
+ TenantId = tenantId,
49
97
  CreatedAt = DateTime.UtcNow,
50
98
  CreatedBy = createdBy
51
99
  };
52
100
  }
53
- {{else}}
101
+ {{else if isOptional}}
102
+ /// <param name="tenantId">Tenant identifier — null for shared (cross-tenant) data</param>
54
103
  public static {{name}} Create(
55
- Guid tenantId,
56
- string code,
104
+ Guid? tenantId = null,
57
105
  string? createdBy = null)
58
106
  {
59
107
  return new {{name}}
60
108
  {
61
109
  Id = Guid.NewGuid(),
62
110
  TenantId = tenantId,
63
- Code = code.ToLowerInvariant(),
64
111
  CreatedAt = DateTime.UtcNow,
65
112
  CreatedBy = createdBy
66
113
  };
67
114
  }
68
- {{/if}}
69
-
70
- /// <summary>
71
- /// Update the entity
72
- /// </summary>
73
- public void Update(string? updatedBy = null)
115
+ {{else if isScoped}}
116
+ /// <param name="tenantId">Tenant identifier — null for shared/platform data</param>
117
+ /// <param name="scope">Visibility scope (default: Tenant)</param>
118
+ public static {{name}} Create(
119
+ Guid? tenantId = null,
120
+ EntityScope scope = EntityScope.Tenant,
121
+ string? createdBy = null)
74
122
  {
75
- UpdatedAt = DateTime.UtcNow;
76
- UpdatedBy = updatedBy;
77
- }
123
+ if (scope == EntityScope.Tenant && tenantId == null)
124
+ throw new ArgumentException("TenantId is required when scope is Tenant", nameof(tenantId));
78
125
 
79
- /// <summary>
80
- /// Soft delete the entity
81
- /// </summary>
82
- public void SoftDelete(string? deletedBy = null)
126
+ return new {{name}}
127
+ {
128
+ Id = Guid.NewGuid(),
129
+ TenantId = tenantId,
130
+ Scope = scope,
131
+ CreatedAt = DateTime.UtcNow,
132
+ CreatedBy = createdBy
133
+ };
134
+ }
135
+ {{else}}
136
+ public static {{name}} Create(
137
+ string? createdBy = null)
83
138
  {
84
- IsDeleted = true;
85
- DeletedAt = DateTime.UtcNow;
86
- DeletedBy = deletedBy;
139
+ return new {{name}}
140
+ {
141
+ Id = Guid.NewGuid(),
142
+ CreatedAt = DateTime.UtcNow,
143
+ CreatedBy = createdBy
144
+ };
87
145
  }
146
+ {{/if}}
88
147
 
89
148
  /// <summary>
90
- /// Restore a soft-deleted entity
149
+ /// Update the entity
91
150
  /// </summary>
92
- public void Restore(string? restoredBy = null)
151
+ public void Update(string? updatedBy = null)
93
152
  {
94
- IsDeleted = false;
95
- DeletedAt = null;
96
- DeletedBy = null;
97
153
  UpdatedAt = DateTime.UtcNow;
98
- UpdatedBy = restoredBy;
154
+ UpdatedBy = updatedBy;
99
155
  }
100
156
  }
101
157
 
102
158
  // ============================================================================
103
- // Base Entity Classes (SmartStack.Domain.Common)
159
+ // Base Entity Classes (SmartStack.Domain.Common) — Reference Only
104
160
  // ============================================================================
105
161
  /*
106
- /// <summary>
107
- /// Base entity for tenant-scoped entities (default)
108
- /// </summary>
109
- public abstract class BaseEntity : ITenantEntity, IHasCode, ISoftDeletable, IVersioned
110
- {
111
- public Guid Id { get; set; } = Guid.NewGuid();
112
- public Guid TenantId { get; set; }
113
-
114
- private string _code = string.Empty;
115
- public string Code
116
- {
117
- get => _code;
118
- set => _code = value?.ToLowerInvariant() ?? string.Empty;
119
- }
120
-
121
- public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
122
- public DateTime? UpdatedAt { get; set; }
123
- public string? CreatedBy { get; set; }
124
- public string? UpdatedBy { get; set; }
125
-
126
- public bool IsDeleted { get; set; }
127
- public DateTime? DeletedAt { get; set; }
128
- public string? DeletedBy { get; set; }
129
-
130
- public byte[] RowVersion { get; set; } = Array.Empty<byte>();
131
- }
132
-
133
- /// <summary>
134
- /// Base entity for system-wide entities (no tenant isolation)
135
- /// </summary>
136
- public abstract class SystemEntity : ISystemEntity, IHasCode, ISoftDeletable, IVersioned
137
- {
138
- public Guid Id { get; set; } = Guid.NewGuid();
139
-
140
- private string _code = string.Empty;
141
- public string Code
142
- {
143
- get => _code;
144
- set => _code = value?.ToLowerInvariant() ?? string.Empty;
145
- }
146
-
147
- public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
148
- public DateTime? UpdatedAt { get; set; }
149
- public string? CreatedBy { get; set; }
150
- public string? UpdatedBy { get; set; }
151
-
152
- public bool IsDeleted { get; set; }
153
- public DateTime? DeletedAt { get; set; }
154
- public string? DeletedBy { get; set; }
155
-
156
- public byte[] RowVersion { get; set; } = Array.Empty<byte>();
157
- }
162
+ BaseEntity: Id (Guid), CreatedAt (DateTime), UpdatedAt (DateTime?)
163
+ ITenantEntity: TenantId (Guid) — strict tenant isolation
164
+ IOptionalTenantEntity: TenantId (Guid?) — cross-tenant shared data (null = shared)
165
+ IScopedTenantEntity: TenantId (Guid?) + Scope — with EntityScope (Tenant/Shared/Platform)
166
+ IAuditableEntity: CreatedBy (string?), UpdatedBy (string?)
158
167
  */
159
168
 
160
169
  // ============================================================================
161
170
  // EF Core Configuration
162
171
  // ============================================================================
163
- namespace {{infrastructureNamespace}}.Persistence.Configurations;
172
+ namespace {{infrastructureNamespace}}.Persistence.Configurations{{#if configNamespaceSuffix}}.{{configNamespaceSuffix}}{{/if}};
164
173
 
165
174
  using Microsoft.EntityFrameworkCore;
166
175
  using Microsoft.EntityFrameworkCore.Metadata.Builders;
@@ -170,62 +179,61 @@ public class {{name}}Configuration : IEntityTypeConfiguration<{{name}}>
170
179
  {
171
180
  public void Configure(EntityTypeBuilder<{{name}}> builder)
172
181
  {
173
- // Table name with schema and domain prefix
174
182
  builder.ToTable("{{tablePrefix}}{{name}}s", "{{schema}}");
175
183
 
176
- // Primary key
177
184
  builder.HasKey(e => e.Id);
178
185
 
179
- {{#unless isSystemEntity}}
180
- // Multi-tenant
186
+ {{#if isStrict}}
187
+ // Multi-tenant (strict: TenantId required)
181
188
  builder.Property(e => e.TenantId).IsRequired();
182
- builder.HasIndex(e => e.TenantId);
183
-
184
- // Code: lowercase, unique per tenant (filtered for soft delete)
185
- builder.Property(e => e.Code).HasMaxLength(100).IsRequired();
186
- builder.HasIndex(e => new { e.TenantId, e.Code })
187
- .IsUnique()
188
- .HasFilter("[IsDeleted] = 0")
189
- .HasDatabaseName("IX_{{tablePrefix}}{{name}}s_Tenant_Code_Unique");
190
- {{else}}
191
- // Code: lowercase, unique (filtered for soft delete)
192
- builder.Property(e => e.Code).HasMaxLength(100).IsRequired();
193
- builder.HasIndex(e => e.Code)
194
- .IsUnique()
195
- .HasFilter("[IsDeleted] = 0")
196
- .HasDatabaseName("IX_{{tablePrefix}}{{name}}s_Code_Unique");
197
- {{/unless}}
198
-
199
- // Concurrency token
200
- builder.Property(e => e.RowVersion).IsRowVersion();
201
-
202
- // Audit fields
189
+ builder.HasIndex(e => e.TenantId)
190
+ .HasDatabaseName("IX_{{tablePrefix}}{{name}}s_TenantId");
191
+
192
+ {{else if isOptional}}
193
+ // Multi-tenant (optional: TenantId nullable null = shared across tenants)
194
+ builder.Property(e => e.TenantId).IsRequired(false);
195
+ builder.HasIndex(e => e.TenantId)
196
+ .HasDatabaseName("IX_{{tablePrefix}}{{name}}s_TenantId");
197
+
198
+ {{else if isScoped}}
199
+ // Multi-tenant (scoped: TenantId nullable + EntityScope)
200
+ builder.Property(e => e.TenantId).IsRequired(false);
201
+ builder.HasIndex(e => e.TenantId)
202
+ .HasDatabaseName("IX_{{tablePrefix}}{{name}}s_TenantId");
203
+
204
+ builder.Property(e => e.Scope)
205
+ .IsRequired()
206
+ .HasConversion<string>()
207
+ .HasMaxLength(20);
208
+ builder.HasIndex(e => new { e.TenantId, e.Scope })
209
+ .HasDatabaseName("IX_{{tablePrefix}}{{name}}s_TenantId_Scope");
210
+
211
+ {{/if}}
212
+ // Audit fields (from IAuditableEntity)
203
213
  builder.Property(e => e.CreatedBy).HasMaxLength(256);
204
214
  builder.Property(e => e.UpdatedBy).HasMaxLength(256);
205
- builder.Property(e => e.DeletedBy).HasMaxLength(256);
206
215
 
207
216
  {{#if baseEntity}}
208
- // Relationship to {{baseEntity}} (1:1)
217
+ // Relationship to {{baseEntity}}
209
218
  builder.HasOne(e => e.{{baseEntity}})
210
- .WithOne()
211
- .HasForeignKey<{{name}}>(e => e.{{baseEntity}}Id)
212
- .OnDelete(DeleteBehavior.Cascade);
219
+ .WithMany()
220
+ .HasForeignKey(e => e.{{baseEntity}}Id)
221
+ .OnDelete(DeleteBehavior.Restrict);
213
222
 
214
- // Index on foreign key
215
223
  builder.HasIndex(e => e.{{baseEntity}}Id)
216
- .IsUnique();
224
+ .HasDatabaseName("IX_{{tablePrefix}}{{name}}s_{{baseEntity}}Id");
217
225
  {{/if}}
218
226
 
219
- // TODO: Add additional configuration
227
+ // TODO: Add business property configurations (HasMaxLength, IsRequired, indexes)
220
228
  }
221
229
  }
222
230
 
223
231
  // ============================================================================
224
- // DbContext Update (add to ApplicationDbContext.cs)
232
+ // DbContext Update (add to your DbContext interface + implementation)
225
233
  // ============================================================================
226
234
  // public DbSet<{{name}}> {{name}}s => Set<{{name}}>();
227
235
 
228
236
  // ============================================================================
229
237
  // Migration Command
230
238
  // ============================================================================
231
- // dotnet ef migrations add core_vX.X.X_XXX_Add{{name}} --context ApplicationDbContext
239
+ // dotnet ef migrations add core_vX.X.X_XXX_Add{{name}} --context CoreDbContext
@@ -16,8 +16,8 @@ import type {
16
16
  {{name}}CreateRequest,
17
17
  {{name}}UpdateRequest,
18
18
  {{name}}ListResponse,
19
- PaginatedRequest,
20
- PaginatedResponse
19
+ PaginationParams,
20
+ PaginatedResult
21
21
  } from '../types/{{nameLower}}';
22
22
 
23
23
  const ROUTE = getRoute('{{navRoute}}');
@@ -27,7 +27,7 @@ export const {{nameLower}}Api = {
27
27
  /**
28
28
  * Get all {{name}}s with pagination
29
29
  */
30
- async getAll(params?: PaginatedRequest): Promise<PaginatedResponse<{{name}}>> {
30
+ async getAll(params?: PaginationParams): Promise<PaginatedResult<{{name}}>> {
31
31
  const response = await apiClient.get<{{name}}ListResponse>(ROUTE.api, { params });
32
32
  return response.data;
33
33
  },
@@ -76,7 +76,7 @@ export const {{nameLower}}Api = {
76
76
  /**
77
77
  * Search {{name}}s
78
78
  */
79
- async search(query: string, params?: PaginatedRequest): Promise<PaginatedResponse<{{name}}>> {
79
+ async search(query: string, params?: PaginationParams): Promise<PaginatedResult<{{name}}>> {
80
80
  const response = await apiClient.get<{{name}}ListResponse>(`${ROUTE.api}/search`, {
81
81
  params: { q: query, ...params }
82
82
  });
@@ -53,12 +53,18 @@ public class {{name}}ControllerTests : IClassFixture<WebApplicationFactory<Progr
53
53
  public async Task GetAll_WhenCalled_ShouldReturn200WithList()
54
54
  {
55
55
  // Arrange
56
- var items = new List<{{name}}Response>
56
+ var items = new PaginatedResult<{{name}}Response>
57
57
  {
58
- new() { Id = Guid.NewGuid(), Code = "TEST-001" },
59
- new() { Id = Guid.NewGuid(), Code = "TEST-002" }
58
+ Items = new List<{{name}}Response>
59
+ {
60
+ new() { Id = Guid.NewGuid(), Code = "TEST-001" },
61
+ new() { Id = Guid.NewGuid(), Code = "TEST-002" }
62
+ },
63
+ Total = 2,
64
+ Page = 1,
65
+ PageSize = 10
60
66
  };
61
- _mockService.Setup(x => x.GetAllAsync(It.IsAny<CancellationToken>()))
67
+ _mockService.Setup(x => x.GetAllAsync(It.IsAny<PaginationParams>(), It.IsAny<CancellationToken>()))
62
68
  .ReturnsAsync(items);
63
69
 
64
70
  // Act
@@ -66,8 +72,9 @@ public class {{name}}ControllerTests : IClassFixture<WebApplicationFactory<Progr
66
72
 
67
73
  // Assert
68
74
  response.StatusCode.Should().Be(HttpStatusCode.OK);
69
- var result = await response.Content.ReadFromJsonAsync<List<{{name}}Response>>();
70
- result.Should().HaveCount(2);
75
+ var result = await response.Content.ReadFromJsonAsync<PaginatedResult<{{name}}Response>>();
76
+ result.Should().NotBeNull();
77
+ result!.Items.Should().HaveCount(2);
71
78
  }
72
79
 
73
80
  [Fact]
@@ -297,11 +304,17 @@ public class {{name}}ControllerTests : IClassFixture<WebApplicationFactory<Progr
297
304
  {
298
305
  // Arrange
299
306
  var tenantAId = Guid.NewGuid();
300
- var tenantAItems = new List<{{name}}Response>
307
+ var tenantAItems = new PaginatedResult<{{name}}Response>
301
308
  {
302
- new() { Id = Guid.NewGuid(), TenantId = tenantAId, Code = "A-001" }
309
+ Items = new List<{{name}}Response>
310
+ {
311
+ new() { Id = Guid.NewGuid(), TenantId = tenantAId, Code = "A-001" }
312
+ },
313
+ Total = 1,
314
+ Page = 1,
315
+ PageSize = 10
303
316
  };
304
- _mockService.Setup(x => x.GetAllAsync(It.IsAny<CancellationToken>()))
317
+ _mockService.Setup(x => x.GetAllAsync(It.IsAny<PaginationParams>(), It.IsAny<CancellationToken>()))
305
318
  .ReturnsAsync(tenantAItems);
306
319
 
307
320
  // Act
@@ -309,8 +322,9 @@ public class {{name}}ControllerTests : IClassFixture<WebApplicationFactory<Progr
309
322
 
310
323
  // Assert
311
324
  response.StatusCode.Should().Be(HttpStatusCode.OK);
312
- var result = await response.Content.ReadFromJsonAsync<List<{{name}}Response>>();
313
- result.Should().OnlyContain(x => x.TenantId == tenantAId);
325
+ var result = await response.Content.ReadFromJsonAsync<PaginatedResult<{{name}}Response>>();
326
+ result.Should().NotBeNull();
327
+ result!.Items.Should().OnlyContain(x => x.TenantId == tenantAId);
314
328
  }
315
329
 
316
330
  [Fact]
@@ -377,16 +391,25 @@ public class {{name}}ControllerTests : IClassFixture<WebApplicationFactory<Progr
377
391
  var items = Enumerable.Range(1, 100)
378
392
  .Select(i => new {{name}}Response { Id = Guid.NewGuid(), Code = $"TEST-{i:D3}" })
379
393
  .ToList();
380
- _mockService.Setup(x => x.GetAllAsync(It.IsAny<CancellationToken>()))
381
- .ReturnsAsync(items.Take(10).ToList());
394
+ var paginatedResult = new PaginatedResult<{{name}}Response>
395
+ {
396
+ Items = items.Take(10).ToList(),
397
+ Total = 100,
398
+ Page = 1,
399
+ PageSize = 10
400
+ };
401
+ _mockService.Setup(x => x.GetAllAsync(It.IsAny<PaginationParams>(), It.IsAny<CancellationToken>()))
402
+ .ReturnsAsync(paginatedResult);
382
403
 
383
404
  // Act
384
405
  var response = await _client.GetAsync("/api/{{lowerName}}?page=1&pageSize=10");
385
406
 
386
407
  // Assert
387
408
  response.StatusCode.Should().Be(HttpStatusCode.OK);
388
- var result = await response.Content.ReadFromJsonAsync<List<{{name}}Response>>();
389
- result.Should().HaveCount(10);
409
+ var result = await response.Content.ReadFromJsonAsync<PaginatedResult<{{name}}Response>>();
410
+ result.Should().NotBeNull();
411
+ result!.Items.Should().HaveCount(10);
412
+ result.Total.Should().Be(100);
390
413
  }
391
414
 
392
415
  #endregion
@@ -87,28 +87,40 @@ public class {{name}}ServiceTests
87
87
  CreateValid{{name}}(),
88
88
  CreateValid{{name}}()
89
89
  };
90
- _mockRepository.Setup(x => x.GetAllAsync(It.IsAny<CancellationToken>()))
91
- .ReturnsAsync(entities);
90
+ _mockRepository.Setup(x => x.GetAllAsync(It.IsAny<PaginationParams>(), It.IsAny<CancellationToken>()))
91
+ .ReturnsAsync(new PaginatedResult<{{name}}>
92
+ {
93
+ Items = entities,
94
+ Total = 2,
95
+ Page = 1,
96
+ PageSize = 10
97
+ });
92
98
 
93
99
  // Act
94
- var result = await _sut.GetAllAsync();
100
+ var result = await _sut.GetAllAsync(new PaginationParams { Page = 1, PageSize = 10 });
95
101
 
96
102
  // Assert
97
- result.Should().HaveCount(2);
103
+ result.Items.Should().HaveCount(2);
98
104
  }
99
105
 
100
106
  [Fact]
101
107
  public async Task GetAllAsync_WhenNoEntities_ShouldReturnEmptyList()
102
108
  {
103
109
  // Arrange
104
- _mockRepository.Setup(x => x.GetAllAsync(It.IsAny<CancellationToken>()))
105
- .ReturnsAsync(new List<{{name}}>());
110
+ _mockRepository.Setup(x => x.GetAllAsync(It.IsAny<PaginationParams>(), It.IsAny<CancellationToken>()))
111
+ .ReturnsAsync(new PaginatedResult<{{name}}>
112
+ {
113
+ Items = new List<{{name}}>(),
114
+ Total = 0,
115
+ Page = 1,
116
+ PageSize = 10
117
+ });
106
118
 
107
119
  // Act
108
- var result = await _sut.GetAllAsync();
120
+ var result = await _sut.GetAllAsync(new PaginationParams { Page = 1, PageSize = 10 });
109
121
 
110
122
  // Assert
111
- result.Should().BeEmpty();
123
+ result.Items.Should().BeEmpty();
112
124
  }
113
125
 
114
126
  #endregion
@@ -7,7 +7,7 @@ description: |
7
7
  - Developing feature by feature without /business-analyse
8
8
  - Fixing or extending code generated by /ralph-loop
9
9
  - Any incremental SmartStack development task
10
- argument-hint: "[-a] [-x] [-s] [-t] [-e] [-r] [-pr] <task description>"
10
+ argument-hint: "[-a] [-x] [-s] [-e] [-r] [-pr] <task description>"
11
11
  ---
12
12
 
13
13
  <objective>
@@ -21,7 +21,7 @@ Execute incremental SmartStack development using the APEX methodology. This skil
21
21
  ```bash
22
22
  /apex add absence management to HR module # Add section to module
23
23
  /apex -a fix the leave request controller # Auto fix
24
- /apex -a -x -s -t -pr add PDF export to Orders # Full workflow
24
+ /apex -a -x -s -pr add PDF export to Orders # Full workflow
25
25
  /apex -e add status field to Project entity # Economy (no agents)
26
26
  /apex -r # Resume previous
27
27
  ```
@@ -35,7 +35,6 @@ Execute incremental SmartStack development using the APEX methodology. This skil
35
35
  | `-a` | Auto mode: skip confirmations |
36
36
  | `-x` | Deep review: adversarial code review (beyond step-04 automated checks) |
37
37
  | `-s` | Save mode: output to `.claude/output/apex/` |
38
- | `-t` | Test mode: scaffold + run tests via MCP |
39
38
  | `-e` | Economy mode: no subagents |
40
39
  | `-r` | Resume: continue from previous state |
41
40
  | `-pr` | PR mode: create pull request at end |
@@ -51,7 +50,6 @@ Execute incremental SmartStack development using the APEX methodology. This skil
51
50
  | `{auto_mode}` | boolean | Skip confirmations |
52
51
  | `{examine_mode}` | boolean | Adversarial review |
53
52
  | `{save_mode}` | boolean | Save outputs to `.claude/output/apex/{task-id}/` |
54
- | `{test_mode}` | boolean | Include test steps (07-08) |
55
53
  | `{economy_mode}` | boolean | No subagents |
56
54
  | `{pr_mode}` | boolean | Create PR at end |
57
55
  | `{context_code}` | string | "business", "platform", "personal" |
@@ -88,8 +86,8 @@ Execute incremental SmartStack development using the APEX methodology. This skil
88
86
  | 04 | `steps/step-04-examine.md` | Opus | eXamine: MCP validation, build, 21 POST-CHECKs, acceptance criteria |
89
87
  | 05 | `steps/step-05-deep-review.md` | Opus | Deep Review: adversarial code review (if -x) |
90
88
  | 06 | `steps/step-06-resolve.md` | Opus | Fix BLOCKING findings (if any) |
91
- | 07 | `steps/step-07-tests.md` | Opus | Scaffold tests via MCP (if -t) |
92
- | 08 | `steps/step-08-run-tests.md` | Opus | Run tests until 100% pass (if -t) |
89
+ | 07 | `steps/step-07-tests.md` | Opus | Scaffold tests via MCP |
90
+ | 08 | `steps/step-08-run-tests.md` | Opus | Run tests until 100% pass |
93
91
  </step_files>
94
92
 
95
93
  <apex_phases>
@@ -101,10 +99,10 @@ Execute incremental SmartStack development using the APEX methodology. This skil
101
99
  | **A** — Analyze | 01 | Yes | Explore existing code |
102
100
  | **P** — Plan | 02 | Yes | File-by-file plan with skill/MCP mapping |
103
101
  | **E** — Execute | 03 | Yes | Orchestrate creation via skills and MCP |
104
- | **X** — eXamine | 04 | Yes | 21 POST-CHECKs, MCP validation, build, acceptance criteria |
102
+ | **X** — eXamine | 04 | Yes | 30 POST-CHECKs, MCP validation, build, acceptance criteria |
105
103
  | *Deep Review* | 05 (if -x) | No | Adversarial code review beyond automated checks |
106
104
  | *Resolve* | 06 (if BLOCKING) | No | Fix BLOCKING findings |
107
- | *Tests* | 07-08 (if -t) | No | Scaffold and run tests |
105
+ | *Tests* | 07-08 | **Yes** | Scaffold and run tests |
108
106
  </apex_phases>
109
107
 
110
108
  <reference_files>
@@ -157,6 +155,6 @@ Execute incremental SmartStack development using the APEX methodology. This skil
157
155
  - MCP validate_conventions: 0 errors
158
156
  - Build: dotnet build PASS + npm run typecheck PASS (if frontend)
159
157
  - Seed data complete (navigation, permissions, roles, provider) if required
160
- - Tests: 100% pass, >= 80% coverage (if -t)
158
+ - Tests: 100% pass, >= 80% coverage
161
159
  - Commits atomic per layer
162
160
  </success_criteria>