feat(quick): replace auto-increment number with YYMMDD-xxx timestamp ID (#1012)

Quick task IDs were sequential integers (001, 002...) computed by reading
the local .planning/quick/ directory max value. Two users running /gsd:quick
simultaneously would get the same number, causing directory collisions on git.

Replace with a collision-resistant format: YYMMDD-xxx where xxx is the
number of 2-second blocks elapsed since midnight, encoded as 3 lowercase
Base36 characters (000–xbz). Practical collision window is ~2 seconds per
user — effectively zero for any realistic team workflow.

- init.cjs: remove nextNum scan logic, generate quickId from wall clock
- quick.md: rename all next_num refs to quick_id, update directory patterns
- init.test.cjs: rewrite cmdInitQuick tests for new ID format

Co-authored-by: yanbing <yanbing@corp.netease.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
闫冰
2026-03-13 01:35:59 +08:00
committed by GitHub
parent e97851ebd2
commit 75d21866c8
3 changed files with 87 additions and 66 deletions

View File

@@ -255,18 +255,18 @@ function cmdInitQuick(cwd, description, raw) {
const now = new Date();
const slug = description ? generateSlugInternal(description)?.substring(0, 40) : null;
// Find next quick task number
const quickDir = path.join(cwd, '.planning', 'quick');
let nextNum = 1;
try {
const existing = fs.readdirSync(quickDir)
.filter(f => /^\d+-/.test(f))
.map(f => parseInt(f.split('-')[0], 10))
.filter(n => !isNaN(n));
if (existing.length > 0) {
nextNum = Math.max(...existing) + 1;
}
} catch {}
// Generate collision-resistant quick task ID: YYMMDD-xxx
// xxx = 2-second precision blocks since midnight, encoded as 3-char Base36 (lowercase)
// Range: 000 (00:00:00) to xbz (23:59:58), guaranteed 3 chars for any time of day.
// Provides ~2s uniqueness window per user — practically collision-free across a team.
const yy = String(now.getFullYear()).slice(-2);
const mm = String(now.getMonth() + 1).padStart(2, '0');
const dd = String(now.getDate()).padStart(2, '0');
const dateStr = yy + mm + dd;
const secondsSinceMidnight = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds();
const timeBlocks = Math.floor(secondsSinceMidnight / 2);
const timeEncoded = timeBlocks.toString(36).padStart(3, '0');
const quickId = dateStr + '-' + timeEncoded;
const result = {
// Models
@@ -279,7 +279,7 @@ function cmdInitQuick(cwd, description, raw) {
commit_docs: config.commit_docs,
// Quick task info
next_num: nextNum,
quick_id: quickId,
slug: slug,
description: description || null,
@@ -289,7 +289,7 @@ function cmdInitQuick(cwd, description, raw) {
// Paths
quick_dir: '.planning/quick',
task_dir: slug ? `.planning/quick/${nextNum}-${slug}` : null,
task_dir: slug ? `.planning/quick/${quickId}-${slug}` : null,
// File existence
roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),

View File

@@ -72,7 +72,7 @@ INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init quick "$DESCRIP
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
```
Parse JSON for: `planner_model`, `executor_model`, `checker_model`, `verifier_model`, `commit_docs`, `next_num`, `slug`, `date`, `timestamp`, `quick_dir`, `task_dir`, `roadmap_exists`, `planning_exists`.
Parse JSON for: `planner_model`, `executor_model`, `checker_model`, `verifier_model`, `commit_docs`, `quick_id`, `slug`, `date`, `timestamp`, `quick_dir`, `task_dir`, `roadmap_exists`, `planning_exists`.
**If `roadmap_exists` is false:** Error — Quick mode requires an active project with ROADMAP.md. Run `/gsd:new-project` first.
@@ -93,13 +93,13 @@ mkdir -p "${task_dir}"
Create the directory for this quick task:
```bash
QUICK_DIR=".planning/quick/${next_num}-${slug}"
QUICK_DIR=".planning/quick/${quick_id}-${slug}"
mkdir -p "$QUICK_DIR"
```
Report to user:
```
Creating quick task ${next_num}: ${DESCRIPTION}
Creating quick task ${quick_id}: ${DESCRIPTION}
Directory: ${QUICK_DIR}
```
@@ -180,10 +180,10 @@ Collect all decisions into `$DECISIONS`.
**4.5d. Write CONTEXT.md**
Write `${QUICK_DIR}/${next_num}-CONTEXT.md` using the standard context template structure:
Write `${QUICK_DIR}/${quick_id}-CONTEXT.md` using the standard context template structure:
```markdown
# Quick Task ${next_num}: ${DESCRIPTION} - Context
# Quick Task ${quick_id}: ${DESCRIPTION} - Context
**Gathered:** ${date}
**Status:** Ready for planning
@@ -230,7 +230,7 @@ ${any_specs_adrs_or_docs_referenced_during_discussion}
Note: Quick task CONTEXT.md omits `<code_context>` and `<deferred>` sections (no codebase scouting, no phase scope to defer to). Keep it lean. The `<canonical_refs>` section is included when external docs were referenced — omit it only if no external docs apply.
Report: `Context captured: ${QUICK_DIR}/${next_num}-CONTEXT.md`
Report: `Context captured: ${QUICK_DIR}/${quick_id}-CONTEXT.md`
---
@@ -252,7 +252,7 @@ Task(
<files_to_read>
- .planning/STATE.md (Project State)
- ./CLAUDE.md (if exists — follow project-specific guidelines)
${DISCUSS_MODE ? '- ' + QUICK_DIR + '/' + next_num + '-CONTEXT.md (User decisions — locked, do not revisit)' : ''}
${DISCUSS_MODE ? '- ' + QUICK_DIR + '/' + quick_id + '-CONTEXT.md (User decisions — locked, do not revisit)' : ''}
</files_to_read>
**Project skills:** Check .claude/skills/ or .agents/skills/ directory (if either exists) — read SKILL.md files, plans should account for project skill rules
@@ -269,7 +269,7 @@ ${FULL_MODE ? '- Each task MUST have `files`, `action`, `verify`, `done` fields'
</constraints>
<output>
Write plan to: ${QUICK_DIR}/${next_num}-PLAN.md
Write plan to: ${QUICK_DIR}/${quick_id}-PLAN.md
Return: ## PLANNING COMPLETE with plan path
</output>
",
@@ -280,11 +280,11 @@ Return: ## PLANNING COMPLETE with plan path
```
After planner returns:
1. Verify plan exists at `${QUICK_DIR}/${next_num}-PLAN.md`
1. Verify plan exists at `${QUICK_DIR}/${quick_id}-PLAN.md`
2. Extract plan count (typically 1 for quick tasks)
3. Report: "Plan created: ${QUICK_DIR}/${next_num}-PLAN.md"
3. Report: "Plan created: ${QUICK_DIR}/${quick_id}-PLAN.md"
If plan not found, error: "Planner failed to create ${next_num}-PLAN.md"
If plan not found, error: "Planner failed to create ${quick_id}-PLAN.md"
---
@@ -309,7 +309,7 @@ Checker prompt:
**Task Description:** ${DESCRIPTION}
<files_to_read>
- ${QUICK_DIR}/${next_num}-PLAN.md (Plan to verify)
- ${QUICK_DIR}/${quick_id}-PLAN.md (Plan to verify)
</files_to_read>
**Scope:** This is a quick task, not a full phase. Skip checks that require a ROADMAP phase goal.
@@ -361,7 +361,7 @@ Revision prompt:
**Mode:** quick-full (revision)
<files_to_read>
- ${QUICK_DIR}/${next_num}-PLAN.md (Existing plan)
- ${QUICK_DIR}/${quick_id}-PLAN.md (Existing plan)
</files_to_read>
**Checker issues:** ${structured_issues_from_checker}
@@ -401,10 +401,10 @@ Spawn gsd-executor with plan reference:
```
Task(
prompt="
Execute quick task ${next_num}.
Execute quick task ${quick_id}.
<files_to_read>
- ${QUICK_DIR}/${next_num}-PLAN.md (Plan)
- ${QUICK_DIR}/${quick_id}-PLAN.md (Plan)
- .planning/STATE.md (Project state)
- ./CLAUDE.md (Project instructions, if exists)
- .claude/skills/ or .agents/skills/ (Project skills, if either exists — list skills, read SKILL.md for each, follow relevant rules during implementation)
@@ -413,7 +413,7 @@ Execute quick task ${next_num}.
<constraints>
- Execute all tasks in the plan
- Commit each task atomically
- Create summary at: ${QUICK_DIR}/${next_num}-SUMMARY.md
- Create summary at: ${QUICK_DIR}/${quick_id}-SUMMARY.md
- Do NOT update ROADMAP.md (quick tasks are separate from planned phases)
</constraints>
",
@@ -424,13 +424,13 @@ Execute quick task ${next_num}.
```
After executor returns:
1. Verify summary exists at `${QUICK_DIR}/${next_num}-SUMMARY.md`
1. Verify summary exists at `${QUICK_DIR}/${quick_id}-SUMMARY.md`
2. Extract commit hash from executor output
3. Report completion status
**Known Claude Code bug (classifyHandoffIfNeeded):** If executor reports "failed" with error `classifyHandoffIfNeeded is not defined`, this is a Claude Code runtime bug — not a real failure. Check if summary file exists and git log shows commits. If so, treat as successful.
If summary not found, error: "Executor failed to create ${next_num}-SUMMARY.md"
If summary not found, error: "Executor failed to create ${quick_id}-SUMMARY.md"
Note: For quick tasks producing multiple plans (rare), spawn executors in parallel waves per execute-phase patterns.
@@ -456,10 +456,10 @@ Task directory: ${QUICK_DIR}
Task goal: ${DESCRIPTION}
<files_to_read>
- ${QUICK_DIR}/${next_num}-PLAN.md (Plan)
- ${QUICK_DIR}/${quick_id}-PLAN.md (Plan)
</files_to_read>
Check must_haves against actual codebase. Create VERIFICATION.md at ${QUICK_DIR}/${next_num}-VERIFICATION.md.",
Check must_haves against actual codebase. Create VERIFICATION.md at ${QUICK_DIR}/${quick_id}-VERIFICATION.md.",
subagent_type="gsd-verifier",
model="{verifier_model}",
description="Verify: ${DESCRIPTION}"
@@ -468,7 +468,7 @@ Check must_haves against actual codebase. Create VERIFICATION.md at ${QUICK_DIR}
Read verification status:
```bash
grep "^status:" "${QUICK_DIR}/${next_num}-VERIFICATION.md" | cut -d: -f2 | tr -d ' '
grep "^status:" "${QUICK_DIR}/${quick_id}-VERIFICATION.md" | cut -d: -f2 | tr -d ' '
```
Store as `$VERIFICATION_STATUS`.
@@ -517,19 +517,19 @@ Use `date` from init:
**If `$FULL_MODE` (or table has Status column):**
```markdown
| ${next_num} | ${DESCRIPTION} | ${date} | ${commit_hash} | ${VERIFICATION_STATUS} | [${next_num}-${slug}](./quick/${next_num}-${slug}/) |
| ${quick_id} | ${DESCRIPTION} | ${date} | ${commit_hash} | ${VERIFICATION_STATUS} | [${quick_id}-${slug}](./quick/${quick_id}-${slug}/) |
```
**If NOT `$FULL_MODE` (and table has no Status column):**
```markdown
| ${next_num} | ${DESCRIPTION} | ${date} | ${commit_hash} | [${next_num}-${slug}](./quick/${next_num}-${slug}/) |
| ${quick_id} | ${DESCRIPTION} | ${date} | ${commit_hash} | [${quick_id}-${slug}](./quick/${quick_id}-${slug}/) |
```
**7d. Update "Last activity" line:**
Use `date` from init:
```
Last activity: ${date} - Completed quick task ${next_num}: ${DESCRIPTION}
Last activity: ${date} - Completed quick task ${quick_id}: ${DESCRIPTION}
```
Use Edit tool to make these changes atomically
@@ -541,14 +541,14 @@ Use Edit tool to make these changes atomically
Stage and commit quick task artifacts:
Build file list:
- `${QUICK_DIR}/${next_num}-PLAN.md`
- `${QUICK_DIR}/${next_num}-SUMMARY.md`
- `${QUICK_DIR}/${quick_id}-PLAN.md`
- `${QUICK_DIR}/${quick_id}-SUMMARY.md`
- `.planning/STATE.md`
- If `$DISCUSS_MODE` and context file exists: `${QUICK_DIR}/${next_num}-CONTEXT.md`
- If `$FULL_MODE` and verification file exists: `${QUICK_DIR}/${next_num}-VERIFICATION.md`
- If `$DISCUSS_MODE` and context file exists: `${QUICK_DIR}/${quick_id}-CONTEXT.md`
- If `$FULL_MODE` and verification file exists: `${QUICK_DIR}/${quick_id}-VERIFICATION.md`
```bash
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(quick-${next_num}): ${DESCRIPTION}" --files ${file_list}
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(quick-${quick_id}): ${DESCRIPTION}" --files ${file_list}
```
Get final commit hash:
@@ -564,10 +564,10 @@ Display completion output:
GSD > QUICK TASK COMPLETE (FULL MODE)
Quick Task ${next_num}: ${DESCRIPTION}
Quick Task ${quick_id}: ${DESCRIPTION}
Summary: ${QUICK_DIR}/${next_num}-SUMMARY.md
Verification: ${QUICK_DIR}/${next_num}-VERIFICATION.md (${VERIFICATION_STATUS})
Summary: ${QUICK_DIR}/${quick_id}-SUMMARY.md
Verification: ${QUICK_DIR}/${quick_id}-VERIFICATION.md (${VERIFICATION_STATUS})
Commit: ${commit_hash}
---
@@ -581,9 +581,9 @@ Ready for next task: /gsd:quick
GSD > QUICK TASK COMPLETE
Quick Task ${next_num}: ${DESCRIPTION}
Quick Task ${quick_id}: ${DESCRIPTION}
Summary: ${QUICK_DIR}/${next_num}-SUMMARY.md
Summary: ${QUICK_DIR}/${quick_id}-SUMMARY.md
Commit: ${commit_hash}
---
@@ -598,13 +598,13 @@ Ready for next task: /gsd:quick
- [ ] User provides task description
- [ ] `--full` and `--discuss` flags parsed from arguments when present
- [ ] Slug generated (lowercase, hyphens, max 40 chars)
- [ ] Next number calculated (001, 002, 003...)
- [ ] Directory created at `.planning/quick/NNN-slug/`
- [ ] (--discuss) Gray areas identified and presented, decisions captured in `${next_num}-CONTEXT.md`
- [ ] `${next_num}-PLAN.md` created by planner (honors CONTEXT.md decisions when --discuss)
- [ ] Quick ID generated (YYMMDD-xxx format, 2s Base36 precision)
- [ ] Directory created at `.planning/quick/YYMMDD-xxx-slug/`
- [ ] (--discuss) Gray areas identified and presented, decisions captured in `${quick_id}-CONTEXT.md`
- [ ] `${quick_id}-PLAN.md` created by planner (honors CONTEXT.md decisions when --discuss)
- [ ] (--full) Plan checker validates plan, revision loop capped at 2
- [ ] `${next_num}-SUMMARY.md` created by executor
- [ ] (--full) `${next_num}-VERIFICATION.md` created by verifier
- [ ] `${quick_id}-SUMMARY.md` created by executor
- [ ] (--full) `${quick_id}-VERIFICATION.md` created by verifier
- [ ] STATE.md updated with quick task row (Status column when --full)
- [ ] Artifacts committed
</success_criteria>

View File

@@ -629,15 +629,28 @@ describe('cmdInitQuick', () => {
cleanup(tmpDir);
});
test('with description generates slug and task_dir', () => {
test('with description generates slug and task_dir with YYMMDD-xxx format', () => {
const result = runGsdTools('init quick "Fix login bug"', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.slug, 'fix-login-bug');
assert.strictEqual(output.next_num, 1);
assert.strictEqual(output.task_dir, '.planning/quick/1-fix-login-bug');
assert.strictEqual(output.description, 'Fix login bug');
// quick_id must match YYMMDD-xxx (6 digits, dash, 3 base36 chars)
assert.ok(/^\d{6}-[0-9a-z]{3}$/.test(output.quick_id),
`quick_id should match YYMMDD-xxx, got: "${output.quick_id}"`);
// task_dir must use the new ID format
assert.ok(output.task_dir.startsWith('.planning/quick/'),
`task_dir should start with .planning/quick/, got: "${output.task_dir}"`);
assert.ok(output.task_dir.endsWith('-fix-login-bug'),
`task_dir should end with -fix-login-bug, got: "${output.task_dir}"`);
assert.ok(/^\.planning\/quick\/\d{6}-[0-9a-z]{3}-fix-login-bug$/.test(output.task_dir),
`task_dir format wrong: "${output.task_dir}"`);
// next_num must NOT be present
assert.ok(!('next_num' in output), 'next_num should not be in output');
});
test('without description returns null slug and task_dir', () => {
@@ -648,19 +661,27 @@ describe('cmdInitQuick', () => {
assert.strictEqual(output.slug, null);
assert.strictEqual(output.task_dir, null);
assert.strictEqual(output.description, null);
assert.strictEqual(output.next_num, 1);
// quick_id is still generated even without description
assert.ok(/^\d{6}-[0-9a-z]{3}$/.test(output.quick_id),
`quick_id should match YYMMDD-xxx, got: "${output.quick_id}"`);
});
test('next number increments from existing entries', () => {
const quickDir = path.join(tmpDir, '.planning', 'quick');
fs.mkdirSync(path.join(quickDir, '1-old-task'), { recursive: true });
fs.mkdirSync(path.join(quickDir, '3-another-task'), { recursive: true });
test('two rapid calls produce different quick_ids (no collision within 2s window)', () => {
// Both calls happen within the same test, which is sub-second.
// They may or may not land in the same 2-second block. We just verify format.
const r1 = runGsdTools('init quick "Task one"', tmpDir);
const r2 = runGsdTools('init quick "Task two"', tmpDir);
assert.ok(r1.success && r2.success);
const result = runGsdTools('init quick "New task"', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const o1 = JSON.parse(r1.output);
const o2 = JSON.parse(r2.output);
const output = JSON.parse(result.output);
assert.strictEqual(output.next_num, 4);
assert.ok(/^\d{6}-[0-9a-z]{3}$/.test(o1.quick_id));
assert.ok(/^\d{6}-[0-9a-z]{3}$/.test(o2.quick_id));
// Directories are distinct because slugs differ
assert.notStrictEqual(o1.task_dir, o2.task_dir);
});
test('long description truncates slug to 40 chars', () => {