devflow-kit 2.4.0 → 2.5.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 +156 -0
- package/README.md +86 -18
- package/dist/agents/git.md +824 -0
- package/dist/cli/commands/agents.js +6 -1
- package/dist/cli/commands/attribution-prompts.js +1 -1
- package/dist/cli/commands/compliance-prompts.js +1 -1
- package/dist/cli/commands/compliance.js +23 -1
- package/dist/cli/commands/init-seed.js +24 -26
- package/dist/cli/commands/init.js +502 -71
- package/dist/cli/commands/install-report.js +205 -0
- package/dist/cli/commands/knowledge/index.js +2 -2
- package/dist/cli/commands/knowledge/toggle.js +27 -37
- package/dist/cli/commands/learning.js +37 -30
- package/dist/cli/commands/memory.js +79 -69
- package/dist/cli/commands/prompt-io.js +4 -4
- package/dist/cli/commands/security.js +76 -16
- package/dist/cli/commands/skills.js +53 -7
- package/dist/cli/commands/tracker-prompts.js +145 -0
- package/dist/cli/commands/tracker.js +405 -0
- package/dist/cli/commands/uninstall.js +211 -65
- package/dist/cli.js +2 -0
- package/dist/commands/bug-analysis.md +22 -4
- package/dist/commands/code-review.md +44 -15
- package/dist/commands/debug.md +20 -6
- package/dist/commands/dynamic-build.md +289 -67
- package/dist/commands/dynamic-plan.md +60 -21
- package/dist/commands/dynamic-profile.md +1 -1
- package/dist/commands/dynamic-tickets.md +58 -8
- package/dist/commands/explore.md +2 -2
- package/dist/commands/implement.md +241 -53
- package/dist/commands/plan.md +88 -17
- package/dist/commands/release.md +64 -17
- package/dist/commands/resolve.md +138 -58
- package/dist/commands/self-review.md +2 -2
- package/dist/core/agent-models.js +55 -12
- package/dist/core/assets.js +58 -2
- package/dist/core/evidence-policy.js +147 -0
- package/dist/core/feature-config.js +130 -64
- package/dist/core/feature-switch.js +112 -0
- package/dist/core/flags.js +4 -4
- package/dist/core/manifest.js +33 -7
- package/dist/core/mds-variants.js +861 -0
- package/dist/core/model-discovery.js +12 -1
- package/dist/core/plugins.js +357 -9
- package/dist/core/project-paths.js +1 -1
- 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/tracker.js +407 -0
- 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/installer.js +1213 -31
- package/dist/targets/claude-code/legacy.js +5 -0
- package/dist/targets/claude-code/post-install.js +196 -74
- package/dist/targets/claude-code/tracker-install.js +161 -0
- package/package.json +4 -3
- package/src/assets/agents/code.md +42 -4
- package/src/assets/agents/design.md +1 -1
- package/src/assets/agents/git.mds +827 -0
- package/src/assets/agents/knowledge.md +1 -1
- package/src/assets/agents/learning.md +11 -0
- package/src/assets/agents/synthesize.md +1 -1
- package/src/assets/agents/test.md +16 -5
- package/src/assets/agents/tracker.md +467 -0
- package/src/assets/agents/validate.md +7 -5
- package/src/assets/commands/_partials/_engine.mds +11 -9
- package/src/assets/commands/_partials/_evidence_policy.mds +30 -0
- package/src/assets/commands/_partials/_knowledge.mds +2 -2
- package/src/assets/commands/_partials/_plan_contract.mds +22 -7
- package/src/assets/commands/_partials/_preamble.mds +1 -1
- package/src/assets/commands/_partials/_publication.mds +3 -1
- 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 +15 -5
- package/src/assets/commands/code-review.mds +34 -14
- package/src/assets/commands/debug.mds +11 -4
- package/src/assets/commands/dynamic-build.mds +227 -41
- package/src/assets/commands/dynamic-plan.mds +35 -13
- package/src/assets/commands/dynamic-tickets.mds +47 -5
- package/src/assets/commands/implement.mds +206 -52
- package/src/assets/commands/plan.mds +70 -17
- package/src/assets/commands/release.md +64 -17
- package/src/assets/commands/resolve.mds +126 -56
- 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 +299 -0
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +5 -8
- package/src/assets/scripts/hooks/background-memory-update +14 -9
- package/src/assets/scripts/hooks/capture-prompt +6 -2
- package/src/assets/scripts/hooks/capture-question +6 -2
- package/src/assets/scripts/hooks/capture-turn +6 -2
- package/src/assets/scripts/hooks/ensure-devflow-init +1 -1
- package/src/assets/scripts/hooks/ensure-root-gitignore +161 -60
- package/src/assets/scripts/hooks/hook-log-init +3 -1
- package/src/assets/scripts/hooks/json-helper.cjs +223 -5
- package/src/assets/scripts/hooks/lib/project-paths.cjs +1 -1
- package/src/assets/scripts/hooks/memory-worker +15 -8
- package/src/assets/scripts/hooks/pre-compact-memory +12 -8
- package/src/assets/scripts/hooks/preamble +1 -4
- package/src/assets/scripts/hooks/queue-append +68 -24
- package/src/assets/scripts/hooks/session-start-context +355 -8
- package/src/assets/scripts/hooks/session-start-memory +12 -8
- 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 +1065 -0
- package/src/assets/scripts/verify-evidence.cjs +1822 -0
- package/src/assets/skills/compliance/SKILL.md +2 -0
- package/src/assets/skills/docs-framework/SKILL.md +5 -3
- 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/agents/git.md +0 -938
|
@@ -7,13 +7,15 @@ import color from 'picocolors';
|
|
|
7
7
|
import { getInstallationPaths } from '../../targets/claude-code/claude-paths.js';
|
|
8
8
|
import { getGitRoot } from '../../core/git.js';
|
|
9
9
|
import { installViaFileCopy, composeScripts } from '../../targets/claude-code/installer.js';
|
|
10
|
+
import { formatOverlaySummary, formatSkillScopeSummary, formatTrackerAssetSummary, isPluginListUnchanged } from './install-report.js';
|
|
11
|
+
import { convergeTrackerArtifacts } from '../../targets/claude-code/tracker-install.js';
|
|
10
12
|
import { installSettings, installManagedSettings, installClaudeignore, discoverProjectGitRoots, updateGitignore, ensureDevflowGitignore, createDocsStructure, applyUserSecurityDenyList, detectDenyState, resolveSecurityAction, assertHistoricalDenySuperset, loadTemplateDenyEntries, stripUserSecurityDenyList, } from '../../targets/claude-code/post-install.js';
|
|
11
|
-
import { DEVFLOW_PLUGINS, LEGACY_COMMAND_NAMES, LEGACY_RULE_NAMES, buildAssetMaps,
|
|
13
|
+
import { DEVFLOW_PLUGINS, LEGACY_COMMAND_NAMES, LEGACY_RULE_NAMES, buildAssetMaps, buildScopedSkillsMap, buildRulesMap, partitionSelectablePlugins, WORKFLOW_ORDER, parsePluginSelection, resolveFeatureRedirect } from '../../core/plugins.js';
|
|
12
14
|
import { LEGACY_SKILL_NAMES } from '../../targets/claude-code/legacy.js';
|
|
13
15
|
import { detectPlatform, detectShell, getProfilePath, getSafeDeleteInfo, hasSafeDelete } from '../../core/safe-delete.js';
|
|
14
16
|
import { generateSafeDeleteBlock, installToProfile, removeFromProfile, getInstalledVersion, SAFE_DELETE_BLOCK_VERSION } from '../../core/safe-delete-install.js';
|
|
15
17
|
import { addAmbientHook, removeAmbientHook } from './ambient.js';
|
|
16
|
-
import {
|
|
18
|
+
import { convergeMemoryHooks, drainMemoryQueue } from './memory.js';
|
|
17
19
|
import { addCaptureHooks, removeCaptureHooks } from './capture.js';
|
|
18
20
|
import { removeDreamHook } from './legacy-hooks.js';
|
|
19
21
|
import { addProxyHooks, removeProxyHooks, applyProxyEnv, stripProxyEnv, runProxyPreflight, buildRealPreflightDeps } from './proxy.js';
|
|
@@ -26,13 +28,16 @@ import { loadConfig as loadHudConfig, saveConfig as saveHudConfig } from '../../
|
|
|
26
28
|
import { readManifest, writeManifest, resolvePluginList, detectUpgrade } from '../../core/manifest.js';
|
|
27
29
|
import { convergeFlagsIntoSettings, countActiveFlags, readViewMode } from '../../core/flags.js';
|
|
28
30
|
import { addContextHook, removeContextHook, hasContextHook } from './context.js';
|
|
29
|
-
import {
|
|
31
|
+
import { writeManagedConfig, readConfigIfPresent, DEFAULT_CONFIG } from '../../core/feature-config.js';
|
|
32
|
+
import { drainLearningQueue } from '../../core/learning-queue-cleanup.js';
|
|
33
|
+
import { removeManagedDenyList, describeManagedDenyRemoval } from './security.js';
|
|
30
34
|
import { resolveInitSeed, applyCliToggles, resolveResetGatedInputs } from './init-seed.js';
|
|
31
35
|
import { parseFrameworkList, normalizeFrameworks } from '../../core/compliance.js';
|
|
32
36
|
import { formatComplianceSummary, shouldRunComplianceStep, runComplianceStep, buildClackCompliancePrompts, } from './compliance-prompts.js';
|
|
37
|
+
import { applyTrackerSentinel, parseTrackerId, rearmTrackerInference, renameStaleTrackerConventions, DEFAULT_TRACKER_PROVIDER, } from '../../core/tracker.js';
|
|
38
|
+
import { formatTrackerSummary, shouldRunTrackerStep, runTrackerStep, buildClackTrackerPrompts, } from './tracker-prompts.js';
|
|
33
39
|
import { shouldRunAttributionStep, runAttributionStep, buildClackAttributionPrompts, applyAttributionAnswer, attributionSeedFrom, } from './attribution-prompts.js';
|
|
34
40
|
import { convergeFromManifest } from '../../targets/claude-code/compliance-install.js';
|
|
35
|
-
import { getPendingTurnsPath, getPendingTurnsProcessingPath } from '../../core/project-paths.js';
|
|
36
41
|
import * as os from 'os';
|
|
37
42
|
// Re-export pure functions for tests (canonical source is post-install.ts)
|
|
38
43
|
export { substituteSettingsTemplate, computeGitignoreAppend, mergeDenyList, discoverProjectGitRoots } from '../../targets/claude-code/post-install.js';
|
|
@@ -99,6 +104,31 @@ export function formatSweepSummary(report) {
|
|
|
99
104
|
}
|
|
100
105
|
return lines;
|
|
101
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Log each summary line at the severity it carries — the one dispatch every
|
|
109
|
+
* `SummaryLine[]` renderer shares.
|
|
110
|
+
*
|
|
111
|
+
* Exhaustive over `SummaryLine['level']` rather than an `if/else`: a level added
|
|
112
|
+
* to the interface has to be routed here, at compile time, instead of silently
|
|
113
|
+
* degrading to `info` at every call site.
|
|
114
|
+
*/
|
|
115
|
+
function logSummaryLines(lines) {
|
|
116
|
+
for (const line of lines) {
|
|
117
|
+
switch (line.level) {
|
|
118
|
+
case 'info':
|
|
119
|
+
p.log.info(line.message);
|
|
120
|
+
break;
|
|
121
|
+
case 'warn':
|
|
122
|
+
p.log.warn(line.message);
|
|
123
|
+
break;
|
|
124
|
+
default: {
|
|
125
|
+
const _exhaustive = line.level;
|
|
126
|
+
void _exhaustive;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
102
132
|
/**
|
|
103
133
|
* Classify the safe-delete installation state based on the installed version
|
|
104
134
|
* in the user's shell profile.
|
|
@@ -162,6 +192,250 @@ export function resolveComplianceInitState(complianceOption, seedFrameworks) {
|
|
|
162
192
|
// Re-export formatComplianceSummary from compliance-prompts.ts so existing test imports
|
|
163
193
|
// (tests/init-logic.test.ts:1615 — imports from '../src/cli/commands/init.js') keep resolving.
|
|
164
194
|
export { formatComplianceSummary } from './compliance-prompts.js';
|
|
195
|
+
/**
|
|
196
|
+
* Parse the --tracker CLI option into a tracker override.
|
|
197
|
+
*
|
|
198
|
+
* Pure function — no I/O, no side effects; extracted for testability.
|
|
199
|
+
*
|
|
200
|
+
* Returns:
|
|
201
|
+
* {ok: true, value} — override state derived from the option
|
|
202
|
+
* {ok: false, error} — not a registry provider ID (caller handles exit)
|
|
203
|
+
* undefined — option was not supplied; no override
|
|
204
|
+
*
|
|
205
|
+
* There is no `--no-tracker` (decision D-E): `--tracker github` IS the off
|
|
206
|
+
* switch, because `provider:'github'` is the off position. Parsing is strict —
|
|
207
|
+
* reject, never repair — so `--tracker jira-cloud` exits rather than silently
|
|
208
|
+
* selecting jira.
|
|
209
|
+
*/
|
|
210
|
+
export function resolveTrackerInitState(trackerOption) {
|
|
211
|
+
if (typeof trackerOption !== 'string')
|
|
212
|
+
return undefined;
|
|
213
|
+
const parsed = parseTrackerId(trackerOption);
|
|
214
|
+
if (!parsed.ok)
|
|
215
|
+
return { ok: false, error: parsed.error };
|
|
216
|
+
return { ok: true, value: { provider: parsed.value } };
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* The outcome line for a provider that arrived as `--tracker <id>`.
|
|
220
|
+
*
|
|
221
|
+
* D-TRACKER-CLI-SURFACE [PF-029]: the Advanced path prints no end-of-wizard
|
|
222
|
+
* summary, and `--tracker` suppresses the wizard step that would otherwise
|
|
223
|
+
* print one, so the CLI-override arm is the only place the selection can
|
|
224
|
+
* surface there. Without this line `devflow init --advanced --tracker jira`
|
|
225
|
+
* changes the machine-wide provider with nothing on screen — the same
|
|
226
|
+
* unreachable-step failure the wizard gate exists to prevent, arrived at from
|
|
227
|
+
* the flag side. Recommended already has its surface in the summary note's
|
|
228
|
+
* Tracker row; both paths or the step is invisible on one.
|
|
229
|
+
*
|
|
230
|
+
* Pure — the caller renders. The summary half is `formatTrackerSummary`, the one
|
|
231
|
+
* spelling every tracker surface shares.
|
|
232
|
+
*/
|
|
233
|
+
export function trackerOverrideMessage(provider) {
|
|
234
|
+
return {
|
|
235
|
+
level: provider === DEFAULT_TRACKER_PROVIDER ? 'info' : 'success',
|
|
236
|
+
text: `Tracker: ${formatTrackerSummary(provider)}`,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/** The real adapter — the ONE binding of each owner into the init lifecycle. */
|
|
240
|
+
export function buildTrackerLifecycleIO() {
|
|
241
|
+
return {
|
|
242
|
+
writeManifest,
|
|
243
|
+
renameStaleConventions: renameStaleTrackerConventions,
|
|
244
|
+
convergeArtifacts: (claudeDir, provider, warn) => convergeTrackerArtifacts({ claudeDir, provider, warn }),
|
|
245
|
+
rearmInference: rearmTrackerInference,
|
|
246
|
+
applySentinel: applyTrackerSentinel,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Persist the installation manifest, then converge the tracker artifacts against
|
|
251
|
+
* the provider that was actually persisted.
|
|
252
|
+
*
|
|
253
|
+
* D-TRACKER-CONVERGE: the manifest write and the three tracker file-lifecycle
|
|
254
|
+
* owners are ONE unit because their relative order is the invariant, not an
|
|
255
|
+
* implementation detail (PF-015). The manifest write is explicitly failable —
|
|
256
|
+
* init must not abort on it — so converging the sentinel, the attempt counter or
|
|
257
|
+
* the conventions file ahead of it leaves the artifacts disagreeing in both
|
|
258
|
+
* directions: github→jira writes a sentinel for a provider the manifest never
|
|
259
|
+
* records (a per-session fork cost forever), and jira→github removes the
|
|
260
|
+
* sentinel, renames tracker.md to .bak and leaves the manifest on jira (silent
|
|
261
|
+
* permanent degradation with no re-trigger). Writing first and gating the three
|
|
262
|
+
* owners on `manifestWritten` makes the artifacts converge all-or-none, and puts
|
|
263
|
+
* this call site in the same order as the sibling `devflow tracker --set`
|
|
264
|
+
* (src/cli/commands/tracker.ts): rename → persist → rearm → sentinel.
|
|
265
|
+
*
|
|
266
|
+
* 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 artifacts
|
|
268
|
+
* cannot converge on a value other than the one on disk.
|
|
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.
|
|
274
|
+
*
|
|
275
|
+
* Every step reports rather than aborts (PF-009's isolation posture): a
|
|
276
|
+
* feature-state change must never fail `devflow init`.
|
|
277
|
+
*/
|
|
278
|
+
export async function persistManifestThenConvergeTracker(opts) {
|
|
279
|
+
const { devflowDir, claudeDir, manifestData, previousProvider, io } = opts;
|
|
280
|
+
const provider = manifestData.features.tracker.provider;
|
|
281
|
+
const messages = [];
|
|
282
|
+
// The gate. Non-fatal for the install (which has already succeeded) but
|
|
283
|
+
// decisive for the tracker artifacts: an unpersisted selection converges none
|
|
284
|
+
// of them, so the on-disk state stays internally consistent and the next
|
|
285
|
+
// `devflow init` retries the whole transition from an unchanged starting point.
|
|
286
|
+
try {
|
|
287
|
+
await io.writeManifest(devflowDir, manifestData);
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
messages.push({
|
|
291
|
+
level: 'warn',
|
|
292
|
+
text: `Failed to write installation manifest (install succeeded): ${error instanceof Error ? error.message : error}`,
|
|
293
|
+
});
|
|
294
|
+
messages.push({
|
|
295
|
+
level: 'warn',
|
|
296
|
+
text: `Tracker selection (${provider}) was not persisted — the sentinel, attempt counter and ` +
|
|
297
|
+
`conventions file are unchanged. Re-run devflow init, or devflow tracker --set ${provider}.`,
|
|
298
|
+
});
|
|
299
|
+
return { manifestWritten: false, converged: false, agent: 'unchanged', messages };
|
|
300
|
+
}
|
|
301
|
+
// Move a now-stale conventions file aside (the writer arm of the provider change).
|
|
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).
|
|
328
|
+
const agentWarnings = [];
|
|
329
|
+
const artifacts = await io.convergeArtifacts(claudeDir, provider, (msg) => agentWarnings.push(msg));
|
|
330
|
+
for (const text of agentWarnings)
|
|
331
|
+
messages.push({ level: 'warn', text });
|
|
332
|
+
// C2: the sentinel converges in BOTH directions, and only the WRITE is gated.
|
|
333
|
+
// provider ≠ github → a write, when a spawnable agent is actually there.
|
|
334
|
+
// provider = github → a removal. ALWAYS attempted, because leaving a stale
|
|
335
|
+
// sentinel behind costs every future session a fork for a provider the
|
|
336
|
+
// user has left, and a failed agent removal is not a reason to keep it.
|
|
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;
|
|
347
|
+
const [rearm, sentinel] = await Promise.all([
|
|
348
|
+
// [DR-22] The documented re-arm path: devflow init resets the attempt counter
|
|
349
|
+
// so a previously-capped inference gets another five tries.
|
|
350
|
+
io.rearmInference(devflowDir),
|
|
351
|
+
// [DR-10] Converge the presence sentinel: written for jira/linear, removed for
|
|
352
|
+
// github. This is what keeps the GitHub SessionStart path at one stat and zero forks.
|
|
353
|
+
io.applySentinel(devflowDir, advertisable ? provider : DEFAULT_TRACKER_PROVIDER),
|
|
354
|
+
]);
|
|
355
|
+
if (!rearm.ok)
|
|
356
|
+
messages.push({ level: 'warn', text: rearm.error });
|
|
357
|
+
if (!sentinel.ok)
|
|
358
|
+
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
|
+
return {
|
|
368
|
+
manifestWritten: true,
|
|
369
|
+
converged: artifacts.converged && sentinel.ok,
|
|
370
|
+
agent: artifacts.agent,
|
|
371
|
+
messages,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Drain this repo's memory and learning queues for each feature init switched
|
|
376
|
+
* off, so stale turns are not processed on a future re-enable — the same drains
|
|
377
|
+
* `devflow memory --disable` and `devflow learning --disable` perform. Other
|
|
378
|
+
* repos' queues are inert: every gate reads the machine-wide switch, so nothing
|
|
379
|
+
* appends to or processes them while the feature is off.
|
|
380
|
+
*
|
|
381
|
+
* D-INIT-DRAIN-AFTER-SWITCH: the drain runs only AFTER the manifest holding the
|
|
382
|
+
* switch is persisted (`manifestWritten`), the order the standalone toggles use
|
|
383
|
+
* (write the switch, then drain). The capture hooks read the manifest on every
|
|
384
|
+
* turn, so draining first left a window — the whole install — in which a
|
|
385
|
+
* concurrent session, still reading the old "on", appended turns that then
|
|
386
|
+
* survived the disable. When the manifest write failed the feature is still on
|
|
387
|
+
* everywhere, so its queue is live and is left alone.
|
|
388
|
+
*/
|
|
389
|
+
export async function drainDisabledFeatureQueues(opts, io = { drainMemoryQueue, drainLearningQueue }) {
|
|
390
|
+
if (!opts.manifestWritten || opts.gitRoot === null)
|
|
391
|
+
return;
|
|
392
|
+
if (!opts.memoryEnabled)
|
|
393
|
+
await io.drainMemoryQueue(opts.gitRoot);
|
|
394
|
+
if (!opts.learningEnabled)
|
|
395
|
+
await io.drainLearningQueue(opts.gitRoot);
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* The manifest `devflow init --hud-only` writes. Pure — never mutates `existing`.
|
|
399
|
+
*
|
|
400
|
+
* D-HUD-ONLY-PRESERVE: over a prior install, --hud-only installs the HUD and
|
|
401
|
+
* nothing else, so the manifest keeps every recorded value — plugins, version,
|
|
402
|
+
* scope, installedAt and every feature — and only `features.hud` turns on. The
|
|
403
|
+
* earlier shape rewrote the whole record as a HUD-only fresh install (ambient,
|
|
404
|
+
* memory, learning, knowledge, rules and proxy all `false`, plugins `[]`) while
|
|
405
|
+
* leaving those features' artifacts on disk: the record stopped describing the
|
|
406
|
+
* machine, the next re-init seeded every feature off (ADR-014), and with
|
|
407
|
+
* memory/learning/knowledge switched by the manifest alone
|
|
408
|
+
* (D-FEATURES-MACHINE-WIDE) a HUD install would have really disabled them
|
|
409
|
+
* everywhere. `version` is kept too: --hud-only reinstalls no plugin, and a
|
|
410
|
+
* bumped version would make the next init skip the upgrade it still owes.
|
|
411
|
+
*
|
|
412
|
+
* With no prior manifest the result is the fresh HUD-only record: every other
|
|
413
|
+
* feature off, which is what is installed.
|
|
414
|
+
*/
|
|
415
|
+
export function buildHudOnlyManifest(existing, version, scope, now) {
|
|
416
|
+
if (existing !== null) {
|
|
417
|
+
return {
|
|
418
|
+
...existing,
|
|
419
|
+
features: { ...existing.features, hud: true },
|
|
420
|
+
updatedAt: now,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
version,
|
|
425
|
+
plugins: [],
|
|
426
|
+
scope,
|
|
427
|
+
features: {
|
|
428
|
+
ambient: false, memory: false, hud: true, knowledge: false,
|
|
429
|
+
learning: false, rules: false, flags: {}, proxy: false,
|
|
430
|
+
compliance: { enabled: false, frameworks: [] },
|
|
431
|
+
// The exported default, not a literal — the one constant every other
|
|
432
|
+
// module reads, so a moved default moves here too.
|
|
433
|
+
tracker: { provider: DEFAULT_TRACKER_PROVIDER },
|
|
434
|
+
},
|
|
435
|
+
installedAt: now,
|
|
436
|
+
updatedAt: now,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
165
439
|
export const initCommand = new Command('init')
|
|
166
440
|
.description('Initialize Devflow for Claude Code')
|
|
167
441
|
.option('--scope <type>', 'Installation scope: user or local (project-only)', /^(user|local)$/i)
|
|
@@ -183,6 +457,7 @@ export const initCommand = new Command('init')
|
|
|
183
457
|
.option('--no-proxy', 'Disable external model routing')
|
|
184
458
|
.option('--compliance <list>', 'Enable compliance with comma-separated framework IDs (e.g., gdpr,hipaa)')
|
|
185
459
|
.option('--no-compliance', 'Disable compliance (artifacts removed; frameworks remembered for re-enable)')
|
|
460
|
+
.option('--tracker <id>', 'Issue tracker provider: github, jira, or linear')
|
|
186
461
|
.option('--security <mode>', 'Security deny list location: user, managed, or none', /^(user|managed|none)$/i)
|
|
187
462
|
.option('--hud-only', 'Install only the HUD (no plugins, hooks, or extras)')
|
|
188
463
|
.option('--recommended', 'Apply recommended defaults after plugin selection (skip advanced prompts)')
|
|
@@ -261,28 +536,15 @@ export const initCommand = new Command('init')
|
|
|
261
536
|
p.log.error(`Failed to install HUD scripts: ${error instanceof Error ? error.message : error}`);
|
|
262
537
|
process.exit(1);
|
|
263
538
|
}
|
|
264
|
-
// Read existing manifest
|
|
265
|
-
//
|
|
539
|
+
// Read the existing manifest: over a prior install, --hud-only touches only
|
|
540
|
+
// the HUD (D-HUD-ONLY-PRESERVE in buildHudOnlyManifest).
|
|
266
541
|
let existingHudManifest = null;
|
|
267
542
|
try {
|
|
268
543
|
existingHudManifest = await readManifest(devflowDir);
|
|
269
544
|
}
|
|
270
545
|
catch { /* absent on fresh install — existingHudManifest stays null */ }
|
|
271
|
-
// Write minimal manifest
|
|
272
|
-
const now = new Date().toISOString();
|
|
273
546
|
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
|
-
});
|
|
547
|
+
await writeManifest(devflowDir, buildHudOnlyManifest(existingHudManifest, version, scope, new Date().toISOString()));
|
|
286
548
|
}
|
|
287
549
|
catch { /* non-fatal */ }
|
|
288
550
|
p.log.success('HUD installed');
|
|
@@ -319,7 +581,7 @@ export const initCommand = new Command('init')
|
|
|
319
581
|
// registry defaults — including viewMode 'default' (an externally-set /focus in settings.json
|
|
320
582
|
// must not survive a factory reset).
|
|
321
583
|
const { seedManifest, seedConfig, seedSettings } = resolveResetGatedInputs(!!options.reset, existingManifest, earlyProjectConfig, earlySettingsJson ?? '');
|
|
322
|
-
const seed = resolveInitSeed(seedManifest,
|
|
584
|
+
const seed = resolveInitSeed(seedManifest, seedSettings, DEVFLOW_PLUGINS);
|
|
323
585
|
// Early validation: parse --compliance <list> at the boundary before any prompts (PF-parse-at-boundary).
|
|
324
586
|
// options.compliance: string → --compliance <list>; false → --no-compliance; undefined → not passed
|
|
325
587
|
let cliComplianceOverride;
|
|
@@ -333,6 +595,20 @@ export const initCommand = new Command('init')
|
|
|
333
595
|
cliComplianceOverride = complianceStateResult.value;
|
|
334
596
|
}
|
|
335
597
|
}
|
|
598
|
+
// Early validation: parse --tracker <id> at the boundary before any prompts.
|
|
599
|
+
// Strict — reject, never repair — so a typo'd provider exits here rather than
|
|
600
|
+
// installing mechanics for a tracker the user did not name.
|
|
601
|
+
let cliTrackerOverride;
|
|
602
|
+
{
|
|
603
|
+
const trackerStateResult = resolveTrackerInitState(options.tracker);
|
|
604
|
+
if (trackerStateResult !== undefined) {
|
|
605
|
+
if (!trackerStateResult.ok) {
|
|
606
|
+
p.log.error(trackerStateResult.error);
|
|
607
|
+
process.exit(1);
|
|
608
|
+
}
|
|
609
|
+
cliTrackerOverride = trackerStateResult.value;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
336
612
|
// Select plugins to install
|
|
337
613
|
let selectedPlugins = [];
|
|
338
614
|
if (options.plugin) {
|
|
@@ -506,6 +782,9 @@ export const initCommand = new Command('init')
|
|
|
506
782
|
// CLI override applied below in both Recommended and Advanced paths.
|
|
507
783
|
let complianceEnabled = seed.features.compliance.enabled;
|
|
508
784
|
let complianceFrameworks = seed.features.compliance.frameworks;
|
|
785
|
+
// tracker: manifest-group (like proxy and compliance); seed from prior manifest.
|
|
786
|
+
// CLI override applied below in both Recommended and Advanced paths.
|
|
787
|
+
let trackerProvider = seed.features.tracker.provider;
|
|
509
788
|
let enabledFlags = { ...seed.flags };
|
|
510
789
|
// viewModeExplicit: true when --reset is passed; signals resolveFinalViewMode to let the
|
|
511
790
|
// seed-time view-mode win over an externally-set value in settings.json.
|
|
@@ -520,6 +799,47 @@ export const initCommand = new Command('init')
|
|
|
520
799
|
// The final value is written to the manifest and consumed by the dedicated security step.
|
|
521
800
|
let securityMode = 'user'; // placeholder; overwritten below by resolve
|
|
522
801
|
let managedSettingsConfirmed = false;
|
|
802
|
+
/**
|
|
803
|
+
* Run the tracker wizard step for one wizard path.
|
|
804
|
+
*
|
|
805
|
+
* D-TRACKER-CALLSHAPE: both paths share one gate (shouldRunTrackerStep), one
|
|
806
|
+
* cancel idiom and one prompt adapter; they differ only in the mode they
|
|
807
|
+
* declare, the provider they seed from, and whether the step's own outcome
|
|
808
|
+
* line is emitted. Holding all three differences as parameters keeps the
|
|
809
|
+
* shared half single-sourced, so the paths cannot drift the way two
|
|
810
|
+
* hand-copied call sites do.
|
|
811
|
+
*
|
|
812
|
+
* Returns the chosen state, or undefined when the gate declined to run — the
|
|
813
|
+
* caller then owns the CLI-override fallback, which is the only other way the
|
|
814
|
+
* provider can change on that path.
|
|
815
|
+
*/
|
|
816
|
+
const runTrackerStepAt = async (mode, seedProvider, emitMessages) => {
|
|
817
|
+
if (!shouldRunTrackerStep({
|
|
818
|
+
mode,
|
|
819
|
+
modePromptShown,
|
|
820
|
+
isTTY: process.stdin.isTTY,
|
|
821
|
+
hasCliOverride: cliTrackerOverride !== undefined,
|
|
822
|
+
})) {
|
|
823
|
+
return undefined;
|
|
824
|
+
}
|
|
825
|
+
const trackerStep = await runTrackerStep({
|
|
826
|
+
seed: { provider: seedProvider },
|
|
827
|
+
prompts: buildClackTrackerPrompts(),
|
|
828
|
+
});
|
|
829
|
+
if (trackerStep.kind === 'cancelled') {
|
|
830
|
+
p.cancel('Installation cancelled.');
|
|
831
|
+
process.exit(0);
|
|
832
|
+
}
|
|
833
|
+
if (emitMessages) {
|
|
834
|
+
for (const msg of trackerStep.messages) {
|
|
835
|
+
if (msg.level === 'success')
|
|
836
|
+
p.log.success(msg.text);
|
|
837
|
+
else
|
|
838
|
+
p.log.info(msg.text);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
return trackerStep.state;
|
|
842
|
+
};
|
|
523
843
|
// Safe-delete detection (both paths need this)
|
|
524
844
|
const platform = detectPlatform();
|
|
525
845
|
const shell = detectShell();
|
|
@@ -552,6 +872,13 @@ export const initCommand = new Command('init')
|
|
|
552
872
|
// Step messages not emitted here — the Recommended summary note (below) already
|
|
553
873
|
// prints the Compliance line from complianceSummary via formatComplianceSummary.
|
|
554
874
|
}
|
|
875
|
+
// Tracker wizard step — same gate as compliance, so both wizard paths are
|
|
876
|
+
// governed by the one documented gate table (AC-3.6). Runs only when the
|
|
877
|
+
// Setup-mode prompt actually ran, so --recommended and !isTTY stay promptless.
|
|
878
|
+
// emitMessages=false: the Recommended summary note (below) prints the
|
|
879
|
+
// Tracker line via formatTrackerSummary, so the step's own outcome line
|
|
880
|
+
// would be a duplicate.
|
|
881
|
+
const wizardTracker = await runTrackerStepAt('recommended', seed.features.tracker.provider, false);
|
|
555
882
|
// No attribution step here: the suppress-attribution question is Advanced-only (D27).
|
|
556
883
|
// Recommended silently carries the seeded value in enabledFlags — fresh installs get
|
|
557
884
|
// the registry default (off), re-inits get prior state. See shouldRunAttributionStep.
|
|
@@ -567,6 +894,8 @@ export const initCommand = new Command('init')
|
|
|
567
894
|
rules: options.rules,
|
|
568
895
|
proxy: options.proxy,
|
|
569
896
|
compliance: cliComplianceOverride ?? wizardCompliance,
|
|
897
|
+
// Precedence: cliOverride ?? wizardResult ?? seed (applyCliToggles supplies the seed arm).
|
|
898
|
+
tracker: cliTrackerOverride ?? wizardTracker,
|
|
570
899
|
});
|
|
571
900
|
ambientEnabled = effectiveFeatures.ambient;
|
|
572
901
|
memoryEnabled = effectiveFeatures.memory;
|
|
@@ -577,6 +906,7 @@ export const initCommand = new Command('init')
|
|
|
577
906
|
proxyEnabled = effectiveFeatures.proxy;
|
|
578
907
|
complianceEnabled = effectiveFeatures.compliance.enabled;
|
|
579
908
|
complianceFrameworks = effectiveFeatures.compliance.frameworks;
|
|
909
|
+
trackerProvider = effectiveFeatures.tracker.provider;
|
|
580
910
|
// enabledFlags is already initialised to seed.flags above.
|
|
581
911
|
// Compute safe-delete block synchronously so we know whether to fetch installed version
|
|
582
912
|
if (profilePath && safeDeleteAvailable) {
|
|
@@ -612,6 +942,10 @@ export const initCommand = new Command('init')
|
|
|
612
942
|
`Knowledge bases: ${knowledgeEnabled ? 'enabled' : 'disabled'}`,
|
|
613
943
|
`Ext model routing: ${proxyEnabled ? 'enabled' : 'disabled'}`,
|
|
614
944
|
`Compliance: ${complianceSummary}`,
|
|
945
|
+
// Recommended emits no per-step outcome lines, so this summary row is the
|
|
946
|
+
// tracker step's ONLY surface on this path — both surfaces or it is
|
|
947
|
+
// invisible on one path.
|
|
948
|
+
`Tracker: ${formatTrackerSummary(trackerProvider)}`,
|
|
615
949
|
`View mode: ${readViewMode(enabledFlags)}`,
|
|
616
950
|
`Claude Code flags: ${defaultFlagCount} configured`,
|
|
617
951
|
`${claudeignoreEnabled ? '.claudeignore: created' : ''}`,
|
|
@@ -657,7 +991,8 @@ export const initCommand = new Command('init')
|
|
|
657
991
|
'compaction. Clear your session at any point and resume right\n' +
|
|
658
992
|
'where you left off.\n\n' +
|
|
659
993
|
'Runs a background agent on session stop that consumes additional\n' +
|
|
660
|
-
'tokens. Consider skipping if token usage is a concern
|
|
994
|
+
'tokens. Consider skipping if token usage is a concern.\n' +
|
|
995
|
+
'Applies to every project.', 'Working Memory');
|
|
661
996
|
const memoryChoice = await p.confirm({
|
|
662
997
|
message: 'Enable working memory? (Recommended)',
|
|
663
998
|
initialValue: seed.features.memory,
|
|
@@ -690,7 +1025,8 @@ export const initCommand = new Command('init')
|
|
|
690
1025
|
else {
|
|
691
1026
|
p.note('Per-feature knowledge bases capture cross-cutting patterns,\n' +
|
|
692
1027
|
'conventions, and gotchas. Created and updated automatically\n' +
|
|
693
|
-
'when workflows touch a documented area (write-through model)
|
|
1028
|
+
'when workflows touch a documented area (write-through model).\n' +
|
|
1029
|
+
'Applies to every project.', 'Feature Knowledge Bases');
|
|
694
1030
|
const knowledgeChoice = await p.confirm({
|
|
695
1031
|
message: 'Enable feature knowledge bases? (Recommended)',
|
|
696
1032
|
initialValue: seed.features.knowledge,
|
|
@@ -707,7 +1043,7 @@ export const initCommand = new Command('init')
|
|
|
707
1043
|
else {
|
|
708
1044
|
p.note('Detects architectural decisions and pitfalls from your session\n' +
|
|
709
1045
|
'dialogs. Runs a background agent on session stop that consumes\n' +
|
|
710
|
-
'additional tokens.', 'Learning (Decision/Pitfall Tracking)');
|
|
1046
|
+
'additional tokens. Applies to every project.', 'Learning (Decision/Pitfall Tracking)');
|
|
711
1047
|
const learningChoice = await p.confirm({
|
|
712
1048
|
message: 'Enable learning? (Recommended)',
|
|
713
1049
|
initialValue: seed.features.learning,
|
|
@@ -797,6 +1133,28 @@ export const initCommand = new Command('init')
|
|
|
797
1133
|
// No third case in practice: on this path the predicate only returns false for a
|
|
798
1134
|
// CLI override (isTTY is guaranteed true by the non-TTY guard above). If it ever
|
|
799
1135
|
// did, the seed values assigned at declaration stand — which is the right default.
|
|
1136
|
+
// Tracker feature (after compliance, before attribution). Gated by the same
|
|
1137
|
+
// shouldRunTrackerStep predicate as the Recommended path, so the documented
|
|
1138
|
+
// gate table is the single authority for both and they cannot drift.
|
|
1139
|
+
// This call site is the one that matters on RE-INIT: re-init is Advanced-only
|
|
1140
|
+
// by construction, so a Recommended-only wiring would be dead there.
|
|
1141
|
+
// emitMessages=true: Advanced has no end-of-wizard summary recap, so the
|
|
1142
|
+
// step's outcome line is this path's ONLY surface — mandatory, not decorative.
|
|
1143
|
+
const advancedTracker = await runTrackerStepAt('advanced', trackerProvider, true);
|
|
1144
|
+
if (advancedTracker !== undefined) {
|
|
1145
|
+
trackerProvider = advancedTracker.provider;
|
|
1146
|
+
}
|
|
1147
|
+
else if (cliTrackerOverride !== undefined) {
|
|
1148
|
+
// --tracker passed explicitly — honour without prompting, and say so.
|
|
1149
|
+
// The gate declined the step, and this path has no summary recap, so
|
|
1150
|
+
// this line is the selection's ONLY surface (D-TRACKER-CLI-SURFACE).
|
|
1151
|
+
trackerProvider = cliTrackerOverride.provider;
|
|
1152
|
+
const overrideLine = trackerOverrideMessage(trackerProvider);
|
|
1153
|
+
if (overrideLine.level === 'success')
|
|
1154
|
+
p.log.success(overrideLine.text);
|
|
1155
|
+
else
|
|
1156
|
+
p.log.info(overrideLine.text);
|
|
1157
|
+
}
|
|
800
1158
|
// Attribution feature (after compliance, before flags). This is the ONLY call site —
|
|
801
1159
|
// the attribution question is Advanced-only (D27); the Recommended path never asks and
|
|
802
1160
|
// silently carries the seeded value. The gate stays an explicit predicate call so the
|
|
@@ -993,6 +1351,10 @@ export const initCommand = new Command('init')
|
|
|
993
1351
|
}
|
|
994
1352
|
}
|
|
995
1353
|
// Detect current deny list state in user settings (read-only; write happens in security step)
|
|
1354
|
+
// Whether the managed settings file holds a Devflow deny entry — the security
|
|
1355
|
+
// step's `none` branch removes it only then (and only then stops the spinner
|
|
1356
|
+
// for a possible sudo prompt).
|
|
1357
|
+
let managedDenyDetected = false;
|
|
996
1358
|
{
|
|
997
1359
|
const userSettingsJson = earlySettingsJson;
|
|
998
1360
|
let managedExists = false;
|
|
@@ -1005,6 +1367,7 @@ export const initCommand = new Command('init')
|
|
|
1005
1367
|
}
|
|
1006
1368
|
catch { /* absent or unsupported platform */ }
|
|
1007
1369
|
const detected = detectDenyState(userSettingsJson, managedExists, managedContentJson);
|
|
1370
|
+
managedDenyDetected = detected.managed;
|
|
1008
1371
|
const flagValue = options.security;
|
|
1009
1372
|
const manifestMode = existingManifest?.features.security;
|
|
1010
1373
|
const resolution = resolveSecurityAction(flagValue, manifestMode, detected, process.stdin.isTTY);
|
|
@@ -1068,9 +1431,17 @@ export const initCommand = new Command('init')
|
|
|
1068
1431
|
if (ambientEnabled && ambientPlugin && !pluginsToInstall.includes(ambientPlugin)) {
|
|
1069
1432
|
pluginsToInstall.push(ambientPlugin);
|
|
1070
1433
|
}
|
|
1071
|
-
//
|
|
1072
|
-
//
|
|
1073
|
-
|
|
1434
|
+
// The EFFECTIVE selection — what the manifest will record, resolved here
|
|
1435
|
+
// rather than at manifest-write time because the skills install set is
|
|
1436
|
+
// derived from it. On a full install it is `pluginsToInstall`; on a partial
|
|
1437
|
+
// install (`--plugin=X`) it merges the prior manifest's plugins with X, so a
|
|
1438
|
+
// previously-installed plugin's skills survive an add-one run (AC-22).
|
|
1439
|
+
const installedPluginNames = pluginsToInstall.map(pl => pl.name);
|
|
1440
|
+
const effectivePluginNames = resolvePluginList(installedPluginNames, existingManifest, !!options.plugin);
|
|
1441
|
+
const effectivePlugins = DEVFLOW_PLUGINS.filter(pl => effectivePluginNames.includes(pl.name));
|
|
1442
|
+
// Skills: the effective selection's closure — every plugin's own skills plus
|
|
1443
|
+
// the ones it requires. Scoped like rules, agents and commands already are.
|
|
1444
|
+
const skillsMap = buildScopedSkillsMap(effectivePlugins);
|
|
1074
1445
|
// Agents: install only from selected plugins
|
|
1075
1446
|
const { agentsMap } = buildAssetMaps(pluginsToInstall);
|
|
1076
1447
|
// Rules: install only from selected plugins (plugin-scoped, not universal)
|
|
@@ -1107,16 +1478,23 @@ export const initCommand = new Command('init')
|
|
|
1107
1478
|
catch { /* absent — no legacy artifact */ }
|
|
1108
1479
|
// Install via file copy
|
|
1109
1480
|
let installReport;
|
|
1481
|
+
const installWarnings = [];
|
|
1110
1482
|
try {
|
|
1111
1483
|
installReport = await installViaFileCopy({
|
|
1112
1484
|
plugins: pluginsToInstall,
|
|
1485
|
+
effectivePlugins,
|
|
1113
1486
|
claudeDir,
|
|
1114
1487
|
devflowDir,
|
|
1115
1488
|
skillsMap,
|
|
1116
1489
|
agentsMap,
|
|
1117
1490
|
rulesMap,
|
|
1491
|
+
trackerProvider,
|
|
1118
1492
|
isPartialInstall: !!options.plugin,
|
|
1119
1493
|
spinner: s,
|
|
1494
|
+
// Non-fatal install notices with no other channel (skipped symlinks in the
|
|
1495
|
+
// generated reference tree, mode-normalisation failures) reach the user rather
|
|
1496
|
+
// than the void. Collected now, emitted after the spinner stops.
|
|
1497
|
+
warn: (msg) => { installWarnings.push(msg); },
|
|
1120
1498
|
});
|
|
1121
1499
|
}
|
|
1122
1500
|
catch (error) {
|
|
@@ -1400,18 +1778,18 @@ export const initCommand = new Command('init')
|
|
|
1400
1778
|
content = ambientEnabled ? await addAmbientHook(cleanedForAmbient, devflowDir) : cleanedForAmbient;
|
|
1401
1779
|
// Capture hooks — always-on (like the context hook below), remove-then-add for
|
|
1402
1780
|
// 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. MUST run before
|
|
1781
|
+
// each script gates its own per-queue write on the machine-wide switch, so there
|
|
1782
|
+
// is no CLI-level enable/disable toggle here. MUST run before convergeMemoryHooks below
|
|
1405
1783
|
// so capture-turn lands before memory-worker in the Stop array (AC-C2 ordering:
|
|
1406
1784
|
// append-before-spawn).
|
|
1407
1785
|
const cleanedForCapture = removeCaptureHooks(content);
|
|
1408
1786
|
content = addCaptureHooks(cleanedForCapture, devflowDir);
|
|
1409
|
-
// Memory hooks —
|
|
1410
|
-
//
|
|
1411
|
-
// Learning agent (spawned via
|
|
1412
|
-
//
|
|
1413
|
-
|
|
1414
|
-
content =
|
|
1787
|
+
// Memory hooks — Stop (memory-worker), SessionStart (session-start-memory),
|
|
1788
|
+
// PreCompact — through the same transform `devflow memory --enable/--disable`
|
|
1789
|
+
// uses (D-FEATURES-MACHINE-WIDE). Learning agent (spawned via
|
|
1790
|
+
// session-start-context directive) handles decision/pitfall detection.
|
|
1791
|
+
// Knowledge is handled in-command via write-through (knowledge_writeback MDS partial).
|
|
1792
|
+
content = convergeMemoryHooks(content, memoryEnabled, devflowDir);
|
|
1415
1793
|
// HUD statusLine
|
|
1416
1794
|
content = hudEnabled
|
|
1417
1795
|
? addHudStatusLine(content, devflowDir)
|
|
@@ -1480,32 +1858,21 @@ export const initCommand = new Command('init')
|
|
|
1480
1858
|
p.log.warn(`Could not configure settings.json: ${err instanceof Error ? err.message : err}. ` +
|
|
1481
1859
|
'Manifest records intended state; run devflow init again to retry.');
|
|
1482
1860
|
}
|
|
1483
|
-
// Write .devflow/config.json
|
|
1484
|
-
//
|
|
1485
|
-
//
|
|
1486
|
-
//
|
|
1487
|
-
//
|
|
1861
|
+
// Write .devflow/config.json — facts about this repo, never a feature switch
|
|
1862
|
+
// (memory/learning/knowledge are the manifest's alone, D-FEATURES-MACHINE-WIDE;
|
|
1863
|
+
// the managed write drops their retired per-repo keys). A managed
|
|
1864
|
+
// read-modify-write, not a whole-file write: init owns only reviewPublication,
|
|
1865
|
+
// and every other key in the file — the hand-written per-repo `tracker`
|
|
1866
|
+
// override first among them — is carried from disk, under --reset too
|
|
1867
|
+
// (D-CONFIG-PRESERVE-UNMANAGED in feature-config.ts, avoids PF-071).
|
|
1488
1868
|
if (gitRoot) {
|
|
1489
|
-
await
|
|
1490
|
-
memory: memoryEnabled,
|
|
1491
|
-
learning: learningEnabled,
|
|
1492
|
-
knowledge: knowledgeEnabled,
|
|
1869
|
+
await writeManagedConfig(gitRoot, {
|
|
1493
1870
|
// reviewPublication has no prompt, so it is carried over from the
|
|
1494
1871
|
// reset-gated snapshot rather than re-read from disk: seedConfig is null
|
|
1495
1872
|
// under --reset, which is what collapses the field back to 'auto' with
|
|
1496
1873
|
// every other feature (PF-015 — read the post-gate binding, not the file).
|
|
1497
|
-
reviewPublication: seedConfig?.reviewPublication ??
|
|
1874
|
+
reviewPublication: seedConfig?.reviewPublication ?? DEFAULT_CONFIG.reviewPublication,
|
|
1498
1875
|
});
|
|
1499
|
-
// Drain orphaned queue files when memory is disabled so stale turns
|
|
1500
|
-
// don't process on a future re-enable. Mirrors memory.ts --disable drain.
|
|
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
|
-
}
|
|
1509
1876
|
}
|
|
1510
1877
|
// Configure HUD
|
|
1511
1878
|
const existingHud = loadHudConfig();
|
|
@@ -1626,11 +1993,26 @@ export const initCommand = new Command('init')
|
|
|
1626
1993
|
}
|
|
1627
1994
|
}
|
|
1628
1995
|
else if (securityMode === 'none') {
|
|
1629
|
-
// None: strip Devflow deny entries from
|
|
1630
|
-
//
|
|
1996
|
+
// None: strip Devflow deny entries from EVERY location, as
|
|
1997
|
+
// `devflow security --disable` does. User settings first, via the
|
|
1998
|
+
// canonical helper (atomic temp+rename; ENOENT-safe; only-write-if-changed).
|
|
1631
1999
|
const stripResult = await stripUserSecurityDenyList(userSettingsPath);
|
|
1632
2000
|
if (stripResult && verbose)
|
|
1633
2001
|
p.log.info(`Security deny list removed (${stripResult.removed.length} entries stripped)`);
|
|
2002
|
+
// Then managed settings, through the one removal security.ts also uses.
|
|
2003
|
+
// Before this, `none` left the managed file in place: the deny list kept
|
|
2004
|
+
// applying — at the highest precedence — while the manifest said `none`.
|
|
2005
|
+
// A permission failure is reported, never thrown (the install already
|
|
2006
|
+
// succeeded; the manifest records the choice and a re-run can retry).
|
|
2007
|
+
if (managedDenyDetected) {
|
|
2008
|
+
s.stop('Removing managed security settings (may prompt for sudo password)...');
|
|
2009
|
+
const managedMsg = describeManagedDenyRemoval(await removeManagedDenyList(rootDir, verbose));
|
|
2010
|
+
if (managedMsg.level === 'warn')
|
|
2011
|
+
p.log.warn(managedMsg.text);
|
|
2012
|
+
else if (verbose)
|
|
2013
|
+
p.log.info(managedMsg.text);
|
|
2014
|
+
s.start('Finalizing installation...');
|
|
2015
|
+
}
|
|
1634
2016
|
}
|
|
1635
2017
|
else {
|
|
1636
2018
|
// Exhaustive guard — if TypeScript reaches here, a new SecurityMode variant was added
|
|
@@ -1683,12 +2065,21 @@ export const initCommand = new Command('init')
|
|
|
1683
2065
|
// failed removal leaves a retired asset live. Both must surface.
|
|
1684
2066
|
// After I09, the installer's knownNames set unions FEATURE_OWNED_SKILLS, so
|
|
1685
2067
|
// devflow:compliance is never swept here — no suppression predicate is needed.
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
2068
|
+
logSummaryLines(formatSweepSummary(installReport));
|
|
2069
|
+
// Reference-overlay reporting: the overlay rewrites files inside an installed skill
|
|
2070
|
+
// the user may have shadowed, and reports any unit it had to leave alone (PF-015).
|
|
2071
|
+
logSummaryLines(formatOverlaySummary(installReport, trackerProvider));
|
|
2072
|
+
// Skill-scoping reporting: a deselected skill is deleted and a dormant shadow
|
|
2073
|
+
// is inert, and neither is distinguishable from "never installed" on disk.
|
|
2074
|
+
//
|
|
2075
|
+
// L2: "the plugin list is unchanged" means a prior manifest EXISTS and its
|
|
2076
|
+
// plugin set equals this run's. A first install had nothing to remove, so
|
|
2077
|
+
// there is no upgrade to explain — the removal notice would be addressed to
|
|
2078
|
+
// a user who never had the skills.
|
|
2079
|
+
const pluginListUnchanged = isPluginListUnchanged(existingManifest?.plugins ?? null, effectivePluginNames);
|
|
2080
|
+
logSummaryLines(formatSkillScopeSummary(installReport, pluginListUnchanged));
|
|
2081
|
+
for (const warning of installWarnings)
|
|
2082
|
+
p.log.warn(warning);
|
|
1692
2083
|
const installedSet = new Set(pluginsToInstall.flatMap(p => p.commands).filter(c => c.length > 0));
|
|
1693
2084
|
const orderedCommands = WORKFLOW_ORDER.filter(cmd => installedSet.has(cmd));
|
|
1694
2085
|
if (orderedCommands.length > 0) {
|
|
@@ -1741,11 +2132,13 @@ export const initCommand = new Command('init')
|
|
|
1741
2132
|
p.log.info(`Deduplication: ${agentsMap.size} unique agents (from ${totalAgentDeclarations} declarations)`);
|
|
1742
2133
|
}
|
|
1743
2134
|
// Write installation manifest for upgrade tracking (non-fatal — install already succeeded)
|
|
1744
|
-
const installedPluginNames = pluginsToInstall.map(pl => pl.name);
|
|
1745
2135
|
const now = new Date().toISOString();
|
|
1746
2136
|
const manifestData = {
|
|
1747
2137
|
version,
|
|
1748
|
-
|
|
2138
|
+
// Resolved above, before the install, because the skills install set is
|
|
2139
|
+
// derived from it — one binding, so the manifest can never record a
|
|
2140
|
+
// selection other than the one the assets were installed for.
|
|
2141
|
+
plugins: effectivePluginNames,
|
|
1749
2142
|
scope,
|
|
1750
2143
|
// Snapshot of known plugin names at this install — used by resolveSeedPlugins on next init
|
|
1751
2144
|
// to detect new non-optional plugins and auto-adopt them.
|
|
@@ -1767,16 +2160,54 @@ export const initCommand = new Command('init')
|
|
|
1767
2160
|
// and Advanced wizard selection. convergeComplianceArtifacts was called above.
|
|
1768
2161
|
// normalizeFrameworks: dedup + filter unknowns before persisting.
|
|
1769
2162
|
compliance: { enabled: complianceEnabled, frameworks: normalizeFrameworks(complianceFrameworks) },
|
|
2163
|
+
// Resolved tracker selection. Already a validated TrackerProvider — it came
|
|
2164
|
+
// through parseTrackerId (CLI), the typed wizard select, or the seed, which
|
|
2165
|
+
// itself came through normalizeTrackerFeature on read.
|
|
2166
|
+
tracker: { provider: trackerProvider },
|
|
1770
2167
|
},
|
|
1771
2168
|
installedAt: existingManifest?.installedAt ?? now,
|
|
1772
2169
|
updatedAt: now,
|
|
1773
2170
|
};
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
2171
|
+
// ── Manifest write + tracker selection lifecycle (the ONE call site) ──────
|
|
2172
|
+
// persistManifestThenConvergeTracker owns the ordering invariant: the three
|
|
2173
|
+
// tracker file-lifecycle owners in src/core/tracker.ts converge only against
|
|
2174
|
+
// a provider the manifest actually persisted (D-TRACKER-CONVERGE, PF-015).
|
|
2175
|
+
const trackerLifecycle = await persistManifestThenConvergeTracker({
|
|
2176
|
+
devflowDir,
|
|
2177
|
+
claudeDir,
|
|
2178
|
+
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
|
+
io: buildTrackerLifecycleIO(),
|
|
2184
|
+
});
|
|
2185
|
+
for (const msg of trackerLifecycle.messages) {
|
|
2186
|
+
if (msg.level === 'warn')
|
|
2187
|
+
p.log.warn(msg.text);
|
|
2188
|
+
else
|
|
2189
|
+
p.log.info(msg.text);
|
|
2190
|
+
}
|
|
2191
|
+
// Only now that the machine-wide switch is on disk (D-INIT-DRAIN-AFTER-SWITCH).
|
|
2192
|
+
await drainDisabledFeatureQueues({
|
|
2193
|
+
gitRoot,
|
|
2194
|
+
memoryEnabled,
|
|
2195
|
+
learningEnabled,
|
|
2196
|
+
manifestWritten: trackerLifecycle.manifestWritten,
|
|
2197
|
+
});
|
|
2198
|
+
// Name the active provider and what the selection moved. The reference
|
|
2199
|
+
// counts come from the install report rather than being recomputed: the
|
|
2200
|
+
// overlay is what actually installed and pruned them, so a second count
|
|
2201
|
+
// here could only ever disagree with it.
|
|
2202
|
+
const trackerLines = formatTrackerAssetSummary({
|
|
2203
|
+
provider: trackerProvider,
|
|
2204
|
+
previous: existingManifest?.features.tracker.provider,
|
|
2205
|
+
isDefault: trackerProvider === DEFAULT_TRACKER_PROVIDER,
|
|
2206
|
+
installedRefs: installReport.overlaidRefs.length,
|
|
2207
|
+
removedRefs: installReport.sweptOrphans.filter(o => o.kind === 'reference').length,
|
|
2208
|
+
agent: trackerLifecycle.agent,
|
|
2209
|
+
});
|
|
2210
|
+
logSummaryLines(trackerLines);
|
|
1780
2211
|
// External model routing status line (Advanced path / explicit --proxy flag only)
|
|
1781
2212
|
if (proxyEnabled) {
|
|
1782
2213
|
p.log.info(`External model routing: ${color.green('enabled')} — takes effect in new Claude Code sessions`);
|