devflow-kit 2.5.0 → 3.0.1
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 +82 -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 +246 -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
|
@@ -7,18 +7,19 @@
|
|
|
7
7
|
* Avoids PF-009: per-artifact failures are warn-not-throw.
|
|
8
8
|
* Avoids PF-015: enable/disable each converge BOTH artifacts unconditionally.
|
|
9
9
|
* The evidence-policy lines (--status, and the --enable/--set suggestion) come
|
|
10
|
-
* from src/core/evidence-policy.ts, the seam onto the package's own
|
|
11
|
-
* the CLI prints .devflow/
|
|
10
|
+
* from src/core/evidence-policy.ts, the seam onto the package's own resolvers;
|
|
11
|
+
* the CLI prints the keys to add to .devflow/project.json and never writes it
|
|
12
|
+
* (applies ADR-024).
|
|
12
13
|
*/
|
|
13
14
|
import { Command } from 'commander';
|
|
14
15
|
import { promises as fs } from 'fs';
|
|
15
16
|
import * as path from 'path';
|
|
16
17
|
import * as p from '@clack/prompts';
|
|
17
18
|
import color from 'picocolors';
|
|
18
|
-
import {
|
|
19
|
+
import { COMPLIANCE_FRAMEWORKS, normalizeFrameworks, parseFrameworkList, } from '../../core/compliance.js';
|
|
19
20
|
import { frameworkChoices, FRAMEWORK_SELECT_MESSAGE } from './compliance-prompts.js';
|
|
20
21
|
import { COMPLIANCE_SKILL_TOKENS } from '../../core/compliance-compose.js';
|
|
21
|
-
import { evidencePolicyStatusLine, evidencePolicySuggestion, formatEvidencePolicyUnavailable, loadEvidencePolicyModule, } from '../../core/evidence-policy.js';
|
|
22
|
+
import { evidencePolicyStatusLine, evidencePolicySuggestion, formatEvidencePolicyUnavailable, loadEvidencePolicyModule, loadSettingsModule, repoComplianceStatusLines, } from '../../core/evidence-policy.js';
|
|
22
23
|
import { readManifest, writeManifest } from '../../core/manifest.js';
|
|
23
24
|
import { convergeFromManifest } from '../../targets/claude-code/compliance-install.js';
|
|
24
25
|
import { validateRuleShadow, validateSkillShadow } from '../../targets/claude-code/installer.js';
|
|
@@ -65,7 +66,7 @@ export function resolveComplianceCliAction(current, action, setFrameworks) {
|
|
|
65
66
|
messages: [
|
|
66
67
|
{
|
|
67
68
|
level: 'success',
|
|
68
|
-
text: 'Compliance disabled —
|
|
69
|
+
text: 'Compliance disabled — rule removed, frameworks remembered for re-enable',
|
|
69
70
|
},
|
|
70
71
|
],
|
|
71
72
|
};
|
|
@@ -93,23 +94,17 @@ export function resolveComplianceCliAction(current, action, setFrameworks) {
|
|
|
93
94
|
}
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
|
-
// ──
|
|
97
|
+
// ── Manifest classification ────────────────────────────────────────────────────
|
|
97
98
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
99
|
+
* The manifest framework IDs the registry does not know — a hand-edited or
|
|
100
|
+
* newer-devflow manifest. Every install drops them (normalizeFrameworks), so
|
|
101
|
+
* `--status` names them with the one remedy that removes them: `--set`.
|
|
101
102
|
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* logic in isolation.
|
|
103
|
+
* Installed reference files are no drift signal: every install carries all six
|
|
104
|
+
* (D-COMPLIANCE-INSTALL-ALWAYS), whatever the manifest selects.
|
|
105
105
|
*/
|
|
106
|
-
export function
|
|
107
|
-
|
|
108
|
-
const missing = manifestFrameworks.filter(id => !installedSet.has(id));
|
|
109
|
-
return {
|
|
110
|
-
validMissing: missing.filter(id => registryIds.has(id)),
|
|
111
|
-
invalidIds: missing.filter(id => !registryIds.has(id)),
|
|
112
|
-
};
|
|
106
|
+
export function unknownFrameworkIds(manifestFrameworks, registryIds) {
|
|
107
|
+
return manifestFrameworks.filter(id => !registryIds.has(id));
|
|
113
108
|
}
|
|
114
109
|
// ── Status helpers ─────────────────────────────────────────────────────────────
|
|
115
110
|
/** Returns true if the compliance skill dir exists at the install target. */
|
|
@@ -122,23 +117,6 @@ async function skillInstalled(claudeDir) {
|
|
|
122
117
|
return false;
|
|
123
118
|
}
|
|
124
119
|
}
|
|
125
|
-
/** Returns the set of installed framework reference IDs from the skill dir. */
|
|
126
|
-
async function installedRefIds(claudeDir) {
|
|
127
|
-
const refDir = path.join(claudeDir, 'skills', 'devflow:compliance', 'references');
|
|
128
|
-
try {
|
|
129
|
-
const entries = await fs.readdir(refDir);
|
|
130
|
-
return entries
|
|
131
|
-
.filter(e => e.endsWith('.md') && !ALWAYS_PRESENT_REFS.includes(e))
|
|
132
|
-
.map(e => path.basename(e, '.md'))
|
|
133
|
-
// S58: sanitize — keep only entries whose basename matches the expected id
|
|
134
|
-
// shape (lowercase letters, digits, hyphens). Strips terminal-escape sequences
|
|
135
|
-
// or path segments that could be injected via a crafted filename.
|
|
136
|
-
.filter(id => /^[a-z0-9-]+$/.test(id));
|
|
137
|
-
}
|
|
138
|
-
catch {
|
|
139
|
-
return [];
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
120
|
/** Returns true if the compliance rule file exists at the install target. */
|
|
143
121
|
async function ruleInstalled(claudeDir) {
|
|
144
122
|
try {
|
|
@@ -180,7 +158,7 @@ async function skillShadowState(devflowDir) {
|
|
|
180
158
|
export const complianceCommand = new Command('compliance')
|
|
181
159
|
.description('Enable, disable, or configure the compliance feature')
|
|
182
160
|
.option('--enable', 'Enable compliance (restores previously selected frameworks)')
|
|
183
|
-
.option('--disable', 'Disable compliance (
|
|
161
|
+
.option('--disable', 'Disable compliance (rule removed; frameworks remembered for re-enable)')
|
|
184
162
|
.option('--status', 'Show compliance state: manifest, installed artifacts, shadow presence, and the evidence policy for the current repository')
|
|
185
163
|
.option('--set <list>', 'Set active frameworks (comma-separated IDs); enables compliance. Use --set "" for zero frameworks (generic controls only)')
|
|
186
164
|
.action(async (options) => {
|
|
@@ -221,24 +199,22 @@ export const complianceCommand = new Command('compliance')
|
|
|
221
199
|
? current.frameworks.join(', ')
|
|
222
200
|
: color.dim('none declared');
|
|
223
201
|
const rulesEnabled = manifest.features.rules;
|
|
224
|
-
const [skillOk,
|
|
202
|
+
const [skillOk, ruleOk, isRuleShadowed, skillShadow] = await Promise.all([
|
|
225
203
|
skillInstalled(claudeDir),
|
|
226
|
-
installedRefIds(claudeDir),
|
|
227
204
|
ruleInstalled(claudeDir),
|
|
228
205
|
ruleShadowed(devflowDir),
|
|
229
206
|
skillShadowState(devflowDir),
|
|
230
207
|
]);
|
|
231
|
-
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
const driftInstalled = refIds.filter(id => !manifestSet.has(id));
|
|
238
|
-
const { validMissing, invalidIds } = classifyDriftMissing(current.frameworks, refIds, registrySet);
|
|
208
|
+
const invalidIds = unknownFrameworkIds(current.frameworks, new Set(COMPLIANCE_FRAMEWORKS.map(fw => fw.id)));
|
|
209
|
+
// The repository's own declaration (.devflow/project.json) and, while the
|
|
210
|
+
// retired policy file is in the working tree, the hint to migrate it. Both come from
|
|
211
|
+
// the local settings resolver — one git call, no network — and add nothing
|
|
212
|
+
// when the repository declares nothing (the block is then unchanged).
|
|
213
|
+
const repoLines = repoComplianceStatusLines(loadSettingsModule(), { dir: process.cwd() });
|
|
239
214
|
const lines = [
|
|
240
215
|
`State: ${enabledLabel}`,
|
|
241
216
|
`Frameworks: ${fwLabel}`,
|
|
217
|
+
...repoLines,
|
|
242
218
|
'',
|
|
243
219
|
`Skill: ${skillOk ? color.green('installed') : color.dim('not installed')}` +
|
|
244
220
|
(skillShadow === 'composition-skipped'
|
|
@@ -254,20 +230,11 @@ export const complianceCommand = new Command('compliance')
|
|
|
254
230
|
'',
|
|
255
231
|
// The repository in cwd, resolved by the package's own resolver with the
|
|
256
232
|
// compliance state already read above (D-POLICY-CJS-SEAM). Bounded: at most
|
|
257
|
-
//
|
|
233
|
+
// three `gh` calls, each with a timeout, so offline degrades to a flagged line.
|
|
258
234
|
evidencePolicyStatusLine(loadEvidencePolicyModule(), { dir: process.cwd(), compliance: current }),
|
|
259
235
|
];
|
|
260
|
-
if (
|
|
236
|
+
if (invalidIds.length > 0) {
|
|
261
237
|
lines.push('');
|
|
262
|
-
if (driftInstalled.length > 0 || validMissing.length > 0) {
|
|
263
|
-
lines.push(color.yellow('Artifact drift detected (run devflow compliance --enable to reconcile):'));
|
|
264
|
-
if (driftInstalled.length > 0) {
|
|
265
|
-
lines.push(` Installed not in manifest: ${driftInstalled.join(', ')}`);
|
|
266
|
-
}
|
|
267
|
-
if (validMissing.length > 0) {
|
|
268
|
-
lines.push(` In manifest but not installed: ${validMissing.join(', ')}`);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
238
|
for (const id of invalidIds) {
|
|
272
239
|
lines.push(color.red(` unknown framework id in manifest (ignored): ${id} — remove with --set`));
|
|
273
240
|
}
|
|
@@ -342,15 +309,19 @@ export const complianceCommand = new Command('compliance')
|
|
|
342
309
|
p.log.info(color.dim('Note: compliance rule withheld (rules disabled) — ' +
|
|
343
310
|
'run `devflow rules --enable` to install the stamped rule'));
|
|
344
311
|
}
|
|
345
|
-
// Suggest the team
|
|
346
|
-
// .devflow/
|
|
312
|
+
// Suggest the team file compliance now implies. Printed, never written:
|
|
313
|
+
// .devflow/project.json is team-owned (D-POLICY-NO-WRITE, applies ADR-024).
|
|
347
314
|
if (resolved.nextState.enabled) {
|
|
348
315
|
const policyModule = loadEvidencePolicyModule();
|
|
316
|
+
const settingsModule = loadSettingsModule();
|
|
349
317
|
if (!policyModule.ok) {
|
|
350
318
|
p.log.warn(formatEvidencePolicyUnavailable(policyModule.error));
|
|
351
319
|
}
|
|
320
|
+
else if (!settingsModule.ok) {
|
|
321
|
+
p.log.warn(formatEvidencePolicyUnavailable(settingsModule.error));
|
|
322
|
+
}
|
|
352
323
|
else {
|
|
353
|
-
const suggestion = evidencePolicySuggestion(resolved.nextState, policyModule.value);
|
|
324
|
+
const suggestion = evidencePolicySuggestion(resolved.nextState, policyModule.value, settingsModule.value);
|
|
354
325
|
if (suggestion !== null)
|
|
355
326
|
p.note(suggestion, 'Evidence policy');
|
|
356
327
|
}
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { devflowHookOwner, hasHook, removeHooks, runHookCommand, } from '../../targets/claude-code/hooks.js';
|
|
2
2
|
// ─── Context hook utilities ────────────────────────────────────────────────
|
|
3
3
|
//
|
|
4
4
|
// The session-start-context hook is always-on (registered unconditionally by
|
|
5
5
|
// init, removed by uninstall). It has internal sentinel awareness per feature.
|
|
6
6
|
const CONTEXT_HOOK_MARKER = 'session-start-context';
|
|
7
|
+
/**
|
|
8
|
+
* D-EXACT-HOOK-OWNER: the context hook is devflow's only when its command ends in
|
|
9
|
+
* `/scripts/hooks/run-hook session-start-context` (hooks.ts), under any directory.
|
|
10
|
+
* It has been registered through run-hook since it first shipped, so there is no
|
|
11
|
+
* legacy form to recognise.
|
|
12
|
+
*/
|
|
13
|
+
const isContextHook = devflowHookOwner([CONTEXT_HOOK_MARKER]);
|
|
7
14
|
/**
|
|
8
15
|
* Add the session-start-context hook to SessionStart in settings JSON.
|
|
9
16
|
* Idempotent — returns unchanged JSON if hook already present.
|
|
@@ -13,46 +20,24 @@ export function addContextHook(settingsJson, devflowDir) {
|
|
|
13
20
|
return settingsJson;
|
|
14
21
|
}
|
|
15
22
|
const settings = JSON.parse(settingsJson);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
hooks: [
|
|
22
|
-
{
|
|
23
|
-
type: 'command',
|
|
24
|
-
command: hookCommand,
|
|
25
|
-
timeout: 10,
|
|
26
|
-
},
|
|
27
|
-
],
|
|
28
|
-
};
|
|
29
|
-
if (!settings.hooks.SessionStart) {
|
|
30
|
-
settings.hooks.SessionStart = [];
|
|
31
|
-
}
|
|
32
|
-
settings.hooks.SessionStart.push(newEntry);
|
|
23
|
+
settings.hooks ??= {};
|
|
24
|
+
settings.hooks.SessionStart ??= [];
|
|
25
|
+
settings.hooks.SessionStart.push({
|
|
26
|
+
hooks: [{ type: 'command', command: runHookCommand(devflowDir, CONTEXT_HOOK_MARKER), timeout: 10 }],
|
|
27
|
+
});
|
|
33
28
|
return JSON.stringify(settings, null, 2) + '\n';
|
|
34
29
|
}
|
|
35
30
|
/**
|
|
36
31
|
* Remove the session-start-context hook from settings JSON.
|
|
37
32
|
* Idempotent — returns unchanged JSON if hook not present.
|
|
38
|
-
*
|
|
33
|
+
* Removes the single hook, so the other hooks of its matcher group and every
|
|
34
|
+
* other SessionStart group stay in place (D-EXACT-HOOK-OWNER).
|
|
39
35
|
*/
|
|
40
36
|
export function removeContextHook(settingsJson) {
|
|
41
37
|
const settings = JSON.parse(settingsJson);
|
|
42
|
-
if (!settings
|
|
38
|
+
if (!removeHooks(settings, 'SessionStart', isContextHook)) {
|
|
43
39
|
return settingsJson;
|
|
44
40
|
}
|
|
45
|
-
const before = settings.hooks.SessionStart.length;
|
|
46
|
-
settings.hooks.SessionStart = settings.hooks.SessionStart.filter((matcher) => !matcher.hooks.some((h) => h.command.includes(CONTEXT_HOOK_MARKER)));
|
|
47
|
-
if (settings.hooks.SessionStart.length === before) {
|
|
48
|
-
return settingsJson;
|
|
49
|
-
}
|
|
50
|
-
if (settings.hooks.SessionStart.length === 0) {
|
|
51
|
-
delete settings.hooks.SessionStart;
|
|
52
|
-
}
|
|
53
|
-
if (Object.keys(settings.hooks).length === 0) {
|
|
54
|
-
delete settings.hooks;
|
|
55
|
-
}
|
|
56
41
|
return JSON.stringify(settings, null, 2) + '\n';
|
|
57
42
|
}
|
|
58
43
|
/**
|
|
@@ -61,6 +46,6 @@ export function removeContextHook(settingsJson) {
|
|
|
61
46
|
*/
|
|
62
47
|
export function hasContextHook(input) {
|
|
63
48
|
const settings = typeof input === 'string' ? JSON.parse(input) : input;
|
|
64
|
-
return settings
|
|
49
|
+
return hasHook(settings, 'SessionStart', isContextHook);
|
|
65
50
|
}
|
|
66
51
|
//# sourceMappingURL=context.js.map
|
|
@@ -4,17 +4,47 @@ import * as path from 'path';
|
|
|
4
4
|
import * as p from '@clack/prompts';
|
|
5
5
|
import color from 'picocolors';
|
|
6
6
|
import { getClaudeDirectory, getHomeDirectory } from '../../targets/claude-code/claude-paths.js';
|
|
7
|
-
|
|
7
|
+
import { writeSettingsFileAtomic } from '../../core/fs-atomic.js';
|
|
8
|
+
function isPlainObject(value) {
|
|
9
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The settings object and its `env` (undefined when absent), or why not.
|
|
13
|
+
*
|
|
14
|
+
* D-DEBUG-ENV-OBJECT: Claude Code reads `env` as an object of variables. A
|
|
15
|
+
* present `env` that is anything else — an array, a string, `null` — is
|
|
16
|
+
* rejected, never repaired and never written through: a key set on an array is
|
|
17
|
+
* dropped by JSON.stringify, so the command would report success and write
|
|
18
|
+
* nothing, and replacing the value would discard what the user wrote.
|
|
19
|
+
*/
|
|
20
|
+
function parseSettingsEnv(settingsJson) {
|
|
21
|
+
let settings;
|
|
22
|
+
try {
|
|
23
|
+
settings = JSON.parse(settingsJson);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return { ok: false, error: { kind: 'malformed' } };
|
|
27
|
+
}
|
|
28
|
+
if (!isPlainObject(settings))
|
|
29
|
+
return { ok: false, error: { kind: 'malformed' } };
|
|
30
|
+
if (!Object.prototype.hasOwnProperty.call(settings, 'env'))
|
|
31
|
+
return { ok: true, settings, env: undefined };
|
|
32
|
+
const env = settings.env;
|
|
33
|
+
if (!isPlainObject(env))
|
|
34
|
+
return { ok: false, error: { kind: 'env-not-object' } };
|
|
35
|
+
return { ok: true, settings, env };
|
|
36
|
+
}
|
|
8
37
|
/**
|
|
9
38
|
* Apply DEVFLOW_HOOK_DEBUG=1 to a settings JSON string.
|
|
10
39
|
* Returns a new serialized settings string. Does not mutate.
|
|
11
40
|
* Follows the applyFlags pattern from flags.ts.
|
|
12
41
|
*/
|
|
13
42
|
export function applyDebugTrace(settingsJson) {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
43
|
+
const parsed = parseSettingsEnv(settingsJson);
|
|
44
|
+
if (!parsed.ok)
|
|
45
|
+
return parsed;
|
|
46
|
+
const next = { ...parsed.settings, env: { ...parsed.env, DEVFLOW_HOOK_DEBUG: '1' } };
|
|
47
|
+
return { ok: true, value: JSON.stringify(next, null, 2) + '\n' };
|
|
18
48
|
}
|
|
19
49
|
/**
|
|
20
50
|
* Remove DEVFLOW_HOOK_DEBUG from a settings JSON string.
|
|
@@ -23,15 +53,28 @@ export function applyDebugTrace(settingsJson) {
|
|
|
23
53
|
* Follows the stripFlags pattern from flags.ts.
|
|
24
54
|
*/
|
|
25
55
|
export function stripDebugTrace(settingsJson) {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
56
|
+
const parsed = parseSettingsEnv(settingsJson);
|
|
57
|
+
if (!parsed.ok)
|
|
58
|
+
return parsed;
|
|
59
|
+
if (parsed.env === undefined)
|
|
60
|
+
return { ok: true, value: JSON.stringify(parsed.settings, null, 2) + '\n' };
|
|
61
|
+
const without = (obj, key) => Object.fromEntries(Object.entries(obj).filter(([k]) => k !== key));
|
|
62
|
+
const env = without(parsed.env, 'DEVFLOW_HOOK_DEBUG');
|
|
63
|
+
const next = Object.keys(env).length === 0 ? without(parsed.settings, 'env') : { ...parsed.settings, env };
|
|
64
|
+
return { ok: true, value: JSON.stringify(next, null, 2) + '\n' };
|
|
65
|
+
}
|
|
66
|
+
/** The message a rejected settings file gets. Pure. */
|
|
67
|
+
export function describeDebugSettingsError(error) {
|
|
68
|
+
switch (error.kind) {
|
|
69
|
+
case 'malformed':
|
|
70
|
+
return 'settings.json is malformed — fix it before modifying env vars';
|
|
71
|
+
case 'env-not-object':
|
|
72
|
+
return 'settings.json has an "env" that is not an object — fix it before modifying env vars';
|
|
73
|
+
default: {
|
|
74
|
+
const exhaustive = error;
|
|
75
|
+
return exhaustive;
|
|
32
76
|
}
|
|
33
77
|
}
|
|
34
|
-
return JSON.stringify(settings, null, 2) + '\n';
|
|
35
78
|
}
|
|
36
79
|
/**
|
|
37
80
|
* Read the debug tracing state from a settings JSON string.
|
|
@@ -90,29 +133,25 @@ export const debugCommand = new Command('debug')
|
|
|
90
133
|
settingsJson = '{}';
|
|
91
134
|
}
|
|
92
135
|
if (options.enable) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
updated
|
|
96
|
-
|
|
97
|
-
catch {
|
|
98
|
-
p.log.error('settings.json is malformed — fix it before modifying env vars');
|
|
136
|
+
const updated = applyDebugTrace(settingsJson);
|
|
137
|
+
if (!updated.ok) {
|
|
138
|
+
p.log.error(describeDebugSettingsError(updated.error));
|
|
139
|
+
process.exitCode = 1;
|
|
99
140
|
return;
|
|
100
141
|
}
|
|
101
|
-
await
|
|
142
|
+
await writeSettingsFileAtomic(settingsPath, updated.value);
|
|
102
143
|
p.log.success('Hook debug tracing enabled');
|
|
103
144
|
p.log.info(color.dim('Remember to disable after debugging: devflow debug --disable'));
|
|
104
145
|
return;
|
|
105
146
|
}
|
|
106
147
|
if (options.disable) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
updated
|
|
110
|
-
|
|
111
|
-
catch {
|
|
112
|
-
p.log.error('settings.json is malformed — fix it before modifying env vars');
|
|
148
|
+
const updated = stripDebugTrace(settingsJson);
|
|
149
|
+
if (!updated.ok) {
|
|
150
|
+
p.log.error(describeDebugSettingsError(updated.error));
|
|
151
|
+
process.exitCode = 1;
|
|
113
152
|
return;
|
|
114
153
|
}
|
|
115
|
-
await
|
|
154
|
+
await writeSettingsFileAtomic(settingsPath, updated.value);
|
|
116
155
|
p.log.success('Hook debug tracing disabled');
|
|
117
156
|
return;
|
|
118
157
|
}
|
|
@@ -23,7 +23,7 @@ import color from 'picocolors';
|
|
|
23
23
|
import { getClaudeDirectory, getDevFlowDirectory, } from '../../targets/claude-code/claude-paths.js';
|
|
24
24
|
import { FLAG_REGISTRY, findFlag, convergeFlagsIntoSettings, parseFlagValueInput, formatFlagValue, effectiveDisplay, neutralValueOf, describeFlagKind, expectedInputFor, } from '../../core/flags.js';
|
|
25
25
|
import { readManifest, writeManifest } from '../../core/manifest.js';
|
|
26
|
-
import {
|
|
26
|
+
import { writeSettingsFileAtomic } from '../../core/fs-atomic.js';
|
|
27
27
|
import { sanitizeCell } from '../tui/cells.js';
|
|
28
28
|
// Static imports for pure view-state helpers — no TTY machinery (applies PF-017).
|
|
29
29
|
// runFlagsTui stays lazily imported in handleBare to keep TTY module out of
|
|
@@ -98,7 +98,7 @@ manifest, opts = { viewModeExplicit: false }) {
|
|
|
98
98
|
// Settings write — independent error path (avoids PF-015 fan-out).
|
|
99
99
|
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
100
100
|
try {
|
|
101
|
-
await
|
|
101
|
+
await writeSettingsFileAtomic(settingsPath, updatedSettings);
|
|
102
102
|
}
|
|
103
103
|
catch (err) {
|
|
104
104
|
p.log.error(`Failed to write settings.json: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -456,7 +456,7 @@ async function handleBare(claudeDir, devflowDir) {
|
|
|
456
456
|
// The read captured before runFlagsTui is a stale snapshot by the time the
|
|
457
457
|
// user saves — any concurrent writer (proxy enable, devflow agents, Claude
|
|
458
458
|
// Code /config) that ran during the session would be silently overwritten by
|
|
459
|
-
// the atomic rename in
|
|
459
|
+
// the atomic rename in writeSettingsFileAtomic. Re-reading rebases the flag
|
|
460
460
|
// write onto current content and ensures convergeFlagsIntoSettings sees the
|
|
461
461
|
// fresh viewMode (applies PF-022 — file state, not config state, is reality).
|
|
462
462
|
const freshSettings = await readSettingsSafe(path.join(claudeDir, 'settings.json'));
|
package/dist/cli/commands/hud.js
CHANGED
|
@@ -5,12 +5,13 @@ import * as p from '@clack/prompts';
|
|
|
5
5
|
import color from 'picocolors';
|
|
6
6
|
import { getClaudeDirectory, getDevFlowDirectory } from '../../targets/claude-code/claude-paths.js';
|
|
7
7
|
import { syncManifestFeature } from '../../core/manifest.js';
|
|
8
|
-
import {
|
|
8
|
+
import { writeSettingsFileAtomic } from '../../core/fs-atomic.js';
|
|
9
9
|
import { HUD_COMPONENTS, loadConfig, saveConfig, } from '../../hud/config.js';
|
|
10
10
|
/**
|
|
11
11
|
* Add the HUD statusLine to settings JSON.
|
|
12
12
|
* Idempotent — returns unchanged JSON if HUD already set.
|
|
13
|
-
* Upgrades legacy statusline.sh to hud.sh automatically
|
|
13
|
+
* Upgrades devflow's legacy statusline.sh to hud.sh automatically; a statusLine
|
|
14
|
+
* that is not devflow's is returned unchanged (D-HUD-EXACT-OWNER).
|
|
14
15
|
*/
|
|
15
16
|
export function addHudStatusLine(settingsJson, devflowDir) {
|
|
16
17
|
const settings = JSON.parse(settingsJson);
|
|
@@ -54,16 +55,39 @@ export function hasHudStatusLine(settingsJson) {
|
|
|
54
55
|
return false;
|
|
55
56
|
return isDevFlowStatusLine(settings.statusLine);
|
|
56
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* The statusLine command endings devflow has ever written: the HUD, and the
|
|
60
|
+
* pre-HUD `statusline.sh` it replaced.
|
|
61
|
+
*/
|
|
62
|
+
const DEVFLOW_STATUSLINE_SUFFIXES = [
|
|
63
|
+
'/.devflow/scripts/hud.sh',
|
|
64
|
+
'/.devflow/scripts/statusline.sh',
|
|
65
|
+
];
|
|
57
66
|
/**
|
|
58
67
|
* Check if an existing statusLine belongs to Devflow (HUD or legacy statusline).
|
|
59
|
-
*
|
|
68
|
+
*
|
|
69
|
+
* D-HUD-EXACT-OWNER: a statusLine is devflow's only when its command ends in
|
|
70
|
+
* `/.devflow/scripts/hud.sh` or the legacy `/.devflow/scripts/statusline.sh`, under
|
|
71
|
+
* any parent directory — so installs from the custom-directory and local-scope era
|
|
72
|
+
* are still recognised. A bare `statusline.sh` (the Claude Code docs' own example,
|
|
73
|
+
* `~/.claude/statusline.sh`) or a path that merely contains a `devflow` segment is
|
|
74
|
+
* the user's (applies ADR-024: remove or replace only what devflow provably wrote).
|
|
75
|
+
* Backslashes are read as slashes so a Windows install is matched the same way. A
|
|
76
|
+
* hand-edited command that is not a string is the user's, as in `endsWithAny`.
|
|
77
|
+
*
|
|
78
|
+
* Every caller converges through this one predicate: `addHudStatusLine` (init's
|
|
79
|
+
* settings pass with the HUD on, `init --hud-only`, `hud --enable`),
|
|
80
|
+
* `removeHudStatusLine` (init's settings pass with `--no-hud`, `hud --disable`,
|
|
81
|
+
* uninstall's `runCleanupPhase`), and `hasHudStatusLine` / `hasNonDevFlowStatusLine`
|
|
82
|
+
* (`hud --enable`, `hud --status`).
|
|
60
83
|
*/
|
|
61
84
|
function isDevFlowStatusLine(statusLine) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
85
|
+
// Parsed from a hand-editable settings.json, so the declared type is not a guarantee.
|
|
86
|
+
const raw = statusLine.command;
|
|
87
|
+
if (typeof raw !== 'string')
|
|
88
|
+
return false;
|
|
89
|
+
const cmd = raw.trim().replace(/\\/g, '/');
|
|
90
|
+
return DEVFLOW_STATUSLINE_SUFFIXES.some((suffix) => cmd.endsWith(suffix));
|
|
67
91
|
}
|
|
68
92
|
/**
|
|
69
93
|
* Check if an existing statusLine belongs to a non-Devflow tool.
|
|
@@ -171,7 +195,7 @@ export function createHudCommand() {
|
|
|
171
195
|
}
|
|
172
196
|
}
|
|
173
197
|
const updated = addHudStatusLine(settingsContent, devflowDir);
|
|
174
|
-
await
|
|
198
|
+
await writeSettingsFileAtomic(settingsPath, updated);
|
|
175
199
|
}
|
|
176
200
|
// Always update config and sync manifest — removing the already-enabled
|
|
177
201
|
// early-return makes --enable self-healing symmetric with --disable.
|
|
@@ -209,7 +233,7 @@ export function createHudCommand() {
|
|
|
209
233
|
const settingsContent = await fs.readFile(settingsPath, 'utf-8');
|
|
210
234
|
const updated = removeHudStatusLine(settingsContent);
|
|
211
235
|
if (updated !== settingsContent) {
|
|
212
|
-
await
|
|
236
|
+
await writeSettingsFileAtomic(settingsPath, updated);
|
|
213
237
|
statusLineRemoved = true;
|
|
214
238
|
}
|
|
215
239
|
}
|
|
@@ -32,9 +32,9 @@ export const FEATURE_DEFAULTS = {
|
|
|
32
32
|
* Resolve feature booleans for the init seed: every feature comes from
|
|
33
33
|
* manifest.features, with registry defaults when the manifest is absent.
|
|
34
34
|
*
|
|
35
|
-
* D-FEATURES-
|
|
36
|
-
* knowledge are
|
|
37
|
-
* no per-repo input at all. Seeding from whatever repo init happens to run in
|
|
35
|
+
* D-FEATURES-NARROW-ONLY (src/core/feature-switch.ts): the machine switches for
|
|
36
|
+
* memory, learning and knowledge are recorded in the manifest alone (a repository
|
|
37
|
+
* only narrows them at read time), so the seed takes no per-repo input at all. Seeding from whatever repo init happens to run in
|
|
38
38
|
* would flip the switch for EVERY repo as a side effect — a stale per-repo
|
|
39
39
|
* `true` would silently re-enable a feature the user turned off. ADR-014's
|
|
40
40
|
* state-aware re-init preserves the prior machine-wide choice, the manifest's.
|
|
@@ -116,7 +116,7 @@ export function resolveSeedFlags(manifestFlags, registry = FLAG_REGISTRY) {
|
|
|
116
116
|
* - Otherwise → split + adopt newly-added non-optional selectable plugins
|
|
117
117
|
* whose name is ∉ knownPlugins and ∉ manifestPlugins
|
|
118
118
|
*
|
|
119
|
-
*
|
|
119
|
+
* The plugins init adds itself (devflow-core-skills, devflow-ambient) are filtered
|
|
120
120
|
* out by partitionSelectablePlugins and never appear in the returned buckets.
|
|
121
121
|
*/
|
|
122
122
|
export function resolveSeedPlugins(manifestPlugins, knownPlugins, allPlugins) {
|
|
@@ -157,6 +157,42 @@ export function resolveSeedPlugins(manifestPlugins, knownPlugins, allPlugins) {
|
|
|
157
157
|
}
|
|
158
158
|
return { workflowPlugins, languagePlugins };
|
|
159
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* The plugins an init run installs, from the selection and the ambient switch.
|
|
162
|
+
*
|
|
163
|
+
* `selectedPlugins` is empty when nothing picked a selection (a fresh
|
|
164
|
+
* non-interactive install), and then every non-optional plugin is installed.
|
|
165
|
+
* `devflow-core-skills` is always added; `devflow-ambient` is added exactly
|
|
166
|
+
* when ambient mode is on.
|
|
167
|
+
*
|
|
168
|
+
* D-AMBIENT-FOLLOWS-SWITCH: `devflow-ambient` is installed iff ambient mode is
|
|
169
|
+
* on, on EVERY path. It is not a selectable plugin (`EXCLUDED` keeps it out of
|
|
170
|
+
* both multiselect buckets and out of the re-init seed, see
|
|
171
|
+
* {@link resolveSeedPlugins}); since #150 the ambient switch is what includes
|
|
172
|
+
* it, and #150's own test plan reads "`init --no-ambient` — confirm ambient
|
|
173
|
+
* plugin NOT installed". The fresh default branch used to take every
|
|
174
|
+
* non-optional plugin, ambient included, so a first `init --no-ambient`
|
|
175
|
+
* recorded `devflow-ambient` in the manifest and the re-init — seeded without
|
|
176
|
+
* it — dropped it: the first install was the wrong one, not the re-init
|
|
177
|
+
* (#388 AC-3). The default branch therefore leaves ambient out and lets the
|
|
178
|
+
* switch below add it, exactly as it does for an explicit selection.
|
|
179
|
+
*
|
|
180
|
+
* Pure function — no I/O; returns a new array, never mutates its inputs.
|
|
181
|
+
*/
|
|
182
|
+
export function resolvePluginsToInstall(selectedPlugins, ambientEnabled, allPlugins) {
|
|
183
|
+
let pluginsToInstall = selectedPlugins.length > 0
|
|
184
|
+
? allPlugins.filter(p => selectedPlugins.includes(p.name))
|
|
185
|
+
: allPlugins.filter(p => !p.optional && p.name !== 'devflow-ambient');
|
|
186
|
+
const coreSkillsPlugin = allPlugins.find(p => p.name === 'devflow-core-skills');
|
|
187
|
+
if (pluginsToInstall.length > 0 && coreSkillsPlugin && !pluginsToInstall.includes(coreSkillsPlugin)) {
|
|
188
|
+
pluginsToInstall = [coreSkillsPlugin, ...pluginsToInstall];
|
|
189
|
+
}
|
|
190
|
+
const ambientPlugin = allPlugins.find(p => p.name === 'devflow-ambient');
|
|
191
|
+
if (ambientEnabled && ambientPlugin && !pluginsToInstall.includes(ambientPlugin)) {
|
|
192
|
+
pluginsToInstall = [...pluginsToInstall, ambientPlugin];
|
|
193
|
+
}
|
|
194
|
+
return pluginsToInstall;
|
|
195
|
+
}
|
|
160
196
|
/**
|
|
161
197
|
* Extract the attribution suppression state from a settings JSON string.
|
|
162
198
|
*
|