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
8 changes: 2 additions & 6 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}
Expand Down
74 changes: 74 additions & 0 deletions internal/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -281,6 +318,43 @@ 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)
}
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) {
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 {
Expand Down
2 changes: 1 addition & 1 deletion memory-bank/ops/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 1 addition & 1 deletion memory-bank/ops/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
4 changes: 2 additions & 2 deletions memory-bank/use-cases/UC-001-adopt-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
Loading