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,20 +1,23 @@
|
|
|
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
13
|
import { formatOverlaySummary, formatSkillScopeSummary, formatTrackerAssetSummary, isPluginListUnchanged } from './install-report.js';
|
|
11
14
|
import { convergeTrackerArtifacts } from '../../targets/claude-code/tracker-install.js';
|
|
12
|
-
import { installSettings, installManagedSettings, installClaudeignore, discoverProjectGitRoots,
|
|
15
|
+
import { installSettings, installManagedSettings, installClaudeignore, discoverProjectGitRoots, ensureDevflowGitignore, applyUserSecurityDenyList, detectDenyState, resolveSecurityAction, assertHistoricalDenySuperset, loadTemplateDenyEntries, stripUserSecurityDenyList, } from '../../targets/claude-code/post-install.js';
|
|
13
16
|
import { DEVFLOW_PLUGINS, LEGACY_COMMAND_NAMES, LEGACY_RULE_NAMES, buildAssetMaps, buildScopedSkillsMap, buildRulesMap, partitionSelectablePlugins, WORKFLOW_ORDER, parsePluginSelection, resolveFeatureRedirect } from '../../core/plugins.js';
|
|
14
17
|
import { LEGACY_SKILL_NAMES } from '../../targets/claude-code/legacy.js';
|
|
15
18
|
import { detectPlatform, detectShell, getProfilePath, getSafeDeleteInfo, hasSafeDelete } from '../../core/safe-delete.js';
|
|
16
19
|
import { generateSafeDeleteBlock, installToProfile, removeFromProfile, getInstalledVersion, SAFE_DELETE_BLOCK_VERSION } from '../../core/safe-delete-install.js';
|
|
17
|
-
import {
|
|
20
|
+
import { convergeAmbientHooks } from './ambient.js';
|
|
18
21
|
import { convergeMemoryHooks, drainMemoryQueue } from './memory.js';
|
|
19
22
|
import { addCaptureHooks, removeCaptureHooks } from './capture.js';
|
|
20
23
|
import { removeDreamHook } from './legacy-hooks.js';
|
|
@@ -28,19 +31,20 @@ import { loadConfig as loadHudConfig, saveConfig as saveHudConfig } from '../../
|
|
|
28
31
|
import { readManifest, writeManifest, resolvePluginList, detectUpgrade } from '../../core/manifest.js';
|
|
29
32
|
import { convergeFlagsIntoSettings, countActiveFlags, readViewMode } from '../../core/flags.js';
|
|
30
33
|
import { addContextHook, removeContextHook, hasContextHook } from './context.js';
|
|
34
|
+
import { writeSettingsFileAtomic } from '../../core/fs-atomic.js';
|
|
31
35
|
import { writeManagedConfig, readConfigIfPresent, DEFAULT_CONFIG } from '../../core/feature-config.js';
|
|
32
36
|
import { drainLearningQueue } from '../../core/learning-queue-cleanup.js';
|
|
33
37
|
import { removeManagedDenyList, describeManagedDenyRemoval } from './security.js';
|
|
34
|
-
import { resolveInitSeed, applyCliToggles, resolveResetGatedInputs } from './init-seed.js';
|
|
38
|
+
import { resolveInitSeed, applyCliToggles, resolveResetGatedInputs, resolvePluginsToInstall } from './init-seed.js';
|
|
35
39
|
import { parseFrameworkList, normalizeFrameworks } from '../../core/compliance.js';
|
|
36
40
|
import { formatComplianceSummary, shouldRunComplianceStep, runComplianceStep, buildClackCompliancePrompts, } from './compliance-prompts.js';
|
|
37
|
-
import { applyTrackerSentinel, parseTrackerId, rearmTrackerInference,
|
|
41
|
+
import { applyTrackerSentinel, parseTrackerId, rearmTrackerInference, DEFAULT_TRACKER_PROVIDER, } from '../../core/tracker.js';
|
|
38
42
|
import { formatTrackerSummary, shouldRunTrackerStep, runTrackerStep, buildClackTrackerPrompts, } from './tracker-prompts.js';
|
|
39
43
|
import { shouldRunAttributionStep, runAttributionStep, buildClackAttributionPrompts, applyAttributionAnswer, attributionSeedFrom, } from './attribution-prompts.js';
|
|
40
44
|
import { convergeFromManifest } from '../../targets/claude-code/compliance-install.js';
|
|
41
45
|
import * as os from 'os';
|
|
42
46
|
// Re-export pure functions for tests (canonical source is post-install.ts)
|
|
43
|
-
export { substituteSettingsTemplate,
|
|
47
|
+
export { substituteSettingsTemplate, mergeDenyList, discoverProjectGitRoots } from '../../targets/claude-code/post-install.js';
|
|
44
48
|
export { addAmbientHook, removeAmbientHook, hasAmbientHook } from './ambient.js';
|
|
45
49
|
export { addMemoryHooks, removeMemoryHooks, hasMemoryHooks } from './memory.js';
|
|
46
50
|
export { addCaptureHooks, removeCaptureHooks, hasCaptureHooks } from './capture.js';
|
|
@@ -240,8 +244,7 @@ export function trackerOverrideMessage(provider) {
|
|
|
240
244
|
export function buildTrackerLifecycleIO() {
|
|
241
245
|
return {
|
|
242
246
|
writeManifest,
|
|
243
|
-
|
|
244
|
-
convergeArtifacts: (claudeDir, provider, warn) => convergeTrackerArtifacts({ claudeDir, provider, warn }),
|
|
247
|
+
convergeArtifacts: (claudeDir, warn) => convergeTrackerArtifacts({ claudeDir, warn }),
|
|
245
248
|
rearmInference: rearmTrackerInference,
|
|
246
249
|
applySentinel: applyTrackerSentinel,
|
|
247
250
|
};
|
|
@@ -250,33 +253,26 @@ export function buildTrackerLifecycleIO() {
|
|
|
250
253
|
* Persist the installation manifest, then converge the tracker artifacts against
|
|
251
254
|
* the provider that was actually persisted.
|
|
252
255
|
*
|
|
253
|
-
* D-TRACKER-CONVERGE: the manifest write and the
|
|
254
|
-
*
|
|
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
|
|
255
258
|
* implementation detail (PF-015). The manifest write is explicitly failable —
|
|
256
|
-
* init must not abort on it — so converging the sentinel
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
* this call site in the same order as the sibling `devflow tracker --set`
|
|
264
|
-
* (src/cli/commands/tracker.ts): rename → persist → rearm → sentinel.
|
|
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.
|
|
265
266
|
*
|
|
266
267
|
* The provider is read from `manifestData.features.tracker.provider` rather than
|
|
267
|
-
* taken as a separate argument, so there is exactly one binding and the
|
|
268
|
-
* cannot
|
|
269
|
-
*
|
|
270
|
-
* `previousProvider` is the caller's REAL prior manifest value, never the
|
|
271
|
-
* --reset-gated seed: under --reset the resolved provider collapses to github
|
|
272
|
-
* while the prior provider is still jira/linear, and that IS a transition the
|
|
273
|
-
* stale-conventions rename has to fire on.
|
|
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.
|
|
274
270
|
*
|
|
275
271
|
* Every step reports rather than aborts (PF-009's isolation posture): a
|
|
276
272
|
* feature-state change must never fail `devflow init`.
|
|
277
273
|
*/
|
|
278
274
|
export async function persistManifestThenConvergeTracker(opts) {
|
|
279
|
-
const { devflowDir, claudeDir, manifestData,
|
|
275
|
+
const { devflowDir, claudeDir, manifestData, io } = opts;
|
|
280
276
|
const provider = manifestData.features.tracker.provider;
|
|
281
277
|
const messages = [];
|
|
282
278
|
// The gate. Non-fatal for the install (which has already succeeded) but
|
|
@@ -293,77 +289,35 @@ export async function persistManifestThenConvergeTracker(opts) {
|
|
|
293
289
|
});
|
|
294
290
|
messages.push({
|
|
295
291
|
level: 'warn',
|
|
296
|
-
text: `Tracker selection (${provider}) was not persisted — the sentinel
|
|
297
|
-
`
|
|
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}.`,
|
|
298
294
|
});
|
|
299
295
|
return { manifestWritten: false, converged: false, agent: 'unchanged', messages };
|
|
300
296
|
}
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
// D-TRACKER-PARALLEL: the rename stays strictly ahead of the other two. It is
|
|
304
|
-
// the only step that reads the PREVIOUS provider and the only one that reports
|
|
305
|
-
// a transition, so keeping it first fixes the message order (the transition
|
|
306
|
-
// notice always precedes any owner warning) and keeps the sequence readable as
|
|
307
|
-
// "settle the old provider, then converge the new one". The two that follow
|
|
308
|
-
// touch disjoint files — the attempt counter and the presence sentinel —
|
|
309
|
-
// depend on nothing the other writes, and both report through TrackerResult
|
|
310
|
-
// instead of throwing (PF-014), so they run concurrently and their warnings
|
|
311
|
-
// are pushed in a fixed order regardless of which settles first.
|
|
312
|
-
const transition = await io.renameStaleConventions(devflowDir, previousProvider, provider);
|
|
313
|
-
if (transition.kind === 'renamed') {
|
|
314
|
-
messages.push({
|
|
315
|
-
level: 'info',
|
|
316
|
-
text: `Tracker provider changed — previous ${transition.previous} conventions moved to ` +
|
|
317
|
-
`${color.dim(transition.to)}`,
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
else if (transition.kind === 'failed') {
|
|
321
|
-
messages.push({ level: 'warn', text: transition.error });
|
|
322
|
-
}
|
|
323
|
-
// The fourth owner — the Tracker agent file. SEQUENTIAL, and strictly before
|
|
324
|
-
// the pair below, because the sentinel's WRITE is gated on its outcome: a
|
|
325
|
-
// sentinel that advertises jira while the agent it would spawn is missing is
|
|
326
|
-
// the drifted state this whole ordering exists to prevent (design review H6 —
|
|
327
|
-
// the parallel pair stays a parallel pair, it is not flattened to make room).
|
|
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.
|
|
328
299
|
const agentWarnings = [];
|
|
329
|
-
const artifacts = await io.convergeArtifacts(claudeDir,
|
|
300
|
+
const artifacts = await io.convergeArtifacts(claudeDir, (msg) => agentWarnings.push(msg));
|
|
330
301
|
for (const text of agentWarnings)
|
|
331
302
|
messages.push({ level: 'warn', text });
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
// The write gate reads `agentPresent`, not `converged`. `converged` answers
|
|
339
|
-
// "did THIS run copy it", and reading that alone is wrong in both directions:
|
|
340
|
-
// a re-copy that fails over an already-installed agent would disable a provider
|
|
341
|
-
// that still works, and merely SUPPRESSING the write leaves the PREVIOUS
|
|
342
|
-
// provider's sentinel in place — so a jira → linear init whose agent copy
|
|
343
|
-
// failed goes on advertising jira, which is the state the suppression exists to
|
|
344
|
-
// prevent. Not-spawnable therefore REMOVES, through the one sentinel owner in
|
|
345
|
-
// src/core/tracker.ts (D-TRACKER-OWNER), never an inline fs.rm here.
|
|
346
|
-
const advertisable = provider === DEFAULT_TRACKER_PROVIDER || artifacts.agentPresent;
|
|
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.
|
|
347
308
|
const [rearm, sentinel] = await Promise.all([
|
|
348
|
-
// [DR-22] The documented re-arm path: devflow init resets the attempt
|
|
309
|
+
// [DR-22] The documented re-arm path: devflow init resets the attempt counters
|
|
349
310
|
// so a previously-capped inference gets another five tries.
|
|
350
311
|
io.rearmInference(devflowDir),
|
|
351
|
-
// [DR-10] Converge the
|
|
352
|
-
//
|
|
353
|
-
|
|
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),
|
|
354
316
|
]);
|
|
355
317
|
if (!rearm.ok)
|
|
356
318
|
messages.push({ level: 'warn', text: rearm.error });
|
|
357
319
|
if (!sentinel.ok)
|
|
358
320
|
messages.push({ level: 'warn', text: sentinel.error });
|
|
359
|
-
if (!advertisable) {
|
|
360
|
-
messages.push({
|
|
361
|
-
level: 'warn',
|
|
362
|
-
text: `Tracker sentinel removed — no ${provider} agent is installed, so nothing advertises a ` +
|
|
363
|
-
`provider whose agent is missing and no session will try to spawn it. ` +
|
|
364
|
-
`Re-run devflow init, or devflow tracker --set ${provider}.`,
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
321
|
return {
|
|
368
322
|
manifestWritten: true,
|
|
369
323
|
converged: artifacts.converged && sentinel.ok,
|
|
@@ -385,34 +339,38 @@ export async function persistManifestThenConvergeTracker(opts) {
|
|
|
385
339
|
* concurrent session, still reading the old "on", appended turns that then
|
|
386
340
|
* survived the disable. When the manifest write failed the feature is still on
|
|
387
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.
|
|
388
346
|
*/
|
|
389
347
|
export async function drainDisabledFeatureQueues(opts, io = { drainMemoryQueue, drainLearningQueue }) {
|
|
390
|
-
if (!opts.manifestWritten
|
|
348
|
+
if (!opts.manifestWritten)
|
|
391
349
|
return;
|
|
392
|
-
if (!opts.memoryEnabled)
|
|
350
|
+
if (!opts.memoryEnabled && opts.gitRoot !== null)
|
|
393
351
|
await io.drainMemoryQueue(opts.gitRoot);
|
|
394
|
-
if (!opts.learningEnabled)
|
|
395
|
-
await io.drainLearningQueue(opts.
|
|
352
|
+
if (!opts.learningEnabled && opts.ledgerRoot !== null)
|
|
353
|
+
await io.drainLearningQueue(opts.ledgerRoot);
|
|
396
354
|
}
|
|
397
355
|
/**
|
|
398
356
|
* The manifest `devflow init --hud-only` writes. Pure — never mutates `existing`.
|
|
399
357
|
*
|
|
400
358
|
* D-HUD-ONLY-PRESERVE: over a prior install, --hud-only installs the HUD and
|
|
401
359
|
* nothing else, so the manifest keeps every recorded value — plugins, version,
|
|
402
|
-
*
|
|
360
|
+
* installedAt and every feature — and only `features.hud` turns on. The
|
|
403
361
|
* earlier shape rewrote the whole record as a HUD-only fresh install (ambient,
|
|
404
362
|
* memory, learning, knowledge, rules and proxy all `false`, plugins `[]`) while
|
|
405
363
|
* leaving those features' artifacts on disk: the record stopped describing the
|
|
406
364
|
* machine, the next re-init seeded every feature off (ADR-014), and with
|
|
407
365
|
* memory/learning/knowledge switched by the manifest alone
|
|
408
|
-
* (D-FEATURES-
|
|
366
|
+
* (D-FEATURES-NARROW-ONLY) a HUD install would have really disabled them
|
|
409
367
|
* everywhere. `version` is kept too: --hud-only reinstalls no plugin, and a
|
|
410
368
|
* bumped version would make the next init skip the upgrade it still owes.
|
|
411
369
|
*
|
|
412
370
|
* With no prior manifest the result is the fresh HUD-only record: every other
|
|
413
371
|
* feature off, which is what is installed.
|
|
414
372
|
*/
|
|
415
|
-
export function buildHudOnlyManifest(existing, version,
|
|
373
|
+
export function buildHudOnlyManifest(existing, version, now) {
|
|
416
374
|
if (existing !== null) {
|
|
417
375
|
return {
|
|
418
376
|
...existing,
|
|
@@ -423,7 +381,7 @@ export function buildHudOnlyManifest(existing, version, scope, now) {
|
|
|
423
381
|
return {
|
|
424
382
|
version,
|
|
425
383
|
plugins: [],
|
|
426
|
-
scope,
|
|
384
|
+
scope: 'user',
|
|
427
385
|
features: {
|
|
428
386
|
ambient: false, memory: false, hud: true, knowledge: false,
|
|
429
387
|
learning: false, rules: false, flags: {}, proxy: false,
|
|
@@ -436,9 +394,84 @@ export function buildHudOnlyManifest(existing, version, scope, now) {
|
|
|
436
394
|
updatedAt: now,
|
|
437
395
|
};
|
|
438
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
|
+
}
|
|
439
472
|
export const initCommand = new Command('init')
|
|
440
473
|
.description('Initialize Devflow for Claude Code')
|
|
441
|
-
.
|
|
474
|
+
.addOption(new Option('--scope <type>', 'Retired: Devflow installs machine-wide only').hideHelp())
|
|
442
475
|
.option('--verbose', 'Show detailed installation output')
|
|
443
476
|
.option('--plugin <names>', 'Install specific plugin(s), comma-separated (e.g., implement,code-review)')
|
|
444
477
|
.option('--ambient', 'Enable ambient mode (orchestrator charter + plan handoff)')
|
|
@@ -456,8 +489,8 @@ export const initCommand = new Command('init')
|
|
|
456
489
|
.option('--proxy', 'Enable external model routing (GPT models via your OpenAI/Codex subscription)')
|
|
457
490
|
.option('--no-proxy', 'Disable external model routing')
|
|
458
491
|
.option('--compliance <list>', 'Enable compliance with comma-separated framework IDs (e.g., gdpr,hipaa)')
|
|
459
|
-
.option('--no-compliance', 'Disable compliance (
|
|
460
|
-
.option('--tracker <id>', '
|
|
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')
|
|
461
494
|
.option('--security <mode>', 'Security deny list location: user, managed, or none', /^(user|managed|none)$/i)
|
|
462
495
|
.option('--hud-only', 'Install only the HUD (no plugins, hooks, or extras)')
|
|
463
496
|
.option('--recommended', 'Apply recommended defaults after plugin selection (skip advanced prompts)')
|
|
@@ -483,30 +516,21 @@ export const initCommand = new Command('init')
|
|
|
483
516
|
p.log.error('--reset and --plugin are mutually exclusive. Use --reset alone to restore defaults, or --plugin to update a specific plugin.');
|
|
484
517
|
process.exit(1);
|
|
485
518
|
}
|
|
486
|
-
//
|
|
487
|
-
|
|
488
|
-
if (
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
}
|
|
492
|
-
else if (options.scope) {
|
|
493
|
-
const normalizedScope = options.scope.toLowerCase();
|
|
494
|
-
if (normalizedScope !== 'user' && normalizedScope !== 'local') {
|
|
495
|
-
p.log.error('Invalid scope. Use "user" or "local"');
|
|
496
|
-
process.exit(1);
|
|
497
|
-
}
|
|
498
|
-
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);
|
|
499
524
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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);
|
|
503
530
|
}
|
|
531
|
+
const { homeDir, claudeDir, devflowDir } = resolvedPaths.value;
|
|
504
532
|
// --hud-only: install only HUD (skip plugins, hooks, extras)
|
|
505
533
|
if (options.hudOnly) {
|
|
506
|
-
// Resolve paths
|
|
507
|
-
const paths = await getInstallationPaths(scope);
|
|
508
|
-
const claudeDir = paths.claudeDir;
|
|
509
|
-
const devflowDir = paths.devflowDir;
|
|
510
534
|
// Save HUD config
|
|
511
535
|
const existingHud = loadHudConfig();
|
|
512
536
|
saveHudConfig({ enabled: true, detail: existingHud.detail });
|
|
@@ -521,7 +545,7 @@ export const initCommand = new Command('init')
|
|
|
521
545
|
content = '{}';
|
|
522
546
|
}
|
|
523
547
|
const updated = addHudStatusLine(content, devflowDir);
|
|
524
|
-
await
|
|
548
|
+
await writeSettingsFileAtomic(settingsPath, updated);
|
|
525
549
|
}
|
|
526
550
|
catch (error) {
|
|
527
551
|
p.log.error(`Failed to update settings: ${error instanceof Error ? error.message : error}`);
|
|
@@ -544,7 +568,7 @@ export const initCommand = new Command('init')
|
|
|
544
568
|
}
|
|
545
569
|
catch { /* absent on fresh install — existingHudManifest stays null */ }
|
|
546
570
|
try {
|
|
547
|
-
await writeManifest(devflowDir, buildHudOnlyManifest(existingHudManifest, version,
|
|
571
|
+
await writeManifest(devflowDir, buildHudOnlyManifest(existingHudManifest, version, new Date().toISOString()));
|
|
548
572
|
}
|
|
549
573
|
catch { /* non-fatal */ }
|
|
550
574
|
p.log.success('HUD installed');
|
|
@@ -552,28 +576,30 @@ export const initCommand = new Command('init')
|
|
|
552
576
|
p.outro(color.green('HUD-only install complete.'));
|
|
553
577
|
return;
|
|
554
578
|
}
|
|
555
|
-
// ──
|
|
556
|
-
// Best-effort: if path resolution fails here, seed falls back to fresh-install defaults.
|
|
557
|
-
// The authoritative error gate for failed path resolution remains at the install-begins
|
|
558
|
-
// spinner (see "Resolving paths" below). Hoisted above multiselect so Phase 4 can
|
|
559
|
-
// pre-seed plugin/flag/feature prompts.
|
|
579
|
+
// ── Hoisted reads: the prior state that seeds the prompts (InitSeed, Phase 4) ──
|
|
560
580
|
let existingManifest = null;
|
|
561
|
-
|
|
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;
|
|
562
598
|
let earlySettingsJson = null;
|
|
563
|
-
let earlyGitRoot = null;
|
|
564
599
|
try {
|
|
565
|
-
|
|
566
|
-
existingManifest = await readManifest(earlyPaths.devflowDir);
|
|
567
|
-
earlyGitRoot = earlyPaths.gitRoot ?? await getGitRoot();
|
|
568
|
-
if (earlyGitRoot) {
|
|
569
|
-
earlyProjectConfig = await readConfigIfPresent(earlyGitRoot);
|
|
570
|
-
}
|
|
571
|
-
try {
|
|
572
|
-
earlySettingsJson = await fs.readFile(path.join(earlyPaths.claudeDir, 'settings.json'), 'utf-8');
|
|
573
|
-
}
|
|
574
|
-
catch { /* settings.json absent — treated as empty */ }
|
|
600
|
+
earlySettingsJson = await fs.readFile(path.join(claudeDir, 'settings.json'), 'utf-8');
|
|
575
601
|
}
|
|
576
|
-
catch { /*
|
|
602
|
+
catch { /* settings.json absent — treated as empty */ }
|
|
577
603
|
// --reset: factory reset — treat as a fresh install for all seeding and routing decisions.
|
|
578
604
|
// The REAL existingManifest / earlySettingsJson are still used below for installedAt
|
|
579
605
|
// preservation, upgrade messaging, and security deny-state detection. resolveResetGatedInputs
|
|
@@ -717,7 +743,8 @@ export const initCommand = new Command('init')
|
|
|
717
743
|
// When no --plugin flag is given and a manifest exists, the seed carries the prior
|
|
718
744
|
// selection (existing plugins ∪ new non-optional plugins not yet in knownPlugins).
|
|
719
745
|
// Fresh non-interactive installs (no manifest) fall through to the default path
|
|
720
|
-
// in
|
|
746
|
+
// in resolvePluginsToInstall: every non-optional plugin, with devflow-ambient
|
|
747
|
+
// following the ambient switch (D-AMBIENT-FOLLOWS-SWITCH).
|
|
721
748
|
if (!options.plugin && !process.stdin.isTTY && seedManifest !== null) {
|
|
722
749
|
selectedPlugins = [...seed.workflowPlugins, ...seed.languagePlugins];
|
|
723
750
|
}
|
|
@@ -791,7 +818,7 @@ export const initCommand = new Command('init')
|
|
|
791
818
|
// --reset empties the settings snapshot via resolveResetGatedInputs so seed.flags['view-mode']
|
|
792
819
|
// collapses to 'default', and explicit=true makes it take effect at settings write time.
|
|
793
820
|
let viewModeExplicit = !!options.reset;
|
|
794
|
-
let claudeignoreEnabled = !!
|
|
821
|
+
let claudeignoreEnabled = !!gitRoot;
|
|
795
822
|
let discoveredProjects = [];
|
|
796
823
|
let safeDeleteAction = 'skip';
|
|
797
824
|
let safeDeleteBlock = null;
|
|
@@ -914,10 +941,10 @@ export const initCommand = new Command('init')
|
|
|
914
941
|
safeDeleteBlock = generateSafeDeleteBlock(shell, process.platform, trashCmd);
|
|
915
942
|
}
|
|
916
943
|
// Run independent I/O in parallel: project discovery + safe-delete version check
|
|
917
|
-
const needsDiscovery =
|
|
944
|
+
const needsDiscovery = gitRoot !== null;
|
|
918
945
|
const needsVersionCheck = safeDeleteBlock && profilePath;
|
|
919
946
|
const [discoveredResult, installedVersionResult] = await Promise.all([
|
|
920
|
-
needsDiscovery ?
|
|
947
|
+
needsDiscovery ? discoverRepoRoots(claudeDir, homeDir) : Promise.resolve([]),
|
|
921
948
|
needsVersionCheck ? getInstalledVersion(profilePath) : Promise.resolve(0),
|
|
922
949
|
]);
|
|
923
950
|
discoveredProjects = discoveredResult;
|
|
@@ -1200,45 +1227,29 @@ export const initCommand = new Command('init')
|
|
|
1200
1227
|
p.log.info(`Flags: ${activeCount} active — customize any time with 'devflow flags'`);
|
|
1201
1228
|
}
|
|
1202
1229
|
// .claudeignore prompt
|
|
1203
|
-
if (
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
process.exit(0);
|
|
1223
|
-
}
|
|
1224
|
-
claudeignoreEnabled = claudeignoreChoice;
|
|
1225
|
-
}
|
|
1226
|
-
else {
|
|
1227
|
-
const claudeignoreChoice = await p.confirm({
|
|
1228
|
-
message: 'Create .claudeignore? (Recommended)',
|
|
1229
|
-
initialValue: true,
|
|
1230
|
-
});
|
|
1231
|
-
if (p.isCancel(claudeignoreChoice)) {
|
|
1232
|
-
p.cancel('Installation cancelled.');
|
|
1233
|
-
process.exit(0);
|
|
1234
|
-
}
|
|
1235
|
-
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);
|
|
1236
1249
|
}
|
|
1250
|
+
claudeignoreEnabled = claudeignoreChoice;
|
|
1237
1251
|
}
|
|
1238
1252
|
else {
|
|
1239
|
-
p.note('Creates a .claudeignore in this project that excludes\n' +
|
|
1240
|
-
'secrets, API keys, dependencies, and build artifacts from\n' +
|
|
1241
|
-
'Claude\'s context window.', '.claudeignore');
|
|
1242
1253
|
const claudeignoreChoice = await p.confirm({
|
|
1243
1254
|
message: 'Create .claudeignore? (Recommended)',
|
|
1244
1255
|
initialValue: true,
|
|
@@ -1279,8 +1290,8 @@ export const initCommand = new Command('init')
|
|
|
1279
1290
|
}
|
|
1280
1291
|
}
|
|
1281
1292
|
}
|
|
1282
|
-
// Security deny list placement (
|
|
1283
|
-
if (
|
|
1293
|
+
// Security deny list placement (TTY only)
|
|
1294
|
+
if (process.stdin.isTTY) {
|
|
1284
1295
|
p.note('Devflow includes a security deny list that blocks dangerous\n' +
|
|
1285
1296
|
'commands (rm -rf, sudo, eval, etc). It can be installed as a\n' +
|
|
1286
1297
|
'read-only system file or in your editable settings.json.', 'Security Deny List');
|
|
@@ -1323,32 +1334,17 @@ export const initCommand = new Command('init')
|
|
|
1323
1334
|
// ╭──────────────────────────────────────────────────────────╮
|
|
1324
1335
|
// │ All prompts collected — installation begins │
|
|
1325
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);
|
|
1326
1341
|
const s = p.spinner();
|
|
1327
|
-
s.start('
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
let devflowDir;
|
|
1331
|
-
let gitRoot = null;
|
|
1332
|
-
try {
|
|
1333
|
-
const paths = await getInstallationPaths(scope);
|
|
1334
|
-
claudeDir = paths.claudeDir;
|
|
1335
|
-
devflowDir = paths.devflowDir;
|
|
1336
|
-
gitRoot = paths.gitRoot ?? earlyGitRoot;
|
|
1342
|
+
s.start('Installing');
|
|
1343
|
+
if (upgrade?.isUpgrade) {
|
|
1344
|
+
s.message(`Upgrading from v${upgrade.previousVersion} to v${version}`);
|
|
1337
1345
|
}
|
|
1338
|
-
|
|
1339
|
-
s.
|
|
1340
|
-
p.log.error(`Path configuration error: ${error instanceof Error ? error.message : error}`);
|
|
1341
|
-
process.exit(1);
|
|
1342
|
-
}
|
|
1343
|
-
// existingManifest was read early above (hoisted for seed computation); use it here for upgrade detection
|
|
1344
|
-
if (existingManifest) {
|
|
1345
|
-
const upgrade = detectUpgrade(version, existingManifest.version);
|
|
1346
|
-
if (upgrade.isUpgrade) {
|
|
1347
|
-
s.message(`Upgrading from v${upgrade.previousVersion} to v${version}`);
|
|
1348
|
-
}
|
|
1349
|
-
else if (upgrade.isSameVersion) {
|
|
1350
|
-
s.message('Reinstalling same version');
|
|
1351
|
-
}
|
|
1346
|
+
else if (upgrade?.isSameVersion) {
|
|
1347
|
+
s.message('Reinstalling same version');
|
|
1352
1348
|
}
|
|
1353
1349
|
// Detect current deny list state in user settings (read-only; write happens in security step)
|
|
1354
1350
|
// Whether the managed settings file holds a Devflow deny entry — the security
|
|
@@ -1396,41 +1392,19 @@ export const initCommand = new Command('init')
|
|
|
1396
1392
|
}
|
|
1397
1393
|
// Validate target directory
|
|
1398
1394
|
s.message('Validating target directory');
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
await fs.mkdir(claudeDir, { recursive: true });
|
|
1402
|
-
}
|
|
1403
|
-
catch (error) {
|
|
1404
|
-
s.stop('Installation failed');
|
|
1405
|
-
p.log.error(`Failed to create ${claudeDir}: ${error}`);
|
|
1406
|
-
process.exit(1);
|
|
1407
|
-
}
|
|
1395
|
+
try {
|
|
1396
|
+
await fs.access(claudeDir);
|
|
1408
1397
|
}
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
s.stop('Installation failed');
|
|
1415
|
-
p.log.error(`Claude Code not detected at ${claudeDir}`);
|
|
1416
|
-
p.log.info('Install from: https://claude.ai/download');
|
|
1417
|
-
process.exit(1);
|
|
1418
|
-
}
|
|
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);
|
|
1419
1403
|
}
|
|
1420
1404
|
// Resolve plugins and deduplication maps
|
|
1421
1405
|
s.message('Installing components');
|
|
1422
1406
|
const rootDir = getPackageRoot();
|
|
1423
|
-
|
|
1424
|
-
? DEVFLOW_PLUGINS.filter(p => selectedPlugins.includes(p.name))
|
|
1425
|
-
: DEVFLOW_PLUGINS.filter(p => !p.optional);
|
|
1426
|
-
const coreSkillsPlugin = DEVFLOW_PLUGINS.find(p => p.name === 'devflow-core-skills');
|
|
1427
|
-
if (pluginsToInstall.length > 0 && coreSkillsPlugin && !pluginsToInstall.includes(coreSkillsPlugin)) {
|
|
1428
|
-
pluginsToInstall = [coreSkillsPlugin, ...pluginsToInstall];
|
|
1429
|
-
}
|
|
1430
|
-
const ambientPlugin = DEVFLOW_PLUGINS.find(p => p.name === 'devflow-ambient');
|
|
1431
|
-
if (ambientEnabled && ambientPlugin && !pluginsToInstall.includes(ambientPlugin)) {
|
|
1432
|
-
pluginsToInstall.push(ambientPlugin);
|
|
1433
|
-
}
|
|
1407
|
+
const pluginsToInstall = resolvePluginsToInstall(selectedPlugins, ambientEnabled, DEVFLOW_PLUGINS);
|
|
1434
1408
|
// The EFFECTIVE selection — what the manifest will record, resolved here
|
|
1435
1409
|
// rather than at manifest-write time because the skills install set is
|
|
1436
1410
|
// derived from it. On a full install it is `pluginsToInstall`; on a partial
|
|
@@ -1450,12 +1424,11 @@ export const initCommand = new Command('init')
|
|
|
1450
1424
|
// Migrations clean up ~/.devflow runtime data and never touch the installer's copy
|
|
1451
1425
|
// targets, so their position relative to installViaFileCopy carries no dependency.
|
|
1452
1426
|
// Migrations are always-run-unapplied: helpers short-circuit when the target data is
|
|
1453
|
-
// absent, so fresh installs are safe no-ops. State lives at the
|
|
1454
|
-
//
|
|
1427
|
+
// absent, so fresh installs are safe no-ops. State lives at the machine root
|
|
1428
|
+
// ~/.devflow (D30).
|
|
1455
1429
|
{
|
|
1456
1430
|
const { runMigrations } = await import('../../core/migrations.js');
|
|
1457
|
-
|
|
1458
|
-
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);
|
|
1459
1432
|
}
|
|
1460
1433
|
// devflow-compliance was a selectable plugin in earlier releases; it is now a built-in
|
|
1461
1434
|
// feature (devflow compliance --enable/--disable). If the prior manifest still lists it
|
|
@@ -1488,7 +1461,6 @@ export const initCommand = new Command('init')
|
|
|
1488
1461
|
skillsMap,
|
|
1489
1462
|
agentsMap,
|
|
1490
1463
|
rulesMap,
|
|
1491
|
-
trackerProvider,
|
|
1492
1464
|
isPartialInstall: !!options.plugin,
|
|
1493
1465
|
spinner: s,
|
|
1494
1466
|
// Non-fatal install notices with no other channel (skipped symlinks in the
|
|
@@ -1514,13 +1486,13 @@ export const initCommand = new Command('init')
|
|
|
1514
1486
|
manifest: { features: { compliance: { enabled: complianceEnabled, frameworks: complianceFrameworks }, rules: rulesEnabled } },
|
|
1515
1487
|
warn: (msg) => p.log.warn(msg),
|
|
1516
1488
|
});
|
|
1517
|
-
// I41: emit legacy-upgrade notice when compliance is disabled AND pre-existing
|
|
1518
|
-
//
|
|
1519
|
-
//
|
|
1520
|
-
//
|
|
1521
|
-
// 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.
|
|
1522
1494
|
if (!complianceEnabled && (convergeResult.removedPreexisting || hadComplianceRule)) {
|
|
1523
|
-
p.log.info('Compliance
|
|
1495
|
+
p.log.info('Compliance rule removed — if you previously had devflow-compliance installed, ' +
|
|
1524
1496
|
'run `devflow compliance --enable` to re-enable with your framework selection.');
|
|
1525
1497
|
}
|
|
1526
1498
|
}
|
|
@@ -1773,20 +1745,20 @@ export const initCommand = new Command('init')
|
|
|
1773
1745
|
try {
|
|
1774
1746
|
let content = await fs.readFile(settingsPath, 'utf-8');
|
|
1775
1747
|
const original = content;
|
|
1776
|
-
// Ambient
|
|
1777
|
-
|
|
1778
|
-
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);
|
|
1779
1750
|
// Capture hooks — always-on (like the context hook below), remove-then-add for
|
|
1780
1751
|
// upgrade safety. Queue-append only (capture-prompt/capture-turn/capture-question);
|
|
1781
1752
|
// each script gates its own per-queue write on the machine-wide switch, so there
|
|
1782
|
-
// is no CLI-level enable/disable toggle here.
|
|
1783
|
-
// so capture-turn lands before memory-worker in the Stop array
|
|
1784
|
-
//
|
|
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.
|
|
1785
1757
|
const cleanedForCapture = removeCaptureHooks(content);
|
|
1786
1758
|
content = addCaptureHooks(cleanedForCapture, devflowDir);
|
|
1787
1759
|
// Memory hooks — Stop (memory-worker), SessionStart (session-start-memory),
|
|
1788
1760
|
// PreCompact — through the same transform `devflow memory --enable/--disable`
|
|
1789
|
-
// uses (D-FEATURES-
|
|
1761
|
+
// uses (D-FEATURES-NARROW-ONLY). Learning agent (spawned via
|
|
1790
1762
|
// session-start-context directive) handles decision/pitfall detection.
|
|
1791
1763
|
// Knowledge is handled in-command via write-through (knowledge_writeback MDS partial).
|
|
1792
1764
|
content = convergeMemoryHooks(content, memoryEnabled, devflowDir);
|
|
@@ -1843,7 +1815,7 @@ export const initCommand = new Command('init')
|
|
|
1843
1815
|
if (proxyEnabled)
|
|
1844
1816
|
content = applyProxyEnv(content, effectivePort);
|
|
1845
1817
|
if (content !== original) {
|
|
1846
|
-
await
|
|
1818
|
+
await writeSettingsFileAtomic(settingsPath, content);
|
|
1847
1819
|
if (verbose) {
|
|
1848
1820
|
if (ambientEnabled)
|
|
1849
1821
|
p.log.success('Ambient mode hook installed');
|
|
@@ -1858,28 +1830,33 @@ export const initCommand = new Command('init')
|
|
|
1858
1830
|
p.log.warn(`Could not configure settings.json: ${err instanceof Error ? err.message : err}. ` +
|
|
1859
1831
|
'Manifest records intended state; run devflow init again to retry.');
|
|
1860
1832
|
}
|
|
1861
|
-
// Write .devflow/config.json — facts about this repo
|
|
1862
|
-
//
|
|
1863
|
-
//
|
|
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
|
|
1864
1838
|
// read-modify-write, not a whole-file write: init owns only reviewPublication,
|
|
1865
1839
|
// and every other key in the file — the hand-written per-repo `tracker`
|
|
1866
1840
|
// override first among them — is carried from disk, under --reset too
|
|
1867
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).
|
|
1868
1843
|
if (gitRoot) {
|
|
1869
|
-
await writeManagedConfig(gitRoot, {
|
|
1844
|
+
const configWrite = await writeManagedConfig(gitRoot, {
|
|
1870
1845
|
// reviewPublication has no prompt, so it is carried over from the
|
|
1871
1846
|
// reset-gated snapshot rather than re-read from disk: seedConfig is null
|
|
1872
1847
|
// under --reset, which is what collapses the field back to 'auto' with
|
|
1873
1848
|
// every other feature (PF-015 — read the post-gate binding, not the file).
|
|
1874
1849
|
reviewPublication: seedConfig?.reviewPublication ?? DEFAULT_CONFIG.reviewPublication,
|
|
1875
1850
|
});
|
|
1851
|
+
if (!configWrite.ok)
|
|
1852
|
+
p.log.warn(formatManagedConfigWriteError(configWrite.error));
|
|
1876
1853
|
}
|
|
1877
1854
|
// Configure HUD
|
|
1878
1855
|
const existingHud = loadHudConfig();
|
|
1879
1856
|
saveHudConfig({ enabled: hudEnabled, detail: existingHud.detail });
|
|
1880
1857
|
// File extras
|
|
1881
1858
|
if (claudeignoreEnabled) {
|
|
1882
|
-
if (
|
|
1859
|
+
if (discoveredProjects.length > 0) {
|
|
1883
1860
|
const results = await Promise.all(discoveredProjects.map(root => installClaudeignore(root, rootDir, verbose)));
|
|
1884
1861
|
const created = results.filter(Boolean).length;
|
|
1885
1862
|
if (created > 0) {
|
|
@@ -1894,18 +1871,12 @@ export const initCommand = new Command('init')
|
|
|
1894
1871
|
}
|
|
1895
1872
|
}
|
|
1896
1873
|
// Deterministically ensure .devflow/ is gitignored at the repo root — independent
|
|
1897
|
-
// of
|
|
1874
|
+
// of every feature toggle. The always-on ensure-root-gitignore
|
|
1898
1875
|
// hook covers projects that never re-run init; this covers the init-time path so a
|
|
1899
1876
|
// fresh install never tracks .devflow/. Decoupled from memory (avoids PF-014).
|
|
1900
1877
|
if (gitRoot) {
|
|
1901
1878
|
await ensureDevflowGitignore(gitRoot, verbose);
|
|
1902
1879
|
}
|
|
1903
|
-
if (scope === 'local' && gitRoot) {
|
|
1904
|
-
await updateGitignore(gitRoot, verbose);
|
|
1905
|
-
}
|
|
1906
|
-
if (scope === 'local') {
|
|
1907
|
-
await createDocsStructure(verbose);
|
|
1908
|
-
}
|
|
1909
1880
|
// Safe-delete execution (decision was captured during prompt phase)
|
|
1910
1881
|
if (safeDeleteAction === 'install' && safeDeleteBlock && profilePath) {
|
|
1911
1882
|
await installToProfile(profilePath, safeDeleteBlock);
|
|
@@ -2068,7 +2039,7 @@ export const initCommand = new Command('init')
|
|
|
2068
2039
|
logSummaryLines(formatSweepSummary(installReport));
|
|
2069
2040
|
// Reference-overlay reporting: the overlay rewrites files inside an installed skill
|
|
2070
2041
|
// the user may have shadowed, and reports any unit it had to leave alone (PF-015).
|
|
2071
|
-
logSummaryLines(formatOverlaySummary(installReport
|
|
2042
|
+
logSummaryLines(formatOverlaySummary(installReport));
|
|
2072
2043
|
// Skill-scoping reporting: a deselected skill is deleted and a dormant shadow
|
|
2073
2044
|
// is inert, and neither is distinguishable from "never installed" on disk.
|
|
2074
2045
|
//
|
|
@@ -2123,7 +2094,6 @@ export const initCommand = new Command('init')
|
|
|
2123
2094
|
.map(plugin => `${color.yellow(plugin.name.padEnd(24))}${color.dim(plugin.description)}`)
|
|
2124
2095
|
.join('\n');
|
|
2125
2096
|
p.note(pluginsList, 'Installed plugins');
|
|
2126
|
-
p.log.info(`Scope: ${scope}`);
|
|
2127
2097
|
p.log.info(`Claude dir: ${claudeDir}`);
|
|
2128
2098
|
p.log.info(`Devflow dir: ${devflowDir}`);
|
|
2129
2099
|
const totalSkillDeclarations = pluginsToInstall.reduce((sum, p) => sum + p.skills.length, 0);
|
|
@@ -2139,7 +2109,7 @@ export const initCommand = new Command('init')
|
|
|
2139
2109
|
// derived from it — one binding, so the manifest can never record a
|
|
2140
2110
|
// selection other than the one the assets were installed for.
|
|
2141
2111
|
plugins: effectivePluginNames,
|
|
2142
|
-
scope,
|
|
2112
|
+
scope: 'user',
|
|
2143
2113
|
// Snapshot of known plugin names at this install — used by resolveSeedPlugins on next init
|
|
2144
2114
|
// to detect new non-optional plugins and auto-adopt them.
|
|
2145
2115
|
knownPlugins: DEVFLOW_PLUGINS.map(p => p.name),
|
|
@@ -2169,17 +2139,13 @@ export const initCommand = new Command('init')
|
|
|
2169
2139
|
updatedAt: now,
|
|
2170
2140
|
};
|
|
2171
2141
|
// ── Manifest write + tracker selection lifecycle (the ONE call site) ──────
|
|
2172
|
-
// persistManifestThenConvergeTracker owns the ordering invariant: the
|
|
2142
|
+
// persistManifestThenConvergeTracker owns the ordering invariant: the
|
|
2173
2143
|
// tracker file-lifecycle owners in src/core/tracker.ts converge only against
|
|
2174
2144
|
// a provider the manifest actually persisted (D-TRACKER-CONVERGE, PF-015).
|
|
2175
2145
|
const trackerLifecycle = await persistManifestThenConvergeTracker({
|
|
2176
2146
|
devflowDir,
|
|
2177
2147
|
claudeDir,
|
|
2178
2148
|
manifestData,
|
|
2179
|
-
// The REAL manifest, not the --reset-gated seed: under --reset the resolved
|
|
2180
|
-
// provider collapses to github while the prior provider is still jira/linear,
|
|
2181
|
-
// and that IS a transition the stale-file rename has to fire on.
|
|
2182
|
-
previousProvider: existingManifest?.features.tracker.provider,
|
|
2183
2149
|
io: buildTrackerLifecycleIO(),
|
|
2184
2150
|
});
|
|
2185
2151
|
for (const msg of trackerLifecycle.messages) {
|
|
@@ -2191,10 +2157,22 @@ export const initCommand = new Command('init')
|
|
|
2191
2157
|
// Only now that the machine-wide switch is on disk (D-INIT-DRAIN-AFTER-SWITCH).
|
|
2192
2158
|
await drainDisabledFeatureQueues({
|
|
2193
2159
|
gitRoot,
|
|
2160
|
+
ledgerRoot: learningEnabled || gitRoot === null ? null : await getLedgerRoot(),
|
|
2194
2161
|
memoryEnabled,
|
|
2195
2162
|
learningEnabled,
|
|
2196
2163
|
manifestWritten: trackerLifecycle.manifestWritten,
|
|
2197
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
|
+
}
|
|
2198
2176
|
// Name the active provider and what the selection moved. The reference
|
|
2199
2177
|
// counts come from the install report rather than being recomputed: the
|
|
2200
2178
|
// overlay is what actually installed and pruned them, so a second count
|