totopo 3.4.0-rc-1 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -106,9 +106,9 @@ Each workspace has a git mode (set via **Manage Workspace > Git mode**) that con
106
106
 
107
107
  | Mode | Local mutations | Remote (push/pull/fetch/clone) |
108
108
  |---|---|---|
109
- | **strict** *(default for new workspaces)* | Blocked — a read-only `git` wrapper allows inspection (`status`, `log`, `diff`, `blame`, `show`, etc.) and rejects mutations (`commit`, `add`, `reset`, `checkout`, etc.) | Blocked at the gitconfig protocol layer |
110
- | **local** *(default for workspaces upgraded from earlier versions)* | Allowed | Blocked at the gitconfig protocol layer |
111
- | **unrestricted** | Allowed | Allowed — totopo enforces no git-specific restrictions |
109
+ | **local** *(default)* | Allowed | Blocked at the gitconfig protocol layer |
110
+ | **strict** | Blocked — a read-only `git` wrapper allows inspection (`status`, `log`, `diff`, `blame`, `show`, etc.) and rejects mutations (`commit`, `add`, `reset`, `checkout`, etc.) | Blocked at the gitconfig protocol layer |
111
+ | **unrestricted** | Allowed | Allowed |
112
112
 
113
113
  The active mode is recorded per workspace in `.lock`, exposed inside the container as `TOTOPO_GIT_MODE`, and reflected in the agent context so each session knows what is permitted. Switching modes recreates the container on the next session.
114
114
 
@@ -278,7 +278,7 @@ export async function run(packageDir, ctx, options) {
278
278
  }
279
279
  const hasGit = existsSync(join(workspaceDir, ".git"));
280
280
  // --- Git mode (per-workspace, host-side .lock) ---------------------------------------------------------------------------------------
281
- const gitMode = readGitMode(ctx.workspaceId) ?? GIT_MODE.strict;
281
+ const gitMode = readGitMode(ctx.workspaceId) ?? GIT_MODE.local;
282
282
  // --- Start container -----------------------------------------------------------------------------------------------------------------
