Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Design

> **Pick up new gitignore lines without --force** · `2026-10-19-pick-up-new-gitignore-lines-without-force`

The sentences in `feature.md` are the contract.

## Ignore repair

`packages/core/src/init.ts` exports `repairRequiredIgnores(projectRoot)`. It loads the existing config and does not write it. It appends missing lines to `.sdd/.gitignore` and to `<changes_path>/.gitignore` through the current `ensureIgnoreEntries` helper. It returns true when a line was added.

When `initProject` sees an existing config and `force` is false, it calls that function and returns `{ created, config, kept: [], warnings: [] }`. It does not merge defaults, save config, or reconcile workflows.

## CLI

`runSpeckitStyleInit` in `packages/cli/src/init-flow.ts`, when the project is initialized, `--force` is absent, and the session is not interactive (`--no-agent` counts as not interactive), calls `repairRequiredIgnores` and returns. It does not call `initProject` with `force: true`. Stdout contains `Added missing ignore lines.` or `Ignore lines are already present.`

The interactive confirm path is unchanged.

## Docs and tests

Update the second-init sentences in `docs/reference/cli.md`, `docs/reference/layout.md`, and `docs/reference/agents.md`.

Replace the core test that expects a throw and an unchanged ignore file. Update the CLI test that expects exit 1 on a second `sdd init --here --no-agent`. Keep the `--force` preservation test.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Feature

> **Pick up new gitignore lines without --force** · `2026-10-19-pick-up-new-gitignore-lines-without-force` · 2026-10-19

Issue #45. Epic `issue-37`, story `S-001`. This story does not close #37.

## Problem / opportunity

A project that already has `.sdd/config.yaml` cannot pick up a new required ignore line. Non-interactive `sdd init` stops in `maybeConfirmNonEmpty` before `initProject` runs. `initProject` itself also throws at the existing-config check when `force` is false, and `ensureIgnoreEntries` (including `role-runs/`) runs only after that check. `--force` appends the line, and it also merges config defaults and refreshes unchanged generated workflows and templates.

## Users & value

A person who upgrades `sdd` in a repo that is already initialized. They should get new ignore lines, including `role-runs/`, without a full re-init.

## Scope

Non-interactive `sdd init` on an existing project, without `--force`, appends missing required lines and exits 0. Required lines are the `.sdd/.gitignore` list (`handoff.md`, `active-context.md`, `index.db`, `index.db-journal`, `index.db-wal`, `index.db-shm`, `models/`, `role-runs/`) and `<changes_path>/.gitignore` line `.active`. A missing ignore file is created. Existing lines and comments stay. The command does not rewrite `.sdd/config.yaml`, memory, workflows, templates, protocol, or other generated files.

Stdout contains one sentence. When a line was added: `Added missing ignore lines.` When every required line is already present: `Ignore lines are already present.` The setup intro may also be printed. The command does not print the already-initialized error.

`initProject` with `force: false` and an existing config does the same ignore update and returns. It does not throw `Already initialized`.

`--force` stays the full refresh. An interactive re-init prompt is unchanged: declining writes nothing; confirming still refreshes unchanged generated files.

`docs/reference/cli.md`, `docs/reference/layout.md`, and `docs/reference/agents.md` say that a second init without `--force` adds missing ignore lines and does not refresh workflows. The old sentence that a second init writes nothing is removed.

## Non-goals

- Do not change #36. Do not refresh an edited workflow or template without `--force`.
- Do not merge config defaults on this path.
- Do not close #37.
- Do not implement #46 or #47 in this story.

## Success criteria

- [x] A project whose `.sdd/.gitignore` ends before `role-runs/` gains that line from `sdd init --here --no-agent` with no `--force`. Exit code is 0. Config and an edited workflow file are byte-for-byte unchanged.
- [x] A second run, when every required line is present, exits 0 with `Ignore lines are already present.` and does not duplicate lines.
- [x] `initProject({ force: false })` on an existing project appends the missing line and does not throw.
- [x] `sdd init --force` still keeps an edited workflow and a custom `changes_path`.
- [x] Declining the interactive re-init prompt writes nothing.

## Risks / open questions

The old #15 test expects exit 1 and the already-initialized explanation. This story replaces that stop for the non-interactive path. The `--force` preservation test stays.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Local test plan

> **Pick up new gitignore lines without --force** · `2026-10-19-pick-up-new-gitignore-lines-without-force`

Node v20.19.3. Branch `story/issue-37/S-001` from main `cb20443`. The `EBADENGINE` warning for `engines.node >= 24` is expected.

