From 2d8c08e6efa1e29a36dec2577159b9abc22cd0f0 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 24 Jul 2026 22:22:07 +0300 Subject: [PATCH 1/3] Allow init to resolve default source --- internal/cli/cli.go | 8 ++--- internal/cli/cli_test.go | 71 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 6 deletions(-) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index cb7f5f3..261ee47 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -197,11 +197,7 @@ func runOwnership(arguments []string, command string, stdout, stderr io.Writer) flags := flag.NewFlagSet("memory-bank-cli "+command, flag.ContinueOnError) flags.SetOutput(stderr) flags.Usage = func() { - if command == "update" { - fmt.Fprintln(stderr, "Usage: memory-bank-cli update [--source DIR --template-version VERSION --source-ref REF] [options]") - } else { - fmt.Fprintln(stderr, "Usage: memory-bank-cli init --source DIR --template-version VERSION --source-ref REF [options]") - } + fmt.Fprintf(stderr, "Usage: memory-bank-cli %s [--source DIR --template-version VERSION --source-ref REF] [options]\n", command) flags.PrintDefaults() } repoRootArgument := addRepoRootFlag(flags) @@ -222,7 +218,7 @@ func runOwnership(arguments []string, command string, stdout, stderr io.Writer) return exitUsage } explicitSource := *sourceRootArgument != "" || *templateVersion != "" || *sourceRef != "" - if (command == "init" && !explicitSource) || (explicitSource && (*sourceRootArgument == "" || *templateVersion == "" || *sourceRef == "")) { + if explicitSource && (*sourceRootArgument == "" || *templateVersion == "" || *sourceRef == "") { fmt.Fprintf(stderr, "memory-bank-cli %s: --source, --template-version, and --source-ref are required\n", command) return exitUsage } diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index 167865b..7c76f36 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -226,6 +226,43 @@ func TestUpdateWithoutSourceUsesRepoUpstreamMain(t *testing.T) { } } +func TestInitWithoutSourceUsesRepoUpstreamMain(t *testing.T) { + repo, source := t.TempDir(), t.TempDir() + if err := os.MkdirAll(filepath.Join(source, "memory-bank", "dna"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(source, "memory-bank", "dna", "rule.md"), []byte("v1\n"), 0o644); err != nil { + t.Fatal(err) + } + ref := commitCLISource(t, source, "initial source") + remote := filepath.Join(t.TempDir(), "upstream.git") + if err := os.MkdirAll(remote, 0o755); err != nil { + t.Fatal(err) + } + runCLIGit(t, remote, "init", "--bare", "--quiet") + runCLIGit(t, source, "branch", "-M", "main") + runCLIGit(t, source, "remote", "add", "origin", remote) + runCLIGit(t, source, "push", "--quiet", "origin", "main") + runCLIGit(t, remote, "symbolic-ref", "HEAD", "refs/heads/main") + checkout := filepath.Join(repo, "memory-bank", ".repo") + if err := os.MkdirAll(filepath.Dir(checkout), 0o755); err != nil { + t.Fatal(err) + } + runCLIGit(t, filepath.Dir(checkout), "clone", "--quiet", remote, ".repo") + + var stdout, stderr bytes.Buffer + if exitCode := Run([]string{"init", "--repo-root", repo}, "test", &stdout, &stderr); exitCode != 0 { + t.Fatalf("default init failed with %d: %s", exitCode, stderr.String()) + } + if got, err := os.ReadFile(filepath.Join(repo, "memory-bank", "dna", "rule.md")); err != nil || string(got) != "v1\n" { + t.Fatalf("default init did not apply main: %q, %v", got, err) + } + lock, err := os.ReadFile(filepath.Join(repo, "memory-bank", ".lock")) + if err != nil || !strings.Contains(string(lock), ref) || !strings.Contains(string(lock), "main@"+ref[:12]) { + t.Fatalf("lock does not record fetched main: %q, %v", lock, err) + } +} + func TestResolveUpdateUpstreamUsesFallbackWithoutRepoCheckout(t *testing.T) { repo, source := t.TempDir(), t.TempDir() if err := os.MkdirAll(filepath.Join(source, "memory-bank"), 0o755); err != nil { @@ -281,6 +318,40 @@ func TestUpdateWithoutSourceRejectsDirtyRepoCheckoutWithoutMutation(t *testing.T } } +func TestInitWithoutSourceRejectsInvalidRepoCheckoutWithoutMutation(t *testing.T) { + repo := t.TempDir() + checkout := filepath.Join(repo, "memory-bank", ".repo") + if err := os.MkdirAll(checkout, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(repo, "sentinel"), []byte("unchanged\n"), 0o644); err != nil { + t.Fatal(err) + } + var stdout, stderr bytes.Buffer + if exitCode := Run([]string{"init", "--repo-root", repo}, "test", &stdout, &stderr); exitCode != 1 || !strings.Contains(stderr.String(), "not a Git checkout") { + t.Fatalf("unexpected exit=%d stderr=%q", exitCode, stderr.String()) + } + if _, err := os.Stat(filepath.Join(repo, "memory-bank", ".lock")); !os.IsNotExist(err) { + t.Fatalf("failed resolution wrote lock: %v", err) + } +} + +func TestInitWithoutSourceRejectsRepoCheckoutWithoutOriginWithoutMutation(t *testing.T) { + repo := t.TempDir() + checkout := filepath.Join(repo, "memory-bank", ".repo") + if err := os.MkdirAll(checkout, 0o755); err != nil { + t.Fatal(err) + } + runCLIGit(t, checkout, "init", "--quiet") + var stdout, stderr bytes.Buffer + if exitCode := Run([]string{"init", "--repo-root", repo}, "test", &stdout, &stderr); exitCode != 1 || !strings.Contains(stderr.String(), "has no usable origin") { + t.Fatalf("unexpected exit=%d stderr=%q", exitCode, stderr.String()) + } + if _, err := os.Stat(filepath.Join(repo, "memory-bank", ".lock")); !os.IsNotExist(err) { + t.Fatalf("failed resolution wrote lock: %v", err) + } +} + func TestDoctorAndAlternativeAgentTarget(t *testing.T) { repo, source := t.TempDir(), t.TempDir() if err := os.MkdirAll(filepath.Join(source, "memory-bank"), 0o755); err != nil { From 2d8d16736831e50a3746e2d7376223967ba6612c Mon Sep 17 00:00:00 2001 From: Danil Pismenny Date: Fri, 24 Jul 2026 22:31:29 +0300 Subject: [PATCH 2/3] feat: allow init without source flags --- internal/cli/cli_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index 7c76f36..ed788c5 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -334,6 +334,9 @@ func TestInitWithoutSourceRejectsInvalidRepoCheckoutWithoutMutation(t *testing.T if _, err := os.Stat(filepath.Join(repo, "memory-bank", ".lock")); !os.IsNotExist(err) { t.Fatalf("failed resolution wrote lock: %v", err) } + if got, err := os.ReadFile(filepath.Join(repo, "sentinel")); err != nil || string(got) != "unchanged\n" { + t.Fatalf("failed resolution changed repository: %q, %v", got, err) + } } func TestInitWithoutSourceRejectsRepoCheckoutWithoutOriginWithoutMutation(t *testing.T) { From 508db305534e20fa42015914ce73b08b0acf7ca5 Mon Sep 17 00:00:00 2001 From: Danil Pismenny Date: Fri, 24 Jul 2026 23:02:12 +0300 Subject: [PATCH 3/3] docs: align init source resolution --- memory-bank/ops/config.md | 2 +- memory-bank/ops/development.md | 2 +- memory-bank/use-cases/UC-001-adopt-template.md | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/memory-bank/ops/config.md b/memory-bank/ops/config.md index 91bd2c2..4e9edeb 100644 --- a/memory-bank/ops/config.md +++ b/memory-bank/ops/config.md @@ -16,7 +16,7 @@ status: active | Repository selection | `--repo-root`; otherwise nearest Git root/current directory is resolved. | | Documentation scope | `lint` and `doctor` use `--scope-root` (default `memory-bank`) and `--max-depth` (default 3). | | Output | `lint`, `doctor`, `init`, `update` support `--json`; lint also has `--version`. | -| Template mutation | `init` requires `--source`, `--template-version`, `--source-ref`. `update` accepts the same reproducible override trio; without it, it fetches `main` from `memory-bank/.repo`'s clean `origin` or `https://github.com/dapi/memory-bank.git`, records its immutable SHA, and accepts `--dry-run` and `--agent-file`. | +| Template mutation | `init` and `update` accept an explicit reproducible `--source`, `--template-version`, `--source-ref` trio. Without it, each fetches `main` from `memory-bank/.repo`'s clean `origin` or `https://github.com/dapi/memory-bank.git`, records its immutable SHA, and accepts `--dry-run` and `--agent-file`. | | Doctor | `--profile` supports `auto`, `template`, `downstream`; `--agent-file` selects one checked instruction file. | There is no confirmed environment-variable, secret, remote endpoint or persistent service configuration. diff --git a/memory-bank/ops/development.md b/memory-bank/ops/development.md index 525aae7..64b3bc6 100644 --- a/memory-bank/ops/development.md +++ b/memory-bank/ops/development.md @@ -20,4 +20,4 @@ go run ./cmd/memory-bank-cli --help go run ./cmd/memory-bank-cli lint --repo-root . ``` -`init` requires a separate clean template checkout, `--template-version`, and `--source-ref` matching its HEAD. `update` accepts the same explicit reproducible inputs, or fetches `main` into a disposable checkout from `memory-bank/.repo`'s clean `origin` (or the default `dapi/memory-bank` upstream). No `.env`, database, service dependency or container workflow is documented. +`init` and `update` accept the same explicit reproducible inputs: a separate clean template checkout, `--template-version`, and `--source-ref` matching its HEAD. Either command may instead fetch `main` into a disposable checkout from `memory-bank/.repo`'s clean `origin` (or the default `dapi/memory-bank` upstream). No `.env`, database, service dependency or container workflow is documented. diff --git a/memory-bank/use-cases/UC-001-adopt-template.md b/memory-bank/use-cases/UC-001-adopt-template.md index fd25cf6..21b7962 100644 --- a/memory-bank/use-cases/UC-001-adopt-template.md +++ b/memory-bank/use-cases/UC-001-adopt-template.md @@ -17,11 +17,11 @@ audience: humans_and_agents ## Trigger and Preconditions -The maintainer needs to adopt a template. A separate clean Git source checkout, its matching full commit SHA, a template version, and a target repository are available. +The maintainer needs to adopt a template. Either a separate clean Git source checkout, its matching full commit SHA, and a template version are available, or the standard upstream is available through `memory-bank/.repo`'s clean `origin` (or the default upstream). ## Main Flow -1. The actor runs `memory-bank-cli init` with source, template version and source ref (optionally using `--dry-run`). During the coordinated payload rename, the pinned source checkout contains exactly one recognized source root: legacy `memory-bank/`, legacy `memory-bank-template/`, or target `template/memory-bank/`; the installed destination remains `memory-bank/`. +1. The actor runs `memory-bank-cli init` (optionally using `--dry-run`), optionally overriding the resolved upstream with source, template version and source ref. During the coordinated payload rename, the pinned source checkout contains exactly one recognized source root: legacy `memory-bank/`, legacy `memory-bank-template/`, or target `template/memory-bank/`; the installed destination remains `memory-bank/`. 2. The CLI validates source/repository boundaries and pins the source. 3. It builds ownership decisions for template payload and configured agent instruction file. 4. On a non-dry successful run, it applies the plan atomically and records `memory-bank/.lock`.