283
283
  const containerOpts = {
284
284
  containerName,
@@ -121,19 +121,19 @@ async function removeShadowPatterns(ctx) {
121
121
  }
122
122
  // --- Git mode menu -----------------------------------------------------------------------------------------------------------------------
123
123
  async function gitModeMenu(ctx) {
124
- const current = readGitMode(ctx.workspaceId) ?? GIT_MODE.strict;
125
- note("Strict read-only local, no remote (recommended)\n" +
126
- "Local local mutations allowed; remote blocked\n" +
124
+ const current = readGitMode(ctx.workspaceId) ?? GIT_MODE.local;
125
+ note("Local — local mutations allowed; remote blocked\n" +
126
+ "Strict read-only; mutations and remote blocked\n" +
127
127
  "Unrestricted — no totopo-enforced restrictions", "Git mode");
128
128
  const choice = await select({
129
129
  message: "Git mode:",
130
130
  options: [
131
131
  {
132
- value: GIT_MODE.strict,
133
- label: "Strict",
134
- hint: current === GIT_MODE.strict ? "current · recommended" : "recommended",
132
+ value: GIT_MODE.local,
133
+ label: "Local",
134
+ hint: current === GIT_MODE.local ? "current · default" : "default",
135
135
  },
136
- { value: GIT_MODE.local, label: "Local", ...(current === GIT_MODE.local ? { hint: "current" } : {}) },
136
+ { value: GIT_MODE.strict, label: "Strict", ...(current === GIT_MODE.strict ? { hint: "current" } : {}) },
137
137
  {
138
138
  value: GIT_MODE.unrestricted,
139
139
  label: "Unrestricted",
@@ -203,7 +203,7 @@ async function resetTotopoYaml(ctx) {
203
203
  // --- Manage Workspace submenu ------------------------------------------------------------------------------------------------------------
204
204
  export async function run(ctx) {
205
205
  while (true) {
206
- const currentGitMode = readGitMode(ctx.workspaceId) ?? GIT_MODE.strict;
206
+ const currentGitMode = readGitMode(ctx.workspaceId) ?? GIT_MODE.local;
207
207
  const options = [
208
208
  { value: "git-mode", label: "Git mode", hint: `current: ${currentGitMode}` },
209
209
  { value: "shadow-paths", label: "Shadow paths", hint: "manage shadow patterns" },
@@ -86,7 +86,7 @@ function renderTemplate(template, vars) {
86
86
  /**
87
87
  * Assembles the agent context markdown injected into each supported agent's config dir at session start.
88
88
  */
89
- export function buildAgentContextDocs(hasGit, shadowPatterns, gitMode = GIT_MODE.strict) {
89
+ export function buildAgentContextDocs(hasGit, shadowPatterns, gitMode = GIT_MODE.local) {
90
90
  const gitSection = loadTemplate(hasGit ? `git-mode-${gitMode}` : "git-unavailable");
91
91
  const shadowSection = shadowPatterns && shadowPatterns.length > 0
92
92
  ? renderTemplate(loadTemplate("shadow-paths"), {
@@ -391,12 +391,11 @@ function migrateRemoveLastCliUpdate() {
391
391
  }
392
392
  }
393
393
  /**
394
- * Pre-v3.4.0: Add the git_mode field to .lock files. Existing workspaces are kept on
395
- * git_mode=local to preserve their behavior; new workspaces created via initWorkspaceDir
396
- * default to git_mode=strict instead. Idempotent - skips files that already have the field.
397
- * Prints a one-time clack note() when any workspace was newly migrated, so users discover
398
- * the new feature without rediscovering it on every subsequent startup. Returns the count
399
- * for testing purposes; the registered Migration entry ignores it.
394
+ * Pre-v3.4.0: Add the git_mode=local field to .lock files. Local is the default, so this
395
+ * is a cosmetic write that makes the field visible on disk; runtime behavior is unchanged
396
+ * for existing workspaces. Idempotent - skips files that already have the field. Prints a
397
+ * one-time clack note() when any workspace was newly migrated so users discover the new
398
+ * feature. Returns the count for testing purposes; the registered Migration entry ignores it.
400
399
  */
401
400
  export function migrateAddGitMode() {
402
401
  const baseDir = getWorkspacesBaseDir();
@@ -418,7 +417,7 @@ export function migrateAddGitMode() {
418
417
  }
419
418
  }
420
419
  if (migrated > 0) {
421
- note(`totopo v3.4.0 introduces git modes for workspaces.\nYour existing workspace${migrated > 1 ? "s have" : " has"} been kept on 'local' (today's behavior — local commits allowed, remote blocked).\nA new 'strict' mode is available (read-only, recommended for new agent sessions).\nSwitch via the totopo menu > Manage Workspace > Git mode.`, "Git modes");
420
+ note(`totopo v3.4.0 introduces git modes for workspaces.\nDefault is 'local' (previous behavior — local commits allowed, remote blocked).\nTwo opt-in modes are available: 'strict' (read-only, all mutations blocked) and 'unrestricted' (no totopo-enforced restrictions).\nSwitch via the totopo menu > Manage Workspace > Git mode.`, "Git modes");
422
421
  }
423
422
  return migrated;
424
423
  }
@@ -54,7 +54,7 @@ function parseLockFile(workspaceId) {
54
54
  return {
55
55
  workspaceRoot: partial.workspaceRoot,
56
56
  activeProfile: partial.activeProfile ?? PROFILE.default,
57
- gitMode: partial.gitMode ?? GIT_MODE.strict,
57
+ gitMode: partial.gitMode ?? GIT_MODE.local,
58
58
  };
59
59
  }
60
60
  catch {
@@ -78,7 +78,7 @@ export function writeLockFile(workspaceId, workspaceRoot) {
78
78
  writeLockFileInternal(workspaceId, {
79
79
  workspaceRoot,
80
80
  activeProfile: existing?.activeProfile ?? PROFILE.default,
81
- gitMode: existing?.gitMode ?? GIT_MODE.strict,
81
+ gitMode: existing?.gitMode ?? GIT_MODE.local,
82
82
  });
83
83
  }
84
84
  /** Read the active profile name. Returns null if lock file is missing. */
@@ -92,13 +92,13 @@ export function writeActiveProfile(workspaceId, profile) {
92
92
  return;
93
93
  writeLockFileInternal(workspaceId, { ...existing, activeProfile: profile });
94
94
  }
95
- /** Read the active git mode. Returns null if lock file is missing. Coerces unknown values to strict. */
95
+ /** Read the active git mode. Returns null if lock file is missing. Coerces unknown values to local. */
96
96
  export function readGitMode(workspaceId) {
97
97
  const parsed = parseLockFile(workspaceId);
98
98
  if (!parsed)
99
99
  return null;
100
100
  const value = parsed.gitMode;
101
- return GIT_MODES.includes(value) ? value : GIT_MODE.strict;
101
+ return GIT_MODES.includes(value) ? value : GIT_MODE.local;
102
102
  }
103
103
  /** Write the active git mode. Preserves workspace root path and active profile. */
104
104
  export function writeGitMode(workspaceId, gitMode) {
@@ -109,7 +109,7 @@ export function writeGitMode(workspaceId, gitMode) {
109
109
  }
110
110
  // --- Workspace directory initialization --------------------------------------------------------------------------------------------------
111
111
  /** Initialize ~/.totopo/workspaces/<workspace_id>/ with lock file and subdirs. */
112
- export function initWorkspaceDir(workspaceId, workspaceRoot, activeProfile = PROFILE.default, gitMode = GIT_MODE.strict) {
112
+ export function initWorkspaceDir(workspaceId, workspaceRoot, activeProfile = PROFILE.default, gitMode = GIT_MODE.local) {
113
113
  const dir = getWorkspaceDir(workspaceId);
114
114
  mkdirSync(join(dir, AGENTS_DIR), { recursive: true });
115
115
  mkdirSync(join(dir, SHADOWS_DIR), { recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "totopo",
3
- "version": "3.4.0-rc-1",
3
+ "version": "3.4.0",
4
4
  "description": "Run AI coding agents safely in your local codebase",
5
5
  "type": "module",
6
6
  "bin": {
@@ -129,7 +129,7 @@ function verifyRemoteBlocked(gitMode, ok, fail, skip) {
129
129
  * through the main startup script's section formatting and error counter.
130
130
  */
131
131
  export function checkGitMode({ ok, fail, skip, run, isRoot }) {
132
- const gitMode = VALID_GIT_MODES.includes(process.env.TOTOPO_GIT_MODE) ? process.env.TOTOPO_GIT_MODE : GIT_MODE.strict;
132
+ const gitMode = VALID_GIT_MODES.includes(process.env.TOTOPO_GIT_MODE) ? process.env.TOTOPO_GIT_MODE : GIT_MODE.local;
133
133
  const protocolValue = gitMode === GIT_MODE.unrestricted ? "always" : "never";
134
134
 
135
135
  if (isRoot) {