| # | Check | Expected |
|---|--------|----------|
| 1 | `initProject` without `force` after `.sdd/.gitignore` is replaced with `extra` | File starts with `extra`, contains `role-runs/`, and a second call does not change it. Config and `feature.yaml` stay byte-identical. |
| 2 | Custom `changes_path: .sdd/changes` and a changes ignore of `extra` | That file becomes `extra` plus `.active`. Config stays the edited text. The default `changes/.gitignore` stays `.active`. |
| 3 | Second `sdd init --here --no-agent` | Exit 0. Stdout contains `Added missing ignore lines.` Config and an edited workflow stay byte-identical. A third run prints `Ignore lines are already present.` and does not change the ignore file. |
| 4 | `sdd init --here --no-agent --force` with a custom `changes_path` and edited workflow and template | Those edits stay. The command still lists the kept files. |
| 5 | Interactive decline | The decline branch in `maybeConfirmNonEmpty` is unchanged and exits before a write. No TTY test. |
| 6 | `pnpm test` | Pass. |
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Local test results

> **Pick up new gitignore lines without --force** · `2026-10-19-pick-up-new-gitignore-lines-without-force`

| # | Result |
|---|--------|
| 1 | PASS. `init-force.test.ts` appends `role-runs/` after `extra` and leaves config and `feature.yaml` unchanged. A second call keeps the same ignore bytes. |
| 2 | PASS. Custom `changes_path: .sdd/changes` gains `.active` under `extra`. Config stays edited. Default `changes/.gitignore` stays `.active`. |
| 3 | PASS. CLI test `re-running init --no-agent adds missing ignore lines and leaves config alone (#45)` passed inside `pnpm test`. |
| 4 | PASS. `pnpm test` includes `init --force preserves custom config paths and lists edited workflows/templates (#15)` and it passed. |
| 5 | Waived. The decline branch still calls `p.cancel` and `process.exit(0)` before any write. This story did not edit that branch, and there is no TTY harness. |
| 6 | PASS. `sdd verify --no-agent` ran `pnpm test` and exited 0. |

---

## Latest run (2026-10-19T12:14:39.495Z)

> Overall: PASS
> Stage: local_verify
<!-- sdd:verify stage="local_verify" head_sha="03f7da8dc931bd299bcf6aab4968782a7e1731ec" result_count="1" -->

- **Workspace tests**: exit 0 (`pnpm test`)
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
id: 2026-10-19-pick-up-new-gitignore-lines-without-force
title: Pick up new gitignore lines without --force
workflow: feature
created: 2026-10-19T11:52:36.779Z
updated: 2026-10-19T12:19:02.351Z
status: in_progress
stage: local_verify
branch: story/issue-37/S-001
epic: issue-37
story: S-001
depends: []
flags: {}
overrides:
skip_stages:
- clarify_intent
- brainstorm
- clarify_design
- clarify_tasks
gates: {}
extra_stages: []
skipped:
- stage: clarify_intent
reason: the approved spec has no open product choice
at: 2026-10-19T12:00:27.871Z
- stage: brainstorm
reason: one approach is enough
at: 2026-10-19T12:00:28.613Z
- stage: clarify_design
reason: the approved design has no open product choice
at: 2026-10-19T12:00:30.171Z
- stage: clarify_tasks
reason: the approved design is the task list
at: 2026-10-19T12:00:31.738Z
gates: {}
verify_results:
local_verify:
ok: true
status: pass
head_sha: 03f7da8dc931bd299bcf6aab4968782a7e1731ec
at: 2026-10-19T12:14:39.495Z
results:
- name: Workspace tests
exitCode: 0
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"version": 1,
"phases": [
{
"phase": "intent",
"startedAt": "2026-10-19T11:52:36.779Z",
"endedAt": "2026-10-19T12:00:27.032Z"
},
{
"phase": "clarify_intent",
"startedAt": "2026-10-19T12:00:27.032Z",
"endedAt": "2026-10-19T12:00:27.872Z"
},
{
"phase": "brainstorm",
"startedAt": "2026-10-19T12:00:27.872Z",
"endedAt": "2026-10-19T12:00:28.614Z"
},
{
"phase": "design",
"startedAt": "2026-10-19T12:00:28.614Z",
"endedAt": "2026-10-19T12:00:29.335Z"
},
{
"phase": "clarify_design",
"startedAt": "2026-10-19T12:00:29.335Z",
"endedAt": "2026-10-19T12:00:30.172Z"
},
{
"phase": "tasks",
"startedAt": "2026-10-19T12:00:30.172Z",
"endedAt": "2026-10-19T12:00:30.917Z"
},
{
"phase": "clarify_tasks",
"startedAt": "2026-10-19T12:00:30.917Z",
"endedAt": "2026-10-19T12:00:31.739Z"
},
{
"phase": "implement",
"startedAt": "2026-10-19T12:00:31.739Z",
"endedAt": "2026-10-19T12:13:21.018Z"
},
{
"phase": "local_verify",
"startedAt": "2026-10-19T12:13:21.018Z"
}
],
"requests": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Tasks

