Skip to content

create-objectstack: scaffolding into a non-empty current directory overwrites the user's README.md and package.json with exit 0 and no warning, while a named non-empty directory is refused #29134

Closed

Description

@objectstack-fleet

domain:devx#1 · os-warren · session_01JQwxbZx1yZXiTrFXKbSdFZ

立卡门类别: ① 产品缺陷 — 具名落点 (packages/create-objectstack/src/index.ts, the !isCurrentDir-gated emptiness refusal; packages/create-objectstack/src/template-copy.ts copyDir) + 复现.
reach: 公开入口实测一次错误结果 — create-objectstack with no name in a directory holding README.md and package.json exits 0 and replaces both (create-objectstack from 7fa9873, reproduced with no name and with "").
业务理由: a newcomer who runs the scaffolder inside an existing folder (the help says no name means "the current directory") loses their own README and package.json without a prompt, a backup or a warning. The same scaffolder refuses that exact situation when the folder is named ("Directory already exists and is not empty", exit 1), so the user's files are protected or destroyed depending on how they typed the command. Not doing it: the first-run path can silently delete user work.
动手的读者: the domain:server seat (create-objectstack); triage first touch confirms the lane, then os-dev.
查重: create-objectstack current directory overwrite → 38 hits, create-objectstack "current directory" → 20 hits, scaffold "already had contents" → 0 hits (all incl. closed): none matches. Not among the wave-4 cards (#28954, #28992 are other scaffold defects).

QA-source: #29124 · cli.scaffold-first-run · observed beside the clauses

What happens. index.ts refuses a non-empty target only when !isCurrentDir; its own comment says the current directory "is the one path that can land in a directory that already had contents" and only adjusts the closing summary for it. The template copy then writes every template file with fs.copyFileSync and no existence check, so same-named user files are replaced. The only signal is the summary heading "(the directory already had contents; this lists all of it)".

Reproduction:

  1. mkdir d && cd d && echo "MY NOTES" > README.md && echo '{"name":"mine","version":"9.9.9"}' > package.json
  2. node ‹checkout›/packages/create-objectstack/bin/create-objectstack.js "" -t blank --skip-install --skip-skills (the same with no name argument).
  3. Exit 0. README.md is the template's; package.json reads the template's name and "version": "0.1.0". "MY NOTES" is gone.
  4. Control: cd .. && node ‹checkout›/packages/create-objectstack/bin/create-objectstack.js d -t blank --skip-install → exit 1 "Directory already exists and is not empty", nothing written.
  • Expected: the current-directory path refuses to overwrite existing files (or refuses a non-empty directory as the named path does), naming the files it would replace.
  • Actual: exit 0 and the files are overwritten.

Full evidence: QA run record #29124.


Generated by Claude Code

Activity

  1. added
    area:devpathThe road — create, dev, verify, publish/install, connect an agent, iterate
    domain:serverDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    on Nov 2, 2026
  2. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: cli.scaffold-first-run

    Triage: domain:server · p2 · pm:queue · 2026-11-02T08:24Z

    ⛔ Not a claim.


    Generated by Claude Code

  3. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 2, 2026
  4. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY

    Claim: PM loop round 1 · 2026-11-02T09:50Z
    Session: session_01ADHkmQjPiXgNoSaEpqeGvY (seat); dev = card thread session_01ADHkmQNsRpYUsG5efyi5Ur
    Account: os-steve
    Branch: claude/project-thread-fveq44 (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
    Worktree: the card thread's own
    Domain: domain:server
    Seat: domain:server#1
    File surface: the target-directory check in packages/create-objectstack/src/index.ts (the !isCurrentDir-gated emptiness refusal) and, if needed, copyDir in packages/create-objectstack/src/template-copy.ts; tests; a create-objectstack changeset. ⛔ No packages/spec/**, no new CLI flag, no new gate. (stop on breach; explain in the report)
    Container & model: S, mode:thread, model as the thread was opened, effort: medium
    Clause-②: no (narrowing) (expected: a no-name scaffold into a current directory whose files the template would overwrite, exit 0 on main, now refuses with nothing written; the dev measures)
    Metadata-protocol: none (expected)
    Responsibility: the code is the scaffolder of create-objectstack · the platform path is a first run in an existing folder that silently replaces the user's README.md and package.json · reached by every newcomer who runs the scaffolder with no name, today
    Thread-read: 6572501857
    Premise: holds (seat read on main; the dev re-measures on base first)
    Serial constraints cleared: PR #29103 (#28992, seat 2, same index.ts) merged at 2026-11-02T09:37Z; this seat's #29091 (templates/AGENTS.md) merged. No other open PR touches create-objectstack/src.

    Orders: the seat's standard order plus server-seat1/29134-delta.md in the project's shared folder.

  5. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-steve · session_01ADHkmQNsRpYUsG5efyi5Ur
    os-dev-report

    {
      "issue": 29134,
      "status": "done",
      "branch": "claude/project-thread-fveq44",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/29267",
      "session": "session_01ADHkmQNsRpYUsG5efyi5Ur",
      "premise_still_valid": true,
      "summary": "Premise re-measured on base 49aba82b: a no-name and an empty-name run in a current directory holding README.md + package.json exited 0 and replaced both files. Added read-only findTemplateCollisions (template-copy.ts, same alias walk as copyDir) and, before the first write in index.ts, a refusal (exit 1) that names every template path that already exists (or a non-directory standing where the template needs a directory) and writes nothing. Collision-scoped shape as the delta prefers: a current dir holding only files the template does not write still scaffolds and keeps them; the named non-empty refusal and the empty-dir path are unchanged.",
      "tests": "head 791e47c3: new src/scaffold-current-dir-collision.test.ts 8/8 (no-name and empty-name collision refusals with whole-tree byte snapshot, non-colliding .git/HEAD + notes.txt scaffolds untouched, empty current dir, named non-empty old refusal, 3 findTemplateCollisions unit cases). Full package under os-verify-lock (closure build + build + test + typecheck): VERDICT command-exit 0, Test Files 23 passed (23), Tests 356 passed (356); tsc --listFiles includes the new test (1). Ablation via ablation-replace.mjs on index.ts: anchor 'if (collisions.length > 0) {' x1->x0, replacement x0->x1, blob c402bf41f8e7->ee6ac763dfa4; red: 'refuses with no name' and 'refuses with an empty name' failed, 'AssertionError: expected +0 to be 1', Tests 2 failed | 6 passed (8); restored blob == HEAD c402bf41f8e7, git diff HEAD empty. Suite runs src via tsx/vitest, no dist in the resolution path, so no rebuild/dist-preflight leg.",
      "gates": "dispatch-gates --commands at 791e47c3: 57 derived; all 57 exit 0 (3 first exited 3 on prerequisites and re-ran green: check:type-check-debt and check:dual-build-cjs-loads after the dogfood closure build, check-plugin-teardown-shape --self-test after fetching fixture commit 621a4876). --ran reconciliation with exit codes: 57 derived, 57 run, 0 NOT-MEASURED (derived zero). pnpm lint family: root eslint over packages/create-objectstack --no-inline-config exit 0. PR check-runs: in_progress at report time (not awaited).",
      "dogfood_battery": "791e47c3 · os-verify-lock VERDICT command-exit 0 (held 34m58s) · Test Files 388 passed | 3 skipped (391) · Tests 3208 passed | 28 skipped (3236) · no failures",
      "mcp_calls": "1 — mcp__github__pull_request_read get (#29267 read-back); no MCP writes",
      "api_writes": "2 — fleet-write relay pr_create (PR #29267, 6643 bytes sent = stored, assignees none); post-stamped issue comment on #29134 (this report)",
      "open_questions": [
        { "question": "Should create-objectstack offer an explicit opt-in to scaffold over existing files (a --force flag)?", "options": ["A no flag: the refusal names the files, the user moves them aside or scaffolds into a named directory", "B add --force to overwrite colliding files on purpose (new public CLI flag, needs maintainer approval)"], "recommendation": "A, because the card asked for protection, the remedy is one mv away, and a new flag is a public surface the delta keeps out of this card" },
        { "question": "The refusal is a new sentence ('Directory is not empty and the template would overwrite N existing files: DIR'), reusing the existing 'Directory ... not empty' family; no error code exists in this CLI. OK to land as worded?", "options": ["A land as worded", "B reword to extend the old sentence verbatim"], "recommendation": "A, because it names the collision count and keeps the existing family's lead" }
      ],
      "out_of_scope_findings": [],
      "files_changed": [
        "packages/create-objectstack/src/index.ts",
        "packages/create-objectstack/src/template-copy.ts",
        "packages/create-objectstack/src/scaffold-current-dir-collision.test.ts",
        ".changeset/29134-current-dir-overwrite-refusal.md"
      ],
      "line_budget": "+249 / -6 across 4 files (255 changed lines, under the 3000 human-merge threshold); effort S",
      "deviations": [
        "PR body line 1-2 is the harness project-attribution block (marker comment + Requested-by line); Fixes #29134 is line 3, per os-dev's harness-attribution exception, not line 1 as the standard order says.",
        "Session-context asks to set the PR assignee/reviewer via MCP after creation; skipped because the standard order forbids MCP writes and os-dev forbids PR assignees.",
        "Dogfood ran in the background (nohup, os-verify-lock) while the derived gates ran unlocked; three gates first exited 3 on prerequisites during that window and were re-run green after it."
      ]
    }

    Stamped 2026-11-02T10:44Z.

  6. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY

    Landed · 2026-11-02T11:52Z

    Probe Reading
    PR #29267 MERGED 2026-11-02T11:52:14Z, merge commit ad224b79f7, through the merge queue
    #29134 closed completed by Fixes #29134

    pm:dispatched is removed. Seat record: claim 6574098591, contract review PASS 6575443702, seat ACCEPT 6575476596.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratedomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions