devflow-kit 2.5.0 → 3.0.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 +73 -0
- package/README.md +44 -19
- package/dist/agents/git.md +13 -15
- package/dist/cli/commands/ambient.js +160 -145
- package/dist/cli/commands/capture.js +29 -55
- package/dist/cli/commands/compliance.js +32 -61
- package/dist/cli/commands/context.js +17 -32
- package/dist/cli/commands/debug.js +65 -26
- package/dist/cli/commands/flags.js +3 -3
- package/dist/cli/commands/hud.js +34 -10
- package/dist/cli/commands/init-seed.js +40 -4
- package/dist/cli/commands/init.js +249 -271
- package/dist/cli/commands/install-report.js +10 -15
- package/dist/cli/commands/knowledge/index.js +1 -1
- package/dist/cli/commands/knowledge/toggle.js +11 -3
- package/dist/cli/commands/learning.js +52 -37
- package/dist/cli/commands/legacy-hooks.js +11 -14
- package/dist/cli/commands/memory.js +67 -78
- package/dist/cli/commands/proxy.js +23 -41
- package/dist/cli/commands/security.js +5 -13
- package/dist/cli/commands/skills.js +21 -3
- package/dist/cli/commands/tracker.js +100 -228
- package/dist/cli/commands/uninstall.js +343 -138
- package/dist/commands/bug-analysis.md +38 -12
- package/dist/commands/code-review.md +70 -21
- package/dist/commands/debug.md +37 -7
- package/dist/commands/dynamic-build.md +66 -17
- package/dist/commands/dynamic-plan.md +19 -8
- package/dist/commands/dynamic-profile.md +24 -10
- package/dist/commands/dynamic-tickets.md +22 -11
- package/dist/commands/explore.md +37 -7
- package/dist/commands/implement.md +96 -32
- package/dist/commands/plan.md +62 -19
- package/dist/commands/release.md +2 -2
- package/dist/commands/research.md +34 -8
- package/dist/commands/resolve.md +65 -17
- package/dist/commands/self-review.md +45 -9
- package/dist/core/compliance-compose.js +27 -27
- package/dist/core/evidence-policy.js +240 -24
- package/dist/core/feature-config.js +94 -25
- package/dist/core/feature-switch.js +1 -1
- package/dist/core/flags.js +30 -2
- package/dist/core/fs-atomic.js +27 -0
- package/dist/core/hook-log-dirs.js +104 -0
- package/dist/core/learning-tuning-config.js +5 -3
- package/dist/core/ledger-root.js +102 -0
- package/dist/core/manifest.js +6 -4
- package/dist/core/mds-variants.js +34 -97
- package/dist/core/migrations.js +49 -23
- package/dist/core/plugins.js +5 -4
- package/dist/core/project-paths.js +0 -17
- package/dist/core/same-location.js +25 -0
- package/dist/core/tracker.js +226 -139
- package/dist/hud/components/config-counts.js +15 -4
- package/dist/hud/components/learning-counts.js +14 -0
- package/dist/hud/config.js +2 -1
- package/dist/hud/cost-history.js +2 -4
- package/dist/hud/git.js +52 -7
- package/dist/hud/index.js +7 -9
- package/dist/skills/git/references/pr/check-merge-readiness.md +1 -1
- package/dist/skills/git/references/pr/ensure-pr-ready.md +1 -1
- package/dist/skills/git/references/pr/update-pr-evidence.md +1 -1
- package/dist/skills/git/references/tracker/_mcp.md +1 -1
- package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +1 -1
- package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +1 -1
- package/dist/skills/git/references/tracker/github/gather-release-evidence.md +2 -2
- package/dist/skills/git/references/tracker/github/manage-debt.md +3 -3
- package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +1 -1
- package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +1 -1
- package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +2 -2
- package/dist/skills/git/references/tracker/jira/manage-debt.md +1 -1
- package/dist/skills/git/references/tracker/jira/post-wave-report.md +1 -1
- package/dist/skills/git/references/tracker/jira/setup-task.md +1 -1
- package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +1 -1
- package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +1 -1
- package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +2 -2
- package/dist/skills/git/references/tracker/linear/manage-debt.md +1 -1
- package/dist/skills/git/references/tracker/linear/post-wave-report.md +1 -1
- package/dist/skills/git/references/tracker/linear/setup-task.md +1 -1
- package/dist/targets/claude-code/claude-paths.js +59 -57
- package/dist/targets/claude-code/compliance-install.js +49 -65
- package/dist/targets/claude-code/hooks.js +108 -3
- package/dist/targets/claude-code/installer.js +30 -57
- package/dist/targets/claude-code/post-install.js +232 -139
- package/dist/targets/claude-code/tracker-install.js +38 -65
- package/package.json +5 -4
- package/src/assets/agents/code.md +4 -3
- package/src/assets/agents/design.md +1 -0
- package/src/assets/agents/git.mds +55 -57
- package/src/assets/agents/knowledge.md +2 -2
- package/src/assets/agents/review.md +3 -1
- package/src/assets/agents/tracker.md +37 -30
- package/src/assets/commands/_partials/_compliance.mds +19 -1
- package/src/assets/commands/_partials/_decisions.mds +15 -3
- package/src/assets/commands/_partials/_docs_root.mds +35 -0
- package/src/assets/commands/_partials/_engine.mds +2 -2
- package/src/assets/commands/_partials/_evidence_policy.mds +3 -3
- package/src/assets/commands/_partials/_factory.mds +1 -1
- package/src/assets/commands/_partials/_knowledge.mds +27 -9
- package/src/assets/commands/_partials/_plan_contract.mds +2 -2
- package/src/assets/commands/_partials/_preamble.mds +1 -1
- package/src/assets/commands/_partials/_publication.mds +6 -2
- package/src/assets/commands/_partials/_settings.mds +28 -0
- package/src/assets/commands/_partials/_ticket_template.mds +3 -3
- package/src/assets/commands/_partials/_tracker.mds +4 -4
- package/src/assets/commands/_partials/_wave.mds +4 -4
- package/src/assets/commands/bug-analysis.mds +19 -17
- package/src/assets/commands/code-review.mds +39 -33
- package/src/assets/commands/debug.mds +4 -5
- package/src/assets/commands/dynamic-build.mds +75 -53
- package/src/assets/commands/dynamic-plan.mds +20 -15
- package/src/assets/commands/dynamic-profile.mds +24 -11
- package/src/assets/commands/dynamic-tickets.mds +25 -20
- package/src/assets/commands/explore.mds +4 -5
- package/src/assets/commands/implement.mds +58 -45
- package/src/assets/commands/plan.mds +34 -29
- package/src/assets/commands/release.md +2 -2
- package/src/assets/commands/research.mds +11 -9
- package/src/assets/commands/resolve.mds +41 -39
- package/src/assets/commands/self-review.mds +24 -25
- package/src/assets/mds/git/_pr.mds +61 -61
- package/src/assets/mds/git/_references.mds +19 -19
- package/src/assets/mds/tracker/_common.mds +8 -8
- package/src/assets/mds/tracker/_github.mds +71 -71
- package/src/assets/mds/tracker/_jira.mds +74 -74
- package/src/assets/mds/tracker/_linear.mds +75 -75
- package/src/assets/mds/tracker/_mcp.mds +23 -17
- package/src/assets/scripts/hooks/background-memory-update +35 -19
- package/src/assets/scripts/hooks/capture-prompt +18 -12
- package/src/assets/scripts/hooks/capture-question +18 -12
- package/src/assets/scripts/hooks/capture-turn +27 -17
- package/src/assets/scripts/hooks/debug-trace +11 -6
- package/src/assets/scripts/hooks/ensure-devflow-init +33 -6
- package/src/assets/scripts/hooks/ensure-proxy +9 -8
- package/src/assets/scripts/hooks/ensure-root-gitignore +111 -36
- package/src/assets/scripts/hooks/git-marker +48 -0
- package/src/assets/scripts/hooks/json-helper.cjs +6 -1
- package/src/assets/scripts/hooks/lib/project-paths.cjs +0 -19
- package/src/assets/scripts/hooks/log-paths +80 -0
- package/src/assets/scripts/hooks/memory-worker +17 -15
- package/src/assets/scripts/hooks/pre-compact-memory +41 -16
- package/src/assets/scripts/hooks/queue-append +104 -30
- package/src/assets/scripts/hooks/resolve-project-root +101 -7
- package/src/assets/scripts/hooks/session-start-context +289 -122
- package/src/assets/scripts/hooks/session-start-memory +35 -16
- package/src/assets/scripts/lib/project-config.cjs +633 -0
- package/src/assets/scripts/resolve-evidence-policy.cjs +300 -220
- package/src/assets/scripts/resolve-settings.cjs +1054 -0
- package/src/assets/scripts/verify-evidence.cjs +1 -1
- package/src/assets/skills/compliance/SKILL.md +2 -2
- package/src/assets/skills/docs-framework/SKILL.md +6 -7
- package/src/assets/skills/docs-framework/references/patterns.md +10 -17
- package/src/assets/skills/gap-analysis/SKILL.md +2 -2
- package/src/assets/skills/git/references/github-api.md +9 -9
- package/src/assets/skills/git/references/patterns.md +1 -1
- package/src/assets/skills/worktree-support/SKILL.md +1 -1
- package/src/assets/skills/worktree-support/references/roots.md +29 -0
- package/src/targets/claude-code/templates/managed-settings.json +25 -9
|
@@ -1,63 +1,47 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { promises as fs } from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
-
import * as os from 'os';
|
|
5
4
|
import * as p from '@clack/prompts';
|
|
6
5
|
import color from 'picocolors';
|
|
7
6
|
import { getClaudeDirectory, getDevFlowDirectory } from '../../targets/claude-code/claude-paths.js';
|
|
8
7
|
import { syncManifestFeature } from '../../core/manifest.js';
|
|
9
|
-
import {
|
|
8
|
+
import { writeSettingsFileAtomic } from '../../core/fs-atomic.js';
|
|
9
|
+
import { HOOKS_DIR_SUFFIX, endsWithAny, ensureHook, hasHook, removeHooks, runHookCommand, runHookSuffix, } from '../../targets/claude-code/hooks.js';
|
|
10
10
|
const PREAMBLE_HOOK_MARKER = 'preamble';
|
|
11
|
-
const LEGACY_HOOK_MARKER = 'ambient-prompt';
|
|
12
|
-
/** Stale marker from previous installs — cleaned on disable/re-enable */
|
|
13
|
-
const CLASSIFICATION_HOOK_MARKER = 'session-start-classification';
|
|
14
11
|
/** SessionStart orchestrator charter hook — presence-gated by ambient toggle */
|
|
15
12
|
const ORCHESTRATOR_HOOK_MARKER = 'session-start-orchestrator';
|
|
13
|
+
/**
|
|
14
|
+
* The command endings of each ambient hook devflow has ever registered.
|
|
15
|
+
*
|
|
16
|
+
* D-AMBIENT-EXACT-HOOK — the ambient instance of D-EXACT-HOOK-OWNER (hooks.ts): a
|
|
17
|
+
* hook is devflow's when its command ENDS in one of these, under any directory — so
|
|
18
|
+
* installs made under a custom or repo-local devflow directory are still recognised —
|
|
19
|
+
* and never because it merely contains a marker word: a user's
|
|
20
|
+
* `~/bin/preamble-logger.sh` or `echo preamble` is theirs (applies ADR-024). The
|
|
21
|
+
* legacy forms are the pre-preamble `ambient-prompt` hook (first a bare
|
|
22
|
+
* `ambient-prompt.sh`, then through `run-hook`) and the retired
|
|
23
|
+
* `session-start-classification` hook, both still swept on enable and disable.
|
|
24
|
+
*/
|
|
25
|
+
const AMBIENT_HOOK_SUFFIXES = {
|
|
26
|
+
preamble: [runHookSuffix(PREAMBLE_HOOK_MARKER)],
|
|
27
|
+
legacyPrompt: [runHookSuffix('ambient-prompt'), `${HOOKS_DIR_SUFFIX}ambient-prompt.sh`],
|
|
28
|
+
classification: [runHookSuffix('session-start-classification')],
|
|
29
|
+
orchestrator: [runHookSuffix(ORCHESTRATOR_HOOK_MARKER)],
|
|
30
|
+
};
|
|
31
|
+
const isPreamble = endsWithAny(AMBIENT_HOOK_SUFFIXES.preamble);
|
|
32
|
+
const isLegacy = endsWithAny(AMBIENT_HOOK_SUFFIXES.legacyPrompt);
|
|
33
|
+
const isAmbient = endsWithAny([...AMBIENT_HOOK_SUFFIXES.preamble, ...AMBIENT_HOOK_SUFFIXES.legacyPrompt]);
|
|
34
|
+
const isClassification = endsWithAny(AMBIENT_HOOK_SUFFIXES.classification);
|
|
35
|
+
const isOrchestrator = endsWithAny(AMBIENT_HOOK_SUFFIXES.orchestrator);
|
|
16
36
|
/**
|
|
17
37
|
* Path where the legacy commands rule was installed.
|
|
18
38
|
* The commands rule was removed — this path now exists only to purge the
|
|
19
39
|
* legacy file from prior installs. Managed by ambient.ts directly (not the
|
|
20
40
|
* plugin rules system), so only ambient enable/disable/init paths clean it up.
|
|
41
|
+
* Resolved under the Claude Code directory (D-CLAUDE-CONFIG-DIR), where the
|
|
42
|
+
* legacy install wrote it.
|
|
21
43
|
*/
|
|
22
|
-
export const COMMANDS_RULE_PATH = path.join(
|
|
23
|
-
/** Filter hook entries from a parsed Settings object for a given event. Returns true if any were removed. */
|
|
24
|
-
function filterHookEntries(settings, eventName, shouldRemove) {
|
|
25
|
-
if (!settings.hooks?.[eventName])
|
|
26
|
-
return false;
|
|
27
|
-
const before = settings.hooks[eventName].length;
|
|
28
|
-
settings.hooks[eventName] = settings.hooks[eventName].filter((matcher) => !shouldRemove(matcher));
|
|
29
|
-
if (settings.hooks[eventName].length === before)
|
|
30
|
-
return false;
|
|
31
|
-
if (settings.hooks[eventName].length === 0) {
|
|
32
|
-
delete settings.hooks[eventName];
|
|
33
|
-
}
|
|
34
|
-
if (Object.keys(settings.hooks).length === 0) {
|
|
35
|
-
delete settings.hooks;
|
|
36
|
-
}
|
|
37
|
-
return true;
|
|
38
|
-
}
|
|
39
|
-
/** Add a hook entry for an event if the marker is not already present. Returns true when an entry was added. */
|
|
40
|
-
function ensureHook(settings, eventName, marker, entry) {
|
|
41
|
-
if (settings.hooks?.[eventName]?.some((m) => m.hooks.some((h) => h.command.includes(marker)))) {
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
settings.hooks ??= {};
|
|
45
|
-
settings.hooks[eventName] ??= [];
|
|
46
|
-
settings.hooks[eventName].push(entry);
|
|
47
|
-
return true;
|
|
48
|
-
}
|
|
49
|
-
function isLegacy(matcher) {
|
|
50
|
-
return matcher.hooks.some((h) => h.command.includes(LEGACY_HOOK_MARKER));
|
|
51
|
-
}
|
|
52
|
-
function isAmbient(matcher) {
|
|
53
|
-
return matcher.hooks.some((h) => h.command.includes(PREAMBLE_HOOK_MARKER) || h.command.includes(LEGACY_HOOK_MARKER));
|
|
54
|
-
}
|
|
55
|
-
function isClassification(matcher) {
|
|
56
|
-
return matcher.hooks.some((h) => h.command.includes(CLASSIFICATION_HOOK_MARKER));
|
|
57
|
-
}
|
|
58
|
-
function isOrchestrator(matcher) {
|
|
59
|
-
return matcher.hooks.some((h) => h.command.includes(ORCHESTRATOR_HOOK_MARKER));
|
|
60
|
-
}
|
|
44
|
+
export const COMMANDS_RULE_PATH = path.join(getClaudeDirectory(), 'rules', 'devflow', 'commands.md');
|
|
61
45
|
/**
|
|
62
46
|
* Remove the legacy commands awareness rule file left by prior installs.
|
|
63
47
|
* Idempotent — no-op if the file does not exist.
|
|
@@ -75,23 +59,44 @@ export async function removeLegacyCommandsRule() {
|
|
|
75
59
|
// filesystem. Neither should abort the caller's primary operation.
|
|
76
60
|
}
|
|
77
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* A predicate matching devflow's own hook (`isOurs`) registered with any command
|
|
64
|
+
* other than `canonical` — i.e. under a directory other than the one `canonical` names.
|
|
65
|
+
*/
|
|
66
|
+
function isMisdirected(isOurs, canonical) {
|
|
67
|
+
return (hook) => isOurs(hook) && (hook.command ?? '').trim() !== canonical;
|
|
68
|
+
}
|
|
78
69
|
/**
|
|
79
70
|
* Add the ambient hooks (preamble UserPromptSubmit + session-start-orchestrator SessionStart)
|
|
80
71
|
* and remove any legacy commands rule. Removes any legacy `ambient-prompt` hook first.
|
|
81
72
|
* Idempotent — each hook is checked before adding so enable repairs partial states.
|
|
82
73
|
* Legacy rule purge runs unconditionally to ensure stale files are always cleaned up.
|
|
74
|
+
*
|
|
75
|
+
* D-AMBIENT-CANONICAL-DIR: enable converges on `devflowDir`. A preamble or
|
|
76
|
+
* orchestrator hook devflow registered under another directory (an earlier enable
|
|
77
|
+
* that inferred its directory from a user's Stop hook, or a retired custom
|
|
78
|
+
* directory) is removed and the hook re-registered at `devflowDir`. Only hooks
|
|
79
|
+
* matched exactly (D-AMBIENT-EXACT-HOOK) are touched, one hook at a time, so the
|
|
80
|
+
* user's hooks and their matcher-group siblings keep their places.
|
|
83
81
|
*/
|
|
84
82
|
export async function addAmbientHook(settingsJson, devflowDir) {
|
|
85
83
|
const settings = JSON.parse(settingsJson);
|
|
86
|
-
const
|
|
84
|
+
const preambleCommand = runHookCommand(devflowDir, PREAMBLE_HOOK_MARKER);
|
|
85
|
+
const orchestratorCommand = runHookCommand(devflowDir, ORCHESTRATOR_HOOK_MARKER);
|
|
86
|
+
const removedLegacy = removeHooks(settings, 'UserPromptSubmit', isLegacy);
|
|
87
87
|
// Sweep stale classification hook from prior installs — symmetric with removeAmbientHook
|
|
88
|
-
const removedClassification =
|
|
89
|
-
const
|
|
90
|
-
|
|
88
|
+
const removedClassification = removeHooks(settings, 'SessionStart', isClassification);
|
|
89
|
+
const removedMisdirected = [
|
|
90
|
+
removeHooks(settings, 'UserPromptSubmit', isMisdirected(isPreamble, preambleCommand)),
|
|
91
|
+
removeHooks(settings, 'SessionStart', isMisdirected(isOrchestrator, orchestratorCommand)),
|
|
92
|
+
].some(Boolean);
|
|
93
|
+
const addedPreamble = ensureHook(settings, 'UserPromptSubmit', isPreamble, { hooks: [{ type: 'command', command: preambleCommand, timeout: 5 }] });
|
|
94
|
+
const addedOrchestrator = ensureHook(settings, 'SessionStart', isOrchestrator, { hooks: [{ type: 'command', command: orchestratorCommand, timeout: 10 }] });
|
|
91
95
|
// Purge legacy commands rule (runs before early-return so stale files are always removed)
|
|
92
96
|
await removeLegacyCommandsRule();
|
|
93
|
-
if (!removedLegacy && !removedClassification && !addedPreamble && !addedOrchestrator)
|
|
97
|
+
if (!removedLegacy && !removedClassification && !removedMisdirected && !addedPreamble && !addedOrchestrator) {
|
|
94
98
|
return settingsJson;
|
|
99
|
+
}
|
|
95
100
|
return JSON.stringify(settings, null, 2) + '\n';
|
|
96
101
|
}
|
|
97
102
|
/**
|
|
@@ -99,22 +104,38 @@ export async function addAmbientHook(settingsJson, devflowDir) {
|
|
|
99
104
|
* Removes preamble + legacy from UserPromptSubmit.
|
|
100
105
|
* Removes session-start-orchestrator from SessionStart.
|
|
101
106
|
* Also removes stale SessionStart classification hook from previous installs.
|
|
102
|
-
* Purges legacy COMMANDS_RULE_PATH if present (runs before early-return)
|
|
107
|
+
* Purges legacy COMMANDS_RULE_PATH if present (runs before early-return), unless
|
|
108
|
+
* `options.purgeLegacyRule` is false — a legacy repo-local uninstall edits a repo's
|
|
109
|
+
* settings and must not touch the user's Claude directory (D-LEGACY-LOCAL-CLEANUP).
|
|
103
110
|
* Idempotent — returns unchanged JSON if no ambient hooks were present.
|
|
104
111
|
* Preserves other hooks. Cleans empty arrays/objects.
|
|
105
112
|
*/
|
|
106
|
-
export async function removeAmbientHook(settingsJson) {
|
|
113
|
+
export async function removeAmbientHook(settingsJson, options = {}) {
|
|
107
114
|
const settings = JSON.parse(settingsJson);
|
|
108
|
-
const removedPrompt =
|
|
109
|
-
const removedOrchestrator =
|
|
115
|
+
const removedPrompt = removeHooks(settings, 'UserPromptSubmit', isAmbient);
|
|
116
|
+
const removedOrchestrator = removeHooks(settings, 'SessionStart', isOrchestrator);
|
|
110
117
|
// Clean up stale classification hooks from previous installs (no longer registered)
|
|
111
|
-
const removedClassification =
|
|
118
|
+
const removedClassification = removeHooks(settings, 'SessionStart', isClassification);
|
|
112
119
|
// Purge legacy commands rule (runs before early-return so stale files are always removed)
|
|
113
|
-
|
|
120
|
+
if (options.purgeLegacyRule !== false)
|
|
121
|
+
await removeLegacyCommandsRule();
|
|
114
122
|
if (!removedPrompt && !removedOrchestrator && !removedClassification)
|
|
115
123
|
return settingsJson;
|
|
116
124
|
return JSON.stringify(settings, null, 2) + '\n';
|
|
117
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Converge the ambient hooks in a settings JSON string to `enabled`.
|
|
128
|
+
*
|
|
129
|
+
* The one ambient transform `devflow init` applies inside its single settings
|
|
130
|
+
* read-modify-write pass: always remove-then-add, which upgrades a legacy
|
|
131
|
+
* `ambient-prompt` hook and re-points devflow's hooks at `devflowDir`. Both halves
|
|
132
|
+
* match hooks exactly (D-AMBIENT-EXACT-HOOK), so a user's own hooks — and their
|
|
133
|
+
* siblings in a shared matcher group — come through byte-identical.
|
|
134
|
+
*/
|
|
135
|
+
export async function convergeAmbientHooks(settingsJson, enabled, devflowDir) {
|
|
136
|
+
const cleaned = await removeAmbientHook(settingsJson);
|
|
137
|
+
return enabled ? addAmbientHook(cleaned, devflowDir) : cleaned;
|
|
138
|
+
}
|
|
118
139
|
/**
|
|
119
140
|
* Check if the ambient hook (legacy or current) is registered in settings JSON or parsed Settings object.
|
|
120
141
|
* Preamble-authoritative: returns true iff the UserPromptSubmit preamble hook is present.
|
|
@@ -122,110 +143,104 @@ export async function removeAmbientHook(settingsJson) {
|
|
|
122
143
|
*/
|
|
123
144
|
export function hasAmbientHook(input) {
|
|
124
145
|
const settings = typeof input === 'string' ? JSON.parse(input) : input;
|
|
125
|
-
return settings
|
|
146
|
+
return hasHook(settings, 'UserPromptSubmit', isAmbient);
|
|
126
147
|
}
|
|
127
148
|
/**
|
|
128
149
|
* Check if the orchestrator SessionStart hook is present in settings JSON or parsed Settings object.
|
|
129
150
|
*/
|
|
130
151
|
function hasOrchestratorHook(input) {
|
|
131
152
|
const settings = typeof input === 'string' ? JSON.parse(input) : input;
|
|
132
|
-
return settings
|
|
153
|
+
return hasHook(settings, 'SessionStart', isOrchestrator);
|
|
133
154
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
settingsContent = await fs.readFile(settingsPath, 'utf-8');
|
|
153
|
-
}
|
|
154
|
-
catch (err) {
|
|
155
|
-
if (err.code !== 'ENOENT')
|
|
156
|
-
throw err;
|
|
157
|
-
if (options.status) {
|
|
158
|
-
p.log.info('Ambient mode: disabled (no settings.json found)');
|
|
155
|
+
/**
|
|
156
|
+
* Build a fresh Commander Command for the `ambient` subcommand.
|
|
157
|
+
* Exported for tests that need per-test isolation (Commander keeps parsed option
|
|
158
|
+
* values on the instance, so a reused command leaks options between runs).
|
|
159
|
+
*/
|
|
160
|
+
export function createAmbientCommand() {
|
|
161
|
+
return new Command('ambient')
|
|
162
|
+
.description('Enable or disable ambient mode (orchestrator charter + plan handoff)')
|
|
163
|
+
.option('--enable', 'Register ambient mode hooks')
|
|
164
|
+
.option('--disable', 'Remove ambient mode hooks')
|
|
165
|
+
.option('--status', 'Check if ambient mode is enabled')
|
|
166
|
+
.action(async (options) => {
|
|
167
|
+
const hasFlag = options.enable || options.disable || options.status;
|
|
168
|
+
if (!hasFlag) {
|
|
169
|
+
p.intro(color.bgMagenta(color.white(' Ambient Mode ')));
|
|
170
|
+
p.note(`${color.cyan('devflow ambient --enable')} Register orchestrator hooks\n` +
|
|
171
|
+
`${color.cyan('devflow ambient --disable')} Remove orchestrator hooks\n` +
|
|
172
|
+
`${color.cyan('devflow ambient --status')} Check current state`, 'Usage');
|
|
159
173
|
return;
|
|
160
174
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
let parsedSettings;
|
|
167
|
-
try {
|
|
168
|
-
parsedSettings = JSON.parse(settingsContent);
|
|
169
|
-
}
|
|
170
|
-
catch (err) {
|
|
171
|
-
p.log.error(`Could not parse settings.json: ${err.message}`);
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
if (options.status) {
|
|
175
|
-
const enabled = hasAmbientHook(parsedSettings);
|
|
176
|
-
const hasOrchestrator = hasOrchestratorHook(parsedSettings);
|
|
177
|
-
const repairHint = enabled !== hasOrchestrator
|
|
178
|
-
? ` ${color.dim('(partial — run devflow ambient --enable to repair)')}`
|
|
179
|
-
: '';
|
|
180
|
-
if (enabled) {
|
|
181
|
-
p.log.info(`Ambient mode: ${color.green('enabled')}${repairHint}`);
|
|
175
|
+
const claudeDir = getClaudeDirectory();
|
|
176
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
177
|
+
let settingsContent;
|
|
178
|
+
try {
|
|
179
|
+
settingsContent = await fs.readFile(settingsPath, 'utf-8');
|
|
182
180
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
// Primary: getDevFlowDirectory() — purpose-built, not coupled to hook path layout.
|
|
190
|
-
// Fallback: infer from Stop hook command path (legacy installs where getDevFlowDirectory
|
|
191
|
-
// may not yet reflect the correct location).
|
|
192
|
-
let devflowDir = getDevFlowDirectory();
|
|
193
|
-
try {
|
|
194
|
-
const stopHook = parsedSettings.hooks?.Stop?.[0]?.hooks?.[0]?.command;
|
|
195
|
-
if (stopHook) {
|
|
196
|
-
const hookBinary = stopHook.split(' ')[0];
|
|
197
|
-
const inferred = path.resolve(hookBinary, '..', '..', '..');
|
|
198
|
-
// Only use inferred path when it differs from the canonical default —
|
|
199
|
-
// this handles legacy installs where the hook was installed to a non-standard location.
|
|
200
|
-
if (inferred !== devflowDir) {
|
|
201
|
-
devflowDir = inferred;
|
|
181
|
+
catch (err) {
|
|
182
|
+
if (err.code !== 'ENOENT')
|
|
183
|
+
throw err;
|
|
184
|
+
if (options.status) {
|
|
185
|
+
p.log.info('Ambient mode: disabled (no settings.json found)');
|
|
186
|
+
return;
|
|
202
187
|
}
|
|
188
|
+
// Create minimal settings.json
|
|
189
|
+
settingsContent = '{}';
|
|
203
190
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
p.log.info('Ambient mode already enabled');
|
|
191
|
+
// Parse settings once, guarded against corrupt files.
|
|
192
|
+
// hasAmbientHook / hasOrchestratorHook accept Settings directly to avoid re-parsing.
|
|
193
|
+
let parsedSettings;
|
|
194
|
+
try {
|
|
195
|
+
parsedSettings = JSON.parse(settingsContent);
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
p.log.error(`Could not parse settings.json: ${err.message}`);
|
|
213
199
|
return;
|
|
214
200
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
201
|
+
if (options.status) {
|
|
202
|
+
const enabled = hasAmbientHook(parsedSettings);
|
|
203
|
+
const hasOrchestrator = hasOrchestratorHook(parsedSettings);
|
|
204
|
+
const repairHint = enabled !== hasOrchestrator
|
|
205
|
+
? ` ${color.dim('(partial — run devflow ambient --enable to repair)')}`
|
|
206
|
+
: '';
|
|
207
|
+
if (enabled) {
|
|
208
|
+
p.log.info(`Ambient mode: ${color.green('enabled')}${repairHint}`);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
p.log.info(`Ambient mode: ${color.dim('disabled')}${repairHint}`);
|
|
212
|
+
}
|
|
224
213
|
return;
|
|
225
214
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
215
|
+
// D-AMBIENT-CANONICAL-DIR: the hooks always point at the canonical devflow
|
|
216
|
+
// directory, where init installs run-hook. Never infer it from settings.json:
|
|
217
|
+
// the first Stop hook is whichever hook the user listed first (a notification
|
|
218
|
+
// sound, say), and a path derived from it names a run-hook that does not
|
|
219
|
+
// exist, so every prompt would fail.
|
|
220
|
+
const devflowDir = getDevFlowDirectory();
|
|
221
|
+
if (options.enable) {
|
|
222
|
+
const updated = await addAmbientHook(settingsContent, devflowDir);
|
|
223
|
+
if (updated === settingsContent) {
|
|
224
|
+
// Both hooks already present — addAmbientHook purges any legacy rule anyway
|
|
225
|
+
p.log.info('Ambient mode already enabled');
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
await writeSettingsFileAtomic(settingsPath, updated);
|
|
229
|
+
await syncManifestFeature(devflowDir, 'ambient', true);
|
|
230
|
+
p.log.success('Ambient mode enabled — orchestrator hooks registered');
|
|
231
|
+
p.log.info(color.dim('Charter at session start, reminder per prompt, plan handoffs auto-run devflow:implement (git repos only)'));
|
|
232
|
+
}
|
|
233
|
+
if (options.disable) {
|
|
234
|
+
const updated = await removeAmbientHook(settingsContent);
|
|
235
|
+
if (updated === settingsContent) {
|
|
236
|
+
p.log.info('Ambient mode already disabled');
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
await writeSettingsFileAtomic(settingsPath, updated);
|
|
240
|
+
await syncManifestFeature(devflowDir, 'ambient', false);
|
|
241
|
+
p.log.success('Ambient mode disabled — hooks removed');
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
export const ambientCommand = createAmbientCommand();
|
|
231
246
|
//# sourceMappingURL=ambient.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { devflowHookOwner, ensureHook, hasHook, removeHooks, runHookCommand, } from '../../targets/claude-code/hooks.js';
|
|
2
2
|
// ─── Capture hook utilities ────────────────────────────────────────────────
|
|
3
3
|
//
|
|
4
4
|
// The capture bundle (capture-prompt, capture-turn, capture-question) is
|
|
@@ -9,19 +9,20 @@ import * as path from 'path';
|
|
|
9
9
|
// queue-append's queue_read_gates). Follows the context.ts add/remove/has
|
|
10
10
|
// pattern rather than memory.ts's toggle pattern.
|
|
11
11
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
12
|
+
// Stop-event concurrency contract: Claude Code runs the hooks of one event in
|
|
13
|
+
// parallel, so array position in settings.json orders nothing at run time —
|
|
14
|
+
// memory-worker can spawn background-memory-update before capture-turn has
|
|
15
|
+
// appended this turn's assistant row. The worker tolerates that: a queue that
|
|
16
|
+
// holds only user rows is left in place and the LLM run skipped
|
|
17
|
+
// (D-QUEUE-NO-ORPHAN-DELETE in background-memory-update), so the next run
|
|
18
|
+
// takes the whole turn. init.ts still registers the capture bundle before the
|
|
19
|
+
// memory bundle, which keeps settings.json stable across re-inits.
|
|
19
20
|
const CAPTURE_PROMPT_MARKER = 'capture-prompt';
|
|
20
21
|
const CAPTURE_TURN_MARKER = 'capture-turn';
|
|
21
22
|
const CAPTURE_QUESTION_MARKER = 'capture-question';
|
|
22
23
|
const CAPTURE_QUESTION_MATCHER = 'AskUserQuestion';
|
|
23
24
|
/**
|
|
24
|
-
* Map of hook event type →
|
|
25
|
+
* Map of hook event type → run-hook marker for the capture hooks.
|
|
25
26
|
* Three hooks total: UserPromptSubmit, Stop, PostToolUse (matcher-scoped).
|
|
26
27
|
*/
|
|
27
28
|
const CAPTURE_HOOK_CONFIG = {
|
|
@@ -29,6 +30,15 @@ const CAPTURE_HOOK_CONFIG = {
|
|
|
29
30
|
Stop: CAPTURE_TURN_MARKER,
|
|
30
31
|
PostToolUse: CAPTURE_QUESTION_MARKER,
|
|
31
32
|
};
|
|
33
|
+
/**
|
|
34
|
+
* D-EXACT-HOOK-OWNER: a capture hook is devflow's only when its command ends in
|
|
35
|
+
* `/scripts/hooks/run-hook <marker>` (hooks.ts), under any directory. The capture
|
|
36
|
+
* hooks have been registered through run-hook since they first shipped, so there
|
|
37
|
+
* is no legacy form to recognise.
|
|
38
|
+
*/
|
|
39
|
+
function isCaptureHook(marker) {
|
|
40
|
+
return devflowHookOwner([marker]);
|
|
41
|
+
}
|
|
32
42
|
/**
|
|
33
43
|
* Add all 3 capture hooks (UserPromptSubmit, Stop, PostToolUse) to settings JSON.
|
|
34
44
|
* Idempotent — skips hooks that already exist. Returns unchanged JSON if all 3 present.
|
|
@@ -40,31 +50,14 @@ export function addCaptureHooks(settingsJson, devflowDir) {
|
|
|
40
50
|
if (hasCaptureHooks(settings)) {
|
|
41
51
|
return settingsJson;
|
|
42
52
|
}
|
|
43
|
-
if (!settings.hooks) {
|
|
44
|
-
settings.hooks = {};
|
|
45
|
-
}
|
|
46
53
|
for (const [hookType, marker] of Object.entries(CAPTURE_HOOK_CONFIG)) {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
hooks: [
|
|
53
|
-
{
|
|
54
|
-
type: 'command',
|
|
55
|
-
command: hookCommand,
|
|
56
|
-
timeout: 10,
|
|
57
|
-
},
|
|
58
|
-
],
|
|
59
|
-
};
|
|
60
|
-
if (hookType === 'PostToolUse') {
|
|
61
|
-
newEntry.matcher = CAPTURE_QUESTION_MATCHER;
|
|
62
|
-
}
|
|
63
|
-
if (!settings.hooks[hookType]) {
|
|
64
|
-
settings.hooks[hookType] = [];
|
|
65
|
-
}
|
|
66
|
-
settings.hooks[hookType].push(newEntry);
|
|
54
|
+
const newEntry = {
|
|
55
|
+
hooks: [{ type: 'command', command: runHookCommand(devflowDir, marker), timeout: 10 }],
|
|
56
|
+
};
|
|
57
|
+
if (hookType === 'PostToolUse') {
|
|
58
|
+
newEntry.matcher = CAPTURE_QUESTION_MATCHER;
|
|
67
59
|
}
|
|
60
|
+
ensureHook(settings, hookType, isCaptureHook(marker), newEntry);
|
|
68
61
|
}
|
|
69
62
|
return JSON.stringify(settings, null, 2) + '\n';
|
|
70
63
|
}
|
|
@@ -81,25 +74,11 @@ export function removeCaptureHooks(input) {
|
|
|
81
74
|
// happened. String input is returned byte-identical (no re-formatting).
|
|
82
75
|
const settingsJson = typeof input === 'string' ? input : JSON.stringify(input, null, 2) + '\n';
|
|
83
76
|
const settings = typeof input === 'string' ? JSON.parse(input) : structuredClone(input);
|
|
84
|
-
if (!settings.hooks) {
|
|
85
|
-
return settingsJson;
|
|
86
|
-
}
|
|
87
77
|
let changed = false;
|
|
88
78
|
for (const [hookType, marker] of Object.entries(CAPTURE_HOOK_CONFIG)) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const before = settings.hooks[hookType].length;
|
|
93
|
-
settings.hooks[hookType] = settings.hooks[hookType].filter((matcher) => !matcher.hooks.some((h) => h.command.includes(marker)));
|
|
94
|
-
if (settings.hooks[hookType].length !== before) {
|
|
95
|
-
changed = true;
|
|
96
|
-
}
|
|
97
|
-
if (settings.hooks[hookType].length === 0) {
|
|
98
|
-
delete settings.hooks[hookType];
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
if (settings.hooks && Object.keys(settings.hooks).length === 0) {
|
|
102
|
-
delete settings.hooks;
|
|
79
|
+
// Evaluate every removal — never short-circuit (PF-015).
|
|
80
|
+
const removed = removeHooks(settings, hookType, isCaptureHook(marker));
|
|
81
|
+
changed = changed || removed;
|
|
103
82
|
}
|
|
104
83
|
if (!changed) {
|
|
105
84
|
return settingsJson;
|
|
@@ -118,15 +97,10 @@ export function hasCaptureHooks(input) {
|
|
|
118
97
|
*/
|
|
119
98
|
export function countCaptureHooks(input) {
|
|
120
99
|
const settings = typeof input === 'string' ? JSON.parse(input) : input;
|
|
121
|
-
if (!settings.hooks) {
|
|
122
|
-
return 0;
|
|
123
|
-
}
|
|
124
100
|
let count = 0;
|
|
125
101
|
for (const [hookType, marker] of Object.entries(CAPTURE_HOOK_CONFIG)) {
|
|
126
|
-
|
|
127
|
-
if (matchers.some((matcher) => matcher.hooks.some((h) => h.command.includes(marker)))) {
|
|
102
|
+
if (hasHook(settings, hookType, isCaptureHook(marker)))
|
|
128
103
|
count++;
|
|
129
|
-
}
|
|
130
104
|
}
|
|
131
105
|
return count;
|
|
132
106
|
}
|