From a8ca8b67af92463544cfc6e26ad5f25558c30111 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Mar 2026 19:31:29 +0000 Subject: [PATCH 1/3] add eject command to decouple from update mechanism Adds /5:eject command that sets ejected flag in .5/version.json. When ejected, update checks, statusline indicators, /5:update, and --upgrade installs are all skipped. Reversible by removing the ejected field from version.json. https://claude.ai/code/session_01UTjW3dbQ44EZmQtuNj9GUM --- bin/install.js | 19 +++++++++- src/commands/5/eject.md | 77 ++++++++++++++++++++++++++++++++++++++ src/commands/5/update.md | 2 + src/hooks/check-updates.js | 5 +++ src/hooks/statusline.js | 27 +++++++------ 5 files changed, 117 insertions(+), 13 deletions(-) create mode 100644 src/commands/5/eject.md diff --git a/bin/install.js b/bin/install.js index aa7c88a..c8894e7 100755 --- a/bin/install.js +++ b/bin/install.js @@ -77,11 +77,23 @@ function getVersionInfo(targetPath, isGlobal) { const needsUpdate = compareVersions(installed, available) < 0; + // Check if ejected from updates + const versionFile = path.join(getDataPath(isGlobal), 'version.json'); + let ejected = false; + let ejectedAt = null; + try { + const data = JSON.parse(fs.readFileSync(versionFile, 'utf8')); + ejected = data.ejected === true; + ejectedAt = data.ejectedAt || null; + } catch (e) {} + return { exists: true, installed, available, - needsUpdate + needsUpdate, + ejected, + ejectedAt }; } @@ -626,6 +638,7 @@ function showCommandsHelp(isGlobal) { log.info(' /5:address-review-findings - Apply review findings & PR comments'); log.info(' /5:configure - Interactive project setup'); log.info(' /5:reconfigure - Refresh docs/skills (no Q&A)'); + log.info(' /5:eject - Eject from update mechanism'); log.info(' /5:unlock - Remove planning guard lock'); log.info(''); log.info(`Config file: ${path.join(getDataPath(isGlobal), 'config.json')}`); @@ -743,6 +756,10 @@ function install(isGlobal, forceUpgrade = false) { log.info(`Upgrading from legacy install to ${versionInfo.available}`); performUpdate(targetPath, sourcePath, isGlobal, versionInfo); return; + } else if (versionInfo.ejected) { + log.warn(`Installation ejected from updates (since ${versionInfo.ejectedAt})`); + log.info('Updates are disabled. To re-enable, remove the "ejected" field from .5/version.json'); + return; } else if (versionInfo.needsUpdate) { log.info(`Installed: ${versionInfo.installed}`); log.info(`Available: ${versionInfo.available}`); diff --git a/src/commands/5/eject.md b/src/commands/5/eject.md new file mode 100644 index 0000000..cba6ede --- /dev/null +++ b/src/commands/5/eject.md @@ -0,0 +1,77 @@ +--- +name: 5:eject +description: Eject from the update mechanism — keep all workflow files but stop receiving updates +allowed-tools: Bash, Read, Write, AskUserQuestion +user-invocable: true +model: haiku +context: fork +--- + + +You are a Workflow Ejector. You decouple the current installation from automatic updates. +After ejecting, you are DONE. + + +# Eject from Update Mechanism + +Ejecting keeps all workflow files in place but permanently opts out of the update system. After ejecting: +- No more update checks on session start +- No update indicators in the statusline +- The `/5:update` command will refuse to run +- The installer (`npx 5-phase-workflow --upgrade`) will skip this project + +This is useful when you've customized workflow files and want to prevent future updates from overwriting your changes. + +**This action is reversible** — the user can re-enable updates by removing the `ejected` field from `.5/version.json`. + +## Step 1: Check Current State + +Read `.5/version.json`. If it doesn't exist, tell the user: "No 5-Phase Workflow installation found." and stop. + +If `ejected` is already `true`, tell the user: "This installation is already ejected (since {ejectedAt}). No updates will be applied." and stop. + +## Step 2: Confirm with User + +Tell the user what ejecting means: + +> **Eject from 5-Phase Workflow updates?** +> +> This will: +> - Stop automatic update checks +> - Remove the update indicator from the statusline +> - Prevent `/5:update` and `npx 5-phase-workflow --upgrade` from modifying workflow files +> +> All current workflow files remain untouched. You can reverse this later by removing the `ejected` field from `.5/version.json`. + +Ask: "Proceed with eject?" + +If the user declines, stop here. + +## Step 3: Eject + +Read `.5/version.json`, add the following fields, and write it back: + +```json +{ + "ejected": true, + "ejectedAt": "" +} +``` + +Preserve all existing fields (packageVersion, installedAt, lastUpdated, installationType, manifest, etc.). + +## Step 4: Clean Up Update Cache + +Remove the update cache file if it exists: + +```bash +rm -f .5/.update-cache.json +``` + +## Step 5: Confirm + +Tell the user: + +> Ejected successfully. This installation (v{packageVersion}) will no longer receive updates. +> +> To reverse this later, remove the `ejected` and `ejectedAt` fields from `.5/version.json`. diff --git a/src/commands/5/update.md b/src/commands/5/update.md index 9a4dd4b..31b3b1d 100644 --- a/src/commands/5/update.md +++ b/src/commands/5/update.md @@ -19,6 +19,8 @@ After reporting the result, you are DONE. Read `.5/version.json` and note the current `installedVersion`. +If the `ejected` field is `true`, tell the user: "This installation has been ejected from the update mechanism (since {ejectedAt}). Updates are disabled. To re-enable updates, remove the `ejected` and `ejectedAt` fields from `.5/version.json`." and **stop here**. + ## Step 2: Run Upgrade ```bash diff --git a/src/hooks/check-updates.js b/src/hooks/check-updates.js index 0b00dc3..3c972a5 100755 --- a/src/hooks/check-updates.js +++ b/src/hooks/check-updates.js @@ -39,6 +39,11 @@ async function checkForUpdates(workspaceDir) { process.exit(0); } + // Skip if ejected + if (versionData.ejected) { + process.exit(0); + } + // Compare versions const installed = versionData.packageVersion; const latestVersion = await getLatestVersion(); diff --git a/src/hooks/statusline.js b/src/hooks/statusline.js index 2937fad..034a737 100644 --- a/src/hooks/statusline.js +++ b/src/hooks/statusline.js @@ -50,18 +50,21 @@ process.stdin.on('end', () => { const versionFile = path.join(dir, '.5', 'version.json'); const versionData = JSON.parse(fs.readFileSync(versionFile, 'utf8')); - // Update check — read latestAvailableVersion from cache file (gitignored) - const cacheFile = path.join(dir, '.5', '.update-cache.json'); - let latest = null; - if (fs.existsSync(cacheFile)) { - try { - const cache = JSON.parse(fs.readFileSync(cacheFile, 'utf8')); - latest = cache.latestAvailableVersion || null; - } catch(e) {} - } - const installed = versionData.packageVersion; - if (latest && installed && compareVersions(installed, latest) < 0) { - updateIndicator = ` | \x1b[33m↑${latest} → /5:update\x1b[0m`; + // Skip update indicator if ejected + if (!versionData.ejected) { + // Update check — read latestAvailableVersion from cache file (gitignored) + const cacheFile = path.join(dir, '.5', '.update-cache.json'); + let latest = null; + if (fs.existsSync(cacheFile)) { + try { + const cache = JSON.parse(fs.readFileSync(cacheFile, 'utf8')); + latest = cache.latestAvailableVersion || null; + } catch(e) {} + } + const installed = versionData.packageVersion; + if (latest && installed && compareVersions(installed, latest) < 0) { + updateIndicator = ` | \x1b[33m↑${latest} → /5:update\x1b[0m`; + } } // Reconfigure check (reads flag file in .5/, gitignored) From 5417a3b520245a72a25748756cad8c51069c074e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Mar 2026 19:37:19 +0000 Subject: [PATCH 2/3] make eject destructive: delete update infrastructure instead of flag Eject now permanently removes update-related files: - .claude/hooks/check-updates.js - .claude/commands/5/update.md and eject.md - .5/version.json and .update-cache.json - check-updates hook entry from settings.json Reverts the flag-based approach (ejected field in version.json). To restore updates after eject, reinstall with npx 5-phase-workflow. https://claude.ai/code/session_01UTjW3dbQ44EZmQtuNj9GUM --- bin/install.js | 18 +-------- src/commands/5/eject.md | 76 ++++++++++++++++++++++---------------- src/commands/5/update.md | 2 - src/hooks/check-updates.js | 5 --- src/hooks/statusline.js | 27 ++++++-------- 5 files changed, 58 insertions(+), 70 deletions(-) diff --git a/bin/install.js b/bin/install.js index c8894e7..8f0744b 100755 --- a/bin/install.js +++ b/bin/install.js @@ -77,23 +77,11 @@ function getVersionInfo(targetPath, isGlobal) { const needsUpdate = compareVersions(installed, available) < 0; - // Check if ejected from updates - const versionFile = path.join(getDataPath(isGlobal), 'version.json'); - let ejected = false; - let ejectedAt = null; - try { - const data = JSON.parse(fs.readFileSync(versionFile, 'utf8')); - ejected = data.ejected === true; - ejectedAt = data.ejectedAt || null; - } catch (e) {} - return { exists: true, installed, available, - needsUpdate, - ejected, - ejectedAt + needsUpdate }; } @@ -756,10 +744,6 @@ function install(isGlobal, forceUpgrade = false) { log.info(`Upgrading from legacy install to ${versionInfo.available}`); performUpdate(targetPath, sourcePath, isGlobal, versionInfo); return; - } else if (versionInfo.ejected) { - log.warn(`Installation ejected from updates (since ${versionInfo.ejectedAt})`); - log.info('Updates are disabled. To re-enable, remove the "ejected" field from .5/version.json'); - return; } else if (versionInfo.needsUpdate) { log.info(`Installed: ${versionInfo.installed}`); log.info(`Available: ${versionInfo.available}`); diff --git a/src/commands/5/eject.md b/src/commands/5/eject.md index cba6ede..5f39a95 100644 --- a/src/commands/5/eject.md +++ b/src/commands/5/eject.md @@ -1,34 +1,36 @@ --- name: 5:eject -description: Eject from the update mechanism — keep all workflow files but stop receiving updates -allowed-tools: Bash, Read, Write, AskUserQuestion +description: Eject from the update mechanism — permanently removes update infrastructure +allowed-tools: Bash, Read, Edit, AskUserQuestion user-invocable: true model: haiku context: fork --- -You are a Workflow Ejector. You decouple the current installation from automatic updates. +You are a Workflow Ejector. You permanently remove the update infrastructure from this installation. After ejecting, you are DONE. # Eject from Update Mechanism -Ejecting keeps all workflow files in place but permanently opts out of the update system. After ejecting: -- No more update checks on session start -- No update indicators in the statusline -- The `/5:update` command will refuse to run -- The installer (`npx 5-phase-workflow --upgrade`) will skip this project +Ejecting permanently removes the update system from this installation. After ejecting: +- The update check hook (`check-updates.js`) is deleted +- The update command (`/5:update`) is deleted +- The eject command (`/5:eject`) is deleted +- Version tracking (`.5/version.json`) is deleted +- The update cache (`.5/.update-cache.json`) is deleted +- The `check-updates.js` hook entry is removed from `.claude/settings.json` -This is useful when you've customized workflow files and want to prevent future updates from overwriting your changes. +All other workflow files (commands, skills, hooks, templates) remain untouched. -**This action is reversible** — the user can re-enable updates by removing the `ejected` field from `.5/version.json`. +**This is irreversible.** To restore update functionality, reinstall with `npx 5-phase-workflow`. ## Step 1: Check Current State -Read `.5/version.json`. If it doesn't exist, tell the user: "No 5-Phase Workflow installation found." and stop. +Read `.5/version.json`. If it doesn't exist, tell the user: "No 5-Phase Workflow installation found (or already ejected)." and stop. -If `ejected` is already `true`, tell the user: "This installation is already ejected (since {ejectedAt}). No updates will be applied." and stop. +Note the `packageVersion` for the confirmation message. ## Step 2: Confirm with User @@ -36,42 +38,54 @@ Tell the user what ejecting means: > **Eject from 5-Phase Workflow updates?** > -> This will: -> - Stop automatic update checks -> - Remove the update indicator from the statusline -> - Prevent `/5:update` and `npx 5-phase-workflow --upgrade` from modifying workflow files +> This will permanently delete: +> - `.claude/hooks/check-updates.js` (update check hook) +> - `.claude/commands/5/update.md` (update command) +> - `.claude/commands/5/eject.md` (this command) +> - `.5/version.json` (version tracking) +> - `.5/.update-cache.json` (update cache) > -> All current workflow files remain untouched. You can reverse this later by removing the `ejected` field from `.5/version.json`. +> The `check-updates.js` hook entry will also be removed from `.claude/settings.json`. +> +> All other workflow files remain untouched. To restore updates later, reinstall with `npx 5-phase-workflow`. Ask: "Proceed with eject?" If the user declines, stop here. -## Step 3: Eject +## Step 3: Delete Update Files -Read `.5/version.json`, add the following fields, and write it back: +Run this command to delete the update-related files: -```json -{ - "ejected": true, - "ejectedAt": "" -} +```bash +rm -f .claude/hooks/check-updates.js .claude/commands/5/update.md .claude/commands/5/eject.md .5/version.json .5/.update-cache.json ``` -Preserve all existing fields (packageVersion, installedAt, lastUpdated, installationType, manifest, etc.). +## Step 4: Clean Up settings.json -## Step 4: Clean Up Update Cache +Read `.claude/settings.json`. Remove the hook entry from the `hooks.SessionStart` array where the command is `node .claude/hooks/check-updates.js`. -Remove the update cache file if it exists: +Specifically, find and remove the object in the `SessionStart` array that looks like: -```bash -rm -f .5/.update-cache.json +```json +{ + "matcher": "startup", + "hooks": [ + { + "type": "command", + "command": "node .claude/hooks/check-updates.js", + "timeout": 10 + } + ] +} ``` +If the `SessionStart` array becomes empty after removal, remove the `SessionStart` key entirely. Write the updated settings back. + ## Step 5: Confirm Tell the user: -> Ejected successfully. This installation (v{packageVersion}) will no longer receive updates. +> Ejected successfully. Update infrastructure has been removed from this installation (was v{packageVersion}). > -> To reverse this later, remove the `ejected` and `ejectedAt` fields from `.5/version.json`. +> To restore update functionality, reinstall with: `npx 5-phase-workflow` diff --git a/src/commands/5/update.md b/src/commands/5/update.md index 31b3b1d..9a4dd4b 100644 --- a/src/commands/5/update.md +++ b/src/commands/5/update.md @@ -19,8 +19,6 @@ After reporting the result, you are DONE. Read `.5/version.json` and note the current `installedVersion`. -If the `ejected` field is `true`, tell the user: "This installation has been ejected from the update mechanism (since {ejectedAt}). Updates are disabled. To re-enable updates, remove the `ejected` and `ejectedAt` fields from `.5/version.json`." and **stop here**. - ## Step 2: Run Upgrade ```bash diff --git a/src/hooks/check-updates.js b/src/hooks/check-updates.js index 3c972a5..0b00dc3 100755 --- a/src/hooks/check-updates.js +++ b/src/hooks/check-updates.js @@ -39,11 +39,6 @@ async function checkForUpdates(workspaceDir) { process.exit(0); } - // Skip if ejected - if (versionData.ejected) { - process.exit(0); - } - // Compare versions const installed = versionData.packageVersion; const latestVersion = await getLatestVersion(); diff --git a/src/hooks/statusline.js b/src/hooks/statusline.js index 034a737..2937fad 100644 --- a/src/hooks/statusline.js +++ b/src/hooks/statusline.js @@ -50,21 +50,18 @@ process.stdin.on('end', () => { const versionFile = path.join(dir, '.5', 'version.json'); const versionData = JSON.parse(fs.readFileSync(versionFile, 'utf8')); - // Skip update indicator if ejected - if (!versionData.ejected) { - // Update check — read latestAvailableVersion from cache file (gitignored) - const cacheFile = path.join(dir, '.5', '.update-cache.json'); - let latest = null; - if (fs.existsSync(cacheFile)) { - try { - const cache = JSON.parse(fs.readFileSync(cacheFile, 'utf8')); - latest = cache.latestAvailableVersion || null; - } catch(e) {} - } - const installed = versionData.packageVersion; - if (latest && installed && compareVersions(installed, latest) < 0) { - updateIndicator = ` | \x1b[33m↑${latest} → /5:update\x1b[0m`; - } + // Update check — read latestAvailableVersion from cache file (gitignored) + const cacheFile = path.join(dir, '.5', '.update-cache.json'); + let latest = null; + if (fs.existsSync(cacheFile)) { + try { + const cache = JSON.parse(fs.readFileSync(cacheFile, 'utf8')); + latest = cache.latestAvailableVersion || null; + } catch(e) {} + } + const installed = versionData.packageVersion; + if (latest && installed && compareVersions(installed, latest) < 0) { + updateIndicator = ` | \x1b[33m↑${latest} → /5:update\x1b[0m`; } // Reconfigure check (reads flag file in .5/, gitignored) From e61b43a39cbc2bf88497fcca7d9ec69707d7cdc9 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Mar 2026 19:40:21 +0000 Subject: [PATCH 3/3] add eject command to README Documents /5:eject in the commands table, project structure, and a new Ejecting subsection under Updating. https://claude.ai/code/session_01UTjW3dbQ44EZmQtuNj9GUM --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index a391fc0..7d1490a 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,7 @@ All commands are available under the `/5:` namespace: | `/5:review-code` | 5 | AI-powered code review (Claude or CodeRabbit) | | `/5:address-review-findings` | 5 | Apply annotated findings and address PR comments | | `/5:quick-implement` | Fast | Streamlined workflow for small tasks | +| `/5:eject` | Utility | Permanently remove update infrastructure | | `/5:unlock` | Utility | Remove planning guard lock | ## Configuration @@ -264,6 +265,7 @@ After installation, your `.claude/` directory will contain: │ ├── discuss-feature.md │ ├── quick-implement.md │ ├── configure.md +│ ├── eject.md │ └── unlock.md ├── skills/ # Atomic operations │ ├── build-project/ @@ -364,6 +366,21 @@ npx 5-phase-workflow - User-created commands, agents, skills, hooks, and templates are preserved - Only workflow-managed files are updated +### Ejecting + +If you want to permanently opt out of the update system (e.g., to customize workflow files without future updates overwriting them), run: + +```bash +/5:eject +``` + +This permanently removes the update infrastructure: +- Deletes `check-updates.js` hook, `update.md` and `eject.md` commands +- Deletes `.5/version.json` and `.5/.update-cache.json` +- Removes the update check hook entry from `.claude/settings.json` + +All other workflow files remain untouched. **This is irreversible.** To restore update functionality, reinstall with `npx 5-phase-workflow`. + ## Development ### Running Tests