- [x] Add `repairRequiredIgnores` and return it from the non-force existing-config path in `initProject`.
- [x] Make non-interactive `sdd init` call that repair and return before the already-initialized error and before `force || already`.
- [x] Update the core and CLI tests for the new exit and the unchanged config and workflow.
- [x] Update `docs/reference/cli.md`, `layout.md`, and `agents.md`.
2 changes: 1 addition & 1 deletion docs/reference/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ This protection covers **`AGENTS.md`, host agent files and folders, `.sdd/protoc
- If you already have an `AGENTS.md`, sdd leaves it alone and prints a short section you can paste into it.
- Files from sdd versions older than this marker count as unmarked. To regenerate one, delete it and re-run.

**Project state under `.sdd/`:** If `.sdd/config.yaml` already exists, non-interactive `sdd init` without `--force` stops and writes nothing, so it does not repair gitignore. On a terminal, confirming the re-init prompt continues and behaves like `--force`. `--force` merges current defaults into that config. It refreshes a workflow, template, protocol, or README only while the file is still an unchanged sdd-generated file. Edited files, and files sdd did not generate, stay as they are even on a first init. `memory/` is never replaced. Host install rewrites `.sdd/agents.json` when the file is missing or when that install runs with force (the `--force` flag, or a confirmed terminal re-init). `sdd init --no-agent` does not write `.sdd/agents.json`. Init refreshes `.sdd/active-context.md`. A fresh init writes `.sdd/.gitignore` for `handoff.md` and `active-context.md`, and `<changes_path>/.gitignore` for `.active`. `--force` appends any of those lines that are missing and leaves the rest of the file in place.
**Project state under `.sdd/`:** If `.sdd/config.yaml` already exists, non-interactive `sdd init` without `--force` appends missing ignore lines and does not rewrite config, memory, or workflows. On a terminal, confirming the re-init prompt continues and behaves like `--force`. `--force` merges current defaults into that config. It refreshes a workflow, template, protocol, or README only while the file is still an unchanged sdd-generated file. Edited files, and files sdd did not generate, stay as they are even on a first init. `memory/` is never replaced. Host install rewrites `.sdd/agents.json` when the file is missing or when that install runs with force (the `--force` flag, or a confirmed terminal re-init). `sdd init --no-agent` does not write `.sdd/agents.json`. A fresh init, and a confirmed or `--force` re-init, refreshes `.sdd/active-context.md`. A fresh init writes `.sdd/.gitignore` for `handoff.md` and `active-context.md`, and `<changes_path>/.gitignore` for `.active`. `--force` appends any of those lines that are missing and leaves the rest of the file in place.

## Agent roles (stubs)

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Install: **[@structured-vibe-coding/cli on npm](https://www.npmjs.com/package/@s
Learn by doing: [Start in 10 minutes](../tutorials/first-change).
One command’s flags: `sdd <command> --help`.

Commit `.sdd/config.yaml`, workflows, templates, `protocol.md`, `memory/`, and change packs. Ignore `handoff.md`, `active-context.md`, `index.db`, `index.db-journal`, `index.db-wal`, `index.db-shm`, `models/`, and `<changes_path>/.active`. `sdd init` writes those gitignore rules. On a project that already has `.sdd/config.yaml`, `sdd init --force` appends any missing ignore line and does not replace the file. `sdd status` prints the change path relative to the repo.
Commit `.sdd/config.yaml`, workflows, templates, `protocol.md`, `memory/`, and change packs. Ignore `handoff.md`, `active-context.md`, `index.db`, `index.db-journal`, `index.db-wal`, `index.db-shm`, `models/`, `role-runs/`, and `<changes_path>/.active`. `sdd init` writes those gitignore rules. On a project that already has `.sdd/config.yaml`, `sdd init` without `--force` appends any missing ignore line and does not rewrite config, memory, or workflows. `sdd init --force` still refreshes unchanged generated files. `sdd status` prints the change path relative to the repo.

---

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ AGENTS.md ← tiny pointer for AIs

Commit the shared process: `.sdd/config.yaml`, `.sdd/workflows/`, `.sdd/templates/`, `.sdd/protocol.md`, `memory/`, and each change pack under the configured changes directory.

Ignore per-machine files. Init writes `.sdd/.gitignore` for `handoff.md` and `active-context.md`, and `<changes_path>/.gitignore` for `.active` (by default `changes/.active`). `.active` is the live change pointer. `handoff.md` and `active-context.md` are a repo-relative handoff and task brief. Commands that move or launch work rewrite them; they do not change on every command. If git already tracks one of them, init warns and leaves the index alone. A second non-interactive `sdd init` without `--force` does not repair gitignore when `.sdd/config.yaml` already exists. Confirming the re-init prompt in a terminal behaves like `--force`. Run `sdd init --force` to append any missing ignore lines without replacing the rest of the file. `.sdd/prices.yaml` is committed project config, written by init when missing, and not regenerated over a user edit.
Ignore per-machine files. Init writes `.sdd/.gitignore` for `handoff.md` and `active-context.md`, and `<changes_path>/.gitignore` for `.active` (by default `changes/.active`). `.active` is the live change pointer. `handoff.md` and `active-context.md` are a repo-relative handoff and task brief. Commands that move or launch work rewrite them; they do not change on every command. If git already tracks one of them, init warns and leaves the index alone. A second non-interactive `sdd init` without `--force` appends missing ignore lines, including `role-runs/`, and does not rewrite config, memory, or workflows. Confirming the re-init prompt in a terminal still behaves like `--force`. `sdd init --force` appends any missing ignore lines and refreshes unchanged generated files. `.sdd/prices.yaml` is committed project config, written by init when missing, and not regenerated over a user edit.

## Keep project data together

Expand Down
25 changes: 18 additions & 7 deletions packages/cli/src/__tests__/cli.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -675,17 +675,28 @@ stages:
expect(await exists(join(root, "AGENTS.md"))).toBe(false);
});

it("re-running init --no-agent explains safe --force behavior (#15)", async () => {
it("re-running init --no-agent adds missing ignore lines and leaves config alone (#45)", async () => {
const root = await mkdtemp(join(tmpdir(), "sdd-cli-noagent-reinit-"));
temps.push(root);
expect(runSdd(root, ["init", "--here", "--no-agent"]).status).toBe(0);
const configPath = join(root, ".sdd/config.yaml");
const workflowPath = join(root, ".sdd/workflows/feature.yaml");
const ignorePath = join(root, ".sdd/.gitignore");
const configBefore = await readFile(configPath, "utf8");
const workflowBefore = await readFile(workflowPath, "utf8");
await writeFile(ignorePath, "extra\n");
const again = runSdd(root, ["init", "--here", "--no-agent"]);
expect(again.status).toBe(1);
const out = again.stderr + again.stdout;
expect(out).toContain("already initialized");
expect(out).toContain("preserves supported config values");
expect(out).toContain("refreshes only unchanged SDD-generated workflows/templates");
expect(out).toContain("edited or untracked files are kept");
expect(again.status, again.stderr + again.stdout).toBe(0);
expect(again.stdout).toContain("Added missing ignore lines.");
const ignore = await readFile(ignorePath, "utf8");
expect(ignore).toContain("role-runs/");
expect(ignore.startsWith("extra\n")).toBe(true);
expect(await readFile(configPath, "utf8")).toBe(configBefore);
expect(await readFile(workflowPath, "utf8")).toBe(workflowBefore);
const third = runSdd(root, ["init", "--here", "--no-agent"]);
expect(third.status, third.stderr + third.stdout).toBe(0);
expect(third.stdout).toContain("Ignore lines are already present.");
expect(await readFile(ignorePath, "utf8")).toBe(ignore);
});

it("init --force preserves custom config paths and lists edited workflows/templates (#15)", async () => {
Expand Down
7 changes: 7 additions & 0 deletions packages/cli/src/init-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
optionForTarget,
parseIntegration,
refreshActiveAgentContext,
repairRequiredIgnores,
} from "@structured-vibe-coding/core";
import pc from "picocolors";

Expand Down Expand Up @@ -273,6 +274,12 @@ export async function runSpeckitStyleInit(args: InitCliArgs): Promise<void> {
const { projectRoot, here } = await resolveProjectPath(args);
const force = Boolean(args.force);

if (!force && (await isInitialized(projectRoot)) && !isInteractive()) {
const added = await repairRequiredIgnores(projectRoot);
console.log(added ? "Added missing ignore lines." : "Ignore lines are already present.");
return;
}

await maybeConfirmNonEmpty(projectRoot, force, here);

if (args.noAgent) {
Expand Down
Loading
Loading