devflow-kit 2.3.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 +177 -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 +144 -0
- package/dist/cli/commands/compliance-prompts.js +7 -11
- package/dist/cli/commands/compliance.js +23 -1
- package/dist/cli/commands/init-seed.js +69 -30
- package/dist/cli/commands/init.js +551 -77
- 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 +38 -0
- package/dist/cli/commands/proxy.js +91 -31
- 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 +233 -77
- 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 +149 -8
- 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 -4
- package/dist/core/proxy-state.js +100 -43
- 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 +303 -104
- 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-proxy +64 -5
- 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 -82
- package/src/assets/skills/git/references/github-api.md +179 -141
- package/src/assets/skills/git/references/patterns.md +11 -12
- 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/targets/claude-code/templates/settings.json +0 -4
- package/src/assets/agents/git.md +0 -938
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
* Users who skip major versions should run uninstall + reinstall.
|
|
8
8
|
*
|
|
9
9
|
* Organized by era to make scanning for duplicates tractable.
|
|
10
|
+
*
|
|
11
|
+
* These lists are FROZEN deletion manifests for names that once shipped: the
|
|
12
|
+
* `*_V2` suffix is the frozen spelling of an era, not a version to bump, and a
|
|
13
|
+
* name is removed from a list only when its pruning window has passed — never
|
|
14
|
+
* renamed to match a current registry name (avoids PF-012).
|
|
10
15
|
*/
|
|
11
16
|
/** Pre-v1.0.0: devflow- prefixed skill names from the original install scheme. */
|
|
12
17
|
const LEGACY_SKILLS_PRE_V1 = [
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { promises as fs, writeFileSync, unlinkSync } from 'fs';
|
|
2
|
-
import {
|
|
2
|
+
import { execFileSync } from 'child_process';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import * as os from 'os';
|
|
5
5
|
import * as p from '@clack/prompts';
|
|
@@ -25,6 +25,28 @@ export function computeGitignoreAppend(existingContent, entries) {
|
|
|
25
25
|
const existingLines = existingContent.split('\n').map(l => l.trim());
|
|
26
26
|
return entries.filter(entry => !existingLines.includes(entry));
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Sentinel line whose presence means the current (v3-and-later) carve-out block is
|
|
30
|
+
* installed. Devflow-unique: no user writes `!.devflow/conventions.md` by hand.
|
|
31
|
+
*/
|
|
32
|
+
const DEVFLOW_GITIGNORE_SENTINEL_V3 = '!.devflow/conventions.md';
|
|
33
|
+
/** Sentinel line whose presence means the v2 carve-out block is installed (no conventions.md line). */
|
|
34
|
+
const DEVFLOW_GITIGNORE_SENTINEL_V2 = '!.devflow/features/*/KNOWLEDGE.md';
|
|
35
|
+
/**
|
|
36
|
+
* The block's final line: ignore the devflow-managed `.claudeignore` file.
|
|
37
|
+
* NOT a sentinel — users legitimately author this line themselves.
|
|
38
|
+
*/
|
|
39
|
+
const CLAUDEIGNORE_LINE = '.claudeignore';
|
|
40
|
+
/** A user's explicit un-ignore of `.claudeignore`; never overridden. */
|
|
41
|
+
const CLAUDEIGNORE_NEGATION = '!.claudeignore';
|
|
42
|
+
/**
|
|
43
|
+
* Re-includes the team-owned evidence policy file (D-GITIGNORE-V5). A COMPLETION
|
|
44
|
+
* line, never a presence sentinel: users may author it themselves, so its presence
|
|
45
|
+
* proves nothing about the devflow block (avoids PF-059). It sits after `.devflow/*`
|
|
46
|
+
* (which it overrides under last-match-wins) and before `.claudeignore`, so the
|
|
47
|
+
* block's final line stays `.claudeignore`.
|
|
48
|
+
*/
|
|
49
|
+
const DEVFLOW_POLICY_LINE = '!.devflow/policy.json';
|
|
28
50
|
/**
|
|
29
51
|
* The shared .devflow/ gitignore block. Everything under .devflow/ is local
|
|
30
52
|
* (memory, learning, docs, locks) EXCEPT:
|
|
@@ -32,6 +54,9 @@ export function computeGitignoreAppend(existingContent, entries) {
|
|
|
32
54
|
* + committed (the Knowledge agent commits them at workflow end).
|
|
33
55
|
* - conventions.md: naming-convention authority written by the Git learn-conventions
|
|
34
56
|
* operation; GIT-TRACKED so the team shares a single naming source.
|
|
57
|
+
* - policy.json: the team-owned evidence policy, read from the default branch by
|
|
58
|
+
* resolve-evidence-policy.cjs; GIT-TRACKED so a team can commit it without `git add -f`.
|
|
59
|
+
* Devflow never writes it.
|
|
35
60
|
*
|
|
36
61
|
* Re-including files under an ignored tree needs a `dir/*` + `!dir/keep` pair at
|
|
37
62
|
* each level — a bare `.devflow/` excludes the directory so git never descends and
|
|
@@ -39,65 +64,121 @@ export function computeGitignoreAppend(existingContent, entries) {
|
|
|
39
64
|
*
|
|
40
65
|
* Kept BYTE-IDENTICAL to the block emitted by src/assets/scripts/hooks/ensure-root-gitignore
|
|
41
66
|
* so the init-time path and the always-on hook path produce the same file.
|
|
42
|
-
*
|
|
43
|
-
* D-GITIGNORE-V3: v3 of the carve-out block (adds !.devflow/conventions.md).
|
|
44
67
|
*/
|
|
45
|
-
|
|
68
|
+
const DEVFLOW_GITIGNORE_BLOCK_LINES = [
|
|
46
69
|
'# Devflow runtime data — local by default (memory, learning, docs, locks).',
|
|
47
|
-
'#
|
|
48
|
-
'#
|
|
49
|
-
'#
|
|
50
|
-
'# to your own .gitignore.',
|
|
70
|
+
'# Shared via git: feature knowledge bases under .devflow/features/ (index.md and',
|
|
71
|
+
'# every {slug}/KNOWLEDGE.md), .devflow/conventions.md (naming authority) and',
|
|
72
|
+
'# .devflow/policy.json (evidence policy). To stop sharing the first two, re-add',
|
|
73
|
+
'# `.devflow/features/` or `.devflow/conventions.md` to your own .gitignore.',
|
|
51
74
|
'.devflow/*',
|
|
52
75
|
'!.devflow/features/',
|
|
53
76
|
'.devflow/features/*',
|
|
54
77
|
'!.devflow/features/index.md',
|
|
55
78
|
'!.devflow/features/*/',
|
|
56
79
|
'.devflow/features/*/*',
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
const
|
|
80
|
+
DEVFLOW_GITIGNORE_SENTINEL_V2,
|
|
81
|
+
DEVFLOW_GITIGNORE_SENTINEL_V3,
|
|
82
|
+
DEVFLOW_POLICY_LINE,
|
|
83
|
+
CLAUDEIGNORE_LINE,
|
|
84
|
+
];
|
|
85
|
+
/** The full carve-out block, `.claudeignore` line included. */
|
|
86
|
+
export const DEVFLOW_GITIGNORE_BLOCK = DEVFLOW_GITIGNORE_BLOCK_LINES.join('\n');
|
|
87
|
+
/**
|
|
88
|
+
* The carve-out block without its final `.claudeignore` line — emitted instead of
|
|
89
|
+
* the full block when the target .gitignore already carries a `.claudeignore` or
|
|
90
|
+
* `!.claudeignore` entry of the user's own.
|
|
91
|
+
*/
|
|
92
|
+
export const DEVFLOW_GITIGNORE_BLOCK_WITHOUT_CLAUDEIGNORE = DEVFLOW_GITIGNORE_BLOCK_LINES.slice(0, -1).join('\n');
|
|
64
93
|
/** The legacy wholesale comment our pre-carve-out writers emitted. */
|
|
65
94
|
const LEGACY_DEVFLOW_COMMENT = '# Devflow runtime data (local by default; remove to share via git)';
|
|
66
95
|
/**
|
|
67
96
|
* PURE: given existing .gitignore content, return the content that ignores
|
|
68
|
-
* `.devflow/` with the feature-knowledge + conventions.md carve-out —
|
|
69
|
-
* when no change is needed. Idempotent: feeding its own output back returns `null`.
|
|
70
|
-
*
|
|
71
|
-
* -
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* -
|
|
97
|
+
* `.devflow/` with the feature-knowledge + conventions.md + policy.json carve-out —
|
|
98
|
+
* or `null` when no change is needed. Idempotent: feeding its own output back returns `null`.
|
|
99
|
+
*
|
|
100
|
+
* D-GITIGNORE-V5: the block is detected ONLY by its own devflow-unique sentinel
|
|
101
|
+
* (`!.devflow/conventions.md`). `.claudeignore` and `!.devflow/policy.json` are
|
|
102
|
+
* COMPLETION lines — users legitimately author both themselves — so each is topped up
|
|
103
|
+
* when missing and never read as proof the block exists. A presence check on a
|
|
104
|
+
* user-authored line inverts both halves of the contract: projects that already carry
|
|
105
|
+
* that line are told the block is installed when it is not, and a user's
|
|
106
|
+
* `!.claudeignore` un-ignore is silently reversed by re-appending `.claudeignore`
|
|
107
|
+
* under last-match-wins (avoids PF-059).
|
|
108
|
+
*
|
|
109
|
+
* `hasClaudeignoreEntry` is true when some whole line, trimmed, is exactly
|
|
110
|
+
* `.claudeignore` OR `!.claudeignore`. Treating both forms as "present" both honours
|
|
111
|
+
* an un-ignore and makes every branch converge on re-run. `hasPolicyLine` is true when
|
|
112
|
+
* some whole line, trimmed, is exactly `!.devflow/policy.json`. The missing completion
|
|
113
|
+
* lines, in block order, are [policy line, `.claudeignore` (only when
|
|
114
|
+
* `!hasClaudeignoreEntry`)].
|
|
115
|
+
*
|
|
116
|
+
* 1. A `/.devflow/` line present → `null` (user opt-out; respect manual config).
|
|
117
|
+
* 2. v3 sentinel present → append the missing completion lines; `null` when none are
|
|
118
|
+
* missing. This is the v4→v5 upgrade: a v4 block gains only the policy line, after
|
|
119
|
+
* its `.claudeignore` line, and keeps its old comment.
|
|
120
|
+
* 3. v2 sentinel present, no v3 → append `!.devflow/conventions.md` followed by the
|
|
121
|
+
* missing completion lines.
|
|
122
|
+
* 4. Legacy bare `.devflow/` present → strip it (+ our old comment), then append the
|
|
123
|
+
* block; no block at all → append the block. The block is emitted MINUS its final
|
|
124
|
+
* `.claudeignore` line when `hasClaudeignoreEntry`. A user's own policy line is
|
|
125
|
+
* duplicated harmlessly here, and the re-run is a no-op.
|
|
126
|
+
* 5. Neither completion line is ever a sentinel. The marker file
|
|
127
|
+
* (`.devflow/.root-gitignore-configured-v5`) is a fast-path claim, never proof.
|
|
128
|
+
*
|
|
129
|
+
* Line matching is whole-line, whitespace-tolerant, exact text — never substring.
|
|
130
|
+
* Both append forms are mirrored byte-for-byte in the shell twin
|
|
131
|
+
* (src/assets/scripts/hooks/ensure-root-gitignore), which is what the cross-implementation
|
|
132
|
+
* parity table in tests/shell-hooks.test.ts pins.
|
|
76
133
|
*/
|
|
77
134
|
export function computeDevflowGitignore(existingContent) {
|
|
78
135
|
const lines = existingContent.split('\n');
|
|
79
136
|
const trimmed = lines.map(l => l.trim());
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
137
|
+
const hasClaudeignoreEntry = trimmed.some(l => l === CLAUDEIGNORE_LINE || l === CLAUDEIGNORE_NEGATION);
|
|
138
|
+
const hasPolicyLine = trimmed.includes(DEVFLOW_POLICY_LINE);
|
|
139
|
+
/** The block-completing lines this file lacks, in block order. */
|
|
140
|
+
const missingCompletionLines = [
|
|
141
|
+
...(hasPolicyLine ? [] : [DEVFLOW_POLICY_LINE]),
|
|
142
|
+
...(hasClaudeignoreEntry ? [] : [CLAUDEIGNORE_LINE]),
|
|
143
|
+
];
|
|
144
|
+
/**
|
|
145
|
+
* Continue an existing devflow block with the lines it is missing. One newline
|
|
146
|
+
* guard, no blank separator — the appended lines belong to the block above them.
|
|
147
|
+
*/
|
|
148
|
+
const appendLines = (body, block) => `${body}${body.endsWith('\n') ? '' : '\n'}${block}\n`;
|
|
149
|
+
/**
|
|
150
|
+
* Start a new block after unrelated content: one blank separator line. Existing
|
|
151
|
+
* trailing newlines are preserved verbatim (no trimEnd, no blank-line dedupe) so
|
|
152
|
+
* the shell twin's `tail -c 1` guard produces the identical bytes.
|
|
153
|
+
*/
|
|
154
|
+
const appendBlock = (body, block) => body.length === 0
|
|
155
|
+
? `${block}\n`
|
|
156
|
+
: `${body}${body.endsWith('\n') ? '' : '\n'}\n${block}\n`;
|
|
157
|
+
// 1. User opt-out wins over every sentinel.
|
|
158
|
+
if (trimmed.includes('/.devflow/'))
|
|
83
159
|
return null;
|
|
84
|
-
//
|
|
85
|
-
|
|
160
|
+
// 2. Block installed (v3 and later) — top up only the completion lines it lacks.
|
|
161
|
+
if (trimmed.includes(DEVFLOW_GITIGNORE_SENTINEL_V3)) {
|
|
162
|
+
return missingCompletionLines.length === 0
|
|
163
|
+
? null
|
|
164
|
+
: appendLines(existingContent, missingCompletionLines.join('\n'));
|
|
165
|
+
}
|
|
166
|
+
// 3. v2 block installed — append the lines it lacks, in block order.
|
|
86
167
|
if (trimmed.includes(DEVFLOW_GITIGNORE_SENTINEL_V2)) {
|
|
87
|
-
|
|
88
|
-
return `${existingContent}${sep}${DEVFLOW_GITIGNORE_SENTINEL_V3}\n`;
|
|
168
|
+
return appendLines(existingContent, [DEVFLOW_GITIGNORE_SENTINEL_V3, ...missingCompletionLines].join('\n'));
|
|
89
169
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
170
|
+
// 4. No devflow block — install one, respecting any .claudeignore entry of the user's own.
|
|
171
|
+
const block = hasClaudeignoreEntry
|
|
172
|
+
? DEVFLOW_GITIGNORE_BLOCK_WITHOUT_CLAUDEIGNORE
|
|
173
|
+
: DEVFLOW_GITIGNORE_BLOCK;
|
|
174
|
+
if (trimmed.includes('.devflow/')) {
|
|
94
175
|
// Upgrade our legacy wholesale entry: drop the bare line + old comment, append block.
|
|
95
176
|
const kept = lines
|
|
96
177
|
.filter(l => l.trim() !== '.devflow/' && l.trim() !== LEGACY_DEVFLOW_COMMENT)
|
|
97
178
|
.join('\n');
|
|
98
|
-
return
|
|
179
|
+
return appendBlock(kept, block);
|
|
99
180
|
}
|
|
100
|
-
return
|
|
181
|
+
return appendBlock(existingContent, block);
|
|
101
182
|
}
|
|
102
183
|
/**
|
|
103
184
|
* Merge Devflow deny entries into an existing settings JSON object.
|
|
@@ -531,11 +612,14 @@ export async function installManagedSettings(rootDir, verbose) {
|
|
|
531
612
|
return false;
|
|
532
613
|
}
|
|
533
614
|
try {
|
|
534
|
-
|
|
535
|
-
//
|
|
615
|
+
execFileSync('sudo', ['mkdir', '-p', managedDir], { stdio: 'inherit' });
|
|
616
|
+
// Stage the JSON in a file and copy it with sudo. Every sudo call takes an
|
|
617
|
+
// argv (execFileSync, no shell), so no path is ever re-parsed by a shell — a
|
|
618
|
+
// package root under a home directory holding a quote cannot break or extend
|
|
619
|
+
// the root command.
|
|
536
620
|
const tmpFile = path.join(rootDir, '.managed-settings-tmp.json');
|
|
537
621
|
await fs.writeFile(tmpFile, content, 'utf-8');
|
|
538
|
-
|
|
622
|
+
execFileSync('sudo', ['cp', tmpFile, managedPath], { stdio: 'inherit' });
|
|
539
623
|
await fs.rm(tmpFile, { force: true });
|
|
540
624
|
if (verbose) {
|
|
541
625
|
p.log.success(`Managed settings written to ${managedPath} (via sudo)`);
|
|
@@ -557,14 +641,26 @@ export async function installManagedSettings(rootDir, verbose) {
|
|
|
557
641
|
* 1. Try direct write/delete
|
|
558
642
|
* 2. If EACCES and TTY, ask user before sudo
|
|
559
643
|
* 3. Non-TTY: return false (caller logs preservation message)
|
|
644
|
+
*
|
|
645
|
+
* `managedPathOverride` exists so a test can point the removal at a temp file;
|
|
646
|
+
* production callers omit it and get the platform's system path. It is a
|
|
647
|
+
* parameter, deliberately not an environment variable: this function may run
|
|
648
|
+
* `sudo rm` / `sudo cp` on the path, and an env-selectable target would let
|
|
649
|
+
* whoever controls the environment aim a root write the user consented to for
|
|
650
|
+
* "managed settings" at any file.
|
|
560
651
|
*/
|
|
561
|
-
export async function removeManagedSettings(rootDir, verbose) {
|
|
652
|
+
export async function removeManagedSettings(rootDir, verbose, managedPathOverride) {
|
|
562
653
|
let managedPath;
|
|
563
|
-
|
|
564
|
-
managedPath =
|
|
654
|
+
if (managedPathOverride !== undefined) {
|
|
655
|
+
managedPath = managedPathOverride;
|
|
565
656
|
}
|
|
566
|
-
|
|
567
|
-
|
|
657
|
+
else {
|
|
658
|
+
try {
|
|
659
|
+
managedPath = getManagedSettingsPath();
|
|
660
|
+
}
|
|
661
|
+
catch {
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
568
664
|
}
|
|
569
665
|
let existingContent;
|
|
570
666
|
try {
|
|
@@ -639,12 +735,12 @@ export async function removeManagedSettings(rootDir, verbose) {
|
|
|
639
735
|
}
|
|
640
736
|
try {
|
|
641
737
|
if (shouldDelete) {
|
|
642
|
-
|
|
738
|
+
execFileSync('sudo', ['rm', managedPath], { stdio: 'inherit' });
|
|
643
739
|
}
|
|
644
740
|
else {
|
|
645
741
|
const tmpFile = path.join(rootDir, '.managed-settings-tmp.json');
|
|
646
742
|
await fs.writeFile(tmpFile, updatedContent, 'utf-8');
|
|
647
|
-
|
|
743
|
+
execFileSync('sudo', ['cp', tmpFile, managedPath], { stdio: 'inherit' });
|
|
648
744
|
await fs.rm(tmpFile, { force: true });
|
|
649
745
|
}
|
|
650
746
|
if (verbose) {
|
|
@@ -712,10 +808,95 @@ export async function stripUserSecurityDenyList(settingsPath) {
|
|
|
712
808
|
await writeFileAtomicExclusive(settingsPath, stripped);
|
|
713
809
|
return { removed };
|
|
714
810
|
}
|
|
811
|
+
/** True for a non-null, non-array object literal. */
|
|
812
|
+
function isPlainObject(value) {
|
|
813
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Every `command` string carried by a hook matcher, tolerating foreign shapes.
|
|
817
|
+
* Returns an empty array for anything that is not `{ hooks: [{ command: string }] }`.
|
|
818
|
+
*/
|
|
819
|
+
function hookCommandsOf(matcher) {
|
|
820
|
+
if (!isPlainObject(matcher) || !Array.isArray(matcher.hooks))
|
|
821
|
+
return [];
|
|
822
|
+
return matcher.hooks
|
|
823
|
+
.filter(isPlainObject)
|
|
824
|
+
.map((h) => h.command)
|
|
825
|
+
.filter((c) => typeof c === 'string');
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Merge Devflow's template hook entries and top-level fields into an existing
|
|
829
|
+
* parsed settings object. Idempotent by exact command string — a hook that is
|
|
830
|
+
* already present is skipped. Sets `statusLine` only when the user has no existing
|
|
831
|
+
* value. Attribution is NOT injected here — managed by the flags pipeline (D27).
|
|
832
|
+
* Mutates `existing` in place.
|
|
833
|
+
*
|
|
834
|
+
* D-SETTINGS-1: merge strategy — never replace; only add devflow entries that are absent.
|
|
835
|
+
* Returns { changed: true } when any field was added.
|
|
836
|
+
*
|
|
837
|
+
* `existing` comes from a hand-editable file, so every branch is shape-guarded:
|
|
838
|
+
* a `hooks` value (or per-event value) that is not the expected object/array shape
|
|
839
|
+
* is left untouched rather than overwritten or thrown on (applies PF-023 — validate
|
|
840
|
+
* at the sink that mutates).
|
|
841
|
+
*
|
|
842
|
+
* Exported for testing.
|
|
843
|
+
*/
|
|
844
|
+
export function mergeDevflowSettingsTemplate(existing, template) {
|
|
845
|
+
let changed = false;
|
|
846
|
+
// Merge hook entries — idempotent by exact command string
|
|
847
|
+
const tmplHooks = isPlainObject(template.hooks) ? template.hooks : {};
|
|
848
|
+
const existingHooksRaw = existing.hooks;
|
|
849
|
+
if (existingHooksRaw === undefined || isPlainObject(existingHooksRaw)) {
|
|
850
|
+
const existingHooks = existingHooksRaw ?? {};
|
|
851
|
+
let hooksChanged = false;
|
|
852
|
+
for (const [event, matchers] of Object.entries(tmplHooks)) {
|
|
853
|
+
if (!Array.isArray(matchers))
|
|
854
|
+
continue;
|
|
855
|
+
for (const matcher of matchers) {
|
|
856
|
+
const cmd = hookCommandsOf(matcher)[0];
|
|
857
|
+
if (!cmd)
|
|
858
|
+
continue;
|
|
859
|
+
const current = existingHooks[event];
|
|
860
|
+
if (current !== undefined && !Array.isArray(current))
|
|
861
|
+
break; // foreign shape — leave the event alone
|
|
862
|
+
const eventArr = current ?? [];
|
|
863
|
+
if (eventArr.some((m) => hookCommandsOf(m).includes(cmd)))
|
|
864
|
+
continue;
|
|
865
|
+
eventArr.push(matcher);
|
|
866
|
+
existingHooks[event] = eventArr;
|
|
867
|
+
hooksChanged = true;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
// Attach only when something was actually added — never introduce an empty
|
|
871
|
+
// `hooks` key into a settings.json that had none.
|
|
872
|
+
if (hooksChanged) {
|
|
873
|
+
existing.hooks = existingHooks;
|
|
874
|
+
changed = true;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
// Set statusLine only if the user has none
|
|
878
|
+
if (existing.statusLine === undefined && template.statusLine !== undefined) {
|
|
879
|
+
existing.statusLine = template.statusLine;
|
|
880
|
+
changed = true;
|
|
881
|
+
}
|
|
882
|
+
// attribution: owned exclusively by the flags pipeline (applyFlags/stripFlags, D27) — a second writer here would race it.
|
|
883
|
+
return { changed };
|
|
884
|
+
}
|
|
715
885
|
/**
|
|
716
886
|
* Install or update settings.json with Devflow configuration.
|
|
717
|
-
*
|
|
718
|
-
*
|
|
887
|
+
*
|
|
888
|
+
* Strategy (D-SETTINGS-1: merge, never overwrite wholesale):
|
|
889
|
+
* - Fresh file: write the template directly.
|
|
890
|
+
* - Existing file: MERGE devflow hook entries and fields into the parsed object.
|
|
891
|
+
* Idempotent by exact command string — existing hooks are never duplicated or removed.
|
|
892
|
+
* Preserves every user key (env, permissions, apiKeyHelper, model, etc.) untouched.
|
|
893
|
+
* - Parse failure: warn and skip; file left byte-identical (never clobber a broken file).
|
|
894
|
+
*
|
|
895
|
+
* The merge is additive only, so it runs without a confirmation prompt: init is called
|
|
896
|
+
* from inside an active spinner (a prompt would render on top of it), and declining
|
|
897
|
+
* could not protect the file anyway — init's own settings pass rewrites the hook set
|
|
898
|
+
* unconditionally right afterwards. A prompt whose answer changes nothing is worse
|
|
899
|
+
* than no prompt.
|
|
719
900
|
*
|
|
720
901
|
* The deny list is handled by init's dedicated security step
|
|
721
902
|
* (applyUserSecurityDenyList / installManagedSettings) after installSettings completes.
|
|
@@ -741,38 +922,30 @@ export async function installSettings(claudeDir, rootDir, devflowDir, verbose) {
|
|
|
741
922
|
}
|
|
742
923
|
return;
|
|
743
924
|
}
|
|
744
|
-
// Settings exist —
|
|
745
|
-
let
|
|
925
|
+
// Settings exist — parse and merge (never overwrite wholesale)
|
|
926
|
+
let existingParsed;
|
|
746
927
|
try {
|
|
747
|
-
|
|
748
|
-
hasHooks = !!existing.hooks;
|
|
928
|
+
existingParsed = JSON.parse(await fs.readFile(settingsPath, 'utf-8'));
|
|
749
929
|
}
|
|
750
|
-
catch {
|
|
751
|
-
|
|
752
|
-
|
|
930
|
+
catch {
|
|
931
|
+
// Parse failure — warn and skip; file left byte-identical (D-SETTINGS-1)
|
|
932
|
+
if (verbose) {
|
|
933
|
+
p.log.warn('settings.json could not be parsed — Devflow hooks not added. ' +
|
|
934
|
+
'Fix the JSON manually and re-run devflow init.');
|
|
935
|
+
}
|
|
753
936
|
return;
|
|
754
937
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
p.cancel('Installation cancelled.');
|
|
763
|
-
process.exit(0);
|
|
764
|
-
}
|
|
765
|
-
if (confirmed) {
|
|
766
|
-
await fs.writeFile(settingsPath, settingsContent, 'utf-8');
|
|
767
|
-
p.log.success('Settings overridden');
|
|
768
|
-
}
|
|
769
|
-
else {
|
|
770
|
-
p.log.info('Keeping existing settings');
|
|
771
|
-
}
|
|
938
|
+
const templateParsed = JSON.parse(settingsContent);
|
|
939
|
+
// Merge template into existing (mutates existingParsed in place).
|
|
940
|
+
// If nothing needs to be added the write is skipped entirely.
|
|
941
|
+
const { changed } = mergeDevflowSettingsTemplate(existingParsed, templateParsed);
|
|
942
|
+
if (!changed) {
|
|
943
|
+
// Already fully configured — nothing to do
|
|
944
|
+
return;
|
|
772
945
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
p.log.
|
|
946
|
+
await writeFileAtomicExclusive(settingsPath, JSON.stringify(existingParsed, null, 2) + '\n');
|
|
947
|
+
if (verbose) {
|
|
948
|
+
p.log.success('Settings updated with Devflow hooks and HUD');
|
|
776
949
|
}
|
|
777
950
|
}
|
|
778
951
|
catch (error) {
|
|
@@ -872,45 +1045,73 @@ export async function updateGitignore(gitRoot, verbose) {
|
|
|
872
1045
|
}
|
|
873
1046
|
}
|
|
874
1047
|
}
|
|
875
|
-
/**
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
1048
|
+
/**
|
|
1049
|
+
* Current carve-out marker version. Bump when the block format changes — together
|
|
1050
|
+
* with the shell twin's stamp (ensure-root-gitignore) and the ensure-devflow-init
|
|
1051
|
+
* fast path, in one commit (D-GITIGNORE-V5).
|
|
1052
|
+
*/
|
|
1053
|
+
const GITIGNORE_MARKER_V5 = '.root-gitignore-configured-v5';
|
|
1054
|
+
/**
|
|
1055
|
+
* Earlier markers, the unversioned (v1) one included — every one is removed
|
|
1056
|
+
* whenever the project is v5-stamped, on the fast path too: an older devflow can
|
|
1057
|
+
* re-stamp one beside v5, and the shell twin drops the same four.
|
|
1058
|
+
*/
|
|
1059
|
+
const LEGACY_GITIGNORE_MARKERS = [
|
|
1060
|
+
'.root-gitignore-configured-v4',
|
|
1061
|
+
'.root-gitignore-configured-v3',
|
|
1062
|
+
'.root-gitignore-configured-v2',
|
|
1063
|
+
'.root-gitignore-configured',
|
|
1064
|
+
];
|
|
1065
|
+
/** Remove every legacy marker; an absent one is a no-op. Call only once v5 is stamped. */
|
|
1066
|
+
async function removeLegacyGitignoreMarkers(devflowDir) {
|
|
1067
|
+
for (const legacy of LEGACY_GITIGNORE_MARKERS) {
|
|
1068
|
+
try {
|
|
1069
|
+
await fs.rm(path.join(devflowDir, legacy), { force: true });
|
|
1070
|
+
}
|
|
1071
|
+
catch { /* ok if absent */ }
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
879
1074
|
/**
|
|
880
1075
|
* Deterministically ensure the project root .gitignore applies the `.devflow/`
|
|
881
|
-
* carve-out (local by default
|
|
1076
|
+
* carve-out (local by default; feature knowledge, conventions.md and the evidence
|
|
1077
|
+
* policy shared via git).
|
|
882
1078
|
*
|
|
883
1079
|
* Manages ONLY `.devflow/` — never `.claude/` — because user-scope installs must
|
|
884
1080
|
* not gitignore `.claude/`. This is the init-time counterpart to the always-on
|
|
885
|
-
* src/assets/scripts/hooks/ensure-root-gitignore shell helper; both
|
|
886
|
-
*
|
|
887
|
-
*
|
|
888
|
-
*
|
|
1081
|
+
* src/assets/scripts/hooks/ensure-root-gitignore shell helper; both resolve the same
|
|
1082
|
+
* shape for a given .gitignore — DEVFLOW_GITIGNORE_BLOCK, or
|
|
1083
|
+
* DEVFLOW_GITIGNORE_BLOCK_WITHOUT_CLAUDEIGNORE when the project owns that entry — and
|
|
1084
|
+
* emit identical bytes, so the two paths are byte-compatible and mutually idempotent.
|
|
1085
|
+
* Called unconditionally (independent of install scope and every feature toggle)
|
|
1086
|
+
* whenever a git root is known.
|
|
889
1087
|
*
|
|
890
|
-
* Uses a versioned marker file (`.devflow/.root-gitignore-configured-
|
|
891
|
-
* detection — the same pattern as the shell twin.
|
|
892
|
-
*
|
|
1088
|
+
* Uses a versioned project-local marker file (`.devflow/.root-gitignore-configured-v5`)
|
|
1089
|
+
* for fast-path detection — the same pattern as the shell twin. The marker is a claim,
|
|
1090
|
+
* not proof, so even a marked install re-reads .gitignore and re-runs
|
|
1091
|
+
* computeDevflowGitignore; bumping the version forces a re-run once per install, which
|
|
1092
|
+
* is how a v4-marked project gains the policy line and is re-stamped v5.
|
|
893
1093
|
*
|
|
894
|
-
* Idempotent:
|
|
895
|
-
*
|
|
1094
|
+
* Idempotent: computeDevflowGitignore returns null for a converged file, so a
|
|
1095
|
+
* marked install performs one read and no write. Errors are swallowed
|
|
1096
|
+
* (verbose-logged) — a gitignore write must never abort init.
|
|
896
1097
|
*/
|
|
897
1098
|
export async function ensureDevflowGitignore(gitRoot, verbose) {
|
|
898
1099
|
try {
|
|
899
1100
|
const devflowDir = path.join(gitRoot, '.devflow');
|
|
900
|
-
const
|
|
1101
|
+
const markerV5 = path.join(devflowDir, GITIGNORE_MARKER_V5);
|
|
901
1102
|
const gitignorePath = path.join(gitRoot, '.gitignore');
|
|
902
|
-
// Fast-path with verification:
|
|
1103
|
+
// Fast-path with verification: v5 marker normally means the block is installed,
|
|
903
1104
|
// but the marker is a claim, not proof — a merge-conflict resolution may have
|
|
904
1105
|
// dropped the block. Even when the marker exists, read .gitignore (one cheap
|
|
905
1106
|
// read) and run computeDevflowGitignore; write only when it returns non-null.
|
|
906
|
-
// Idempotent:
|
|
907
|
-
let
|
|
1107
|
+
// Idempotent: converged file → computeDevflowGitignore returns null → no write.
|
|
1108
|
+
let v5Marked = false;
|
|
908
1109
|
try {
|
|
909
|
-
await fs.access(
|
|
910
|
-
|
|
1110
|
+
await fs.access(markerV5);
|
|
1111
|
+
v5Marked = true;
|
|
911
1112
|
}
|
|
912
1113
|
catch { /* absent */ }
|
|
913
|
-
if (
|
|
1114
|
+
if (v5Marked) {
|
|
914
1115
|
let existingContent = '';
|
|
915
1116
|
try {
|
|
916
1117
|
existingContent = await fs.readFile(gitignorePath, 'utf-8');
|
|
@@ -920,9 +1121,10 @@ export async function ensureDevflowGitignore(gitRoot, verbose) {
|
|
|
920
1121
|
if (healContent !== null) {
|
|
921
1122
|
await fs.writeFile(gitignorePath, healContent, 'utf-8');
|
|
922
1123
|
if (verbose) {
|
|
923
|
-
p.log.success('.gitignore configured (.devflow/ local; feature knowledge + conventions shared)');
|
|
1124
|
+
p.log.success('.gitignore configured (.devflow/ local; feature knowledge + conventions + evidence policy shared)');
|
|
924
1125
|
}
|
|
925
1126
|
}
|
|
1127
|
+
await removeLegacyGitignoreMarkers(devflowDir);
|
|
926
1128
|
return;
|
|
927
1129
|
}
|
|
928
1130
|
let gitignoreContent = '';
|
|
@@ -934,16 +1136,13 @@ export async function ensureDevflowGitignore(gitRoot, verbose) {
|
|
|
934
1136
|
if (newContent !== null) {
|
|
935
1137
|
await fs.writeFile(gitignorePath, newContent, 'utf-8');
|
|
936
1138
|
if (verbose) {
|
|
937
|
-
p.log.success('.gitignore configured (.devflow/ local; feature knowledge + conventions shared)');
|
|
1139
|
+
p.log.success('.gitignore configured (.devflow/ local; feature knowledge + conventions + evidence policy shared)');
|
|
938
1140
|
}
|
|
939
1141
|
}
|
|
940
|
-
// Stamp
|
|
1142
|
+
// Stamp v5 marker so subsequent runs fast-path; drop every legacy marker.
|
|
941
1143
|
await fs.mkdir(devflowDir, { recursive: true });
|
|
942
|
-
await fs.writeFile(
|
|
943
|
-
|
|
944
|
-
await fs.rm(path.join(devflowDir, GITIGNORE_MARKER_V2), { force: true });
|
|
945
|
-
}
|
|
946
|
-
catch { /* ok if absent */ }
|
|
1144
|
+
await fs.writeFile(markerV5, '', 'utf-8');
|
|
1145
|
+
await removeLegacyGitignoreMarkers(devflowDir);
|
|
947
1146
|
}
|
|
948
1147
|
catch (error) {
|
|
949
1148
|
if (verbose) {
|