devflow-kit 1.0.0 → 1.1.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/CHANGELOG.md +30 -0
- package/README.md +13 -6
- package/dist/cli.js +5 -1
- package/dist/commands/ambient.d.ts +18 -0
- package/dist/commands/ambient.js +136 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +97 -10
- package/dist/commands/memory.d.ts +22 -0
- package/dist/commands/memory.js +175 -0
- package/dist/commands/uninstall.js +72 -5
- package/dist/plugins.js +8 -1
- package/dist/utils/post-install.d.ts +12 -0
- package/dist/utils/post-install.js +82 -1
- package/dist/utils/safe-delete-install.d.ts +7 -0
- package/dist/utils/safe-delete-install.js +40 -5
- package/package.json +1 -1
- package/plugins/devflow-ambient/.claude-plugin/plugin.json +7 -0
- package/plugins/devflow-ambient/README.md +49 -0
- package/plugins/devflow-ambient/commands/ambient.md +110 -0
- package/plugins/devflow-ambient/skills/ambient-router/SKILL.md +89 -0
- package/plugins/devflow-ambient/skills/ambient-router/references/skill-catalog.md +64 -0
- package/plugins/devflow-audit-claude/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-code-review/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-core-skills/.claude-plugin/plugin.json +2 -1
- package/plugins/devflow-core-skills/skills/docs-framework/SKILL.md +10 -6
- package/plugins/devflow-core-skills/skills/test-driven-development/SKILL.md +139 -0
- package/plugins/devflow-core-skills/skills/test-driven-development/references/rationalization-prevention.md +111 -0
- package/plugins/devflow-debug/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-implement/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-resolve/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-self-review/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-specify/.claude-plugin/plugin.json +1 -1
- package/scripts/hooks/ambient-prompt.sh +48 -0
- package/scripts/hooks/background-memory-update.sh +49 -8
- package/scripts/hooks/ensure-memory-gitignore.sh +17 -0
- package/scripts/hooks/pre-compact-memory.sh +12 -6
- package/scripts/hooks/session-start-memory.sh +50 -8
- package/scripts/hooks/stop-update-memory.sh +10 -6
- package/shared/skills/ambient-router/SKILL.md +89 -0
- package/shared/skills/ambient-router/references/skill-catalog.md +64 -0
- package/shared/skills/docs-framework/SKILL.md +10 -6
- package/shared/skills/test-driven-development/SKILL.md +139 -0
- package/shared/skills/test-driven-development/references/rationalization-prevention.md +111 -0
- package/src/templates/managed-settings.json +14 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,35 @@ All notable changes to DevFlow will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.1.0] - 2026-03-04
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Ambient mode** — New `devflow-ambient` plugin with `/ambient` command for proportional quality enforcement
|
|
12
|
+
- Intent classification (BUILD/DEBUG/REVIEW/PLAN/EXPLORE/CHAT) auto-loads relevant skills
|
|
13
|
+
- Three depth tiers: QUICK (zero overhead), STANDARD (2-3 skills), ESCALATE (nudge to workflows)
|
|
14
|
+
- Always-on mode via `devflow ambient --enable` or `devflow init --ambient`
|
|
15
|
+
- New `ambient-router` skill for intent/depth classification
|
|
16
|
+
- New `test-driven-development` skill (auto-activates for BUILD tasks)
|
|
17
|
+
- Skills: 24 → 26, Plugins: 8 → 9
|
|
18
|
+
- **Working memory enhancements** — Structured cross-session context preservation
|
|
19
|
+
- Structured sections: Now, Progress, Decisions, Modified Files, Context, Session Log
|
|
20
|
+
- Toggleable via `devflow memory --enable/--disable/--status` or `devflow init --memory/--no-memory`
|
|
21
|
+
- `PROJECT-PATTERNS.md` extraction — background hook accumulates patterns across sessions
|
|
22
|
+
- Directory separation: `.memory/` (session state) vs `.docs/` (reviews/design artifacts)
|
|
23
|
+
- Auto-migration from `.docs/` to `.memory/` with no-clobber semantics
|
|
24
|
+
- Auto-adds `.memory/` and `.docs/` to `.gitignore` on first hook run
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- **Background agent permissions** — Replaced `--dangerously-skip-permissions` with `--tools "Write"` + `--allowedTools` for restricted file access in memory update hooks
|
|
28
|
+
- **Safe-delete auto-upgrade** — `devflow init` now detects outdated safe-delete blocks and silently upgrades them; no manual uninstall/reinstall needed
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- **Ambient depth classification** — Intent now drives depth exclusively; removed 20-word threshold that silently downgraded ~32% of BUILD/DEBUG prompts to QUICK (#73)
|
|
32
|
+
- **Safe-delete file existence** — Filter non-existent files before calling `trash` in bash/zsh, fish, and PowerShell Unix blocks; prevents noisy `trash: file doesn't exist` errors on `rm -f` of missing files (#74)
|
|
33
|
+
- **Safe-delete deny list** — Expanded `rm` deny patterns from 8 to 21, covering `rm -r`, `rm -fr`, and `rm -f` flag variations that could bypass the `rm -rf`-only patterns (#74)
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
8
37
|
## [1.0.0] - 2026-02-25
|
|
9
38
|
|
|
10
39
|
### Added
|
|
@@ -720,6 +749,7 @@ devflow init
|
|
|
720
749
|
|
|
721
750
|
---
|
|
722
751
|
|
|
752
|
+
[1.1.0]: https://github.com/dean0x/devflow/compare/v1.0.0...v1.1.0
|
|
723
753
|
[1.0.0]: https://github.com/dean0x/devflow/compare/v0.9.0...v1.0.0
|
|
724
754
|
[0.9.0]: https://github.com/dean0x/devflow/releases/tag/v0.9.0
|
|
725
755
|
[0.8.1]: https://github.com/dean0x/devflow/releases/tag/v0.8.1
|
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ DevFlow adds structured commands that handle the full lifecycle: specify feature
|
|
|
24
24
|
- **Full-lifecycle implementation** — spec, explore, plan, code, validate, refine in one command
|
|
25
25
|
- **Automatic session memory** — survives restarts, `/clear`, and context compaction
|
|
26
26
|
- **Parallel debugging** — competing hypotheses investigated simultaneously
|
|
27
|
-
- **
|
|
27
|
+
- **26 quality skills** — 12 auto-activating, plus specialized review and agent skills
|
|
28
28
|
|
|
29
29
|
## Quick Start
|
|
30
30
|
|
|
@@ -48,6 +48,7 @@ Then in Claude Code:
|
|
|
48
48
|
| `devflow-resolve` | `/resolve` | Process review issues — fix or defer to tech debt |
|
|
49
49
|
| `devflow-debug` | `/debug` | Parallel hypothesis debugging |
|
|
50
50
|
| `devflow-self-review` | `/self-review` | Self-review workflow (Simplifier + Scrutinizer) |
|
|
51
|
+
| `devflow-ambient` | `/ambient` | Ambient mode — auto-loads relevant skills based on each prompt |
|
|
51
52
|
| `devflow-core-skills` | (auto) | Auto-activating quality enforcement skills |
|
|
52
53
|
|
|
53
54
|
## Command Details
|
|
@@ -116,6 +117,7 @@ The `devflow-core-skills` plugin provides quality enforcement skills that activa
|
|
|
116
117
|
| `git-safety` | Rebasing, force-pushing, merge conflicts |
|
|
117
118
|
| `git-workflow` | Staging files, creating commits, PRs |
|
|
118
119
|
| `github-patterns` | GitHub API operations, PR comments, releases |
|
|
120
|
+
| `test-driven-development` | Implementing new features (RED-GREEN-REFACTOR) |
|
|
119
121
|
| `test-patterns` | Writing or modifying tests |
|
|
120
122
|
| `input-validation` | Creating API endpoints |
|
|
121
123
|
| `typescript` | Working in TypeScript codebases |
|
|
@@ -160,22 +162,25 @@ Three shell hooks run behind the scenes:
|
|
|
160
162
|
|
|
161
163
|
| Hook | When | What |
|
|
162
164
|
|------|------|------|
|
|
163
|
-
| **Stop** | After each response | Updates `.
|
|
165
|
+
| **Stop** | After each response | Updates `.memory/WORKING-MEMORY.md` with current focus, decisions, and progress. Throttled — skips if updated <2 min ago. |
|
|
164
166
|
| **SessionStart** | On startup, `/clear`, resume, compaction | Injects previous working memory + fresh git state as system context. Warns if memory is >1h stale. |
|
|
165
167
|
| **PreCompact** | Before context compaction | Backs up git state to JSON. Bootstraps a minimal working memory from git if none exists yet. |
|
|
166
168
|
|
|
167
|
-
Working memory is **per-project** — scoped to each repo's `.
|
|
169
|
+
Working memory is **per-project** — scoped to each repo's `.memory/` directory. Multiple sessions across different repos don't interfere.
|
|
168
170
|
|
|
169
171
|
## Documentation Structure
|
|
170
172
|
|
|
171
|
-
DevFlow creates project documentation in `.docs/`:
|
|
173
|
+
DevFlow creates project documentation in `.docs/` and working memory in `.memory/`:
|
|
172
174
|
|
|
173
175
|
```
|
|
174
176
|
.docs/
|
|
175
177
|
├── reviews/{branch}/ # Review reports per branch
|
|
176
|
-
|
|
178
|
+
└── design/ # Implementation plans
|
|
179
|
+
|
|
180
|
+
.memory/
|
|
177
181
|
├── WORKING-MEMORY.md # Auto-maintained by Stop hook
|
|
178
|
-
|
|
182
|
+
├── PROJECT-PATTERNS.md # Accumulated patterns across sessions
|
|
183
|
+
└── backup.json # Pre-compact git state snapshot
|
|
179
184
|
```
|
|
180
185
|
|
|
181
186
|
## Workflow Examples
|
|
@@ -209,6 +214,8 @@ Session context is saved and restored automatically via Working Memory hooks —
|
|
|
209
214
|
| `npx devflow-kit init` | Install all plugins |
|
|
210
215
|
| `npx devflow-kit init --plugin=<names>` | Install specific plugin(s) |
|
|
211
216
|
| `npx devflow-kit list` | List available plugins |
|
|
217
|
+
| `npx devflow-kit ambient --enable` | Enable always-on ambient mode |
|
|
218
|
+
| `npx devflow-kit ambient --disable` | Disable ambient mode |
|
|
212
219
|
| `npx devflow-kit uninstall` | Remove DevFlow |
|
|
213
220
|
|
|
214
221
|
### Init Options
|
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,8 @@ import { dirname, join } from 'path';
|
|
|
6
6
|
import { initCommand } from './commands/init.js';
|
|
7
7
|
import { uninstallCommand } from './commands/uninstall.js';
|
|
8
8
|
import { listCommand } from './commands/list.js';
|
|
9
|
+
import { ambientCommand } from './commands/ambient.js';
|
|
10
|
+
import { memoryCommand } from './commands/memory.js';
|
|
9
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
12
|
const __dirname = dirname(__filename);
|
|
11
13
|
// Read version from package.json
|
|
@@ -16,11 +18,13 @@ program
|
|
|
16
18
|
.description('Agentic Development Toolkit for Claude Code\n\nEnhance your AI-assisted development with intelligent commands and workflows.')
|
|
17
19
|
.version(packageJson.version, '-v, --version', 'Display version number')
|
|
18
20
|
.helpOption('-h, --help', 'Display help information')
|
|
19
|
-
.addHelpText('after', '\nExamples:\n $ devflow init Install all DevFlow plugins\n $ devflow init --plugin=implement Install specific plugin\n $ devflow init --plugin=implement,review Install multiple plugins\n $ devflow list List available plugins\n $ devflow uninstall Remove DevFlow from Claude Code\n $ devflow --version Show version\n $ devflow --help Show help\n\nDocumentation:\n https://github.com/dean0x/devflow#readme');
|
|
21
|
+
.addHelpText('after', '\nExamples:\n $ devflow init Install all DevFlow plugins\n $ devflow init --plugin=implement Install specific plugin\n $ devflow init --plugin=implement,review Install multiple plugins\n $ devflow list List available plugins\n $ devflow ambient --enable Enable always-on ambient mode\n $ devflow memory --status Check working memory state\n $ devflow uninstall Remove DevFlow from Claude Code\n $ devflow --version Show version\n $ devflow --help Show help\n\nDocumentation:\n https://github.com/dean0x/devflow#readme');
|
|
20
22
|
// Register commands
|
|
21
23
|
program.addCommand(initCommand);
|
|
22
24
|
program.addCommand(uninstallCommand);
|
|
23
25
|
program.addCommand(listCommand);
|
|
26
|
+
program.addCommand(ambientCommand);
|
|
27
|
+
program.addCommand(memoryCommand);
|
|
24
28
|
// Handle no command
|
|
25
29
|
program.action(() => {
|
|
26
30
|
program.help();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
/**
|
|
3
|
+
* Add the ambient UserPromptSubmit hook to settings JSON.
|
|
4
|
+
* Idempotent — returns unchanged JSON if hook already exists.
|
|
5
|
+
*/
|
|
6
|
+
export declare function addAmbientHook(settingsJson: string, devflowDir: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Remove the ambient UserPromptSubmit hook from settings JSON.
|
|
9
|
+
* Idempotent — returns unchanged JSON if hook not present.
|
|
10
|
+
* Preserves other UserPromptSubmit hooks. Cleans empty arrays/objects.
|
|
11
|
+
*/
|
|
12
|
+
export declare function removeAmbientHook(settingsJson: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Check if the ambient hook is registered in settings JSON.
|
|
15
|
+
*/
|
|
16
|
+
export declare function hasAmbientHook(settingsJson: string): boolean;
|
|
17
|
+
export declare const ambientCommand: Command;
|
|
18
|
+
//# sourceMappingURL=ambient.d.ts.map
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as p from '@clack/prompts';
|
|
5
|
+
import color from 'picocolors';
|
|
6
|
+
import { getClaudeDirectory } from '../utils/paths.js';
|
|
7
|
+
const AMBIENT_HOOK_MARKER = 'ambient-prompt.sh';
|
|
8
|
+
/**
|
|
9
|
+
* Add the ambient UserPromptSubmit hook to settings JSON.
|
|
10
|
+
* Idempotent — returns unchanged JSON if hook already exists.
|
|
11
|
+
*/
|
|
12
|
+
export function addAmbientHook(settingsJson, devflowDir) {
|
|
13
|
+
const settings = JSON.parse(settingsJson);
|
|
14
|
+
if (hasAmbientHook(settingsJson)) {
|
|
15
|
+
return settingsJson;
|
|
16
|
+
}
|
|
17
|
+
if (!settings.hooks) {
|
|
18
|
+
settings.hooks = {};
|
|
19
|
+
}
|
|
20
|
+
const hookCommand = path.join(devflowDir, 'scripts', 'hooks', AMBIENT_HOOK_MARKER);
|
|
21
|
+
const newEntry = {
|
|
22
|
+
hooks: [
|
|
23
|
+
{
|
|
24
|
+
type: 'command',
|
|
25
|
+
command: hookCommand,
|
|
26
|
+
timeout: 5,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
if (!settings.hooks.UserPromptSubmit) {
|
|
31
|
+
settings.hooks.UserPromptSubmit = [];
|
|
32
|
+
}
|
|
33
|
+
settings.hooks.UserPromptSubmit.push(newEntry);
|
|
34
|
+
return JSON.stringify(settings, null, 2) + '\n';
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Remove the ambient UserPromptSubmit hook from settings JSON.
|
|
38
|
+
* Idempotent — returns unchanged JSON if hook not present.
|
|
39
|
+
* Preserves other UserPromptSubmit hooks. Cleans empty arrays/objects.
|
|
40
|
+
*/
|
|
41
|
+
export function removeAmbientHook(settingsJson) {
|
|
42
|
+
const settings = JSON.parse(settingsJson);
|
|
43
|
+
if (!settings.hooks?.UserPromptSubmit) {
|
|
44
|
+
return settingsJson;
|
|
45
|
+
}
|
|
46
|
+
settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit.filter((matcher) => !matcher.hooks.some((h) => h.command.includes(AMBIENT_HOOK_MARKER)));
|
|
47
|
+
if (settings.hooks.UserPromptSubmit.length === 0) {
|
|
48
|
+
delete settings.hooks.UserPromptSubmit;
|
|
49
|
+
}
|
|
50
|
+
if (settings.hooks && Object.keys(settings.hooks).length === 0) {
|
|
51
|
+
delete settings.hooks;
|
|
52
|
+
}
|
|
53
|
+
return JSON.stringify(settings, null, 2) + '\n';
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Check if the ambient hook is registered in settings JSON.
|
|
57
|
+
*/
|
|
58
|
+
export function hasAmbientHook(settingsJson) {
|
|
59
|
+
const settings = JSON.parse(settingsJson);
|
|
60
|
+
if (!settings.hooks?.UserPromptSubmit) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return settings.hooks.UserPromptSubmit.some((matcher) => matcher.hooks.some((h) => h.command.includes(AMBIENT_HOOK_MARKER)));
|
|
64
|
+
}
|
|
65
|
+
export const ambientCommand = new Command('ambient')
|
|
66
|
+
.description('Enable or disable ambient mode (always-on quality enforcement)')
|
|
67
|
+
.option('--enable', 'Register UserPromptSubmit hook for ambient mode')
|
|
68
|
+
.option('--disable', 'Remove ambient mode hook')
|
|
69
|
+
.option('--status', 'Check if ambient mode is enabled')
|
|
70
|
+
.action(async (options) => {
|
|
71
|
+
const hasFlag = options.enable || options.disable || options.status;
|
|
72
|
+
if (!hasFlag) {
|
|
73
|
+
p.intro(color.bgMagenta(color.white(' Ambient Mode ')));
|
|
74
|
+
p.note(`${color.cyan('devflow ambient --enable')} Register always-on hook\n` +
|
|
75
|
+
`${color.cyan('devflow ambient --disable')} Remove always-on hook\n` +
|
|
76
|
+
`${color.cyan('devflow ambient --status')} Check current state`, 'Usage');
|
|
77
|
+
p.outro(color.dim('Or use /ambient <prompt> for one-shot classification'));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const claudeDir = getClaudeDirectory();
|
|
81
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
82
|
+
let settingsContent;
|
|
83
|
+
try {
|
|
84
|
+
settingsContent = await fs.readFile(settingsPath, 'utf-8');
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
if (options.status) {
|
|
88
|
+
p.log.info('Ambient mode: disabled (no settings.json found)');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Create minimal settings.json
|
|
92
|
+
settingsContent = '{}';
|
|
93
|
+
}
|
|
94
|
+
if (options.status) {
|
|
95
|
+
const enabled = hasAmbientHook(settingsContent);
|
|
96
|
+
p.log.info(`Ambient mode: ${enabled ? color.green('enabled') : color.dim('disabled')}`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// Resolve devflow scripts directory from settings.json hooks or default
|
|
100
|
+
let devflowDir;
|
|
101
|
+
try {
|
|
102
|
+
const settings = JSON.parse(settingsContent);
|
|
103
|
+
// Try to extract devflowDir from existing hooks (e.g., Stop hook path)
|
|
104
|
+
const stopHook = settings.hooks?.Stop?.[0]?.hooks?.[0]?.command;
|
|
105
|
+
if (stopHook) {
|
|
106
|
+
// e.g., /Users/dean/.devflow/scripts/hooks/stop-update-memory.sh → /Users/dean/.devflow
|
|
107
|
+
devflowDir = path.resolve(stopHook, '..', '..', '..');
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
devflowDir = path.join(process.env.HOME || '~', '.devflow');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
devflowDir = path.join(process.env.HOME || '~', '.devflow');
|
|
115
|
+
}
|
|
116
|
+
if (options.enable) {
|
|
117
|
+
const updated = addAmbientHook(settingsContent, devflowDir);
|
|
118
|
+
if (updated === settingsContent) {
|
|
119
|
+
p.log.info('Ambient mode already enabled');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
await fs.writeFile(settingsPath, updated, 'utf-8');
|
|
123
|
+
p.log.success('Ambient mode enabled — UserPromptSubmit hook registered');
|
|
124
|
+
p.log.info(color.dim('Relevant skills will now auto-load based on each prompt'));
|
|
125
|
+
}
|
|
126
|
+
if (options.disable) {
|
|
127
|
+
const updated = removeAmbientHook(settingsContent);
|
|
128
|
+
if (updated === settingsContent) {
|
|
129
|
+
p.log.info('Ambient mode already disabled');
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
await fs.writeFile(settingsPath, updated, 'utf-8');
|
|
133
|
+
p.log.success('Ambient mode disabled — hook removed');
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
//# sourceMappingURL=ambient.js.map
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { type PluginDefinition } from '../plugins.js';
|
|
3
3
|
export { substituteSettingsTemplate, computeGitignoreAppend, applyTeamsConfig, stripTeamsConfig, mergeDenyList } from '../utils/post-install.js';
|
|
4
|
+
export { addAmbientHook, removeAmbientHook, hasAmbientHook } from './ambient.js';
|
|
5
|
+
export { addMemoryHooks, removeMemoryHooks, hasMemoryHooks } from './memory.js';
|
|
4
6
|
/**
|
|
5
7
|
* Parse a comma-separated plugin selection string into normalized plugin names.
|
|
6
8
|
* Validates against known plugins; returns invalid names as errors.
|
package/dist/commands/init.js
CHANGED
|
@@ -9,12 +9,16 @@ import { getInstallationPaths } from '../utils/paths.js';
|
|
|
9
9
|
import { getGitRoot } from '../utils/git.js';
|
|
10
10
|
import { isClaudeCliAvailable } from '../utils/cli.js';
|
|
11
11
|
import { installViaCli, installViaFileCopy } from '../utils/installer.js';
|
|
12
|
-
import { installSettings, installManagedSettings, installClaudeignore, updateGitignore, createDocsStructure, } from '../utils/post-install.js';
|
|
12
|
+
import { installSettings, installManagedSettings, installClaudeignore, updateGitignore, createDocsStructure, createMemoryDir, migrateMemoryFiles, } from '../utils/post-install.js';
|
|
13
13
|
import { DEVFLOW_PLUGINS, LEGACY_SKILL_NAMES, LEGACY_COMMAND_NAMES, buildAssetMaps } from '../plugins.js';
|
|
14
14
|
import { detectPlatform, detectShell, getProfilePath, getSafeDeleteInfo, hasSafeDelete } from '../utils/safe-delete.js';
|
|
15
|
-
import { generateSafeDeleteBlock,
|
|
15
|
+
import { generateSafeDeleteBlock, installToProfile, removeFromProfile, getInstalledVersion, SAFE_DELETE_BLOCK_VERSION } from '../utils/safe-delete-install.js';
|
|
16
|
+
import { addAmbientHook } from './ambient.js';
|
|
17
|
+
import { addMemoryHooks, removeMemoryHooks } from './memory.js';
|
|
16
18
|
// Re-export pure functions for tests (canonical source is post-install.ts)
|
|
17
19
|
export { substituteSettingsTemplate, computeGitignoreAppend, applyTeamsConfig, stripTeamsConfig, mergeDenyList } from '../utils/post-install.js';
|
|
20
|
+
export { addAmbientHook, removeAmbientHook, hasAmbientHook } from './ambient.js';
|
|
21
|
+
export { addMemoryHooks, removeMemoryHooks, hasMemoryHooks } from './memory.js';
|
|
18
22
|
const __filename = fileURLToPath(import.meta.url);
|
|
19
23
|
const __dirname = dirname(__filename);
|
|
20
24
|
/**
|
|
@@ -57,6 +61,10 @@ export const initCommand = new Command('init')
|
|
|
57
61
|
.option('--plugin <names>', 'Install specific plugin(s), comma-separated (e.g., implement,code-review)')
|
|
58
62
|
.option('--teams', 'Enable Agent Teams (peer debate, adversarial review)')
|
|
59
63
|
.option('--no-teams', 'Disable Agent Teams (use parallel subagents instead)')
|
|
64
|
+
.option('--ambient', 'Enable ambient mode (auto-loads relevant skills for every prompt)')
|
|
65
|
+
.option('--no-ambient', 'Disable ambient mode')
|
|
66
|
+
.option('--memory', 'Enable working memory (session context preservation)')
|
|
67
|
+
.option('--no-memory', 'Disable working memory hooks')
|
|
60
68
|
.action(async (options) => {
|
|
61
69
|
// Get package version
|
|
62
70
|
const packageJsonPath = path.resolve(__dirname, '../../package.json');
|
|
@@ -152,6 +160,44 @@ export const initCommand = new Command('init')
|
|
|
152
160
|
}
|
|
153
161
|
teamsEnabled = teamsChoice;
|
|
154
162
|
}
|
|
163
|
+
// Ambient mode selection
|
|
164
|
+
let ambientEnabled;
|
|
165
|
+
if (options.ambient !== undefined) {
|
|
166
|
+
ambientEnabled = options.ambient;
|
|
167
|
+
}
|
|
168
|
+
else if (!process.stdin.isTTY) {
|
|
169
|
+
ambientEnabled = false;
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
const ambientChoice = await p.confirm({
|
|
173
|
+
message: 'Enable ambient mode? (auto-loads relevant skills based on each prompt)',
|
|
174
|
+
initialValue: false,
|
|
175
|
+
});
|
|
176
|
+
if (p.isCancel(ambientChoice)) {
|
|
177
|
+
p.cancel('Installation cancelled.');
|
|
178
|
+
process.exit(0);
|
|
179
|
+
}
|
|
180
|
+
ambientEnabled = ambientChoice;
|
|
181
|
+
}
|
|
182
|
+
// Working memory selection (defaults ON — foundational, unlike ambient's false)
|
|
183
|
+
let memoryEnabled;
|
|
184
|
+
if (options.memory !== undefined) {
|
|
185
|
+
memoryEnabled = options.memory;
|
|
186
|
+
}
|
|
187
|
+
else if (!process.stdin.isTTY) {
|
|
188
|
+
memoryEnabled = true;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
const memoryChoice = await p.confirm({
|
|
192
|
+
message: 'Enable working memory? (automatic session context preservation)',
|
|
193
|
+
initialValue: true,
|
|
194
|
+
});
|
|
195
|
+
if (p.isCancel(memoryChoice)) {
|
|
196
|
+
p.cancel('Installation cancelled.');
|
|
197
|
+
process.exit(0);
|
|
198
|
+
}
|
|
199
|
+
memoryEnabled = memoryChoice;
|
|
200
|
+
}
|
|
155
201
|
// Security deny list placement (user scope + TTY only)
|
|
156
202
|
let securityMode = 'user';
|
|
157
203
|
if (scope === 'user' && process.stdin.isTTY) {
|
|
@@ -314,6 +360,41 @@ export const initCommand = new Command('init')
|
|
|
314
360
|
}
|
|
315
361
|
}
|
|
316
362
|
await installSettings(claudeDir, rootDir, devflowDir, verbose, teamsEnabled, effectiveSecurityMode);
|
|
363
|
+
// Install ambient hook if enabled
|
|
364
|
+
if (ambientEnabled) {
|
|
365
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
366
|
+
try {
|
|
367
|
+
const content = await fs.readFile(settingsPath, 'utf-8');
|
|
368
|
+
const updated = addAmbientHook(content, devflowDir);
|
|
369
|
+
if (updated !== content) {
|
|
370
|
+
await fs.writeFile(settingsPath, updated, 'utf-8');
|
|
371
|
+
if (verbose) {
|
|
372
|
+
p.log.success('Ambient mode hook installed');
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
catch { /* settings.json may not exist yet */ }
|
|
377
|
+
}
|
|
378
|
+
// Manage memory hooks based on user choice
|
|
379
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
380
|
+
try {
|
|
381
|
+
const content = await fs.readFile(settingsPath, 'utf-8');
|
|
382
|
+
const updated = memoryEnabled
|
|
383
|
+
? addMemoryHooks(content, devflowDir)
|
|
384
|
+
: removeMemoryHooks(content);
|
|
385
|
+
if (updated !== content) {
|
|
386
|
+
await fs.writeFile(settingsPath, updated, 'utf-8');
|
|
387
|
+
if (verbose) {
|
|
388
|
+
p.log.info(`Working memory ${memoryEnabled ? 'enabled' : 'disabled'}`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
catch { /* settings.json may not exist yet */ }
|
|
393
|
+
// Ensure .memory/ exists when memory is enabled (hooks are no-ops without it)
|
|
394
|
+
if (memoryEnabled) {
|
|
395
|
+
await createMemoryDir(verbose);
|
|
396
|
+
await migrateMemoryFiles(verbose);
|
|
397
|
+
}
|
|
317
398
|
}
|
|
318
399
|
const fileExtras = selectedExtras.filter(e => e !== 'settings' && e !== 'safe-delete');
|
|
319
400
|
if (fileExtras.length > 0) {
|
|
@@ -357,14 +438,20 @@ export const initCommand = new Command('init')
|
|
|
357
438
|
p.log.info(`Then re-run ${color.cyan('devflow init')} to auto-configure safe-delete.`);
|
|
358
439
|
}
|
|
359
440
|
else if (safeDeleteAvailable) {
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
if (
|
|
441
|
+
const trashCmd = safeDeleteInfo.command;
|
|
442
|
+
const block = generateSafeDeleteBlock(shell, process.platform, trashCmd);
|
|
443
|
+
if (block) {
|
|
444
|
+
const installedVersion = await getInstalledVersion(profilePath);
|
|
445
|
+
if (installedVersion === SAFE_DELETE_BLOCK_VERSION) {
|
|
446
|
+
p.log.info(`Safe-delete already configured in ${color.dim(profilePath)}`);
|
|
447
|
+
}
|
|
448
|
+
else if (installedVersion > 0) {
|
|
449
|
+
await removeFromProfile(profilePath);
|
|
450
|
+
await installToProfile(profilePath, block);
|
|
451
|
+
p.log.success(`Safe-delete upgraded in ${color.dim(profilePath)}`);
|
|
452
|
+
p.log.info('Restart your shell or run: ' + color.cyan(`source ${profilePath}`));
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
368
455
|
const confirm = await p.confirm({
|
|
369
456
|
message: `Install safe-delete to ${profilePath}? (overrides rm to use ${trashCmd ?? 'recycle bin'})`,
|
|
370
457
|
initialValue: true,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
/**
|
|
3
|
+
* Add all 3 memory hooks (Stop, SessionStart, PreCompact) to settings JSON.
|
|
4
|
+
* Idempotent — skips hooks that already exist. Returns unchanged JSON if all 3 present.
|
|
5
|
+
*/
|
|
6
|
+
export declare function addMemoryHooks(settingsJson: string, devflowDir: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Remove all memory hooks (Stop, SessionStart, PreCompact) from settings JSON.
|
|
9
|
+
* Idempotent — returns unchanged JSON if no memory hooks present.
|
|
10
|
+
* Preserves non-memory hooks. Cleans empty arrays/objects.
|
|
11
|
+
*/
|
|
12
|
+
export declare function removeMemoryHooks(settingsJson: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Check if ALL 3 memory hooks are registered in settings JSON.
|
|
15
|
+
*/
|
|
16
|
+
export declare function hasMemoryHooks(settingsJson: string): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Count how many of the 3 memory hooks are present (0-3).
|
|
19
|
+
*/
|
|
20
|
+
export declare function countMemoryHooks(settingsJson: string): number;
|
|
21
|
+
export declare const memoryCommand: Command;
|
|
22
|
+
//# sourceMappingURL=memory.d.ts.map
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as p from '@clack/prompts';
|
|
5
|
+
import color from 'picocolors';
|
|
6
|
+
import { getClaudeDirectory, getDevFlowDirectory } from '../utils/paths.js';
|
|
7
|
+
import { createMemoryDir, migrateMemoryFiles } from '../utils/post-install.js';
|
|
8
|
+
/**
|
|
9
|
+
* Map of hook event type → filename marker for the 3 memory hooks.
|
|
10
|
+
*/
|
|
11
|
+
const MEMORY_HOOK_CONFIG = {
|
|
12
|
+
Stop: 'stop-update-memory.sh',
|
|
13
|
+
SessionStart: 'session-start-memory.sh',
|
|
14
|
+
PreCompact: 'pre-compact-memory.sh',
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Add all 3 memory hooks (Stop, SessionStart, PreCompact) to settings JSON.
|
|
18
|
+
* Idempotent — skips hooks that already exist. Returns unchanged JSON if all 3 present.
|
|
19
|
+
*/
|
|
20
|
+
export function addMemoryHooks(settingsJson, devflowDir) {
|
|
21
|
+
const settings = JSON.parse(settingsJson);
|
|
22
|
+
if (hasMemoryHooks(settingsJson)) {
|
|
23
|
+
return settingsJson;
|
|
24
|
+
}
|
|
25
|
+
if (!settings.hooks) {
|
|
26
|
+
settings.hooks = {};
|
|
27
|
+
}
|
|
28
|
+
let changed = false;
|
|
29
|
+
for (const [hookType, marker] of Object.entries(MEMORY_HOOK_CONFIG)) {
|
|
30
|
+
const existing = settings.hooks[hookType] ?? [];
|
|
31
|
+
const alreadyPresent = existing.some((matcher) => matcher.hooks.some((h) => h.command.includes(marker)));
|
|
32
|
+
if (!alreadyPresent) {
|
|
33
|
+
const hookCommand = path.join(devflowDir, 'scripts', 'hooks', marker);
|
|
34
|
+
const newEntry = {
|
|
35
|
+
hooks: [
|
|
36
|
+
{
|
|
37
|
+
type: 'command',
|
|
38
|
+
command: hookCommand,
|
|
39
|
+
timeout: 10,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
if (!settings.hooks[hookType]) {
|
|
44
|
+
settings.hooks[hookType] = [];
|
|
45
|
+
}
|
|
46
|
+
settings.hooks[hookType].push(newEntry);
|
|
47
|
+
changed = true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (!changed) {
|
|
51
|
+
return settingsJson;
|
|
52
|
+
}
|
|
53
|
+
return JSON.stringify(settings, null, 2) + '\n';
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Remove all memory hooks (Stop, SessionStart, PreCompact) from settings JSON.
|
|
57
|
+
* Idempotent — returns unchanged JSON if no memory hooks present.
|
|
58
|
+
* Preserves non-memory hooks. Cleans empty arrays/objects.
|
|
59
|
+
*/
|
|
60
|
+
export function removeMemoryHooks(settingsJson) {
|
|
61
|
+
const settings = JSON.parse(settingsJson);
|
|
62
|
+
if (!settings.hooks) {
|
|
63
|
+
return settingsJson;
|
|
64
|
+
}
|
|
65
|
+
let changed = false;
|
|
66
|
+
for (const [hookType, marker] of Object.entries(MEMORY_HOOK_CONFIG)) {
|
|
67
|
+
if (!settings.hooks[hookType]) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const before = settings.hooks[hookType].length;
|
|
71
|
+
settings.hooks[hookType] = settings.hooks[hookType].filter((matcher) => !matcher.hooks.some((h) => h.command.includes(marker)));
|
|
72
|
+
if (settings.hooks[hookType].length !== before) {
|
|
73
|
+
changed = true;
|
|
74
|
+
}
|
|
75
|
+
if (settings.hooks[hookType].length === 0) {
|
|
76
|
+
delete settings.hooks[hookType];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (settings.hooks && Object.keys(settings.hooks).length === 0) {
|
|
80
|
+
delete settings.hooks;
|
|
81
|
+
}
|
|
82
|
+
if (!changed) {
|
|
83
|
+
return settingsJson;
|
|
84
|
+
}
|
|
85
|
+
return JSON.stringify(settings, null, 2) + '\n';
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Check if ALL 3 memory hooks are registered in settings JSON.
|
|
89
|
+
*/
|
|
90
|
+
export function hasMemoryHooks(settingsJson) {
|
|
91
|
+
return countMemoryHooks(settingsJson) === 3;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Count how many of the 3 memory hooks are present (0-3).
|
|
95
|
+
*/
|
|
96
|
+
export function countMemoryHooks(settingsJson) {
|
|
97
|
+
const settings = JSON.parse(settingsJson);
|
|
98
|
+
if (!settings.hooks) {
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
let count = 0;
|
|
102
|
+
for (const [hookType, marker] of Object.entries(MEMORY_HOOK_CONFIG)) {
|
|
103
|
+
const matchers = settings.hooks[hookType] ?? [];
|
|
104
|
+
if (matchers.some((matcher) => matcher.hooks.some((h) => h.command.includes(marker)))) {
|
|
105
|
+
count++;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return count;
|
|
109
|
+
}
|
|
110
|
+
export const memoryCommand = new Command('memory')
|
|
111
|
+
.description('Enable or disable working memory (session context preservation)')
|
|
112
|
+
.option('--enable', 'Add Stop/SessionStart/PreCompact hooks')
|
|
113
|
+
.option('--disable', 'Remove memory hooks')
|
|
114
|
+
.option('--status', 'Show current state')
|
|
115
|
+
.action(async (options) => {
|
|
116
|
+
const hasFlag = options.enable || options.disable || options.status;
|
|
117
|
+
if (!hasFlag) {
|
|
118
|
+
p.intro(color.bgCyan(color.white(' Working Memory ')));
|
|
119
|
+
p.note(`${color.cyan('devflow memory --enable')} Add memory hooks\n` +
|
|
120
|
+
`${color.cyan('devflow memory --disable')} Remove memory hooks\n` +
|
|
121
|
+
`${color.cyan('devflow memory --status')} Check current state`, 'Usage');
|
|
122
|
+
p.outro(color.dim('Memory hooks provide automatic session context preservation'));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const claudeDir = getClaudeDirectory();
|
|
126
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
127
|
+
let settingsContent;
|
|
128
|
+
try {
|
|
129
|
+
settingsContent = await fs.readFile(settingsPath, 'utf-8');
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
if (options.status) {
|
|
133
|
+
p.log.info('Working memory: disabled (no settings.json found)');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
// Create minimal settings.json
|
|
137
|
+
settingsContent = '{}';
|
|
138
|
+
}
|
|
139
|
+
if (options.status) {
|
|
140
|
+
const count = countMemoryHooks(settingsContent);
|
|
141
|
+
if (count === 3) {
|
|
142
|
+
p.log.info(`Working memory: ${color.green('enabled')} (3/3 hooks)`);
|
|
143
|
+
}
|
|
144
|
+
else if (count === 0) {
|
|
145
|
+
p.log.info(`Working memory: ${color.dim('disabled')}`);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
p.log.info(`Working memory: ${color.yellow(`partial (${count}/3 hooks)`)} — run --enable to fix`);
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const devflowDir = getDevFlowDirectory();
|
|
153
|
+
if (options.enable) {
|
|
154
|
+
const updated = addMemoryHooks(settingsContent, devflowDir);
|
|
155
|
+
if (updated === settingsContent) {
|
|
156
|
+
p.log.info('Working memory already enabled');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
await fs.writeFile(settingsPath, updated, 'utf-8');
|
|
160
|
+
await createMemoryDir(false);
|
|
161
|
+
await migrateMemoryFiles(true);
|
|
162
|
+
p.log.success('Working memory enabled — Stop/SessionStart/PreCompact hooks registered');
|
|
163
|
+
p.log.info(color.dim('Session context will be automatically preserved across conversations'));
|
|
164
|
+
}
|
|
165
|
+
if (options.disable) {
|
|
166
|
+
const updated = removeMemoryHooks(settingsContent);
|
|
167
|
+
if (updated === settingsContent) {
|
|
168
|
+
p.log.info('Working memory already disabled');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
await fs.writeFile(settingsPath, updated, 'utf-8');
|
|
172
|
+
p.log.success('Working memory disabled — hooks removed');
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
//# sourceMappingURL=memory.js.map
|