@c0x12c/spartan-ai-toolkit 1.8.4 → 1.9.1

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.
@@ -10,7 +10,7 @@
10
10
  "name": "spartan-ai-toolkit",
11
11
  "description": "5 workflows, 68 commands, 21 rules, 28 skills, 9 agents — organized in 12 packs with dependencies",
12
12
  "source": "./toolkit",
13
- "version": "1.8.4"
13
+ "version": "1.9.1"
14
14
  }
15
15
  ]
16
16
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spartan-ai-toolkit",
3
- "version": "1.8.4",
3
+ "version": "1.9.1",
4
4
  "description": "Engineering discipline layer for Claude Code — 5 workflows, 68 commands, 21 rules, 28 skills, 9 agents organized in 12 packs",
5
5
  "author": {
6
6
  "name": "Khoa Tran",
package/README.md CHANGED
@@ -316,6 +316,7 @@ Rules are enforced automatically every session. No action needed — they're act
316
316
  | Rule | Pack |
317
317
  |------|------|
318
318
  | `NAMING_CONVENTIONS` | core |
319
+ | `TIMEZONE` | core |
319
320
  | `ARCHITECTURE` | shared-backend |
320
321
  | `SCHEMA` | database |
321
322
  | `ORM_AND_REPO` | database |
@@ -358,6 +359,74 @@ For other tools, copy the rule files from `toolkit/rules/` into your tool's conf
358
359
 
359
360
  ---
360
361
 
362
+ ## Parallel Builds
363
+
364
+ By default, `/spartan:build` creates a **git worktree** per feature — a separate directory with its own branch. This means you can build 2+ features in parallel from different terminals:
365
+
366
+ ```bash
367
+ # Terminal 1 # Terminal 2
368
+ claude claude
369
+ > /spartan:build auth > /spartan:build payments
370
+ # → .claude/worktrees/feature-auth/ # → .claude/worktrees/feature-payments/
371
+ # → PR #1 # → PR #2
372
+ ```
373
+
374
+ No conflicts. Each session gets its own worktree, branch, and PR.
375
+
376
+ To disable worktrees (single-terminal mode), set `worktree: false` in `.spartan/build.yaml`.
377
+
378
+ ---
379
+
380
+ ## Project Config
381
+
382
+ Customize any Spartan command per project. Two config files in `.spartan/`:
383
+
384
+ ### `.spartan/build.yaml` — Build workflow config
385
+
386
+ Controls `/spartan:build` behavior:
387
+
388
+ ```yaml
389
+ worktree: true # git worktree per feature (default: true)
390
+ branch-prefix: "feature" # branch name: [prefix]/[slug]
391
+ max-review-rounds: 3 # review-fix cycles before asking user
392
+ skip-stages: [] # skip: spec, design, plan, ship (never review)
393
+ worktree-symlinks: [] # extra dirs to share across worktrees
394
+
395
+ prompts:
396
+ spec: |
397
+ Always include performance requirements.
398
+ plan: |
399
+ Every task must reference a Jira ticket.
400
+ implement: |
401
+ Add structured logging to new service methods.
402
+ review: |
403
+ Check all API responses include request_id.
404
+ ship: |
405
+ PR title: [PROJ-123] Short description.
406
+ ```
407
+
408
+ ### `.spartan/commands.yaml` — Per-command prompt injection
409
+
410
+ Inject custom instructions into ANY Spartan command:
411
+
412
+ ```yaml
413
+ prompts:
414
+ review: |
415
+ Flag any function longer than 50 lines.
416
+ pr-ready: |
417
+ Always add "Reviewers: @backend-team" for backend changes.
418
+ daily: |
419
+ Include blockers section. Tag by project area.
420
+ debug: |
421
+ Always check CloudWatch logs first.
422
+ migration: |
423
+ Migration files must start with ticket number.
424
+ ```
425
+
426
+ Templates for both files are in `toolkit/templates/`.
427
+
428
+ ---
429
+
361
430
  ## Target Stack
362
431
 
363
432
  Rules and skills are tuned for this stack, but the command framework works with anything:
package/VERSION CHANGED
@@ -1 +1 @@
1
- 1.8.4
1
+ 1.9.1
@@ -14,23 +14,22 @@ You decide which steps to run, which skills to call, and when to move forward. T
14
14
  ```
15
15
  SINGLE FEATURE:
16
16
 
17
- Context → Spec → Design? → Plan+Worktree → Implement → Review Agent → Fix → Ship
18
- │ │ │ │ │ │ │
19
- .memory/ Gate 1 Design Gate 2 Gate 3 Spawn agent Loop Gate 4
20
- Gate EnterWorktree fix until OK
17
+ Context → Spec → Design? → Plan → Implement → Review Agent → Fix → Ship
18
+ │ │ │ │ │ │ │
19
+ .memory/ Gate 1 Design Gate 2 Gate 3 Spawn agent Loop Gate 4
20
+ Gate fix until OK
21
21
 
22
22
  EPIC (multi-feature — auto-detected):
23
23
 
24
- Context → Epic detected → Per feature: Spec/Design/Plan → WorktreeImplementReview → Ship
25
- │ │ │ │ │ │ │
26
- .planning/ read epic fill gaps if needed EnterWorktree parallel by Loop one PR
27
- epics/ dependency
24
+ Context → Epic detected → Per feature: Spec/Design/Plan → ImplementReview Agent Fix → Ship
25
+ │ │ │ │ │ │ │
26
+ .planning/ read epic fill gaps if needed parallel by Spawn agent Loop one PR
27
+ epics/ dependency fix until OK
28
28
 
29
- PARALLEL BUILDS (multiple terminals):
29
+ PARALLEL BUILDS (automatic — each build in its own worktree):
30
30
 
31
- Terminal 1: /spartan:build auth → worktree .claude/worktrees/feature-auth/ → branch feature/auth → PR #1
32
- Terminal 2: /spartan:build payments → worktree .claude/worktrees/feature-payments/ → branch feature/payments → PR #2
33
- (each terminal gets its own worktree, branch, and PR — no conflicts)
31
+ /spartan:build auth → worktree .claude/worktrees/feature-auth/ → PR #1
32
+ /spartan:build payments → worktree .claude/worktrees/feature-payments/ → PR #2
34
33
  ```
35
34
 
36
35
  **Fast path:** For small work (< 1 day, ≤ 4 tasks), you do spec + plan inline. No separate commands needed.
@@ -52,6 +51,86 @@ PARALLEL BUILDS (multiple terminals):
52
51
 
53
52
  ---
54
53
 
54
+ ## FIRST: Load Build Config & Enter Worktree
55
+
56
+ ### Load project build config
57
+
58
+ Check for a project-level build config that overrides default behavior:
59
+
60
+ ```bash
61
+ cat .spartan/build.yaml 2>/dev/null || cat .spartan/build.yml 2>/dev/null
62
+ ```
63
+
64
+ If `.spartan/build.yaml` exists, read it and apply overrides. All fields are optional — omit to use defaults.
65
+
66
+ | Field | Default | What it does |
67
+ |-------|---------|-------------|
68
+ | `worktree` | `true` | Create a git worktree per build. Set `false` for single-terminal workflow. |
69
+ | `branch-prefix` | `"feature"` | Branch name: `[prefix]/[slug]` (e.g., `feature/user-auth`) |
70
+ | `max-review-rounds` | `3` | Max review-fix cycles before asking the user |
71
+ | `skip-stages` | `[]` | Stages to skip. Valid: `spec`, `design`, `plan`, `review`, `ship` |
72
+ | `worktree-symlinks` | `[]` | Extra gitignored dirs to symlink into worktrees |
73
+ | `prompts.spec` | — | Custom instructions injected after spec questions |
74
+ | `prompts.plan` | — | Custom instructions injected into the plan stage |
75
+ | `prompts.implement` | — | Custom instructions injected during implementation |
76
+ | `prompts.review` | — | Custom instructions injected into the review agent's prompt |
77
+ | `prompts.ship` | — | Custom instructions injected before PR creation |
78
+
79
+ **If config has `prompts.*` sections**, inject those instructions at the relevant stage. They run AFTER the built-in instructions.
80
+
81
+ **If config has `skip-stages`**, skip those stages. But NEVER skip `review` even if listed — review is always mandatory.
82
+
83
+ ### Enter Worktree
84
+
85
+ **Default: every build runs in a git worktree.** A worktree is a separate directory with its own branch — lets you run `/spartan:build` in multiple terminals without conflicts.
86
+
87
+ **Check if already in a worktree:**
88
+
89
+ ```bash
90
+ git rev-parse --show-toplevel 2>/dev/null
91
+ git worktree list 2>/dev/null | head -3
92
+ ```
93
+
94
+ If already in a worktree (not the main repo), skip — you're already isolated.
95
+
96
+ **If `worktree: false` in config**, skip worktree creation. Use `git checkout -b` later in Stage 3 instead.
97
+
98
+ **Otherwise, create a worktree now:**
99
+
100
+ Generate a slug from the feature description (e.g., "user auth" → `user-auth`). Use `branch-prefix` from config (default: `feature`).
101
+
102
+ ```
103
+ EnterWorktree(name: "[prefix]-[slug]")
104
+ ```
105
+
106
+ This creates a worktree at `.claude/worktrees/[prefix]-[slug]/` with a new branch and switches your working directory there.
107
+
108
+ **Symlink shared directories** — gitignored dirs don't appear in worktrees. Symlink them from the main repo:
109
+
110
+ ```bash
111
+ MAIN_REPO="$(git worktree list | head -1 | awk '{print $1}')"
112
+ SYMLINKS=".planning .memory .handoff .spartan"
113
+
114
+ # Add extra symlinks from config (worktree-symlinks field)
115
+ for dir in $SYMLINKS; do
116
+ [ -d "$MAIN_REPO/$dir" ] && [ ! -e "$dir" ] && ln -s "$MAIN_REPO/$dir" "$dir"
117
+ done
118
+ ```
119
+
120
+ > "Working in worktree: `[path]` on branch `[prefix]-[slug]`"
121
+
122
+ **If `EnterWorktree` fails** (e.g., name conflict), fall back to manual worktree:
123
+
124
+ ```bash
125
+ SLUG="[slug]"
126
+ MAIN_REPO="$(pwd)"
127
+ git worktree add "$MAIN_REPO/.worktrees/$SLUG" -b "feature/$SLUG" 2>/dev/null
128
+ ```
129
+
130
+ Then tell the user: "Created worktree at `.worktrees/[slug]/`. Open a new terminal there and run `claude` + `/spartan:build`."
131
+
132
+ ---
133
+
55
134
  ## Step 0: Detect Mode & Stack (silent — no questions)
56
135
 
57
136
  Parse the user's input to find the mode:
@@ -183,6 +262,8 @@ Use the approach from `/spartan:spec` — ask questions one at a time, fill the
183
262
 
184
263
  Then continue to the next stage automatically (don't tell the user to run a separate command).
185
264
 
265
+ **Custom spec prompts:** If `.spartan/build.yaml` has `prompts.spec`, apply those instructions now — ask any extra questions, add any extra requirements to the scope.
266
+
186
267
  **GATE 1 — STOP and ask:**
187
268
  > "Here's the scope. Anything to change before I plan?"
188
269
  >
@@ -278,28 +359,24 @@ Uses skills: `ui-ux-pro-max`, frontend rules
278
359
 
279
360
  **CRITICAL: Full-stack means BOTH layers must complete.** Don't move to Gate 3 after finishing backend only. The plan must include frontend tasks and ALL tasks must be done before review. If the spec mentions UI changes, API responses shown to users, or any user-facing behavior — frontend tasks are mandatory.
280
361
 
281
- ### Create feature workspace
362
+ ### Verify workspace
282
363
 
283
- Every build runs in its own **git worktree** — a separate directory with its own branch. This lets you run `/spartan:build` in multiple terminals on the same repo. Each terminal gets its own worktree, branch, and PR. No conflicts.
364
+ Confirm you're in the right place:
284
365
 
285
- Use `EnterWorktree` to create the worktree. The name should be the feature branch slug:
286
-
287
- ```
288
- EnterWorktree(name: "feature-[slug]")
366
+ ```bash
367
+ pwd
368
+ git branch --show-current
289
369
  ```
290
370
 
291
- This creates a worktree at `.claude/worktrees/feature-[slug]/` on a new branch and switches your working directory to it. All file reads, writes, and bash commands now happen inside the worktree.
371
+ **If worktree is enabled (default):** You should already be in a worktree from the "FIRST" step. If not, go back and create one.
292
372
 
293
- **Symlink shared directories** — `.planning/`, `.memory/`, `.handoff/`, and `.spartan/` are gitignored and won't appear in the worktree. Symlink them from the main repo so specs, plans, and memory are shared:
373
+ **If `worktree: false` in config:** Create a branch now:
294
374
 
295
375
  ```bash
296
- MAIN_REPO="$(git worktree list | head -1 | awk '{print $1}')"
297
- for dir in .planning .memory .handoff .spartan; do
298
- [ -d "$MAIN_REPO/$dir" ] && [ ! -e "$dir" ] && ln -s "$MAIN_REPO/$dir" "$dir"
299
- done
376
+ git checkout -b feature/[slug]
300
377
  ```
301
378
 
302
- > "Working in worktree: `.claude/worktrees/feature-[slug]/` on branch `feature-[slug]`"
379
+ **Custom plan prompts:** If `.spartan/build.yaml` has `prompts.plan`, apply those instructions now.
303
380
 
304
381
  Write the first failing test for Task 1. Show it fails.
305
382
 
@@ -485,6 +562,8 @@ ls .planning/specs/*.md .planning/plans/*.md .planning/designs/*.md 2>/dev/null
485
562
 
486
563
  Classify each changed file into backend/frontend/migration using the `file-types` from config (or defaults: `.kt/.java/.go/.py` = backend, `.tsx/.ts/.vue` = frontend, `.sql` = migration).
487
564
 
565
+ **Custom review prompts:** If `.spartan/build.yaml` has `prompts.review`, include those instructions in the review agent's prompt below — they're injected after the standard review stages.
566
+
488
567
  ### Step 3: Spawn the review agent
489
568
 
490
569
  Use the `Agent` tool to spawn a reviewer. **The prompt is built from the config.**
@@ -605,7 +684,7 @@ When the reviewer reports back:
605
684
  5. Spawn the review agent AGAIN with the updated diff — only the remaining/new changes need review
606
685
  6. Repeat until the reviewer says PASS
607
686
 
608
- **Max 3 review rounds.** If still failing after 3 rounds, stop and ask the user:
687
+ **Max review rounds** (default: 3, configurable via `max-review-rounds` in `.spartan/build.yaml`). If still failing after max rounds, stop and ask the user:
609
688
  > "Review found issues I can't fully fix. Here's what's left: [list]. Want to continue anyway or address these manually?"
610
689
 
611
690
  ### Step 4: Capture review learnings
@@ -670,6 +749,8 @@ After review completes: `TeamDelete()` to clean up.
670
749
 
671
750
  ## Stage 6: Ship
672
751
 
752
+ **Custom ship prompts:** If `.spartan/build.yaml` has `prompts.ship`, apply those instructions — PR naming rules, extra checks, deploy notes, etc.
753
+
673
754
  ### Create PR
674
755
  Run the approach from `/spartan:pr-ready`:
675
756
  - Rebase onto main
@@ -691,22 +772,6 @@ mkdir -p .memory/decisions .memory/patterns .memory/knowledge
691
772
 
692
773
  Update `.memory/index.md` if you saved anything.
693
774
 
694
- ### Clean up worktree
695
-
696
- After the PR is created, exit the worktree. Keep it around in case the user needs to make PR review fixes:
697
-
698
- ```
699
- ExitWorktree(action: "keep")
700
- ```
701
-
702
- > "PR created. Worktree kept at `.claude/worktrees/feature-[slug]/` in case you need to push fixes. To clean up later: `git worktree remove .claude/worktrees/feature-[slug]`"
703
-
704
- If the user says the PR is merged and they're done, clean up:
705
-
706
- ```
707
- ExitWorktree(action: "remove")
708
- ```
709
-
710
775
  **GATE 4 — Done.**
711
776
  > "PR created: [link]. Here's what's in it: [summary]."
712
777
 
@@ -763,25 +828,18 @@ Agent(
763
828
  ```
764
829
  Collect results after all finish, then `TeamDelete()`.
765
830
 
766
- ### Step 2: Sort by dependency and create workspace
831
+ ### Step 2: Sort by dependency
767
832
 
768
833
  Read the epic's Features table. Sort features by dependency order:
769
834
  - Features with no dependencies → can build first
770
835
  - Features that depend on others → build after their dependencies are done
771
836
 
772
- Create a worktree for the epic:
773
-
774
- ```
775
- EnterWorktree(name: "feature-[epic-slug]")
776
- ```
777
-
778
- Symlink shared directories:
837
+ **If worktree enabled (default):** You should already be in a worktree from the "FIRST" step.
838
+ **If `worktree: false`:** Create a branch: `git checkout -b feature/[epic-slug]`
779
839
 
780
840
  ```bash
781
- MAIN_REPO="$(git worktree list | head -1 | awk '{print $1}')"
782
- for dir in .planning .memory .handoff .spartan; do
783
- [ -d "$MAIN_REPO/$dir" ] && [ ! -e "$dir" ] && ln -s "$MAIN_REPO/$dir" "$dir"
784
- done
841
+ pwd
842
+ git branch --show-current
785
843
  ```
786
844
 
787
845
  ### Step 3: Implement in dependency order
@@ -851,19 +909,13 @@ Run the full test suite. Then continue to **Stage 5: Review** — the review age
851
909
  If a previous session was interrupted (context overflow, user stopped, etc.), this workflow can resume.
852
910
 
853
911
  **How resume works:**
854
- 1. Check for existing worktrees from a previous build:
855
- ```bash
856
- git worktree list
857
- ls .claude/worktrees/ 2>/dev/null
858
- ```
859
- If a worktree exists for this feature, re-enter it with `EnterWorktree(name: "feature-[slug]")`. Don't create a new one.
860
- 2. Step 0.5 checks for `.handoff/` files and existing `.planning/` artifacts
861
- 3. Determine which stage was completed last:
912
+ 1. Step 0.5 checks for `.handoff/` files and existing `.planning/` artifacts
913
+ 2. Determine which stage was completed last:
862
914
  - Has spec but no plan → resume at Stage 3 (Plan)
863
915
  - Has plan but no commits on feature branch → resume at Stage 4 (Implement)
864
916
  - Has commits but no PR → resume at Stage 5 (Review) or Stage 6 (Ship)
865
- 4. Show the user: "Resuming from [stage] in worktree `[path]`. Here's what was done: [summary]."
866
- 5. Continue from that point.
917
+ 3. Show the user: "Resuming from [stage]. Here's what was done: [summary]."
918
+ 4. Continue from that point.
867
919
 
868
920
  **Don't re-do completed stages.** Read the saved artifacts and move forward.
869
921
 
@@ -886,8 +938,8 @@ If a previous session was interrupted (context overflow, user stopped, etc.), th
886
938
  - **Full-stack = both layers done.** If the feature touches both backend and frontend, you MUST implement both before creating the PR. Backend-only completion is NOT "done" for a full-stack feature.
887
939
  - **Epic = one branch, one PR.** When building from an epic, all features go on one branch and ship as one PR. Don't create separate PRs per feature. Parallelize independent features with Agent Teams when available.
888
940
  - **Epic auto-detection.** If the user's feature name matches an epic in `.planning/epics/`, switch to epic mode automatically. Don't ask.
889
- - **Every build uses a worktree.** Always create a worktree with `EnterWorktree` before writing code. This lets multiple `/spartan:build` sessions run in parallel — each terminal gets its own worktree, branch, and PR. Never `git checkout -b` in the main repo.
890
- - **Worktree cleanup.** After PR is created, `ExitWorktree(action: "keep")` so the user can push fixes. After PR is merged, `ExitWorktree(action: "remove")` to clean up. Users can also clean up manually: `git worktree remove .claude/worktrees/feature-[slug]`.
941
+ - **Worktree by default.** The first step is `EnterWorktree` (unless `worktree: false` in `.spartan/build.yaml`). This lets multiple terminals run `/spartan:build` simultaneously. If already in a worktree, skip. If `EnterWorktree` fails, fall back to manual worktree.
942
+ - **Build config is `.spartan/build.yaml`.** Controls worktrees, branch prefix, max review rounds, skip stages, custom prompts per stage, and extra worktree symlinks. All fields optional.
891
943
 
892
944
  ---
893
945
 
@@ -921,3 +973,8 @@ A feature is NOT done until every applicable item is checked:
921
973
  - [ ] Atomic commits (one per task)
922
974
  - [ ] Review agent passed (all HIGH/MEDIUM issues fixed)
923
975
  - [ ] PR created with summary and test plan
976
+
977
+ ### Worktree
978
+ - [ ] Worktree created and working directory switched (automatic unless `worktree: false`)
979
+ - [ ] All commits pushed to feature branch
980
+ - [ ] Worktree cleaned up after PR merged (`git worktree remove .claude/worktrees/[slug]`)
@@ -48,6 +48,18 @@ This prevents confusion when the user is running multiple windows. Keep it to on
48
48
 
49
49
  ---
50
50
 
51
+ ## Step 0.9: Load Command Config (silent, always runs)
52
+
53
+ Check for per-command custom prompts. Commands config lets users inject custom instructions into any Spartan command.
54
+
55
+ ```bash
56
+ cat .spartan/commands.yaml 2>/dev/null || cat .spartan/commands.yml 2>/dev/null
57
+ ```
58
+
59
+ If the file exists and has a `prompts.[command-name]` entry matching the command being routed to, pass those custom instructions to the command. They're appended after the built-in prompt.
60
+
61
+ ---
62
+
51
63
  ## Step 1: Detect Project Context (silent, no questions)
52
64
 
53
65
  Before asking anything, scan the environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c0x12c/spartan-ai-toolkit",
3
- "version": "1.8.4",
3
+ "version": "1.9.1",
4
4
  "description": "Engineering discipline layer for AI coding agents — commands, rules, skills, agents, and packs for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
package/packs/core.yaml CHANGED
@@ -28,6 +28,7 @@ commands:
28
28
 
29
29
  rules:
30
30
  - core/NAMING_CONVENTIONS.md
31
+ - core/TIMEZONE.md
31
32
  - core/SKILL_AUTHORING.md
32
33
 
33
34
  skills: []
@@ -31,6 +31,7 @@
31
31
  ],
32
32
  "rules": [
33
33
  "core/NAMING_CONVENTIONS.md",
34
+ "core/TIMEZONE.md",
34
35
  "core/SKILL_AUTHORING.md"
35
36
  ],
36
37
  "skills": [],
@@ -0,0 +1,261 @@
1
+ # Timezone Rules
2
+
3
+ ## One Rule: Everything is UTC
4
+
5
+ **Server stores UTC. API sends UTC. API receives UTC. No exceptions.**
6
+
7
+ The frontend is the only place that converts to/from local time — and only for display.
8
+
9
+ ```
10
+ Database (UTC) → Backend (UTC) → API JSON (UTC) → Frontend receives (UTC) → Display (local)
11
+ ← Frontend sends (UTC) ← Input (local → UTC)
12
+ ```
13
+
14
+ ---
15
+
16
+ ## Backend
17
+
18
+ ### Always Use `Instant` — Never `LocalDateTime`
19
+
20
+ `Instant` is UTC by definition. `LocalDateTime` has no timezone info and leads to bugs.
21
+
22
+ ```kotlin
23
+ // CORRECT — Instant is always UTC
24
+ val now: Instant = Instant.now()
25
+ val expiresAt: Instant = Instant.now().plusSeconds(3600)
26
+
27
+ // WRONG — LocalDateTime has no timezone, ambiguous
28
+ val now: LocalDateTime = LocalDateTime.now() // What timezone? Nobody knows.
29
+ val expiresAt: LocalDateTime = LocalDateTime.now().plusHours(1)
30
+ ```
31
+
32
+ ### Never Use `ZonedDateTime` in Business Logic
33
+
34
+ `ZonedDateTime` is only for converting when absolutely needed (e.g., generating a report for a specific timezone). Don't pass it between layers.
35
+
36
+ ```kotlin
37
+ // CORRECT — use Instant everywhere
38
+ data class UserEntity(
39
+ val createdAt: Instant,
40
+ val lastLoginAt: Instant?,
41
+ val subscriptionExpiresAt: Instant?
42
+ )
43
+
44
+ // WRONG — ZonedDateTime in entities/DTOs
45
+ data class UserEntity(
46
+ val createdAt: ZonedDateTime, // NO — carries timezone baggage
47
+ val lastLoginAt: LocalDateTime? // NO — ambiguous
48
+ )
49
+ ```
50
+
51
+ ### Never Store Timezone in the Database
52
+
53
+ Don't add `timezone` columns. Don't save user timezone preferences alongside timestamps. If the frontend needs to display local time, it does the conversion itself.
54
+
55
+ ```sql
56
+ -- CORRECT — just UTC timestamps
57
+ CREATE TABLE events (
58
+ id UUID PRIMARY KEY,
59
+ starts_at TIMESTAMP NOT NULL, -- UTC
60
+ ends_at TIMESTAMP NOT NULL, -- UTC
61
+ created_at TIMESTAMP DEFAULT NOW() -- UTC
62
+ );
63
+
64
+ -- WRONG — storing timezone info
65
+ CREATE TABLE events (
66
+ id UUID PRIMARY KEY,
67
+ starts_at TIMESTAMP NOT NULL,
68
+ ends_at TIMESTAMP NOT NULL,
69
+ timezone TEXT DEFAULT 'America/New_York' -- DON'T DO THIS
70
+ );
71
+ ```
72
+
73
+ ### Jackson Serialization
74
+
75
+ Jackson must serialize all `Instant` fields as ISO 8601 with the `Z` (UTC) suffix. This should be configured globally:
76
+
77
+ ```kotlin
78
+ // ObjectMapper config (usually already set)
79
+ objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
80
+ // Output: "2024-01-15T10:30:00Z"
81
+ ```
82
+
83
+ Every datetime field in API JSON looks like: `"2024-01-15T10:30:00Z"`
84
+
85
+ Never output offsets like `+07:00` or timezone names like `Asia/Ho_Chi_Minh`.
86
+
87
+ ---
88
+
89
+ ## API Contract
90
+
91
+ ### All Datetime Fields Are ISO 8601 UTC
92
+
93
+ ```json
94
+ {
95
+ "created_at": "2024-01-15T10:30:00Z",
96
+ "updated_at": "2024-01-15T14:22:33Z",
97
+ "expires_at": "2024-02-15T00:00:00Z"
98
+ }
99
+ ```
100
+
101
+ ### Request Bodies — Frontend Sends UTC
102
+
103
+ When the frontend sends a datetime, it MUST be UTC:
104
+
105
+ ```json
106
+ {
107
+ "starts_at": "2024-01-20T09:00:00Z",
108
+ "ends_at": "2024-01-20T17:00:00Z"
109
+ }
110
+ ```
111
+
112
+ ### Query Parameters — Also UTC
113
+
114
+ ```
115
+ GET /events?from=2024-01-01T00:00:00Z&to=2024-01-31T23:59:59Z
116
+ ```
117
+
118
+ ### No Timezone Fields in Request or Response
119
+
120
+ ```json
121
+ // WRONG — timezone info in API
122
+ {
123
+ "starts_at": "2024-01-20T09:00:00Z",
124
+ "timezone": "America/New_York"
125
+ }
126
+
127
+ // CORRECT — just UTC, frontend handles display
128
+ {
129
+ "starts_at": "2024-01-20T09:00:00Z"
130
+ }
131
+ ```
132
+
133
+ ---
134
+
135
+ ## Frontend
136
+
137
+ ### Receive UTC, Convert for Display
138
+
139
+ All API responses return UTC. Convert to local only when showing to the user:
140
+
141
+ ```typescript
142
+ // CORRECT — convert at display time
143
+ function formatDate(utcString: string): string {
144
+ return new Date(utcString).toLocaleString()
145
+ // Or use Intl.DateTimeFormat for more control
146
+ }
147
+
148
+ // CORRECT — with specific format
149
+ function formatDate(utcString: string, locale = 'en-US'): string {
150
+ return new Intl.DateTimeFormat(locale, {
151
+ dateStyle: 'medium',
152
+ timeStyle: 'short',
153
+ }).format(new Date(utcString))
154
+ }
155
+ ```
156
+
157
+ ### Send UTC to Server
158
+
159
+ Convert local input to UTC before sending:
160
+
161
+ ```typescript
162
+ // CORRECT — convert to UTC before API call
163
+ const localDate = new Date(userInput) // user picks "Jan 20, 2024 9:00 AM"
164
+ const utcString = localDate.toISOString() // "2024-01-20T02:00:00.000Z" (if user is UTC+7)
165
+
166
+ await api.post('/events', {
167
+ startsAt: utcString, // Always UTC
168
+ })
169
+
170
+ // WRONG — sending local time string
171
+ await api.post('/events', {
172
+ startsAt: '2024-01-20T09:00:00', // No Z suffix — ambiguous!
173
+ })
174
+ ```
175
+
176
+ ### Date Libraries
177
+
178
+ If using a date library (date-fns, dayjs, luxon), still follow the same pattern:
179
+
180
+ ```typescript
181
+ // date-fns example
182
+ import { formatInTimeZone } from 'date-fns-tz'
183
+
184
+ // Display: UTC → user's local timezone
185
+ const display = formatInTimeZone(
186
+ new Date(apiResponse.createdAt), // UTC from API
187
+ Intl.DateTimeFormat().resolvedOptions().timeZone, // user's timezone
188
+ 'MMM d, yyyy h:mm a'
189
+ )
190
+
191
+ // Send: local → UTC
192
+ const utc = new Date(localInput).toISOString()
193
+ ```
194
+
195
+ ### Never Store Timezone in Frontend State
196
+
197
+ Don't track the user's timezone in state or send it to the backend. The browser already knows the timezone — use it at render time.
198
+
199
+ ```typescript
200
+ // WRONG — tracking timezone in state
201
+ const [timezone, setTimezone] = useState('America/New_York')
202
+
203
+ // CORRECT — use browser timezone at render time
204
+ const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone
205
+ ```
206
+
207
+ ---
208
+
209
+ ## Database
210
+
211
+ ### TIMESTAMP Without Timezone
212
+
213
+ Use `TIMESTAMP` (not `TIMESTAMPTZ`). The value is always UTC. No timezone info needed.
214
+
215
+ ```sql
216
+ -- CORRECT
217
+ created_at TIMESTAMP DEFAULT NOW() -- NOW() returns UTC in a UTC-configured server
218
+
219
+ -- ALSO ACCEPTABLE (PostgreSQL stores both as UTC internally)
220
+ created_at TIMESTAMPTZ DEFAULT NOW()
221
+ ```
222
+
223
+ ### Server Must Be Configured for UTC
224
+
225
+ The database server and application server must run in UTC:
226
+
227
+ ```yaml
228
+ # application.yml
229
+ datasources:
230
+ default:
231
+ connection-properties:
232
+ timezone: UTC
233
+ ```
234
+
235
+ ```sql
236
+ -- PostgreSQL: verify server timezone
237
+ SHOW timezone; -- Should return 'UTC'
238
+ ```
239
+
240
+ ---
241
+
242
+ ## Quick Reference
243
+
244
+ | Layer | Type | Format | Example |
245
+ |-------|------|--------|---------|
246
+ | Database | Column type | `TIMESTAMP` | `2024-01-15 10:30:00` |
247
+ | Backend (Kotlin) | Property type | `Instant` | `Instant.now()` |
248
+ | API JSON | String | ISO 8601 + Z | `"2024-01-15T10:30:00Z"` |
249
+ | Frontend (receive) | Parse | `new Date(utcString)` | `new Date("2024-01-15T10:30:00Z")` |
250
+ | Frontend (display) | Format | `toLocaleString()` | `"Jan 15, 2024, 5:30 PM"` (UTC+7) |
251
+ | Frontend (send) | Serialize | `toISOString()` | `"2024-01-15T10:30:00.000Z"` |
252
+
253
+ ## What NOT to Do
254
+
255
+ - Don't use `LocalDateTime` in Kotlin — use `Instant`
256
+ - Don't store timezone names or offsets in the database
257
+ - Don't send timezone info in API requests or responses
258
+ - Don't use `TIMESTAMPTZ` thinking it "stores the timezone" — PostgreSQL converts everything to UTC anyway
259
+ - Don't convert to local time on the backend — that's the frontend's job
260
+ - Don't assume a timezone — let the browser handle it
261
+ - Don't format dates on the server for display — return UTC, let the client format
@@ -0,0 +1,63 @@
1
+ # .spartan/build.yaml — Project-level build config
2
+ # Copy this file to .spartan/build.yaml in your project and customize.
3
+ # All fields are optional. Omit a field to use the default.
4
+
5
+ # --- Worktree ---
6
+
7
+ # Worktree isolation (default: true)
8
+ # When true, /spartan:build creates a git worktree per feature.
9
+ # Each terminal gets its own directory, branch, and PR — no conflicts.
10
+ # Set to false to use simple git checkout (single terminal only).
11
+ worktree: true
12
+
13
+ # Branch prefix (default: "feature")
14
+ # Controls the branch name: [prefix]/[slug] (e.g., "feature/user-auth")
15
+ branch-prefix: "feature"
16
+
17
+ # Extra directories to symlink into worktrees
18
+ # Gitignored dirs (.planning, .memory, .handoff, .spartan) are always symlinked.
19
+ # Add more here if your project has other shared gitignored dirs.
20
+ # worktree-symlinks:
21
+ # - .env
22
+ # - config/local
23
+
24
+ # --- Build stages ---
25
+
26
+ # Max review-fix cycles before asking the user (default: 3)
27
+ # max-review-rounds: 3
28
+
29
+ # Stages to skip (use carefully — most stages exist for a reason)
30
+ # Valid: spec, design, plan, review, ship
31
+ # Note: review can never actually be skipped — it's always enforced
32
+ # skip-stages: []
33
+
34
+ # --- Custom prompts ---
35
+
36
+ # Custom instructions injected into build stages.
37
+ # Claude reads these alongside the built-in workflow instructions.
38
+ # Use this to add project-specific rules, naming conventions, or checklists.
39
+ prompts:
40
+ # Injected after spec questions, before scope is finalized
41
+ # spec: |
42
+ # Always include performance requirements.
43
+ # Ask about expected load/traffic.
44
+
45
+ # Injected into the plan stage, after the task list is generated
46
+ # plan: |
47
+ # Every backend task must include a Flyway migration version check.
48
+ # Frontend tasks must reference the Figma frame URL.
49
+
50
+ # Injected during implementation
51
+ # implement: |
52
+ # Always add structured logging to new service methods.
53
+ # Use the project's error code registry for new error types.
54
+
55
+ # Injected into the review agent's prompt
56
+ # review: |
57
+ # Check that all API responses include request_id for tracing.
58
+ # Verify that new endpoints have rate limiting annotations.
59
+
60
+ # Injected before PR creation
61
+ # ship: |
62
+ # PR title must start with the Jira ticket number.
63
+ # Add "Tested on staging" to the test plan if it touches payments.
@@ -0,0 +1,55 @@
1
+ # .spartan/commands.yaml — Per-command overrides
2
+ # Inject custom prompts into ANY Spartan command. Claude reads these
3
+ # alongside the built-in instructions when the command runs.
4
+ # All fields are optional.
5
+
6
+ # --- Per-command prompt injection ---
7
+ # Key = command name (without "spartan:" prefix)
8
+ # Value = custom instructions appended to the command's prompt
9
+
10
+ # prompts:
11
+ # spec: |
12
+ # Always ask about performance requirements.
13
+ # Include acceptance criteria in BDD format.
14
+ #
15
+ # plan: |
16
+ # Tasks must reference Jira ticket numbers.
17
+ # Estimate hours per task.
18
+ #
19
+ # review: |
20
+ # Check that all new endpoints have OpenAPI annotations.
21
+ # Flag any function longer than 50 lines.
22
+ #
23
+ # pr-ready: |
24
+ # PR title format: [PROJ-123] Short description
25
+ # Always add "Reviewers: @backend-team" for backend changes.
26
+ #
27
+ # daily: |
28
+ # Include blockers section.
29
+ # Tag items by project area (auth, payments, infra).
30
+ #
31
+ # debug: |
32
+ # Always check CloudWatch logs first.
33
+ # Include the request_id in every investigation.
34
+ #
35
+ # migration: |
36
+ # Migration files must start with ticket number.
37
+ # Always add a rollback section.
38
+ #
39
+ # onboard: |
40
+ # Focus on the payments module first — it's the most complex.
41
+ # Skip the legacy admin/ directory.
42
+
43
+ # --- Command defaults ---
44
+ # Override default behavior for specific commands
45
+
46
+ # defaults:
47
+ # build:
48
+ # worktree: true
49
+ # branch-prefix: "feature"
50
+ # max-review-rounds: 3
51
+ # skip-stages: []
52
+ # spec:
53
+ # save-path: ".planning/specs" # where specs are saved
54
+ # review:
55
+ # parallel: true # use Agent Teams for parallel review if available