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
121 changes: 64 additions & 57 deletions .agent/CODEBASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,19 @@ _Generated by `gen-codemap.sh`. Re-run after structural changes._
| | |-- projectmgmt
| | |-- registry.go
| | `-- registry_test.go
| |-- gitconfig
| | |-- gitconfig.go
| | `-- gitconfig_test.go
| |-- gitignore
| | |-- gitignore.go
| | `-- gitignore_test.go
| |-- scaffold
| | |-- color_test.go
| | |-- scaffold.go
| | `-- scaffold_test.go
| | |-- scaffold_test.go
| | |-- term_darwin.go
| | |-- term_linux.go
| | `-- term_other.go
| |-- testflags
| | `-- testflags.go
| `-- trackers
Expand Down Expand Up @@ -82,9 +89,9 @@ _Generated by `gen-codemap.sh`. Re-run after structural changes._
## Public API surface

### Go
internal/cli/cli.go:52:type Version struct {
internal/cli/cli.go:135:type App struct {
internal/cli/cli.go:143:func New(out, errOut io.Writer, version Version) App {
internal/cli/cli.go:66:type Version struct {
internal/cli/cli.go:152:type App struct {
internal/cli/cli.go:160:func New(out, errOut io.Writer, version Version) App {
internal/cli/cli_test.go:15:func TestListFlavors(t *testing.T) {
internal/cli/cli_test.go:28:func TestInitLegacyTargetArgument(t *testing.T) {
internal/cli/cli_test.go:47:func TestInitLegacyTargetArgumentWithoutFlag(t *testing.T) {
Expand All @@ -97,25 +104,35 @@ internal/cli/cli_test.go:149:func TestInitVisibilityLocalAppendsGitignoreBlock(t
internal/cli/cli_test.go:176:func TestInitVisibilityLocalIsIdempotent(t *testing.T) {
internal/cli/cli_test.go:197:func TestInitVisibilitySharedLeavesNoGitignore(t *testing.T) {
internal/cli/cli_test.go:214:func TestInitVisibilityLocalRejectedOnDocCollabFlavor(t *testing.T) {
internal/cli/cli_test.go:227:func TestInitVisibilityUnimplementedModesRejected(t *testing.T) {
internal/cli/cli_test.go:244:func TestInitVisibilityUnknownValueRejected(t *testing.T) {
internal/cli/cli_test.go:256:func TestInitVisibilityLocalDryRunWritesNothing(t *testing.T) {
internal/cli/cli_test.go:273:func TestRejectsUnknownCommandTypo(t *testing.T) {
internal/cli/cli_test.go:283:func TestListTrackers(t *testing.T) {
internal/cli/cli_test.go:299:func TestAddTrackerWritesFilesAndMergesMCP(t *testing.T) {
internal/cli/cli_test.go:330:func TestAddTrackerShipsNoSecretLiteralAndGuidesCredentials(t *testing.T) {
internal/cli/cli_test.go:380:func TestAddTrackerIsIdempotent(t *testing.T) {
internal/cli/cli_test.go:402:func TestAddTrackerMultipleCoexist(t *testing.T) {
internal/cli/cli_test.go:426:func TestAddTrackerRejectsMissingScaffold(t *testing.T) {
internal/cli/cli_test.go:440:func TestAddTrackerRejectsUnknownTracker(t *testing.T) {
internal/cli/cli_test.go:453:func TestTopLevelHelpListsAllSubcommands(t *testing.T) {
internal/cli/cli_test.go:480:func TestSubcommandHelpPrintsFlagsAndExamples(t *testing.T) {
internal/cli/cli_test.go:521:func TestFlaglessSubcommandHelp(t *testing.T) {
internal/cli/cli_test.go:539:func TestUnknownCommandErrorPointsAtHelp(t *testing.T) {
internal/cli/cli_test.go:551:func TestUnknownFlavorErrorPointsAtHelp(t *testing.T) {
internal/cli/cli_test.go:567:func TestHelpFlagsMatchDocs(t *testing.T) {
internal/cli/cli_test.go:594:func TestVersion(t *testing.T) {
internal/cli/cli_test.go:607:func TestVersionDefaultsToDev(t *testing.T) {
internal/cli/cli_test.go:237:func TestInitVisibilityHiddenWritesGitInfoExclude(t *testing.T) {
internal/cli/cli_test.go:272:func TestInitPrivateAliasMatchesHidden(t *testing.T) {
internal/cli/cli_test.go:298:func TestInitPrivateConflictsWithVisibility(t *testing.T) {
internal/cli/cli_test.go:326:func TestInitVisibilityHiddenIsIdempotent(t *testing.T) {
internal/cli/cli_test.go:361:func TestInitVisibilityGlobalDefaultWritesExcludesAndWarns(t *testing.T) {
internal/cli/cli_test.go:395:func TestInitVisibilityGlobalDefaultIsIdempotent(t *testing.T) {
internal/cli/cli_test.go:415:func TestInitVisibilityHiddenRejectedOnDocCollabFlavor(t *testing.T) {
internal/cli/cli_test.go:428:func TestInitPrivateRejectedOnDocCollabFlavor(t *testing.T) {
internal/cli/cli_test.go:441:func TestInitVisibilityHiddenDryRunWritesNothing(t *testing.T) {
internal/cli/cli_test.go:458:func TestInitVisibilityGlobalDefaultDryRunWritesNothing(t *testing.T) {
internal/cli/cli_test.go:479:func TestInitVisibilityGlobalDefaultRejectedOnDocCollabFlavor(t *testing.T) {
internal/cli/cli_test.go:492:func TestInitVisibilityUnknownValueRejected(t *testing.T) {
internal/cli/cli_test.go:504:func TestInitVisibilityLocalDryRunWritesNothing(t *testing.T) {
internal/cli/cli_test.go:521:func TestRejectsUnknownCommandTypo(t *testing.T) {
internal/cli/cli_test.go:531:func TestListTrackers(t *testing.T) {
internal/cli/cli_test.go:547:func TestAddTrackerWritesFilesAndMergesMCP(t *testing.T) {
internal/cli/cli_test.go:578:func TestAddTrackerShipsNoSecretLiteralAndGuidesCredentials(t *testing.T) {
internal/cli/cli_test.go:628:func TestAddTrackerIsIdempotent(t *testing.T) {
internal/cli/cli_test.go:650:func TestAddTrackerMultipleCoexist(t *testing.T) {
internal/cli/cli_test.go:674:func TestAddTrackerRejectsMissingScaffold(t *testing.T) {
internal/cli/cli_test.go:688:func TestAddTrackerRejectsUnknownTracker(t *testing.T) {
internal/cli/cli_test.go:701:func TestTopLevelHelpListsAllSubcommands(t *testing.T) {
internal/cli/cli_test.go:728:func TestSubcommandHelpPrintsFlagsAndExamples(t *testing.T) {
internal/cli/cli_test.go:769:func TestFlaglessSubcommandHelp(t *testing.T) {
internal/cli/cli_test.go:787:func TestUnknownCommandErrorPointsAtHelp(t *testing.T) {
internal/cli/cli_test.go:799:func TestUnknownFlavorErrorPointsAtHelp(t *testing.T) {
internal/cli/cli_test.go:815:func TestHelpFlagsMatchDocs(t *testing.T) {
internal/cli/cli_test.go:842:func TestVersion(t *testing.T) {
internal/cli/cli_test.go:855:func TestVersionDefaultsToDev(t *testing.T) {
internal/flavors/claudecowork/flavor.go:11:func Templates() embed.FS {
internal/flavors/claudecowork/flavor.go:18:func ExecutablePaths() []string {
internal/flavors/claudecowork/flavor.go:25:func NextSteps(target string) string {
Expand All @@ -142,51 +159,41 @@ internal/flavors/registry.go:16:type Registry struct {
internal/flavors/registry.go:20:func DefaultRegistry() Registry {
internal/flavors/registry.go:153:func NewRegistry(items ...Flavor) Registry {
internal/flavors/registry_test.go:18:func TestAllTemplatesParse(t *testing.T) {
internal/gitconfig/gitconfig.go:27:type Runner interface {
internal/gitconfig/gitconfig.go:37:type Env interface {
internal/gitconfig/gitconfig.go:54:func EnsureGlobal(runner Runner, env Env, upsert upsertFunc) (string, error) {
internal/gitconfig/gitconfig.go:83:func GlobalPath(runner Runner, env Env) (string, error) {
internal/gitconfig/gitconfig.go:136:type OSEnv struct{}
internal/gitconfig/gitconfig.go:145:func NewExecRunner() Runner { return execRunner{} }
internal/gitconfig/gitconfig_test.go:98:func TestEnsureGlobalCreatesDefaultPathAndSetsKey(t *testing.T) {
internal/gitconfig/gitconfig_test.go:121:func TestEnsureGlobalHonorsXDGConfigHome(t *testing.T) {
internal/gitconfig/gitconfig_test.go:140:func TestEnsureGlobalHonorsConfiguredExcludesFileAndDoesNotSetKey(t *testing.T) {
internal/gitconfig/gitconfig_test.go:162:func TestEnsureGlobalExpandsTildeInConfiguredPath(t *testing.T) {
internal/gitconfig/gitconfig_test.go:176:func TestEnsureGlobalAppendsToExistingFileWithoutOurBlock(t *testing.T) {
internal/gitconfig/gitconfig_test.go:198:func TestEnsureGlobalIsIdempotent(t *testing.T) {
internal/gitconfig/gitconfig_test.go:223:func TestGlobalPathDoesNotWriteOrSet(t *testing.T) {
internal/gitignore/gitignore.go:48:func Block() string {
internal/gitignore/gitignore.go:63:func LocalPath(target string) (string, error) {
internal/gitignore/gitignore.go:75:func EnsureLocal(target string) (string, error) {
internal/gitignore/gitignore.go:94:func HiddenPath(target string) (string, error) {
internal/gitignore/gitignore.go:115:func EnsureHidden(target string) (string, error) {
internal/gitignore/gitignore.go:139:func Upsert(content string) string {
internal/gitignore/gitignore_test.go:10:func TestBlockHasMarkersAndEnvelope(t *testing.T) {
internal/gitignore/gitignore_test.go:27:func TestEnsureLocalCreatesAndAppends(t *testing.T) {
internal/gitignore/gitignore_test.go:70:func TestEnsureLocalIsIdempotent(t *testing.T) {
internal/gitignore/gitignore_test.go:95:func TestEnsureLocalReplacesStaleBlockInPlace(t *testing.T) {
internal/scaffold/scaffold.go:18:type Options struct {
internal/scaffold/scaffold.go:41:func Run(ctx context.Context, opts Options) error {
internal/scaffold/scaffold.go:76:func Overlay(opts Options, fsys fs.FS, root string) error {
internal/scaffold/scaffold_test.go:18:func TestRunWritesFullstackScaffold(t *testing.T) {
internal/scaffold/scaffold_test.go:43:func TestRunSkipsExistingFilesUnlessForced(t *testing.T) {
internal/scaffold/scaffold_test.go:74:func TestRunDryRunDoesNotWriteFiles(t *testing.T) {
internal/scaffold/scaffold_test.go:98:func TestRunForceReplacesSymlinkWithoutWritingThroughIt(t *testing.T) {
internal/scaffold/scaffold_test.go:122:func TestRunForceRestoresExecutableMode(t *testing.T) {
internal/scaffold/scaffold_test.go:145:func TestRunForceRefusesToReplaceDirectory(t *testing.T) {
internal/scaffold/scaffold_test.go:163:func TestRunRendersPathTemplate(t *testing.T) {
internal/scaffold/scaffold_test.go:186:func TestRunLayersFlavorOverCommon(t *testing.T) {
internal/scaffold/scaffold_test.go:223:func TestRunAgentsOnlySkipsFreshOnlyPathsAndPrefersVariant(t *testing.T) {
internal/scaffold/scaffold_test.go:265:func TestRunFreshModeIgnoresAgentsOnlyVariants(t *testing.T) {
internal/scaffold/scaffold_test.go:295:func TestRunDoesNotInitNestedGitRepo(t *testing.T) {
internal/trackers/ado/ado.go:16:func Templates() embed.FS {
internal/trackers/ado/ado.go:25:func MCPServer() map[string]any {
internal/trackers/gh/gh.go:13:func Templates() embed.FS {
internal/trackers/gh/gh.go:21:func MCPServer() map[string]any {
internal/trackers/jira/jira.go:14:func Templates() embed.FS {
internal/trackers/jira/jira.go:23:func MCPServer() map[string]any {
internal/trackers/mcp.go:21:func MergeMCPServer(target, serverKey string, serverConfig map[string]any) (bool, error) {
internal/trackers/mcp_test.go:13:func TestMergeMCPServerAddsNewEntryToEmptyConfig(t *testing.T) {
internal/trackers/mcp_test.go:35:func TestMergeMCPServerIsIdempotent(t *testing.T) {
internal/trackers/mcp_test.go:62:func TestMergeMCPServerCreatesFileIfMissing(t *testing.T) {
internal/trackers/mcp_test.go:80:func TestMergeMCPServerErrorsOnMalformedJSON(t *testing.T) {
internal/trackers/mcp_test.go:91:func TestMergeMCPServerCreatesMissingMcpServersKey(t *testing.T) {
internal/trackers/mcp_test.go:116:func TestDefaultRegistryListsAllShippedTrackers(t *testing.T) {
internal/trackers/mcp_test.go:137:func TestNoTrackerShipsEmptySecretLiteral(t *testing.T) {
internal/trackers/mcp_test.go:161:func TestTrackerSecretsUseEnvReference(t *testing.T) {
internal/trackers/mcp_test.go:195:func TestRegistryGetReturnsKnownTrackersInError(t *testing.T) {
internal/trackers/registry.go:13:type Registry struct {
internal/trackers/registry.go:18:func DefaultRegistry() Registry {
internal/trackers/registry.go:51:func NewRegistry(items ...Tracker) Registry {
internal/gitignore/gitignore_test.go:128:func TestEnsureHiddenCreatesAndAppends(t *testing.T) {
internal/gitignore/gitignore_test.go:174:func TestEnsureHiddenIsIdempotent(t *testing.T) {
internal/gitignore/gitignore_test.go:196:func TestEnsureHiddenReplacesStaleBlockInPlace(t *testing.T) {
internal/gitignore/gitignore_test.go:234:func TestEnsureHiddenWritesNoGitignore(t *testing.T) {
internal/scaffold/color_test.go:9:func TestColorDisabledForNonTTYOutputs(t *testing.T) {
internal/scaffold/color_test.go:26:func TestColorDisabledByEnvironment(t *testing.T) {
internal/scaffold/color_test.go:50:func TestColorEnabledForTerminalFile(t *testing.T) {

## Stats

```
Total tracked files: 342
Total tracked files: 350
```

<!-- HAND-WRITTEN BELOW — EDIT FREELY -->
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ Flags for `init`:
- `--no-git` — skip `git init` when the target is not already a repository.
- `--dry-run` — print planned writes without changing files.
- `--agents-only` — skip the flavor's fresh-project files; ship only the agentic envelope (AGENTS.md, scripts, devcontainer, Justfile, pre-commit). For adding `agent-init` to an existing project. Supported on every code flavor: `fullstack`, `go-cli`, `go-backend`, `iac`. See [`docs/flavors/go-cli.md`](./docs/flavors/go-cli.md) for a worked example.
- `--visibility` — how the scaffold is tracked by git. `shared` (default) commits it. `local` appends a fenced, idempotent block to the committed `.gitignore` so the scaffold is ignored while the ignore rule stays visible to the team. Code flavors only. See [`docs/cli.md`](./docs/cli.md).
- `--visibility` — how the scaffold is tracked by git. `shared` (default) commits it. `local` appends a fenced, idempotent block to the committed `.gitignore` so the scaffold is ignored while the ignore rule stays visible to the team. `hidden` writes the same block to the never-committed `.git/info/exclude`, leaving no trace for other contributors. `global-default` writes the same block to your machine-wide git excludes file, ignoring the scaffold in every repository on the machine; it prints a warning and the path it edited, and you force-add the scaffold to commit it openly in a given repo. Code flavors only. See [`docs/cli.md`](./docs/cli.md).
- `--private` — alias for `--visibility=hidden`.

The `add-tracker` subcommand extends a `project-management` scaffold with a Jira / Azure DevOps / GitHub integration. Each call writes an `integrations/<tracker>/` cheatsheet and merges an entry into the target's `.mcp.json`. Idempotent and additive — multiple trackers can coexist (useful during migrations). See [`docs/cli.md`](./docs/cli.md) and [`docs/flavors/project-management.md`](./docs/flavors/project-management.md) for details.

Expand Down
5 changes: 4 additions & 1 deletion docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,17 @@ agent-init ./my-tool # path-only form; implies fullstack
| `--no-git` | Skip `git init` when the target is not already a repo. |
| `--dry-run` | Print planned writes without changing files. |
| `--agents-only` | Skip the flavor's fresh-project files; ship only the agentic envelope (AGENTS.md, scripts, devcontainer, Justfile, pre-commit). For adding agents to an existing project. Supported on every code flavor: `fullstack`, `go-cli`, `go-backend`, `iac`. Rejected on doc-collab flavors (`claude-cowork`, `project-management`) since they don't bootstrap a project layout. |
| `--visibility` | How the scaffold is tracked by git. Four planned modes; this build ships `shared` and `local`. `shared` (default) commits the scaffold normally. `local` appends a fenced, idempotent block to the committed `.gitignore` so the team sees the scaffold is ignored but does not carry the files. Code flavors only; rejected on doc-collab flavors. `hidden` (per-repo `.git/info/exclude`) and `global-default` (machine-wide excludes) are accepted spellings but error until they land. |
| `--visibility` | How the scaffold is tracked by git. Four modes, all implemented: `shared`, `local`, `hidden`, `global-default`. `shared` (default) commits the scaffold normally. `local` appends a fenced, idempotent block to the committed `.gitignore` so the team sees the scaffold is ignored but does not carry the files. `hidden` writes the same block to the never-committed `.git/info/exclude`, leaving zero committed trace (per-repo, local to your clone). `global-default` writes the same block to your **machine-wide** git excludes file, ignoring the scaffold in **every** repository on the machine. Code flavors only; rejected on doc-collab flavors. |
| `--private` | Alias for `--visibility=hidden`. Passing it alongside a conflicting `--visibility` errors. |

### Behavior

- The scaffold engine walks the flavor's templates, then the common overlay (if the flavor has one). Existing files are skipped unless `--force` is set.
- After file writes: creates the flavor's declared symlinks (code flavors get the AGENTS.md/CLAUDE.md trio; doc-collab flavors get none), then runs `git init` unless `--no-git`, then prints the flavor's `NextSteps` message.
- With `--agents-only`: paths listed in the flavor's `FreshOnlyPaths` are skipped, and any template named `<file>.agents-only.<ext>` (e.g. `Justfile.agents-only.tmpl`) is written as `<file>.<ext>` in place of the base. See [docs/flavors/go-cli.md](./flavors/go-cli.md) for a worked example.
- With `--visibility=local`: after the scaffold is written (symlinks and `git init` included — visibility controls tracking, not creation), a fenced block is appended to the committed `.gitignore`, creating it if absent. The block covers the agentic envelope (`.agent/`, `/AGENTS.md`, `/CLAUDE.md`, `.devcontainer/`, `/Justfile`, `.pre-commit-config.yaml`). It is delimited by `# >>> agent-init (private) >>>` / `# <<< agent-init <<<` markers, so re-running replaces it in place (never duplicates) and it can be removed by hand to undo. `init` prints the absolute path it edited. `--dry-run` previews the path and block, writing nothing. Block management lives in [internal/gitignore](../internal/gitignore/gitignore.go).
- With `--visibility=hidden` (or `--private`): the identical block is written to `.git/info/exclude` instead of `.gitignore`, creating the `.git/info` directory if absent. `.git/info/exclude` is git's per-repo, never-committed ignore file, so a teammate cloning the repo sees no agent-init trace. The mode is otherwise the same as `local`: idempotent in-place replacement, the absolute path is announced, `--dry-run` previews and writes nothing, and the symlink trio is still created (visibility controls tracking, not creation). Because `.git/info/exclude` does not appear in `git diff`, remember to remove the fenced block by hand to undo.
- With `--visibility=global-default`: the **same** fenced block is written to your machine-wide git excludes file instead of a repo file, so the scaffold is ignored in **every** git repository on the machine. **This is action-at-a-distance.** The command prints a loud machine-wide warning on stderr and always announces the absolute path it edited. The target file is `git config --global core.excludesfile` if set (honored even when it points somewhere unusual); otherwise `${XDG_CONFIG_HOME:-~/.config}/git/ignore`, which is created and set as `core.excludesfile` only when no global excludes is configured. No other global-config key is touched. Idempotent (the marked block is replaced in place) and reversible (remove the block by hand). `--dry-run` resolves and prints the target path and the block but writes nothing and touches no git config. To commit the scaffold openly in a specific repo despite the global default, force-add it there — `git add -f .agent AGENTS.md CLAUDE.md .devcontainer Justfile .pre-commit-config.yaml` — since git never re-ignores a tracked file (gitignore negation cannot re-include a file under an excluded directory, so force-add is the documented override). The global excludes-file resolution lives in [internal/gitconfig](../internal/gitconfig/gitconfig.go); the block content is shared from [internal/gitignore](../internal/gitignore/gitignore.go).
- Source: [scaffold.go:31](../internal/scaffold/scaffold.go#L31) (`Run`), [cli.go:applyVisibility](../internal/cli/cli.go).

### Output
Expand Down
Loading
Loading