devflow-kit 2.4.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 +229 -0
- package/README.md +111 -18
- package/dist/agents/git.md +822 -0
- package/dist/cli/commands/agents.js +6 -1
- package/dist/cli/commands/ambient.js +160 -145
- package/dist/cli/commands/attribution-prompts.js +1 -1
- package/dist/cli/commands/capture.js +29 -55
- package/dist/cli/commands/compliance-prompts.js +1 -1
- package/dist/cli/commands/compliance.js +48 -55
- 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 +61 -27
- package/dist/cli/commands/init.js +649 -240
- package/dist/cli/commands/install-report.js +200 -0
- package/dist/cli/commands/knowledge/index.js +2 -2
- package/dist/cli/commands/knowledge/toggle.js +35 -37
- package/dist/cli/commands/learning.js +79 -57
- package/dist/cli/commands/legacy-hooks.js +11 -14
- package/dist/cli/commands/memory.js +134 -135
- package/dist/cli/commands/prompt-io.js +4 -4
- package/dist/cli/commands/proxy.js +23 -41
- package/dist/cli/commands/security.js +81 -29
- package/dist/cli/commands/skills.js +71 -7
- package/dist/cli/commands/tracker-prompts.js +145 -0
- package/dist/cli/commands/tracker.js +277 -0
- package/dist/cli/commands/uninstall.js +520 -169
- package/dist/cli.js +2 -0
- package/dist/commands/bug-analysis.md +58 -14
- package/dist/commands/code-review.md +110 -32
- package/dist/commands/debug.md +55 -11
- package/dist/commands/dynamic-build.md +344 -73
- package/dist/commands/dynamic-plan.md +77 -27
- package/dist/commands/dynamic-profile.md +25 -11
- package/dist/commands/dynamic-tickets.md +76 -15
- package/dist/commands/explore.md +37 -7
- package/dist/commands/implement.md +314 -62
- package/dist/commands/plan.md +146 -32
- package/dist/commands/release.md +64 -17
- package/dist/commands/research.md +34 -8
- package/dist/commands/resolve.md +196 -68
- package/dist/commands/self-review.md +45 -9
- package/dist/core/agent-models.js +55 -12
- package/dist/core/assets.js +58 -2
- package/dist/core/compliance-compose.js +27 -27
- package/dist/core/evidence-policy.js +363 -0
- package/dist/core/feature-config.js +200 -65
- package/dist/core/feature-switch.js +112 -0
- package/dist/core/flags.js +34 -6
- 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 +38 -10
- package/dist/core/mds-variants.js +798 -0
- package/dist/core/migrations.js +49 -23
- package/dist/core/model-discovery.js +12 -1
- package/dist/core/plugins.js +361 -12
- package/dist/core/project-paths.js +1 -18
- package/dist/core/proxy-log.js +8 -6
- package/dist/core/proxy-state.js +11 -8
- package/dist/core/reference-sweep.js +136 -0
- package/dist/core/same-location.js +25 -0
- package/dist/core/tracker.js +494 -0
- 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/decision-markers.md +19 -0
- package/dist/skills/git/references/learn-conventions.md +56 -0
- package/dist/skills/git/references/pr/check-ci-status.md +14 -0
- package/dist/skills/git/references/pr/check-merge-readiness.md +28 -0
- package/dist/skills/git/references/pr/ensure-pr-ready.md +24 -0
- package/dist/skills/git/references/pr/fetch-review-threads.md +22 -0
- package/dist/skills/git/references/pr/post-resolution-summary.md +40 -0
- package/dist/skills/git/references/pr/post-review-summary.md +42 -0
- package/dist/skills/git/references/pr/resolve-review-threads.md +35 -0
- package/dist/skills/git/references/pr/update-pr-evidence.md +14 -0
- package/dist/skills/git/references/pr/validate-branch.md +18 -0
- package/dist/skills/git/references/publication-gate.md +13 -0
- package/dist/skills/git/references/tracker/_mcp.md +153 -0
- package/dist/skills/git/references/tracker/github/associate-release.md +18 -0
- package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +40 -0
- package/dist/skills/git/references/tracker/github/create-release.md +11 -0
- package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +16 -0
- package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +69 -0
- package/dist/skills/git/references/tracker/github/fetch-issue.md +32 -0
- package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +17 -0
- package/dist/skills/git/references/tracker/github/gather-release-evidence.md +19 -0
- package/dist/skills/git/references/tracker/github/manage-debt.md +101 -0
- package/dist/skills/git/references/tracker/github/post-wave-report.md +28 -0
- package/dist/skills/git/references/tracker/github/setup-task.md +26 -0
- package/dist/skills/git/references/tracker/jira/associate-release.md +18 -0
- package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +49 -0
- package/dist/skills/git/references/tracker/jira/create-release.md +17 -0
- package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +22 -0
- package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +53 -0
- package/dist/skills/git/references/tracker/jira/fetch-issue.md +14 -0
- package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +15 -0
- package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +18 -0
- package/dist/skills/git/references/tracker/jira/manage-debt.md +37 -0
- package/dist/skills/git/references/tracker/jira/post-wave-report.md +33 -0
- package/dist/skills/git/references/tracker/jira/setup-task.md +31 -0
- package/dist/skills/git/references/tracker/linear/associate-release.md +18 -0
- package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +53 -0
- package/dist/skills/git/references/tracker/linear/create-release.md +17 -0
- package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +22 -0
- package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +53 -0
- package/dist/skills/git/references/tracker/linear/fetch-issue.md +14 -0
- package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +15 -0
- package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +18 -0
- package/dist/skills/git/references/tracker/linear/manage-debt.md +37 -0
- package/dist/skills/git/references/tracker/linear/post-wave-report.md +33 -0
- package/dist/skills/git/references/tracker/linear/setup-task.md +32 -0
- package/dist/skills/git/references/trust-rule.md +7 -0
- 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 +1187 -32
- package/dist/targets/claude-code/legacy.js +5 -0
- package/dist/targets/claude-code/post-install.js +366 -151
- package/dist/targets/claude-code/tracker-install.js +134 -0
- package/package.json +8 -6
- package/src/assets/agents/code.md +45 -6
- package/src/assets/agents/design.md +2 -1
- package/src/assets/agents/git.mds +825 -0
- package/src/assets/agents/knowledge.md +3 -3
- package/src/assets/agents/learning.md +11 -0
- package/src/assets/agents/review.md +3 -1
- package/src/assets/agents/synthesize.md +1 -1
- package/src/assets/agents/test.md +16 -5
- package/src/assets/agents/tracker.md +474 -0
- package/src/assets/agents/validate.md +7 -5
- 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 +13 -11
- package/src/assets/commands/_partials/_evidence_policy.mds +30 -0
- 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 +22 -7
- package/src/assets/commands/_partials/_preamble.mds +2 -2
- package/src/assets/commands/_partials/_publication.mds +8 -2
- package/src/assets/commands/_partials/_settings.mds +28 -0
- package/src/assets/commands/_partials/_ticket_template.mds +3 -2
- package/src/assets/commands/_partials/_tracker.mds +18 -0
- package/src/assets/commands/_partials/_wave.mds +16 -10
- package/src/assets/commands/bug-analysis.mds +31 -19
- package/src/assets/commands/code-review.mds +67 -41
- package/src/assets/commands/debug.mds +13 -7
- package/src/assets/commands/dynamic-build.mds +274 -66
- package/src/assets/commands/dynamic-plan.mds +50 -23
- package/src/assets/commands/dynamic-profile.mds +24 -11
- package/src/assets/commands/dynamic-tickets.mds +63 -16
- package/src/assets/commands/explore.mds +4 -5
- package/src/assets/commands/implement.mds +234 -67
- package/src/assets/commands/plan.mds +91 -33
- package/src/assets/commands/release.md +64 -17
- package/src/assets/commands/research.mds +11 -9
- package/src/assets/commands/resolve.mds +150 -78
- package/src/assets/commands/self-review.mds +24 -25
- package/src/assets/mds/git/_pr.mds +331 -0
- package/src/assets/mds/git/_references.mds +135 -0
- package/src/assets/mds/tracker/_common.mds +156 -0
- package/src/assets/mds/tracker/_github.mds +472 -0
- package/src/assets/mds/tracker/_jira.mds +407 -0
- package/src/assets/mds/tracker/_linear.mds +449 -0
- package/src/assets/mds/tracker/_mcp.mds +305 -0
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +5 -8
- package/src/assets/scripts/hooks/background-memory-update +40 -19
- package/src/assets/scripts/hooks/capture-prompt +18 -8
- package/src/assets/scripts/hooks/capture-question +18 -8
- package/src/assets/scripts/hooks/capture-turn +27 -13
- 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 +236 -60
- package/src/assets/scripts/hooks/git-marker +48 -0
- package/src/assets/scripts/hooks/hook-log-init +3 -1
- package/src/assets/scripts/hooks/json-helper.cjs +228 -5
- package/src/assets/scripts/hooks/lib/project-paths.cjs +1 -20
- package/src/assets/scripts/hooks/log-paths +80 -0
- package/src/assets/scripts/hooks/memory-worker +22 -13
- package/src/assets/scripts/hooks/pre-compact-memory +44 -15
- package/src/assets/scripts/hooks/preamble +1 -4
- package/src/assets/scripts/hooks/queue-append +146 -28
- package/src/assets/scripts/hooks/resolve-project-root +101 -7
- package/src/assets/scripts/hooks/session-start-context +534 -20
- package/src/assets/scripts/hooks/session-start-memory +38 -15
- package/src/assets/scripts/lib/project-config.cjs +633 -0
- package/src/assets/scripts/pr-evidence.cjs +1961 -0
- package/src/assets/scripts/redact-secrets.cjs +490 -62
- package/src/assets/scripts/release-trace.cjs +1143 -0
- package/src/assets/scripts/resolve-evidence-policy.cjs +1145 -0
- package/src/assets/scripts/resolve-settings.cjs +1054 -0
- package/src/assets/scripts/verify-evidence.cjs +1822 -0
- package/src/assets/skills/compliance/SKILL.md +4 -2
- package/src/assets/skills/docs-framework/SKILL.md +11 -10
- 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/SKILL.md +8 -78
- package/src/assets/skills/git/references/github-api.md +179 -141
- package/src/assets/skills/git/references/patterns.md +11 -6
- package/src/assets/skills/review-methodology/SKILL.md +1 -1
- package/src/assets/skills/review-methodology/references/patterns.md +6 -61
- package/src/assets/skills/review-methodology/references/violations.md +14 -22
- 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
- package/src/assets/agents/git.md +0 -938
|
@@ -1,19 +1,24 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
1
|
+
import { Command, Option } from 'commander';
|
|
2
2
|
import { promises as fs } from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import { execSync } from 'child_process';
|
|
5
5
|
import * as p from '@clack/prompts';
|
|
6
6
|
import color from 'picocolors';
|
|
7
|
-
import {
|
|
7
|
+
import { resolveInstallationPaths } from '../../targets/claude-code/claude-paths.js';
|
|
8
8
|
import { getGitRoot } from '../../core/git.js';
|
|
9
|
+
import { isSameLocation, withoutHomeRoots } from '../../core/same-location.js';
|
|
10
|
+
import { pruneHookLogDirs, MAX_HOOK_LOG_DIRS } from '../../core/hook-log-dirs.js';
|
|
11
|
+
import { getLedgerRoot } from '../../core/ledger-root.js';
|
|
9
12
|
import { installViaFileCopy, composeScripts } from '../../targets/claude-code/installer.js';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
13
|
+
import { formatOverlaySummary, formatSkillScopeSummary, formatTrackerAssetSummary, isPluginListUnchanged } from './install-report.js';
|
|
14
|
+
import { convergeTrackerArtifacts } from '../../targets/claude-code/tracker-install.js';
|
|
15
|
+
import { installSettings, installManagedSettings, installClaudeignore, discoverProjectGitRoots, ensureDevflowGitignore, applyUserSecurityDenyList, detectDenyState, resolveSecurityAction, assertHistoricalDenySuperset, loadTemplateDenyEntries, stripUserSecurityDenyList, } from '../../targets/claude-code/post-install.js';
|
|
16
|
+
import { DEVFLOW_PLUGINS, LEGACY_COMMAND_NAMES, LEGACY_RULE_NAMES, buildAssetMaps, buildScopedSkillsMap, buildRulesMap, partitionSelectablePlugins, WORKFLOW_ORDER, parsePluginSelection, resolveFeatureRedirect } from '../../core/plugins.js';
|
|
12
17
|
import { LEGACY_SKILL_NAMES } from '../../targets/claude-code/legacy.js';
|
|
13
18
|
import { detectPlatform, detectShell, getProfilePath, getSafeDeleteInfo, hasSafeDelete } from '../../core/safe-delete.js';
|
|
14
19
|
import { generateSafeDeleteBlock, installToProfile, removeFromProfile, getInstalledVersion, SAFE_DELETE_BLOCK_VERSION } from '../../core/safe-delete-install.js';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
20
|
+
import { convergeAmbientHooks } from './ambient.js';
|
|
21
|
+
import { convergeMemoryHooks, drainMemoryQueue } from './memory.js';
|
|
17
22
|
import { addCaptureHooks, removeCaptureHooks } from './capture.js';
|
|
18
23
|
import { removeDreamHook } from './legacy-hooks.js';
|
|
19
24
|
import { addProxyHooks, removeProxyHooks, applyProxyEnv, stripProxyEnv, runProxyPreflight, buildRealPreflightDeps } from './proxy.js';
|
|
@@ -26,16 +31,20 @@ import { loadConfig as loadHudConfig, saveConfig as saveHudConfig } from '../../
|
|
|
26
31
|
import { readManifest, writeManifest, resolvePluginList, detectUpgrade } from '../../core/manifest.js';
|
|
27
32
|
import { convergeFlagsIntoSettings, countActiveFlags, readViewMode } from '../../core/flags.js';
|
|
28
33
|
import { addContextHook, removeContextHook, hasContextHook } from './context.js';
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
34
|
+
import { writeSettingsFileAtomic } from '../../core/fs-atomic.js';
|
|
35
|
+
import { writeManagedConfig, readConfigIfPresent, DEFAULT_CONFIG } from '../../core/feature-config.js';
|
|
36
|
+
import { drainLearningQueue } from '../../core/learning-queue-cleanup.js';
|
|
37
|
+
import { removeManagedDenyList, describeManagedDenyRemoval } from './security.js';
|
|
38
|
+
import { resolveInitSeed, applyCliToggles, resolveResetGatedInputs, resolvePluginsToInstall } from './init-seed.js';
|
|
31
39
|
import { parseFrameworkList, normalizeFrameworks } from '../../core/compliance.js';
|
|
32
40
|
import { formatComplianceSummary, shouldRunComplianceStep, runComplianceStep, buildClackCompliancePrompts, } from './compliance-prompts.js';
|
|
41
|
+
import { applyTrackerSentinel, parseTrackerId, rearmTrackerInference, DEFAULT_TRACKER_PROVIDER, } from '../../core/tracker.js';
|
|
42
|
+
import { formatTrackerSummary, shouldRunTrackerStep, runTrackerStep, buildClackTrackerPrompts, } from './tracker-prompts.js';
|
|
33
43
|
import { shouldRunAttributionStep, runAttributionStep, buildClackAttributionPrompts, applyAttributionAnswer, attributionSeedFrom, } from './attribution-prompts.js';
|
|
34
44
|
import { convergeFromManifest } from '../../targets/claude-code/compliance-install.js';
|
|
35
|
-
import { getPendingTurnsPath, getPendingTurnsProcessingPath } from '../../core/project-paths.js';
|
|
36
45
|
import * as os from 'os';
|
|
37
46
|
// Re-export pure functions for tests (canonical source is post-install.ts)
|
|
38
|
-
export { substituteSettingsTemplate,
|
|
47
|
+
export { substituteSettingsTemplate, mergeDenyList, discoverProjectGitRoots } from '../../targets/claude-code/post-install.js';
|
|
39
48
|
export { addAmbientHook, removeAmbientHook, hasAmbientHook } from './ambient.js';
|
|
40
49
|
export { addMemoryHooks, removeMemoryHooks, hasMemoryHooks } from './memory.js';
|
|
41
50
|
export { addCaptureHooks, removeCaptureHooks, hasCaptureHooks } from './capture.js';
|
|
@@ -99,6 +108,31 @@ export function formatSweepSummary(report) {
|
|
|
99
108
|
}
|
|
100
109
|
return lines;
|
|
101
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Log each summary line at the severity it carries — the one dispatch every
|
|
113
|
+
* `SummaryLine[]` renderer shares.
|
|
114
|
+
*
|
|
115
|
+
* Exhaustive over `SummaryLine['level']` rather than an `if/else`: a level added
|
|
116
|
+
* to the interface has to be routed here, at compile time, instead of silently
|
|
117
|
+
* degrading to `info` at every call site.
|
|
118
|
+
*/
|
|
119
|
+
function logSummaryLines(lines) {
|
|
120
|
+
for (const line of lines) {
|
|
121
|
+
switch (line.level) {
|
|
122
|
+
case 'info':
|
|
123
|
+
p.log.info(line.message);
|
|
124
|
+
break;
|
|
125
|
+
case 'warn':
|
|
126
|
+
p.log.warn(line.message);
|
|
127
|
+
break;
|
|
128
|
+
default: {
|
|
129
|
+
const _exhaustive = line.level;
|
|
130
|
+
void _exhaustive;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
102
136
|
/**
|
|
103
137
|
* Classify the safe-delete installation state based on the installed version
|
|
104
138
|
* in the user's shell profile.
|
|
@@ -162,9 +196,282 @@ export function resolveComplianceInitState(complianceOption, seedFrameworks) {
|
|
|
162
196
|
// Re-export formatComplianceSummary from compliance-prompts.ts so existing test imports
|
|
163
197
|
// (tests/init-logic.test.ts:1615 — imports from '../src/cli/commands/init.js') keep resolving.
|
|
164
198
|
export { formatComplianceSummary } from './compliance-prompts.js';
|
|
199
|
+
/**
|
|
200
|
+
* Parse the --tracker CLI option into a tracker override.
|
|
201
|
+
*
|
|
202
|
+
* Pure function — no I/O, no side effects; extracted for testability.
|
|
203
|
+
*
|
|
204
|
+
* Returns:
|
|
205
|
+
* {ok: true, value} — override state derived from the option
|
|
206
|
+
* {ok: false, error} — not a registry provider ID (caller handles exit)
|
|
207
|
+
* undefined — option was not supplied; no override
|
|
208
|
+
*
|
|
209
|
+
* There is no `--no-tracker` (decision D-E): `--tracker github` IS the off
|
|
210
|
+
* switch, because `provider:'github'` is the off position. Parsing is strict —
|
|
211
|
+
* reject, never repair — so `--tracker jira-cloud` exits rather than silently
|
|
212
|
+
* selecting jira.
|
|
213
|
+
*/
|
|
214
|
+
export function resolveTrackerInitState(trackerOption) {
|
|
215
|
+
if (typeof trackerOption !== 'string')
|
|
216
|
+
return undefined;
|
|
217
|
+
const parsed = parseTrackerId(trackerOption);
|
|
218
|
+
if (!parsed.ok)
|
|
219
|
+
return { ok: false, error: parsed.error };
|
|
220
|
+
return { ok: true, value: { provider: parsed.value } };
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* The outcome line for a provider that arrived as `--tracker <id>`.
|
|
224
|
+
*
|
|
225
|
+
* D-TRACKER-CLI-SURFACE [PF-029]: the Advanced path prints no end-of-wizard
|
|
226
|
+
* summary, and `--tracker` suppresses the wizard step that would otherwise
|
|
227
|
+
* print one, so the CLI-override arm is the only place the selection can
|
|
228
|
+
* surface there. Without this line `devflow init --advanced --tracker jira`
|
|
229
|
+
* changes the machine-wide provider with nothing on screen — the same
|
|
230
|
+
* unreachable-step failure the wizard gate exists to prevent, arrived at from
|
|
231
|
+
* the flag side. Recommended already has its surface in the summary note's
|
|
232
|
+
* Tracker row; both paths or the step is invisible on one.
|
|
233
|
+
*
|
|
234
|
+
* Pure — the caller renders. The summary half is `formatTrackerSummary`, the one
|
|
235
|
+
* spelling every tracker surface shares.
|
|
236
|
+
*/
|
|
237
|
+
export function trackerOverrideMessage(provider) {
|
|
238
|
+
return {
|
|
239
|
+
level: provider === DEFAULT_TRACKER_PROVIDER ? 'info' : 'success',
|
|
240
|
+
text: `Tracker: ${formatTrackerSummary(provider)}`,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/** The real adapter — the ONE binding of each owner into the init lifecycle. */
|
|
244
|
+
export function buildTrackerLifecycleIO() {
|
|
245
|
+
return {
|
|
246
|
+
writeManifest,
|
|
247
|
+
convergeArtifacts: (claudeDir, warn) => convergeTrackerArtifacts({ claudeDir, warn }),
|
|
248
|
+
rearmInference: rearmTrackerInference,
|
|
249
|
+
applySentinel: applyTrackerSentinel,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Persist the installation manifest, then converge the tracker artifacts against
|
|
254
|
+
* the provider that was actually persisted.
|
|
255
|
+
*
|
|
256
|
+
* D-TRACKER-CONVERGE: the manifest write and the tracker file-lifecycle owners
|
|
257
|
+
* are ONE unit because their relative order is the invariant, not an
|
|
258
|
+
* implementation detail (PF-015). The manifest write is explicitly failable —
|
|
259
|
+
* init must not abort on it — so converging the sentinel ahead of it leaves the
|
|
260
|
+
* two disagreeing in both directions: github→jira writes a sentinel naming a
|
|
261
|
+
* provider the manifest never records, and jira→github removes the sentinel while
|
|
262
|
+
* the manifest stays on jira. Writing first and gating the owners on
|
|
263
|
+
* `manifestWritten` makes them converge all-or-none, in the same order as the
|
|
264
|
+
* sibling `devflow tracker --set` (src/cli/commands/tracker.ts): persist → rearm →
|
|
265
|
+
* sentinel.
|
|
266
|
+
*
|
|
267
|
+
* The provider is read from `manifestData.features.tracker.provider` rather than
|
|
268
|
+
* taken as a separate argument, so there is exactly one binding and the sentinel
|
|
269
|
+
* cannot name a value other than the one on disk.
|
|
270
|
+
*
|
|
271
|
+
* Every step reports rather than aborts (PF-009's isolation posture): a
|
|
272
|
+
* feature-state change must never fail `devflow init`.
|
|
273
|
+
*/
|
|
274
|
+
export async function persistManifestThenConvergeTracker(opts) {
|
|
275
|
+
const { devflowDir, claudeDir, manifestData, io } = opts;
|
|
276
|
+
const provider = manifestData.features.tracker.provider;
|
|
277
|
+
const messages = [];
|
|
278
|
+
// The gate. Non-fatal for the install (which has already succeeded) but
|
|
279
|
+
// decisive for the tracker artifacts: an unpersisted selection converges none
|
|
280
|
+
// of them, so the on-disk state stays internally consistent and the next
|
|
281
|
+
// `devflow init` retries the whole transition from an unchanged starting point.
|
|
282
|
+
try {
|
|
283
|
+
await io.writeManifest(devflowDir, manifestData);
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
messages.push({
|
|
287
|
+
level: 'warn',
|
|
288
|
+
text: `Failed to write installation manifest (install succeeded): ${error instanceof Error ? error.message : error}`,
|
|
289
|
+
});
|
|
290
|
+
messages.push({
|
|
291
|
+
level: 'warn',
|
|
292
|
+
text: `Tracker selection (${provider}) was not persisted — the sentinel and attempt counters ` +
|
|
293
|
+
`are unchanged. Re-run devflow init, or devflow tracker --set ${provider}.`,
|
|
294
|
+
});
|
|
295
|
+
return { manifestWritten: false, converged: false, agent: 'unchanged', messages };
|
|
296
|
+
}
|
|
297
|
+
// The Tracker agent file — installed on every machine (D-INSTALL-ALL-PROVIDERS),
|
|
298
|
+
// because a repository can select a provider the machine never did.
|
|
299
|
+
const agentWarnings = [];
|
|
300
|
+
const artifacts = await io.convergeArtifacts(claudeDir, (msg) => agentWarnings.push(msg));
|
|
301
|
+
for (const text of agentWarnings)
|
|
302
|
+
messages.push({ level: 'warn', text });
|
|
303
|
+
// D-TRACKER-PARALLEL: the two owners touch disjoint files — the attempt
|
|
304
|
+
// counters and the sentinel — depend on nothing the other writes, and both
|
|
305
|
+
// report through TrackerResult instead of throwing (PF-014), so they run
|
|
306
|
+
// concurrently and their warnings are pushed in a fixed order regardless of
|
|
307
|
+
// which settles first.
|
|
308
|
+
const [rearm, sentinel] = await Promise.all([
|
|
309
|
+
// [DR-22] The documented re-arm path: devflow init resets the attempt counters
|
|
310
|
+
// so a previously-capped inference gets another five tries.
|
|
311
|
+
io.rearmInference(devflowDir),
|
|
312
|
+
// [DR-10] Converge the sentinel onto the persisted machine provider: its name
|
|
313
|
+
// for jira/linear, removed for github. The SessionStart hook reads it with a
|
|
314
|
+
// builtin, which is what keeps the machine provider free of forks.
|
|
315
|
+
io.applySentinel(devflowDir, provider),
|
|
316
|
+
]);
|
|
317
|
+
if (!rearm.ok)
|
|
318
|
+
messages.push({ level: 'warn', text: rearm.error });
|
|
319
|
+
if (!sentinel.ok)
|
|
320
|
+
messages.push({ level: 'warn', text: sentinel.error });
|
|
321
|
+
return {
|
|
322
|
+
manifestWritten: true,
|
|
323
|
+
converged: artifacts.converged && sentinel.ok,
|
|
324
|
+
agent: artifacts.agent,
|
|
325
|
+
messages,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Drain this repo's memory and learning queues for each feature init switched
|
|
330
|
+
* off, so stale turns are not processed on a future re-enable — the same drains
|
|
331
|
+
* `devflow memory --disable` and `devflow learning --disable` perform. Other
|
|
332
|
+
* repos' queues are inert: every gate reads the machine-wide switch, so nothing
|
|
333
|
+
* appends to or processes them while the feature is off.
|
|
334
|
+
*
|
|
335
|
+
* D-INIT-DRAIN-AFTER-SWITCH: the drain runs only AFTER the manifest holding the
|
|
336
|
+
* switch is persisted (`manifestWritten`), the order the standalone toggles use
|
|
337
|
+
* (write the switch, then drain). The capture hooks read the manifest on every
|
|
338
|
+
* turn, so draining first left a window — the whole install — in which a
|
|
339
|
+
* concurrent session, still reading the old "on", appended turns that then
|
|
340
|
+
* survived the disable. When the manifest write failed the feature is still on
|
|
341
|
+
* everywhere, so its queue is live and is left alone.
|
|
342
|
+
*
|
|
343
|
+
* D-LEDGER-MAIN-WORKTREE: each queue drains where the hooks write it — memory at
|
|
344
|
+
* this checkout's toplevel (`gitRoot`), learning at the ledger root (`ledgerRoot`,
|
|
345
|
+
* getLedgerRoot), which in a linked worktree is the main checkout.
|
|
346
|
+
*/
|
|
347
|
+
export async function drainDisabledFeatureQueues(opts, io = { drainMemoryQueue, drainLearningQueue }) {
|
|
348
|
+
if (!opts.manifestWritten)
|
|
349
|
+
return;
|
|
350
|
+
if (!opts.memoryEnabled && opts.gitRoot !== null)
|
|
351
|
+
await io.drainMemoryQueue(opts.gitRoot);
|
|
352
|
+
if (!opts.learningEnabled && opts.ledgerRoot !== null)
|
|
353
|
+
await io.drainLearningQueue(opts.ledgerRoot);
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* The manifest `devflow init --hud-only` writes. Pure — never mutates `existing`.
|
|
357
|
+
*
|
|
358
|
+
* D-HUD-ONLY-PRESERVE: over a prior install, --hud-only installs the HUD and
|
|
359
|
+
* nothing else, so the manifest keeps every recorded value — plugins, version,
|
|
360
|
+
* installedAt and every feature — and only `features.hud` turns on. The
|
|
361
|
+
* earlier shape rewrote the whole record as a HUD-only fresh install (ambient,
|
|
362
|
+
* memory, learning, knowledge, rules and proxy all `false`, plugins `[]`) while
|
|
363
|
+
* leaving those features' artifacts on disk: the record stopped describing the
|
|
364
|
+
* machine, the next re-init seeded every feature off (ADR-014), and with
|
|
365
|
+
* memory/learning/knowledge switched by the manifest alone
|
|
366
|
+
* (D-FEATURES-NARROW-ONLY) a HUD install would have really disabled them
|
|
367
|
+
* everywhere. `version` is kept too: --hud-only reinstalls no plugin, and a
|
|
368
|
+
* bumped version would make the next init skip the upgrade it still owes.
|
|
369
|
+
*
|
|
370
|
+
* With no prior manifest the result is the fresh HUD-only record: every other
|
|
371
|
+
* feature off, which is what is installed.
|
|
372
|
+
*/
|
|
373
|
+
export function buildHudOnlyManifest(existing, version, now) {
|
|
374
|
+
if (existing !== null) {
|
|
375
|
+
return {
|
|
376
|
+
...existing,
|
|
377
|
+
features: { ...existing.features, hud: true },
|
|
378
|
+
updatedAt: now,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
version,
|
|
383
|
+
plugins: [],
|
|
384
|
+
scope: 'user',
|
|
385
|
+
features: {
|
|
386
|
+
ambient: false, memory: false, hud: true, knowledge: false,
|
|
387
|
+
learning: false, rules: false, flags: {}, proxy: false,
|
|
388
|
+
compliance: { enabled: false, frameworks: [] },
|
|
389
|
+
// The exported default, not a literal — the one constant every other
|
|
390
|
+
// module reads, so a moved default moves here too.
|
|
391
|
+
tracker: { provider: DEFAULT_TRACKER_PROVIDER },
|
|
392
|
+
},
|
|
393
|
+
installedAt: now,
|
|
394
|
+
updatedAt: now,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Decide what `init --scope <value>` does now that there is one install scope.
|
|
399
|
+
* Pure — the action performs the exit.
|
|
400
|
+
*
|
|
401
|
+
* D-SCOPE-RETIRED: `--scope` is kept as a hidden option so existing scripts keep
|
|
402
|
+
* parsing. `user` (any case) is exactly the no-flag install. `local` is refused
|
|
403
|
+
* before anything is written: the repo-local install wrote `<repo>/.claude` and
|
|
404
|
+
* `<repo>/.devflow` while every hook and prompt read `~/.devflow`, so it never
|
|
405
|
+
* worked, and the only thing left to do with one is remove it. Any other value
|
|
406
|
+
* is refused the same way rather than silently treated as `user`.
|
|
407
|
+
*/
|
|
408
|
+
export function resolveRetiredScopeOption(scope) {
|
|
409
|
+
if (scope === undefined || scope.toLowerCase() === 'user')
|
|
410
|
+
return { kind: 'proceed' };
|
|
411
|
+
if (scope.toLowerCase() === 'local') {
|
|
412
|
+
return {
|
|
413
|
+
kind: 'refuse',
|
|
414
|
+
message: 'Project-local installs are no longer supported: Devflow installs machine-wide only. ' +
|
|
415
|
+
'To remove an old project-local install, run `devflow uninstall --scope local` to clean up.',
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
kind: 'refuse',
|
|
420
|
+
message: `Unknown --scope value "${scope}": Devflow installs machine-wide only (omit --scope).`,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
/** The line init prints after removing old hook log folders (D-LOG-DIR-CAP). Pure. */
|
|
424
|
+
export function formatLogPruneLine(report) {
|
|
425
|
+
const base = `Removed ${report.removed} old hook log folder${report.removed === 1 ? '' : 's'} ` +
|
|
426
|
+
`(keeping the ${MAX_HOOK_LOG_DIRS} most recent)`;
|
|
427
|
+
return report.overCap > 0 ? `${base}; ${report.overCap} more go on the next init` : base;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* The warning init prints when the running CLI is older than the one that last
|
|
431
|
+
* installed this machine, or null. Pure.
|
|
432
|
+
*
|
|
433
|
+
* D-INIT-DOWNGRADE-WARN: a downgrade is allowed — an older CLI installs a
|
|
434
|
+
* consistent older devflow — but never silent: its install sweeps every skill,
|
|
435
|
+
* agent and command the newer version added as an orphan, and settings the newer
|
|
436
|
+
* version wrote may mean nothing to it. The warning names both versions and how
|
|
437
|
+
* to get back; nothing blocks.
|
|
438
|
+
*/
|
|
439
|
+
export function formatDowngradeWarning(upgrade, version) {
|
|
440
|
+
if (!upgrade.isDowngrade || upgrade.previousVersion === null)
|
|
441
|
+
return null;
|
|
442
|
+
return `Downgrading: this machine was installed by devflow v${upgrade.previousVersion}, newer than this CLI (v${version}). ` +
|
|
443
|
+
'Assets only the newer version ships will be removed. To keep them, run the newer CLI instead ' +
|
|
444
|
+
'(npx devflow-kit@latest init).';
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* The warning init prints when it leaves a repository's `.devflow/config.json`
|
|
448
|
+
* alone (D-CONFIG-NO-REPAIR). Pure.
|
|
449
|
+
*/
|
|
450
|
+
export function formatManagedConfigWriteError(error) {
|
|
451
|
+
switch (error.kind) {
|
|
452
|
+
case 'malformed':
|
|
453
|
+
return `${error.path} is not a valid config (not a JSON object, or a key appears twice) — left unchanged. ` +
|
|
454
|
+
'Fix it by hand; until then devflow treats it as unreadable.';
|
|
455
|
+
case 'unreadable':
|
|
456
|
+
return `${error.path} could not be read (${error.detail}) — left unchanged.`;
|
|
457
|
+
case 'write-failed':
|
|
458
|
+
return `Could not write ${error.path}: ${error.detail}`;
|
|
459
|
+
default: {
|
|
460
|
+
const exhaustive = error;
|
|
461
|
+
return exhaustive;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* The git repositories Claude has worked in, as project roots: every
|
|
467
|
+
* history.jsonl project with a `.git`, less any rooted at HOME (D-INIT-NOT-HOME).
|
|
468
|
+
*/
|
|
469
|
+
async function discoverRepoRoots(claudeDir, homeDir) {
|
|
470
|
+
return withoutHomeRoots(await discoverProjectGitRoots(claudeDir), homeDir);
|
|
471
|
+
}
|
|
165
472
|
export const initCommand = new Command('init')
|
|
166
473
|
.description('Initialize Devflow for Claude Code')
|
|
167
|
-
.
|
|
474
|
+
.addOption(new Option('--scope <type>', 'Retired: Devflow installs machine-wide only').hideHelp())
|
|
168
475
|
.option('--verbose', 'Show detailed installation output')
|
|
169
476
|
.option('--plugin <names>', 'Install specific plugin(s), comma-separated (e.g., implement,code-review)')
|
|
170
477
|
.option('--ambient', 'Enable ambient mode (orchestrator charter + plan handoff)')
|
|
@@ -182,7 +489,8 @@ export const initCommand = new Command('init')
|
|
|
182
489
|
.option('--proxy', 'Enable external model routing (GPT models via your OpenAI/Codex subscription)')
|
|
183
490
|
.option('--no-proxy', 'Disable external model routing')
|
|
184
491
|
.option('--compliance <list>', 'Enable compliance with comma-separated framework IDs (e.g., gdpr,hipaa)')
|
|
185
|
-
.option('--no-compliance', 'Disable compliance (
|
|
492
|
+
.option('--no-compliance', 'Disable compliance (removes the rule; the skill and framework references stay installed; frameworks remembered for re-enable)')
|
|
493
|
+
.option('--tracker <id>', 'The machine\'s default issue tracker provider: github, jira, or linear')
|
|
186
494
|
.option('--security <mode>', 'Security deny list location: user, managed, or none', /^(user|managed|none)$/i)
|
|
187
495
|
.option('--hud-only', 'Install only the HUD (no plugins, hooks, or extras)')
|
|
188
496
|
.option('--recommended', 'Apply recommended defaults after plugin selection (skip advanced prompts)')
|
|
@@ -208,30 +516,21 @@ export const initCommand = new Command('init')
|
|
|
208
516
|
p.log.error('--reset and --plugin are mutually exclusive. Use --reset alone to restore defaults, or --plugin to update a specific plugin.');
|
|
209
517
|
process.exit(1);
|
|
210
518
|
}
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
if (
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
else if (options.scope) {
|
|
218
|
-
const normalizedScope = options.scope.toLowerCase();
|
|
219
|
-
if (normalizedScope !== 'user' && normalizedScope !== 'local') {
|
|
220
|
-
p.log.error('Invalid scope. Use "user" or "local"');
|
|
221
|
-
process.exit(1);
|
|
222
|
-
}
|
|
223
|
-
scope = normalizedScope;
|
|
519
|
+
// D-SCOPE-RETIRED: refuse a retired --scope value before anything is written.
|
|
520
|
+
const scopeDecision = resolveRetiredScopeOption(options.scope);
|
|
521
|
+
if (scopeDecision.kind === 'refuse') {
|
|
522
|
+
p.log.error(scopeDecision.message);
|
|
523
|
+
process.exit(1);
|
|
224
524
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
525
|
+
// The install locations, resolved once. They fail only with no home directory.
|
|
526
|
+
const resolvedPaths = resolveInstallationPaths();
|
|
527
|
+
if (!resolvedPaths.ok) {
|
|
528
|
+
p.log.error(resolvedPaths.error);
|
|
529
|
+
process.exit(1);
|
|
228
530
|
}
|
|
531
|
+
const { homeDir, claudeDir, devflowDir } = resolvedPaths.value;
|
|
229
532
|
// --hud-only: install only HUD (skip plugins, hooks, extras)
|
|
230
533
|
if (options.hudOnly) {
|
|
231
|
-
// Resolve paths
|
|
232
|
-
const paths = await getInstallationPaths(scope);
|
|
233
|
-
const claudeDir = paths.claudeDir;
|
|
234
|
-
const devflowDir = paths.devflowDir;
|
|
235
534
|
// Save HUD config
|
|
236
535
|
const existingHud = loadHudConfig();
|
|
237
536
|
saveHudConfig({ enabled: true, detail: existingHud.detail });
|
|
@@ -246,7 +545,7 @@ export const initCommand = new Command('init')
|
|
|
246
545
|
content = '{}';
|
|
247
546
|
}
|
|
248
547
|
const updated = addHudStatusLine(content, devflowDir);
|
|
249
|
-
await
|
|
548
|
+
await writeSettingsFileAtomic(settingsPath, updated);
|
|
250
549
|
}
|
|
251
550
|
catch (error) {
|
|
252
551
|
p.log.error(`Failed to update settings: ${error instanceof Error ? error.message : error}`);
|
|
@@ -261,28 +560,15 @@ export const initCommand = new Command('init')
|
|
|
261
560
|
p.log.error(`Failed to install HUD scripts: ${error instanceof Error ? error.message : error}`);
|
|
262
561
|
process.exit(1);
|
|
263
562
|
}
|
|
264
|
-
// Read existing manifest
|
|
265
|
-
//
|
|
563
|
+
// Read the existing manifest: over a prior install, --hud-only touches only
|
|
564
|
+
// the HUD (D-HUD-ONLY-PRESERVE in buildHudOnlyManifest).
|
|
266
565
|
let existingHudManifest = null;
|
|
267
566
|
try {
|
|
268
567
|
existingHudManifest = await readManifest(devflowDir);
|
|
269
568
|
}
|
|
270
569
|
catch { /* absent on fresh install — existingHudManifest stays null */ }
|
|
271
|
-
// Write minimal manifest
|
|
272
|
-
const now = new Date().toISOString();
|
|
273
570
|
try {
|
|
274
|
-
await writeManifest(devflowDir,
|
|
275
|
-
version,
|
|
276
|
-
plugins: [],
|
|
277
|
-
scope,
|
|
278
|
-
features: {
|
|
279
|
-
ambient: false, memory: false, hud: true, knowledge: false,
|
|
280
|
-
learning: false, rules: false, flags: {}, proxy: false,
|
|
281
|
-
compliance: existingHudManifest?.features.compliance ?? { enabled: false, frameworks: [] },
|
|
282
|
-
},
|
|
283
|
-
installedAt: now,
|
|
284
|
-
updatedAt: now,
|
|
285
|
-
});
|
|
571
|
+
await writeManifest(devflowDir, buildHudOnlyManifest(existingHudManifest, version, new Date().toISOString()));
|
|
286
572
|
}
|
|
287
573
|
catch { /* non-fatal */ }
|
|
288
574
|
p.log.success('HUD installed');
|
|
@@ -290,28 +576,30 @@ export const initCommand = new Command('init')
|
|
|
290
576
|
p.outro(color.green('HUD-only install complete.'));
|
|
291
577
|
return;
|
|
292
578
|
}
|
|
293
|
-
// ──
|
|
294
|
-
// Best-effort: if path resolution fails here, seed falls back to fresh-install defaults.
|
|
295
|
-
// The authoritative error gate for failed path resolution remains at the install-begins
|
|
296
|
-
// spinner (see "Resolving paths" below). Hoisted above multiselect so Phase 4 can
|
|
297
|
-
// pre-seed plugin/flag/feature prompts.
|
|
579
|
+
// ── Hoisted reads: the prior state that seeds the prompts (InitSeed, Phase 4) ──
|
|
298
580
|
let existingManifest = null;
|
|
299
|
-
|
|
581
|
+
try {
|
|
582
|
+
existingManifest = await readManifest(devflowDir);
|
|
583
|
+
}
|
|
584
|
+
catch { /* unreadable manifest — seeded as a fresh install */ }
|
|
585
|
+
// D-INIT-NOT-HOME (same-location.ts): a repository rooted at HOME is no
|
|
586
|
+
// project, so init treats it as no repository — no .devflow/config.json (that
|
|
587
|
+
// would be the machine root's), no .claudeignore, no .gitignore block, no
|
|
588
|
+
// per-project migration or queue drain.
|
|
589
|
+
const cwdGitRoot = await getGitRoot();
|
|
590
|
+
const homeRootedRepo = cwdGitRoot !== null && await isSameLocation(cwdGitRoot, homeDir);
|
|
591
|
+
const gitRoot = homeRootedRepo ? null : cwdGitRoot;
|
|
592
|
+
if (homeRootedRepo) {
|
|
593
|
+
p.log.info('This git repository is rooted at your home directory, so init writes no per-repository files here.');
|
|
594
|
+
}
|
|
595
|
+
const earlyProjectConfig = gitRoot
|
|
596
|
+
? await readConfigIfPresent(gitRoot)
|
|
597
|
+
: null;
|
|
300
598
|
let earlySettingsJson = null;
|
|
301
|
-
let earlyGitRoot = null;
|
|
302
599
|
try {
|
|
303
|
-
|
|
304
|
-
existingManifest = await readManifest(earlyPaths.devflowDir);
|
|
305
|
-
earlyGitRoot = earlyPaths.gitRoot ?? await getGitRoot();
|
|
306
|
-
if (earlyGitRoot) {
|
|
307
|
-
earlyProjectConfig = await readConfigIfPresent(earlyGitRoot);
|
|
308
|
-
}
|
|
309
|
-
try {
|
|
310
|
-
earlySettingsJson = await fs.readFile(path.join(earlyPaths.claudeDir, 'settings.json'), 'utf-8');
|
|
311
|
-
}
|
|
312
|
-
catch { /* settings.json absent — treated as empty */ }
|
|
600
|
+
earlySettingsJson = await fs.readFile(path.join(claudeDir, 'settings.json'), 'utf-8');
|
|
313
601
|
}
|
|
314
|
-
catch { /*
|
|
602
|
+
catch { /* settings.json absent — treated as empty */ }
|
|
315
603
|
// --reset: factory reset — treat as a fresh install for all seeding and routing decisions.
|
|
316
604
|
// The REAL existingManifest / earlySettingsJson are still used below for installedAt
|
|
317
605
|
// preservation, upgrade messaging, and security deny-state detection. resolveResetGatedInputs
|
|
@@ -319,7 +607,7 @@ export const initCommand = new Command('init')
|
|
|
319
607
|
// registry defaults — including viewMode 'default' (an externally-set /focus in settings.json
|
|
320
608
|
// must not survive a factory reset).
|
|
321
609
|
const { seedManifest, seedConfig, seedSettings } = resolveResetGatedInputs(!!options.reset, existingManifest, earlyProjectConfig, earlySettingsJson ?? '');
|
|
322
|
-
const seed = resolveInitSeed(seedManifest,
|
|
610
|
+
const seed = resolveInitSeed(seedManifest, seedSettings, DEVFLOW_PLUGINS);
|
|
323
611
|
// Early validation: parse --compliance <list> at the boundary before any prompts (PF-parse-at-boundary).
|
|
324
612
|
// options.compliance: string → --compliance <list>; false → --no-compliance; undefined → not passed
|
|
325
613
|
let cliComplianceOverride;
|
|
@@ -333,6 +621,20 @@ export const initCommand = new Command('init')
|
|
|
333
621
|
cliComplianceOverride = complianceStateResult.value;
|
|
334
622
|
}
|
|
335
623
|
}
|
|
624
|
+
// Early validation: parse --tracker <id> at the boundary before any prompts.
|
|
625
|
+
// Strict — reject, never repair — so a typo'd provider exits here rather than
|
|
626
|
+
// installing mechanics for a tracker the user did not name.
|
|
627
|
+
let cliTrackerOverride;
|
|
628
|
+
{
|
|
629
|
+
const trackerStateResult = resolveTrackerInitState(options.tracker);
|
|
630
|
+
if (trackerStateResult !== undefined) {
|
|
631
|
+
if (!trackerStateResult.ok) {
|
|
632
|
+
p.log.error(trackerStateResult.error);
|
|
633
|
+
process.exit(1);
|
|
634
|
+
}
|
|
635
|
+
cliTrackerOverride = trackerStateResult.value;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
336
638
|
// Select plugins to install
|
|
337
639
|
let selectedPlugins = [];
|
|
338
640
|
if (options.plugin) {
|
|
@@ -441,7 +743,8 @@ export const initCommand = new Command('init')
|
|
|
441
743
|
// When no --plugin flag is given and a manifest exists, the seed carries the prior
|
|
442
744
|
// selection (existing plugins ∪ new non-optional plugins not yet in knownPlugins).
|
|
443
745
|
// Fresh non-interactive installs (no manifest) fall through to the default path
|
|
444
|
-
// in
|
|
746
|
+
// in resolvePluginsToInstall: every non-optional plugin, with devflow-ambient
|
|
747
|
+
// following the ambient switch (D-AMBIENT-FOLLOWS-SWITCH).
|
|
445
748
|
if (!options.plugin && !process.stdin.isTTY && seedManifest !== null) {
|
|
446
749
|
selectedPlugins = [...seed.workflowPlugins, ...seed.languagePlugins];
|
|
447
750
|
}
|
|
@@ -506,13 +809,16 @@ export const initCommand = new Command('init')
|
|
|
506
809
|
// CLI override applied below in both Recommended and Advanced paths.
|
|
507
810
|
let complianceEnabled = seed.features.compliance.enabled;
|
|
508
811
|
let complianceFrameworks = seed.features.compliance.frameworks;
|
|
812
|
+
// tracker: manifest-group (like proxy and compliance); seed from prior manifest.
|
|
813
|
+
// CLI override applied below in both Recommended and Advanced paths.
|
|
814
|
+
let trackerProvider = seed.features.tracker.provider;
|
|
509
815
|
let enabledFlags = { ...seed.flags };
|
|
510
816
|
// viewModeExplicit: true when --reset is passed; signals resolveFinalViewMode to let the
|
|
511
817
|
// seed-time view-mode win over an externally-set value in settings.json.
|
|
512
818
|
// --reset empties the settings snapshot via resolveResetGatedInputs so seed.flags['view-mode']
|
|
513
819
|
// collapses to 'default', and explicit=true makes it take effect at settings write time.
|
|
514
820
|
let viewModeExplicit = !!options.reset;
|
|
515
|
-
let claudeignoreEnabled = !!
|
|
821
|
+
let claudeignoreEnabled = !!gitRoot;
|
|
516
822
|
let discoveredProjects = [];
|
|
517
823
|
let safeDeleteAction = 'skip';
|
|
518
824
|
let safeDeleteBlock = null;
|
|
@@ -520,6 +826,47 @@ export const initCommand = new Command('init')
|
|
|
520
826
|
// The final value is written to the manifest and consumed by the dedicated security step.
|
|
521
827
|
let securityMode = 'user'; // placeholder; overwritten below by resolve
|
|
522
828
|
let managedSettingsConfirmed = false;
|
|
829
|
+
/**
|
|
830
|
+
* Run the tracker wizard step for one wizard path.
|
|
831
|
+
*
|
|
832
|
+
* D-TRACKER-CALLSHAPE: both paths share one gate (shouldRunTrackerStep), one
|
|
833
|
+
* cancel idiom and one prompt adapter; they differ only in the mode they
|
|
834
|
+
* declare, the provider they seed from, and whether the step's own outcome
|
|
835
|
+
* line is emitted. Holding all three differences as parameters keeps the
|
|
836
|
+
* shared half single-sourced, so the paths cannot drift the way two
|
|
837
|
+
* hand-copied call sites do.
|
|
838
|
+
*
|
|
839
|
+
* Returns the chosen state, or undefined when the gate declined to run — the
|
|
840
|
+
* caller then owns the CLI-override fallback, which is the only other way the
|
|
841
|
+
* provider can change on that path.
|
|
842
|
+
*/
|
|
843
|
+
const runTrackerStepAt = async (mode, seedProvider, emitMessages) => {
|
|
844
|
+
if (!shouldRunTrackerStep({
|
|
845
|
+
mode,
|
|
846
|
+
modePromptShown,
|
|
847
|
+
isTTY: process.stdin.isTTY,
|
|
848
|
+
hasCliOverride: cliTrackerOverride !== undefined,
|
|
849
|
+
})) {
|
|
850
|
+
return undefined;
|
|
851
|
+
}
|
|
852
|
+
const trackerStep = await runTrackerStep({
|
|
853
|
+
seed: { provider: seedProvider },
|
|
854
|
+
prompts: buildClackTrackerPrompts(),
|
|
855
|
+
});
|
|
856
|
+
if (trackerStep.kind === 'cancelled') {
|
|
857
|
+
p.cancel('Installation cancelled.');
|
|
858
|
+
process.exit(0);
|
|
859
|
+
}
|
|
860
|
+
if (emitMessages) {
|
|
861
|
+
for (const msg of trackerStep.messages) {
|
|
862
|
+
if (msg.level === 'success')
|
|
863
|
+
p.log.success(msg.text);
|
|
864
|
+
else
|
|
865
|
+
p.log.info(msg.text);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
return trackerStep.state;
|
|
869
|
+
};
|
|
523
870
|
// Safe-delete detection (both paths need this)
|
|
524
871
|
const platform = detectPlatform();
|
|
525
872
|
const shell = detectShell();
|
|
@@ -552,6 +899,13 @@ export const initCommand = new Command('init')
|
|
|
552
899
|
// Step messages not emitted here — the Recommended summary note (below) already
|
|
553
900
|
// prints the Compliance line from complianceSummary via formatComplianceSummary.
|
|
554
901
|
}
|
|
902
|
+
// Tracker wizard step — same gate as compliance, so both wizard paths are
|
|
903
|
+
// governed by the one documented gate table (AC-3.6). Runs only when the
|
|
904
|
+
// Setup-mode prompt actually ran, so --recommended and !isTTY stay promptless.
|
|
905
|
+
// emitMessages=false: the Recommended summary note (below) prints the
|
|
906
|
+
// Tracker line via formatTrackerSummary, so the step's own outcome line
|
|
907
|
+
// would be a duplicate.
|
|
908
|
+
const wizardTracker = await runTrackerStepAt('recommended', seed.features.tracker.provider, false);
|
|
555
909
|
// No attribution step here: the suppress-attribution question is Advanced-only (D27).
|
|
556
910
|
// Recommended silently carries the seeded value in enabledFlags — fresh installs get
|
|
557
911
|
// the registry default (off), re-inits get prior state. See shouldRunAttributionStep.
|
|
@@ -567,6 +921,8 @@ export const initCommand = new Command('init')
|
|
|
567
921
|
rules: options.rules,
|
|
568
922
|
proxy: options.proxy,
|
|
569
923
|
compliance: cliComplianceOverride ?? wizardCompliance,
|
|
924
|
+
// Precedence: cliOverride ?? wizardResult ?? seed (applyCliToggles supplies the seed arm).
|
|
925
|
+
tracker: cliTrackerOverride ?? wizardTracker,
|
|
570
926
|
});
|
|
571
927
|
ambientEnabled = effectiveFeatures.ambient;
|
|
572
928
|
memoryEnabled = effectiveFeatures.memory;
|
|
@@ -577,6 +933,7 @@ export const initCommand = new Command('init')
|
|
|
577
933
|
proxyEnabled = effectiveFeatures.proxy;
|
|
578
934
|
complianceEnabled = effectiveFeatures.compliance.enabled;
|
|
579
935
|
complianceFrameworks = effectiveFeatures.compliance.frameworks;
|
|
936
|
+
trackerProvider = effectiveFeatures.tracker.provider;
|
|
580
937
|
// enabledFlags is already initialised to seed.flags above.
|
|
581
938
|
// Compute safe-delete block synchronously so we know whether to fetch installed version
|
|
582
939
|
if (profilePath && safeDeleteAvailable) {
|
|
@@ -584,10 +941,10 @@ export const initCommand = new Command('init')
|
|
|
584
941
|
safeDeleteBlock = generateSafeDeleteBlock(shell, process.platform, trashCmd);
|
|
585
942
|
}
|
|
586
943
|
// Run independent I/O in parallel: project discovery + safe-delete version check
|
|
587
|
-
const needsDiscovery =
|
|
944
|
+
const needsDiscovery = gitRoot !== null;
|
|
588
945
|
const needsVersionCheck = safeDeleteBlock && profilePath;
|
|
589
946
|
const [discoveredResult, installedVersionResult] = await Promise.all([
|
|
590
|
-
needsDiscovery ?
|
|
947
|
+
needsDiscovery ? discoverRepoRoots(claudeDir, homeDir) : Promise.resolve([]),
|
|
591
948
|
needsVersionCheck ? getInstalledVersion(profilePath) : Promise.resolve(0),
|
|
592
949
|
]);
|
|
593
950
|
discoveredProjects = discoveredResult;
|
|
@@ -612,6 +969,10 @@ export const initCommand = new Command('init')
|
|
|
612
969
|
`Knowledge bases: ${knowledgeEnabled ? 'enabled' : 'disabled'}`,
|
|
613
970
|
`Ext model routing: ${proxyEnabled ? 'enabled' : 'disabled'}`,
|
|
614
971
|
`Compliance: ${complianceSummary}`,
|
|
972
|
+
// Recommended emits no per-step outcome lines, so this summary row is the
|
|
973
|
+
// tracker step's ONLY surface on this path — both surfaces or it is
|
|
974
|
+
// invisible on one path.
|
|
975
|
+
`Tracker: ${formatTrackerSummary(trackerProvider)}`,
|
|
615
976
|
`View mode: ${readViewMode(enabledFlags)}`,
|
|
616
977
|
`Claude Code flags: ${defaultFlagCount} configured`,
|
|
617
978
|
`${claudeignoreEnabled ? '.claudeignore: created' : ''}`,
|
|
@@ -657,7 +1018,8 @@ export const initCommand = new Command('init')
|
|
|
657
1018
|
'compaction. Clear your session at any point and resume right\n' +
|
|
658
1019
|
'where you left off.\n\n' +
|
|
659
1020
|
'Runs a background agent on session stop that consumes additional\n' +
|
|
660
|
-
'tokens. Consider skipping if token usage is a concern
|
|
1021
|
+
'tokens. Consider skipping if token usage is a concern.\n' +
|
|
1022
|
+
'Applies to every project.', 'Working Memory');
|
|
661
1023
|
const memoryChoice = await p.confirm({
|
|
662
1024
|
message: 'Enable working memory? (Recommended)',
|
|
663
1025
|
initialValue: seed.features.memory,
|
|
@@ -690,7 +1052,8 @@ export const initCommand = new Command('init')
|
|
|
690
1052
|
else {
|
|
691
1053
|
p.note('Per-feature knowledge bases capture cross-cutting patterns,\n' +
|
|
692
1054
|
'conventions, and gotchas. Created and updated automatically\n' +
|
|
693
|
-
'when workflows touch a documented area (write-through model)
|
|
1055
|
+
'when workflows touch a documented area (write-through model).\n' +
|
|
1056
|
+
'Applies to every project.', 'Feature Knowledge Bases');
|
|
694
1057
|
const knowledgeChoice = await p.confirm({
|
|
695
1058
|
message: 'Enable feature knowledge bases? (Recommended)',
|
|
696
1059
|
initialValue: seed.features.knowledge,
|
|
@@ -707,7 +1070,7 @@ export const initCommand = new Command('init')
|
|
|
707
1070
|
else {
|
|
708
1071
|
p.note('Detects architectural decisions and pitfalls from your session\n' +
|
|
709
1072
|
'dialogs. Runs a background agent on session stop that consumes\n' +
|
|
710
|
-
'additional tokens.', 'Learning (Decision/Pitfall Tracking)');
|
|
1073
|
+
'additional tokens. Applies to every project.', 'Learning (Decision/Pitfall Tracking)');
|
|
711
1074
|
const learningChoice = await p.confirm({
|
|
712
1075
|
message: 'Enable learning? (Recommended)',
|
|
713
1076
|
initialValue: seed.features.learning,
|
|
@@ -797,6 +1160,28 @@ export const initCommand = new Command('init')
|
|
|
797
1160
|
// No third case in practice: on this path the predicate only returns false for a
|
|
798
1161
|
// CLI override (isTTY is guaranteed true by the non-TTY guard above). If it ever
|
|
799
1162
|
// did, the seed values assigned at declaration stand — which is the right default.
|
|
1163
|
+
// Tracker feature (after compliance, before attribution). Gated by the same
|
|
1164
|
+
// shouldRunTrackerStep predicate as the Recommended path, so the documented
|
|
1165
|
+
// gate table is the single authority for both and they cannot drift.
|
|
1166
|
+
// This call site is the one that matters on RE-INIT: re-init is Advanced-only
|
|
1167
|
+
// by construction, so a Recommended-only wiring would be dead there.
|
|
1168
|
+
// emitMessages=true: Advanced has no end-of-wizard summary recap, so the
|
|
1169
|
+
// step's outcome line is this path's ONLY surface — mandatory, not decorative.
|
|
1170
|
+
const advancedTracker = await runTrackerStepAt('advanced', trackerProvider, true);
|
|
1171
|
+
if (advancedTracker !== undefined) {
|
|
1172
|
+
trackerProvider = advancedTracker.provider;
|
|
1173
|
+
}
|
|
1174
|
+
else if (cliTrackerOverride !== undefined) {
|
|
1175
|
+
// --tracker passed explicitly — honour without prompting, and say so.
|
|
1176
|
+
// The gate declined the step, and this path has no summary recap, so
|
|
1177
|
+
// this line is the selection's ONLY surface (D-TRACKER-CLI-SURFACE).
|
|
1178
|
+
trackerProvider = cliTrackerOverride.provider;
|
|
1179
|
+
const overrideLine = trackerOverrideMessage(trackerProvider);
|
|
1180
|
+
if (overrideLine.level === 'success')
|
|
1181
|
+
p.log.success(overrideLine.text);
|
|
1182
|
+
else
|
|
1183
|
+
p.log.info(overrideLine.text);
|
|
1184
|
+
}
|
|
800
1185
|
// Attribution feature (after compliance, before flags). This is the ONLY call site —
|
|
801
1186
|
// the attribution question is Advanced-only (D27); the Recommended path never asks and
|
|
802
1187
|
// silently carries the seeded value. The gate stays an explicit predicate call so the
|
|
@@ -842,45 +1227,29 @@ export const initCommand = new Command('init')
|
|
|
842
1227
|
p.log.info(`Flags: ${activeCount} active — customize any time with 'devflow flags'`);
|
|
843
1228
|
}
|
|
844
1229
|
// .claudeignore prompt
|
|
845
|
-
if (
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
process.exit(0);
|
|
865
|
-
}
|
|
866
|
-
claudeignoreEnabled = claudeignoreChoice;
|
|
867
|
-
}
|
|
868
|
-
else {
|
|
869
|
-
const claudeignoreChoice = await p.confirm({
|
|
870
|
-
message: 'Create .claudeignore? (Recommended)',
|
|
871
|
-
initialValue: true,
|
|
872
|
-
});
|
|
873
|
-
if (p.isCancel(claudeignoreChoice)) {
|
|
874
|
-
p.cancel('Installation cancelled.');
|
|
875
|
-
process.exit(0);
|
|
876
|
-
}
|
|
877
|
-
claudeignoreEnabled = claudeignoreChoice;
|
|
1230
|
+
if (gitRoot) {
|
|
1231
|
+
discoveredProjects = await discoverRepoRoots(claudeDir, homeDir);
|
|
1232
|
+
p.note('Scans all projects Claude has worked on and creates a\n' +
|
|
1233
|
+
'.claudeignore in each git repository. Excludes secrets,\n' +
|
|
1234
|
+
'API keys, dependencies, and build artifacts from context.', '.claudeignore');
|
|
1235
|
+
if (discoveredProjects.length > 0) {
|
|
1236
|
+
const maxShow = 5;
|
|
1237
|
+
const projectLines = discoveredProjects.slice(0, maxShow).join('\n');
|
|
1238
|
+
const overflow = discoveredProjects.length > maxShow
|
|
1239
|
+
? `\n... (${discoveredProjects.length - maxShow} more)`
|
|
1240
|
+
: '';
|
|
1241
|
+
p.note(projectLines + overflow, `Discovered ${discoveredProjects.length} projects`);
|
|
1242
|
+
const claudeignoreChoice = await p.confirm({
|
|
1243
|
+
message: `Install .claudeignore to ${discoveredProjects.length} projects? (Recommended)`,
|
|
1244
|
+
initialValue: true,
|
|
1245
|
+
});
|
|
1246
|
+
if (p.isCancel(claudeignoreChoice)) {
|
|
1247
|
+
p.cancel('Installation cancelled.');
|
|
1248
|
+
process.exit(0);
|
|
878
1249
|
}
|
|
1250
|
+
claudeignoreEnabled = claudeignoreChoice;
|
|
879
1251
|
}
|
|
880
1252
|
else {
|
|
881
|
-
p.note('Creates a .claudeignore in this project that excludes\n' +
|
|
882
|
-
'secrets, API keys, dependencies, and build artifacts from\n' +
|
|
883
|
-
'Claude\'s context window.', '.claudeignore');
|
|
884
1253
|
const claudeignoreChoice = await p.confirm({
|
|
885
1254
|
message: 'Create .claudeignore? (Recommended)',
|
|
886
1255
|
initialValue: true,
|
|
@@ -921,8 +1290,8 @@ export const initCommand = new Command('init')
|
|
|
921
1290
|
}
|
|
922
1291
|
}
|
|
923
1292
|
}
|
|
924
|
-
// Security deny list placement (
|
|
925
|
-
if (
|
|
1293
|
+
// Security deny list placement (TTY only)
|
|
1294
|
+
if (process.stdin.isTTY) {
|
|
926
1295
|
p.note('Devflow includes a security deny list that blocks dangerous\n' +
|
|
927
1296
|
'commands (rm -rf, sudo, eval, etc). It can be installed as a\n' +
|
|
928
1297
|
'read-only system file or in your editable settings.json.', 'Security Deny List');
|
|
@@ -965,34 +1334,23 @@ export const initCommand = new Command('init')
|
|
|
965
1334
|
// ╭──────────────────────────────────────────────────────────╮
|
|
966
1335
|
// │ All prompts collected — installation begins │
|
|
967
1336
|
// ╰──────────────────────────────────────────────────────────╯
|
|
1337
|
+
const upgrade = existingManifest ? detectUpgrade(version, existingManifest.version) : null;
|
|
1338
|
+
const downgradeWarning = upgrade === null ? null : formatDowngradeWarning(upgrade, version);
|
|
1339
|
+
if (downgradeWarning !== null)
|
|
1340
|
+
p.log.warn(downgradeWarning);
|
|
968
1341
|
const s = p.spinner();
|
|
969
|
-
s.start('
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
let devflowDir;
|
|
973
|
-
let gitRoot = null;
|
|
974
|
-
try {
|
|
975
|
-
const paths = await getInstallationPaths(scope);
|
|
976
|
-
claudeDir = paths.claudeDir;
|
|
977
|
-
devflowDir = paths.devflowDir;
|
|
978
|
-
gitRoot = paths.gitRoot ?? earlyGitRoot;
|
|
1342
|
+
s.start('Installing');
|
|
1343
|
+
if (upgrade?.isUpgrade) {
|
|
1344
|
+
s.message(`Upgrading from v${upgrade.previousVersion} to v${version}`);
|
|
979
1345
|
}
|
|
980
|
-
|
|
981
|
-
s.
|
|
982
|
-
p.log.error(`Path configuration error: ${error instanceof Error ? error.message : error}`);
|
|
983
|
-
process.exit(1);
|
|
984
|
-
}
|
|
985
|
-
// existingManifest was read early above (hoisted for seed computation); use it here for upgrade detection
|
|
986
|
-
if (existingManifest) {
|
|
987
|
-
const upgrade = detectUpgrade(version, existingManifest.version);
|
|
988
|
-
if (upgrade.isUpgrade) {
|
|
989
|
-
s.message(`Upgrading from v${upgrade.previousVersion} to v${version}`);
|
|
990
|
-
}
|
|
991
|
-
else if (upgrade.isSameVersion) {
|
|
992
|
-
s.message('Reinstalling same version');
|
|
993
|
-
}
|
|
1346
|
+
else if (upgrade?.isSameVersion) {
|
|
1347
|
+
s.message('Reinstalling same version');
|
|
994
1348
|
}
|
|
995
1349
|
// Detect current deny list state in user settings (read-only; write happens in security step)
|
|
1350
|
+
// Whether the managed settings file holds a Devflow deny entry — the security
|
|
1351
|
+
// step's `none` branch removes it only then (and only then stops the spinner
|
|
1352
|
+
// for a possible sudo prompt).
|
|
1353
|
+
let managedDenyDetected = false;
|
|
996
1354
|
{
|
|
997
1355
|
const userSettingsJson = earlySettingsJson;
|
|
998
1356
|
let managedExists = false;
|
|
@@ -1005,6 +1363,7 @@ export const initCommand = new Command('init')
|
|
|
1005
1363
|
}
|
|
1006
1364
|
catch { /* absent or unsupported platform */ }
|
|
1007
1365
|
const detected = detectDenyState(userSettingsJson, managedExists, managedContentJson);
|
|
1366
|
+
managedDenyDetected = detected.managed;
|
|
1008
1367
|
const flagValue = options.security;
|
|
1009
1368
|
const manifestMode = existingManifest?.features.security;
|
|
1010
1369
|
const resolution = resolveSecurityAction(flagValue, manifestMode, detected, process.stdin.isTTY);
|
|
@@ -1033,44 +1392,30 @@ export const initCommand = new Command('init')
|
|
|
1033
1392
|
}
|
|
1034
1393
|
// Validate target directory
|
|
1035
1394
|
s.message('Validating target directory');
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
await fs.mkdir(claudeDir, { recursive: true });
|
|
1039
|
-
}
|
|
1040
|
-
catch (error) {
|
|
1041
|
-
s.stop('Installation failed');
|
|
1042
|
-
p.log.error(`Failed to create ${claudeDir}: ${error}`);
|
|
1043
|
-
process.exit(1);
|
|
1044
|
-
}
|
|
1395
|
+
try {
|
|
1396
|
+
await fs.access(claudeDir);
|
|
1045
1397
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
s.stop('Installation failed');
|
|
1052
|
-
p.log.error(`Claude Code not detected at ${claudeDir}`);
|
|
1053
|
-
p.log.info('Install from: https://claude.ai/download');
|
|
1054
|
-
process.exit(1);
|
|
1055
|
-
}
|
|
1398
|
+
catch {
|
|
1399
|
+
s.stop('Installation failed');
|
|
1400
|
+
p.log.error(`Claude Code not detected at ${claudeDir}`);
|
|
1401
|
+
p.log.info('Install from: https://claude.ai/download');
|
|
1402
|
+
process.exit(1);
|
|
1056
1403
|
}
|
|
1057
1404
|
// Resolve plugins and deduplication maps
|
|
1058
1405
|
s.message('Installing components');
|
|
1059
1406
|
const rootDir = getPackageRoot();
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
const
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
// commands need skills from other plugins to function)
|
|
1073
|
-
const skillsMap = buildFullSkillsMap();
|
|
1407
|
+
const pluginsToInstall = resolvePluginsToInstall(selectedPlugins, ambientEnabled, DEVFLOW_PLUGINS);
|
|
1408
|
+
// The EFFECTIVE selection — what the manifest will record, resolved here
|
|
1409
|
+
// rather than at manifest-write time because the skills install set is
|
|
1410
|
+
// derived from it. On a full install it is `pluginsToInstall`; on a partial
|
|
1411
|
+
// install (`--plugin=X`) it merges the prior manifest's plugins with X, so a
|
|
1412
|
+
// previously-installed plugin's skills survive an add-one run (AC-22).
|
|
1413
|
+
const installedPluginNames = pluginsToInstall.map(pl => pl.name);
|
|
1414
|
+
const effectivePluginNames = resolvePluginList(installedPluginNames, existingManifest, !!options.plugin);
|
|
1415
|
+
const effectivePlugins = DEVFLOW_PLUGINS.filter(pl => effectivePluginNames.includes(pl.name));
|
|
1416
|
+
// Skills: the effective selection's closure — every plugin's own skills plus
|
|
1417
|
+
// the ones it requires. Scoped like rules, agents and commands already are.
|
|
1418
|
+
const skillsMap = buildScopedSkillsMap(effectivePlugins);
|
|
1074
1419
|
// Agents: install only from selected plugins
|
|
1075
1420
|
const { agentsMap } = buildAssetMaps(pluginsToInstall);
|
|
1076
1421
|
// Rules: install only from selected plugins (plugin-scoped, not universal)
|
|
@@ -1079,12 +1424,11 @@ export const initCommand = new Command('init')
|
|
|
1079
1424
|
// Migrations clean up ~/.devflow runtime data and never touch the installer's copy
|
|
1080
1425
|
// targets, so their position relative to installViaFileCopy carries no dependency.
|
|
1081
1426
|
// Migrations are always-run-unapplied: helpers short-circuit when the target data is
|
|
1082
|
-
// absent, so fresh installs are safe no-ops. State lives at the
|
|
1083
|
-
//
|
|
1427
|
+
// absent, so fresh installs are safe no-ops. State lives at the machine root
|
|
1428
|
+
// ~/.devflow (D30).
|
|
1084
1429
|
{
|
|
1085
1430
|
const { runMigrations } = await import('../../core/migrations.js');
|
|
1086
|
-
|
|
1087
|
-
await runMigrationsWithFallback(discoveredProjects, gitRoot, userDevflowDir, { warn: p.log.warn, info: p.log.info, success: p.log.success }, verbose, runMigrations);
|
|
1431
|
+
await runMigrationsWithFallback(discoveredProjects, gitRoot, devflowDir, { warn: p.log.warn, info: p.log.info, success: p.log.success }, verbose, runMigrations);
|
|
1088
1432
|
}
|
|
1089
1433
|
// devflow-compliance was a selectable plugin in earlier releases; it is now a built-in
|
|
1090
1434
|
// feature (devflow compliance --enable/--disable). If the prior manifest still lists it
|
|
@@ -1107,9 +1451,11 @@ export const initCommand = new Command('init')
|
|
|
1107
1451
|
catch { /* absent — no legacy artifact */ }
|
|
1108
1452
|
// Install via file copy
|
|
1109
1453
|
let installReport;
|
|
1454
|
+
const installWarnings = [];
|
|
1110
1455
|
try {
|
|
1111
1456
|
installReport = await installViaFileCopy({
|
|
1112
1457
|
plugins: pluginsToInstall,
|
|
1458
|
+
effectivePlugins,
|
|
1113
1459
|
claudeDir,
|
|
1114
1460
|
devflowDir,
|
|
1115
1461
|
skillsMap,
|
|
@@ -1117,6 +1463,10 @@ export const initCommand = new Command('init')
|
|
|
1117
1463
|
rulesMap,
|
|
1118
1464
|
isPartialInstall: !!options.plugin,
|
|
1119
1465
|
spinner: s,
|
|
1466
|
+
// Non-fatal install notices with no other channel (skipped symlinks in the
|
|
1467
|
+
// generated reference tree, mode-normalisation failures) reach the user rather
|
|
1468
|
+
// than the void. Collected now, emitted after the spinner stops.
|
|
1469
|
+
warn: (msg) => { installWarnings.push(msg); },
|
|
1120
1470
|
});
|
|
1121
1471
|
}
|
|
1122
1472
|
catch (error) {
|
|
@@ -1136,13 +1486,13 @@ export const initCommand = new Command('init')
|
|
|
1136
1486
|
manifest: { features: { compliance: { enabled: complianceEnabled, frameworks: complianceFrameworks }, rules: rulesEnabled } },
|
|
1137
1487
|
warn: (msg) => p.log.warn(msg),
|
|
1138
1488
|
});
|
|
1139
|
-
// I41: emit legacy-upgrade notice when compliance is disabled AND pre-existing
|
|
1140
|
-
//
|
|
1141
|
-
//
|
|
1142
|
-
//
|
|
1143
|
-
// converge probes
|
|
1489
|
+
// I41: emit legacy-upgrade notice when compliance is disabled AND a pre-existing rule
|
|
1490
|
+
// was found. The skill is no signal — converge installs it on every machine
|
|
1491
|
+
// (D-COMPLIANCE-INSTALL-ALWAYS) — so removedPreexisting reports the rule alone, on a
|
|
1492
|
+
// partial install; hadComplianceRule covers full installs, where installViaFileCopy
|
|
1493
|
+
// wipes the rules dir before converge probes it.
|
|
1144
1494
|
if (!complianceEnabled && (convergeResult.removedPreexisting || hadComplianceRule)) {
|
|
1145
|
-
p.log.info('Compliance
|
|
1495
|
+
p.log.info('Compliance rule removed — if you previously had devflow-compliance installed, ' +
|
|
1146
1496
|
'run `devflow compliance --enable` to re-enable with your framework selection.');
|
|
1147
1497
|
}
|
|
1148
1498
|
}
|
|
@@ -1395,23 +1745,23 @@ export const initCommand = new Command('init')
|
|
|
1395
1745
|
try {
|
|
1396
1746
|
let content = await fs.readFile(settingsPath, 'utf-8');
|
|
1397
1747
|
const original = content;
|
|
1398
|
-
// Ambient
|
|
1399
|
-
|
|
1400
|
-
content = ambientEnabled ? await addAmbientHook(cleanedForAmbient, devflowDir) : cleanedForAmbient;
|
|
1748
|
+
// Ambient hooks — remove-then-add, upgrading a legacy ambient-prompt hook to preamble
|
|
1749
|
+
content = await convergeAmbientHooks(content, ambientEnabled, devflowDir);
|
|
1401
1750
|
// Capture hooks — always-on (like the context hook below), remove-then-add for
|
|
1402
1751
|
// upgrade safety. Queue-append only (capture-prompt/capture-turn/capture-question);
|
|
1403
|
-
// each script gates its own per-queue write
|
|
1404
|
-
// is no CLI-level enable/disable toggle here.
|
|
1405
|
-
// so capture-turn lands before memory-worker in the Stop array
|
|
1406
|
-
//
|
|
1752
|
+
// each script gates its own per-queue write on the machine-wide switch, so there
|
|
1753
|
+
// is no CLI-level enable/disable toggle here. Runs before convergeMemoryHooks below
|
|
1754
|
+
// so capture-turn lands before memory-worker in the Stop array, matching what
|
|
1755
|
+
// `devflow memory --enable` produces (AC-C2). The Stop hooks still run in
|
|
1756
|
+
// parallel; the memory worker tolerates a not-yet-appended turn.
|
|
1407
1757
|
const cleanedForCapture = removeCaptureHooks(content);
|
|
1408
1758
|
content = addCaptureHooks(cleanedForCapture, devflowDir);
|
|
1409
|
-
// Memory hooks —
|
|
1410
|
-
//
|
|
1411
|
-
// Learning agent (spawned via
|
|
1412
|
-
//
|
|
1413
|
-
|
|
1414
|
-
content =
|
|
1759
|
+
// Memory hooks — Stop (memory-worker), SessionStart (session-start-memory),
|
|
1760
|
+
// PreCompact — through the same transform `devflow memory --enable/--disable`
|
|
1761
|
+
// uses (D-FEATURES-NARROW-ONLY). Learning agent (spawned via
|
|
1762
|
+
// session-start-context directive) handles decision/pitfall detection.
|
|
1763
|
+
// Knowledge is handled in-command via write-through (knowledge_writeback MDS partial).
|
|
1764
|
+
content = convergeMemoryHooks(content, memoryEnabled, devflowDir);
|
|
1415
1765
|
// HUD statusLine
|
|
1416
1766
|
content = hudEnabled
|
|
1417
1767
|
? addHudStatusLine(content, devflowDir)
|
|
@@ -1465,7 +1815,7 @@ export const initCommand = new Command('init')
|
|
|
1465
1815
|
if (proxyEnabled)
|
|
1466
1816
|
content = applyProxyEnv(content, effectivePort);
|
|
1467
1817
|
if (content !== original) {
|
|
1468
|
-
await
|
|
1818
|
+
await writeSettingsFileAtomic(settingsPath, content);
|
|
1469
1819
|
if (verbose) {
|
|
1470
1820
|
if (ambientEnabled)
|
|
1471
1821
|
p.log.success('Ambient mode hook installed');
|
|
@@ -1480,39 +1830,33 @@ export const initCommand = new Command('init')
|
|
|
1480
1830
|
p.log.warn(`Could not configure settings.json: ${err instanceof Error ? err.message : err}. ` +
|
|
1481
1831
|
'Manifest records intended state; run devflow init again to retry.');
|
|
1482
1832
|
}
|
|
1483
|
-
// Write .devflow/config.json
|
|
1484
|
-
//
|
|
1485
|
-
//
|
|
1486
|
-
//
|
|
1487
|
-
//
|
|
1833
|
+
// Write .devflow/config.json — facts about this repo. The machine switches for
|
|
1834
|
+
// memory/learning/knowledge are the manifest's; a hand-written `features`
|
|
1835
|
+
// object here only narrows them and is carried, never written
|
|
1836
|
+
// (D-FEATURES-NARROW-ONLY; the managed write drops the retired top-level
|
|
1837
|
+
// per-repo keys). A managed
|
|
1838
|
+
// read-modify-write, not a whole-file write: init owns only reviewPublication,
|
|
1839
|
+
// and every other key in the file — the hand-written per-repo `tracker`
|
|
1840
|
+
// override first among them — is carried from disk, under --reset too
|
|
1841
|
+
// (D-CONFIG-PRESERVE-UNMANAGED in feature-config.ts, avoids PF-071).
|
|
1842
|
+
// A malformed or unreadable file is left untouched and named (D-CONFIG-NO-REPAIR).
|
|
1488
1843
|
if (gitRoot) {
|
|
1489
|
-
await
|
|
1490
|
-
memory: memoryEnabled,
|
|
1491
|
-
learning: learningEnabled,
|
|
1492
|
-
knowledge: knowledgeEnabled,
|
|
1844
|
+
const configWrite = await writeManagedConfig(gitRoot, {
|
|
1493
1845
|
// reviewPublication has no prompt, so it is carried over from the
|
|
1494
1846
|
// reset-gated snapshot rather than re-read from disk: seedConfig is null
|
|
1495
1847
|
// under --reset, which is what collapses the field back to 'auto' with
|
|
1496
1848
|
// every other feature (PF-015 — read the post-gate binding, not the file).
|
|
1497
|
-
reviewPublication: seedConfig?.reviewPublication ??
|
|
1849
|
+
reviewPublication: seedConfig?.reviewPublication ?? DEFAULT_CONFIG.reviewPublication,
|
|
1498
1850
|
});
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
if (!memoryEnabled) {
|
|
1502
|
-
await Promise.all([
|
|
1503
|
-
fs.unlink(getPendingTurnsPath(gitRoot)).catch((e) => { if (e.code !== 'ENOENT')
|
|
1504
|
-
throw e; }),
|
|
1505
|
-
fs.unlink(getPendingTurnsProcessingPath(gitRoot)).catch((e) => { if (e.code !== 'ENOENT')
|
|
1506
|
-
throw e; }),
|
|
1507
|
-
]);
|
|
1508
|
-
}
|
|
1851
|
+
if (!configWrite.ok)
|
|
1852
|
+
p.log.warn(formatManagedConfigWriteError(configWrite.error));
|
|
1509
1853
|
}
|
|
1510
1854
|
// Configure HUD
|
|
1511
1855
|
const existingHud = loadHudConfig();
|
|
1512
1856
|
saveHudConfig({ enabled: hudEnabled, detail: existingHud.detail });
|
|
1513
1857
|
// File extras
|
|
1514
1858
|
if (claudeignoreEnabled) {
|
|
1515
|
-
if (
|
|
1859
|
+
if (discoveredProjects.length > 0) {
|
|
1516
1860
|
const results = await Promise.all(discoveredProjects.map(root => installClaudeignore(root, rootDir, verbose)));
|
|
1517
1861
|
const created = results.filter(Boolean).length;
|
|
1518
1862
|
if (created > 0) {
|
|
@@ -1527,18 +1871,12 @@ export const initCommand = new Command('init')
|
|
|
1527
1871
|
}
|
|
1528
1872
|
}
|
|
1529
1873
|
// Deterministically ensure .devflow/ is gitignored at the repo root — independent
|
|
1530
|
-
// of
|
|
1874
|
+
// of every feature toggle. The always-on ensure-root-gitignore
|
|
1531
1875
|
// hook covers projects that never re-run init; this covers the init-time path so a
|
|
1532
1876
|
// fresh install never tracks .devflow/. Decoupled from memory (avoids PF-014).
|
|
1533
1877
|
if (gitRoot) {
|
|
1534
1878
|
await ensureDevflowGitignore(gitRoot, verbose);
|
|
1535
1879
|
}
|
|
1536
|
-
if (scope === 'local' && gitRoot) {
|
|
1537
|
-
await updateGitignore(gitRoot, verbose);
|
|
1538
|
-
}
|
|
1539
|
-
if (scope === 'local') {
|
|
1540
|
-
await createDocsStructure(verbose);
|
|
1541
|
-
}
|
|
1542
1880
|
// Safe-delete execution (decision was captured during prompt phase)
|
|
1543
1881
|
if (safeDeleteAction === 'install' && safeDeleteBlock && profilePath) {
|
|
1544
1882
|
await installToProfile(profilePath, safeDeleteBlock);
|
|
@@ -1626,11 +1964,26 @@ export const initCommand = new Command('init')
|
|
|
1626
1964
|
}
|
|
1627
1965
|
}
|
|
1628
1966
|
else if (securityMode === 'none') {
|
|
1629
|
-
// None: strip Devflow deny entries from
|
|
1630
|
-
//
|
|
1967
|
+
// None: strip Devflow deny entries from EVERY location, as
|
|
1968
|
+
// `devflow security --disable` does. User settings first, via the
|
|
1969
|
+
// canonical helper (atomic temp+rename; ENOENT-safe; only-write-if-changed).
|
|
1631
1970
|
const stripResult = await stripUserSecurityDenyList(userSettingsPath);
|
|
1632
1971
|
if (stripResult && verbose)
|
|
1633
1972
|
p.log.info(`Security deny list removed (${stripResult.removed.length} entries stripped)`);
|
|
1973
|
+
// Then managed settings, through the one removal security.ts also uses.
|
|
1974
|
+
// Before this, `none` left the managed file in place: the deny list kept
|
|
1975
|
+
// applying — at the highest precedence — while the manifest said `none`.
|
|
1976
|
+
// A permission failure is reported, never thrown (the install already
|
|
1977
|
+
// succeeded; the manifest records the choice and a re-run can retry).
|
|
1978
|
+
if (managedDenyDetected) {
|
|
1979
|
+
s.stop('Removing managed security settings (may prompt for sudo password)...');
|
|
1980
|
+
const managedMsg = describeManagedDenyRemoval(await removeManagedDenyList(rootDir, verbose));
|
|
1981
|
+
if (managedMsg.level === 'warn')
|
|
1982
|
+
p.log.warn(managedMsg.text);
|
|
1983
|
+
else if (verbose)
|
|
1984
|
+
p.log.info(managedMsg.text);
|
|
1985
|
+
s.start('Finalizing installation...');
|
|
1986
|
+
}
|
|
1634
1987
|
}
|
|
1635
1988
|
else {
|
|
1636
1989
|
// Exhaustive guard — if TypeScript reaches here, a new SecurityMode variant was added
|
|
@@ -1683,12 +2036,21 @@ export const initCommand = new Command('init')
|
|
|
1683
2036
|
// failed removal leaves a retired asset live. Both must surface.
|
|
1684
2037
|
// After I09, the installer's knownNames set unions FEATURE_OWNED_SKILLS, so
|
|
1685
2038
|
// devflow:compliance is never swept here — no suppression predicate is needed.
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
2039
|
+
logSummaryLines(formatSweepSummary(installReport));
|
|
2040
|
+
// Reference-overlay reporting: the overlay rewrites files inside an installed skill
|
|
2041
|
+
// the user may have shadowed, and reports any unit it had to leave alone (PF-015).
|
|
2042
|
+
logSummaryLines(formatOverlaySummary(installReport));
|
|
2043
|
+
// Skill-scoping reporting: a deselected skill is deleted and a dormant shadow
|
|
2044
|
+
// is inert, and neither is distinguishable from "never installed" on disk.
|
|
2045
|
+
//
|
|
2046
|
+
// L2: "the plugin list is unchanged" means a prior manifest EXISTS and its
|
|
2047
|
+
// plugin set equals this run's. A first install had nothing to remove, so
|
|
2048
|
+
// there is no upgrade to explain — the removal notice would be addressed to
|
|
2049
|
+
// a user who never had the skills.
|
|
2050
|
+
const pluginListUnchanged = isPluginListUnchanged(existingManifest?.plugins ?? null, effectivePluginNames);
|
|
2051
|
+
logSummaryLines(formatSkillScopeSummary(installReport, pluginListUnchanged));
|
|
2052
|
+
for (const warning of installWarnings)
|
|
2053
|
+
p.log.warn(warning);
|
|
1692
2054
|
const installedSet = new Set(pluginsToInstall.flatMap(p => p.commands).filter(c => c.length > 0));
|
|
1693
2055
|
const orderedCommands = WORKFLOW_ORDER.filter(cmd => installedSet.has(cmd));
|
|
1694
2056
|
if (orderedCommands.length > 0) {
|
|
@@ -1732,7 +2094,6 @@ export const initCommand = new Command('init')
|
|
|
1732
2094
|
.map(plugin => `${color.yellow(plugin.name.padEnd(24))}${color.dim(plugin.description)}`)
|
|
1733
2095
|
.join('\n');
|
|
1734
2096
|
p.note(pluginsList, 'Installed plugins');
|
|
1735
|
-
p.log.info(`Scope: ${scope}`);
|
|
1736
2097
|
p.log.info(`Claude dir: ${claudeDir}`);
|
|
1737
2098
|
p.log.info(`Devflow dir: ${devflowDir}`);
|
|
1738
2099
|
const totalSkillDeclarations = pluginsToInstall.reduce((sum, p) => sum + p.skills.length, 0);
|
|
@@ -1741,12 +2102,14 @@ export const initCommand = new Command('init')
|
|
|
1741
2102
|
p.log.info(`Deduplication: ${agentsMap.size} unique agents (from ${totalAgentDeclarations} declarations)`);
|
|
1742
2103
|
}
|
|
1743
2104
|
// Write installation manifest for upgrade tracking (non-fatal — install already succeeded)
|
|
1744
|
-
const installedPluginNames = pluginsToInstall.map(pl => pl.name);
|
|
1745
2105
|
const now = new Date().toISOString();
|
|
1746
2106
|
const manifestData = {
|
|
1747
2107
|
version,
|
|
1748
|
-
|
|
1749
|
-
|
|
2108
|
+
// Resolved above, before the install, because the skills install set is
|
|
2109
|
+
// derived from it — one binding, so the manifest can never record a
|
|
2110
|
+
// selection other than the one the assets were installed for.
|
|
2111
|
+
plugins: effectivePluginNames,
|
|
2112
|
+
scope: 'user',
|
|
1750
2113
|
// Snapshot of known plugin names at this install — used by resolveSeedPlugins on next init
|
|
1751
2114
|
// to detect new non-optional plugins and auto-adopt them.
|
|
1752
2115
|
knownPlugins: DEVFLOW_PLUGINS.map(p => p.name),
|
|
@@ -1767,16 +2130,62 @@ export const initCommand = new Command('init')
|
|
|
1767
2130
|
// and Advanced wizard selection. convergeComplianceArtifacts was called above.
|
|
1768
2131
|
// normalizeFrameworks: dedup + filter unknowns before persisting.
|
|
1769
2132
|
compliance: { enabled: complianceEnabled, frameworks: normalizeFrameworks(complianceFrameworks) },
|
|
2133
|
+
// Resolved tracker selection. Already a validated TrackerProvider — it came
|
|
2134
|
+
// through parseTrackerId (CLI), the typed wizard select, or the seed, which
|
|
2135
|
+
// itself came through normalizeTrackerFeature on read.
|
|
2136
|
+
tracker: { provider: trackerProvider },
|
|
1770
2137
|
},
|
|
1771
2138
|
installedAt: existingManifest?.installedAt ?? now,
|
|
1772
2139
|
updatedAt: now,
|
|
1773
2140
|
};
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
2141
|
+
// ── Manifest write + tracker selection lifecycle (the ONE call site) ──────
|
|
2142
|
+
// persistManifestThenConvergeTracker owns the ordering invariant: the
|
|
2143
|
+
// tracker file-lifecycle owners in src/core/tracker.ts converge only against
|
|
2144
|
+
// a provider the manifest actually persisted (D-TRACKER-CONVERGE, PF-015).
|
|
2145
|
+
const trackerLifecycle = await persistManifestThenConvergeTracker({
|
|
2146
|
+
devflowDir,
|
|
2147
|
+
claudeDir,
|
|
2148
|
+
manifestData,
|
|
2149
|
+
io: buildTrackerLifecycleIO(),
|
|
2150
|
+
});
|
|
2151
|
+
for (const msg of trackerLifecycle.messages) {
|
|
2152
|
+
if (msg.level === 'warn')
|
|
2153
|
+
p.log.warn(msg.text);
|
|
2154
|
+
else
|
|
2155
|
+
p.log.info(msg.text);
|
|
2156
|
+
}
|
|
2157
|
+
// Only now that the machine-wide switch is on disk (D-INIT-DRAIN-AFTER-SWITCH).
|
|
2158
|
+
await drainDisabledFeatureQueues({
|
|
2159
|
+
gitRoot,
|
|
2160
|
+
ledgerRoot: learningEnabled || gitRoot === null ? null : await getLedgerRoot(),
|
|
2161
|
+
memoryEnabled,
|
|
2162
|
+
learningEnabled,
|
|
2163
|
+
manifestWritten: trackerLifecycle.manifestWritten,
|
|
2164
|
+
});
|
|
2165
|
+
// The hooks' per-directory log folders, capped (D-LOG-DIR-CAP): one pass
|
|
2166
|
+
// clears every folder it scans beyond the cap; only a backlog beyond the
|
|
2167
|
+
// scan bound (MAX_LOG_DIRS_SCANNED, 100,000) waits for the next init.
|
|
2168
|
+
const logPrune = await pruneHookLogDirs(path.join(devflowDir, 'logs'));
|
|
2169
|
+
if (!logPrune.ok) {
|
|
2170
|
+
if (verbose)
|
|
2171
|
+
p.log.warn(`Could not prune hook log folders: ${logPrune.error}`);
|
|
2172
|
+
}
|
|
2173
|
+
else if (logPrune.value.removed > 0) {
|
|
2174
|
+
p.log.info(formatLogPruneLine(logPrune.value));
|
|
2175
|
+
}
|
|
2176
|
+
// Name the active provider and what the selection moved. The reference
|
|
2177
|
+
// counts come from the install report rather than being recomputed: the
|
|
2178
|
+
// overlay is what actually installed and pruned them, so a second count
|
|
2179
|
+
// here could only ever disagree with it.
|
|
2180
|
+
const trackerLines = formatTrackerAssetSummary({
|
|
2181
|
+
provider: trackerProvider,
|
|
2182
|
+
previous: existingManifest?.features.tracker.provider,
|
|
2183
|
+
isDefault: trackerProvider === DEFAULT_TRACKER_PROVIDER,
|
|
2184
|
+
installedRefs: installReport.overlaidRefs.length,
|
|
2185
|
+
removedRefs: installReport.sweptOrphans.filter(o => o.kind === 'reference').length,
|
|
2186
|
+
agent: trackerLifecycle.agent,
|
|
2187
|
+
});
|
|
2188
|
+
logSummaryLines(trackerLines);
|
|
1780
2189
|
// External model routing status line (Advanced path / explicit --proxy flag only)
|
|
1781
2190
|
if (proxyEnabled) {
|
|
1782
2191
|
p.log.info(`External model routing: ${color.green('enabled')} — takes effect in new Claude Code sessions`);
|