From 75d21866c8d616305ffb9214b2d1ffba56e9f4dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=97=AB=E5=86=B0?= Date: Fri, 13 Mar 2026 01:35:59 +0800 Subject: [PATCH] feat(quick): replace auto-increment number with YYMMDD-xxx timestamp ID (#1012) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Co-authored-by: Claude Opus 4.6 --- get-shit-done/bin/lib/init.cjs | 28 +++++------ get-shit-done/workflows/quick.md | 80 ++++++++++++++++---------------- tests/init.test.cjs | 45 +++++++++++++----- 3 files changed, 87 insertions(+), 66 deletions(-) diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs index 7e551a01f..70ffed8b7 100644 --- a/get-shit-done/bin/lib/init.cjs +++ b/get-shit-done/bin/lib/init.cjs @@ -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'), diff --git a/get-shit-done/workflows/quick.md b/get-shit-done/workflows/quick.md index e0251d74e..4c4ebc4bb 100644 --- a/get-shit-done/workflows/quick.md +++ b/get-shit-done/workflows/quick.md @@ -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 `` and `` sections (no codebase scouting, no phase scope to defer to). Keep it lean. The `` 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( - .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)' : ''} **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' -Write plan to: ${QUICK_DIR}/${next_num}-PLAN.md +Write plan to: ${QUICK_DIR}/${quick_id}-PLAN.md Return: ## PLANNING COMPLETE with plan path ", @@ -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} -- ${QUICK_DIR}/${next_num}-PLAN.md (Plan to verify) +- ${QUICK_DIR}/${quick_id}-PLAN.md (Plan to verify) **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) -- ${QUICK_DIR}/${next_num}-PLAN.md (Existing plan) +- ${QUICK_DIR}/${quick_id}-PLAN.md (Existing plan) **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}. -- ${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}. - 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) ", @@ -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} -- ${QUICK_DIR}/${next_num}-PLAN.md (Plan) +- ${QUICK_DIR}/${quick_id}-PLAN.md (Plan) -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 diff --git a/tests/init.test.cjs b/tests/init.test.cjs index 13af9efd8..61a011eaa 100644 --- a/tests/init.test.cjs +++ b/tests/init.test.cjs @@ -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', () => {