devflow-kit 2.5.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +73 -0
- package/README.md +44 -19
- package/dist/agents/git.md +13 -15
- package/dist/cli/commands/ambient.js +160 -145
- package/dist/cli/commands/capture.js +29 -55
- package/dist/cli/commands/compliance.js +32 -61
- package/dist/cli/commands/context.js +17 -32
- package/dist/cli/commands/debug.js +65 -26
- package/dist/cli/commands/flags.js +3 -3
- package/dist/cli/commands/hud.js +34 -10
- package/dist/cli/commands/init-seed.js +40 -4
- package/dist/cli/commands/init.js +249 -271
- package/dist/cli/commands/install-report.js +10 -15
- package/dist/cli/commands/knowledge/index.js +1 -1
- package/dist/cli/commands/knowledge/toggle.js +11 -3
- package/dist/cli/commands/learning.js +52 -37
- package/dist/cli/commands/legacy-hooks.js +11 -14
- package/dist/cli/commands/memory.js +67 -78
- package/dist/cli/commands/proxy.js +23 -41
- package/dist/cli/commands/security.js +5 -13
- package/dist/cli/commands/skills.js +21 -3
- package/dist/cli/commands/tracker.js +100 -228
- package/dist/cli/commands/uninstall.js +343 -138
- package/dist/commands/bug-analysis.md +38 -12
- package/dist/commands/code-review.md +70 -21
- package/dist/commands/debug.md +37 -7
- package/dist/commands/dynamic-build.md +66 -17
- package/dist/commands/dynamic-plan.md +19 -8
- package/dist/commands/dynamic-profile.md +24 -10
- package/dist/commands/dynamic-tickets.md +22 -11
- package/dist/commands/explore.md +37 -7
- package/dist/commands/implement.md +96 -32
- package/dist/commands/plan.md +62 -19
- package/dist/commands/release.md +2 -2
- package/dist/commands/research.md +34 -8
- package/dist/commands/resolve.md +65 -17
- package/dist/commands/self-review.md +45 -9
- package/dist/core/compliance-compose.js +27 -27
- package/dist/core/evidence-policy.js +240 -24
- package/dist/core/feature-config.js +94 -25
- package/dist/core/feature-switch.js +1 -1
- package/dist/core/flags.js +30 -2
- package/dist/core/fs-atomic.js +27 -0
- package/dist/core/hook-log-dirs.js +104 -0
- package/dist/core/learning-tuning-config.js +5 -3
- package/dist/core/ledger-root.js +102 -0
- package/dist/core/manifest.js +6 -4
- package/dist/core/mds-variants.js +34 -97
- package/dist/core/migrations.js +49 -23
- package/dist/core/plugins.js +5 -4
- package/dist/core/project-paths.js +0 -17
- package/dist/core/same-location.js +25 -0
- package/dist/core/tracker.js +226 -139
- package/dist/hud/components/config-counts.js +15 -4
- package/dist/hud/components/learning-counts.js +14 -0
- package/dist/hud/config.js +2 -1
- package/dist/hud/cost-history.js +2 -4
- package/dist/hud/git.js +52 -7
- package/dist/hud/index.js +7 -9
- package/dist/skills/git/references/pr/check-merge-readiness.md +1 -1
- package/dist/skills/git/references/pr/ensure-pr-ready.md +1 -1
- package/dist/skills/git/references/pr/update-pr-evidence.md +1 -1
- package/dist/skills/git/references/tracker/_mcp.md +1 -1
- package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +1 -1
- package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +1 -1
- package/dist/skills/git/references/tracker/github/gather-release-evidence.md +2 -2
- package/dist/skills/git/references/tracker/github/manage-debt.md +3 -3
- package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +1 -1
- package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +1 -1
- package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +2 -2
- package/dist/skills/git/references/tracker/jira/manage-debt.md +1 -1
- package/dist/skills/git/references/tracker/jira/post-wave-report.md +1 -1
- package/dist/skills/git/references/tracker/jira/setup-task.md +1 -1
- package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +1 -1
- package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +1 -1
- package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +2 -2
- package/dist/skills/git/references/tracker/linear/manage-debt.md +1 -1
- package/dist/skills/git/references/tracker/linear/post-wave-report.md +1 -1
- package/dist/skills/git/references/tracker/linear/setup-task.md +1 -1
- package/dist/targets/claude-code/claude-paths.js +59 -57
- package/dist/targets/claude-code/compliance-install.js +49 -65
- package/dist/targets/claude-code/hooks.js +108 -3
- package/dist/targets/claude-code/installer.js +30 -57
- package/dist/targets/claude-code/post-install.js +232 -139
- package/dist/targets/claude-code/tracker-install.js +38 -65
- package/package.json +5 -4
- package/src/assets/agents/code.md +4 -3
- package/src/assets/agents/design.md +1 -0
- package/src/assets/agents/git.mds +55 -57
- package/src/assets/agents/knowledge.md +2 -2
- package/src/assets/agents/review.md +3 -1
- package/src/assets/agents/tracker.md +37 -30
- package/src/assets/commands/_partials/_compliance.mds +19 -1
- package/src/assets/commands/_partials/_decisions.mds +15 -3
- package/src/assets/commands/_partials/_docs_root.mds +35 -0
- package/src/assets/commands/_partials/_engine.mds +2 -2
- package/src/assets/commands/_partials/_evidence_policy.mds +3 -3
- package/src/assets/commands/_partials/_factory.mds +1 -1
- package/src/assets/commands/_partials/_knowledge.mds +27 -9
- package/src/assets/commands/_partials/_plan_contract.mds +2 -2
- package/src/assets/commands/_partials/_preamble.mds +1 -1
- package/src/assets/commands/_partials/_publication.mds +6 -2
- package/src/assets/commands/_partials/_settings.mds +28 -0
- package/src/assets/commands/_partials/_ticket_template.mds +3 -3
- package/src/assets/commands/_partials/_tracker.mds +4 -4
- package/src/assets/commands/_partials/_wave.mds +4 -4
- package/src/assets/commands/bug-analysis.mds +19 -17
- package/src/assets/commands/code-review.mds +39 -33
- package/src/assets/commands/debug.mds +4 -5
- package/src/assets/commands/dynamic-build.mds +75 -53
- package/src/assets/commands/dynamic-plan.mds +20 -15
- package/src/assets/commands/dynamic-profile.mds +24 -11
- package/src/assets/commands/dynamic-tickets.mds +25 -20
- package/src/assets/commands/explore.mds +4 -5
- package/src/assets/commands/implement.mds +58 -45
- package/src/assets/commands/plan.mds +34 -29
- package/src/assets/commands/release.md +2 -2
- package/src/assets/commands/research.mds +11 -9
- package/src/assets/commands/resolve.mds +41 -39
- package/src/assets/commands/self-review.mds +24 -25
- package/src/assets/mds/git/_pr.mds +61 -61
- package/src/assets/mds/git/_references.mds +19 -19
- package/src/assets/mds/tracker/_common.mds +8 -8
- package/src/assets/mds/tracker/_github.mds +71 -71
- package/src/assets/mds/tracker/_jira.mds +74 -74
- package/src/assets/mds/tracker/_linear.mds +75 -75
- package/src/assets/mds/tracker/_mcp.mds +23 -17
- package/src/assets/scripts/hooks/background-memory-update +35 -19
- package/src/assets/scripts/hooks/capture-prompt +18 -12
- package/src/assets/scripts/hooks/capture-question +18 -12
- package/src/assets/scripts/hooks/capture-turn +27 -17
- package/src/assets/scripts/hooks/debug-trace +11 -6
- package/src/assets/scripts/hooks/ensure-devflow-init +33 -6
- package/src/assets/scripts/hooks/ensure-proxy +9 -8
- package/src/assets/scripts/hooks/ensure-root-gitignore +111 -36
- package/src/assets/scripts/hooks/git-marker +48 -0
- package/src/assets/scripts/hooks/json-helper.cjs +6 -1
- package/src/assets/scripts/hooks/lib/project-paths.cjs +0 -19
- package/src/assets/scripts/hooks/log-paths +80 -0
- package/src/assets/scripts/hooks/memory-worker +17 -15
- package/src/assets/scripts/hooks/pre-compact-memory +41 -16
- package/src/assets/scripts/hooks/queue-append +104 -30
- package/src/assets/scripts/hooks/resolve-project-root +101 -7
- package/src/assets/scripts/hooks/session-start-context +289 -122
- package/src/assets/scripts/hooks/session-start-memory +35 -16
- package/src/assets/scripts/lib/project-config.cjs +633 -0
- package/src/assets/scripts/resolve-evidence-policy.cjs +300 -220
- package/src/assets/scripts/resolve-settings.cjs +1054 -0
- package/src/assets/scripts/verify-evidence.cjs +1 -1
- package/src/assets/skills/compliance/SKILL.md +2 -2
- package/src/assets/skills/docs-framework/SKILL.md +6 -7
- package/src/assets/skills/docs-framework/references/patterns.md +10 -17
- package/src/assets/skills/gap-analysis/SKILL.md +2 -2
- package/src/assets/skills/git/references/github-api.md +9 -9
- package/src/assets/skills/git/references/patterns.md +1 -1
- package/src/assets/skills/worktree-support/SKILL.md +1 -1
- package/src/assets/skills/worktree-support/references/roots.md +29 -0
- package/src/targets/claude-code/templates/managed-settings.json +25 -9
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { promises as fs, writeFileSync, unlinkSync } from 'fs';
|
|
2
2
|
import { execFileSync } from 'child_process';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
-
import * as os from 'os';
|
|
5
4
|
import * as p from '@clack/prompts';
|
|
6
5
|
import { getManagedSettingsPath } from './claude-paths.js';
|
|
7
|
-
import {
|
|
8
|
-
import { writeFileAtomicExclusive } from '../../core/fs-atomic.js';
|
|
6
|
+
import { writeSettingsFileAtomic } from '../../core/fs-atomic.js';
|
|
9
7
|
function isNodeSystemError(error) {
|
|
10
8
|
return (error instanceof Error &&
|
|
11
9
|
'code' in error &&
|
|
@@ -13,18 +11,15 @@ function isNodeSystemError(error) {
|
|
|
13
11
|
}
|
|
14
12
|
/**
|
|
15
13
|
* Replace ${DEVFLOW_DIR} placeholders in a settings template.
|
|
14
|
+
*
|
|
15
|
+
* D-ONE-HOME: the settings template's install-time placeholder is the one
|
|
16
|
+
* surviving spelling of that name. It is a template token substituted here with
|
|
17
|
+
* the machine root (always ~/.devflow) — not an environment variable, and no
|
|
18
|
+
* runtime reader resolves it (tests/guards/one-home.test.ts pins both sites).
|
|
16
19
|
*/
|
|
17
20
|
export function substituteSettingsTemplate(template, devflowDir) {
|
|
18
21
|
return template.replace(/\$\{DEVFLOW_DIR\}/g, devflowDir);
|
|
19
22
|
}
|
|
20
|
-
/**
|
|
21
|
-
* Compute which entries need appending to a .gitignore file.
|
|
22
|
-
* Returns only entries not already present.
|
|
23
|
-
*/
|
|
24
|
-
export function computeGitignoreAppend(existingContent, entries) {
|
|
25
|
-
const existingLines = existingContent.split('\n').map(l => l.trim());
|
|
26
|
-
return entries.filter(entry => !existingLines.includes(entry));
|
|
27
|
-
}
|
|
28
23
|
/**
|
|
29
24
|
* Sentinel line whose presence means the current (v3-and-later) carve-out block is
|
|
30
25
|
* installed. Devflow-unique: no user writes `!.devflow/conventions.md` by hand.
|
|
@@ -40,13 +35,24 @@ const CLAUDEIGNORE_LINE = '.claudeignore';
|
|
|
40
35
|
/** A user's explicit un-ignore of `.claudeignore`; never overridden. */
|
|
41
36
|
const CLAUDEIGNORE_NEGATION = '!.claudeignore';
|
|
42
37
|
/**
|
|
43
|
-
* Re-includes the
|
|
44
|
-
* line, never a presence sentinel: users may author it themselves, so its presence
|
|
38
|
+
* Re-includes the retired evidence-policy file (D-GITIGNORE-V5,
|
|
39
|
+
* D-POLICY-JSON-RETIRED). A COMPLETION line, never a presence sentinel: users may author it themselves, so its presence
|
|
45
40
|
* proves nothing about the devflow block (avoids PF-059). It sits after `.devflow/*`
|
|
46
41
|
* (which it overrides under last-match-wins) and before `.claudeignore`, so the
|
|
47
42
|
* block's final line stays `.claudeignore`.
|
|
48
43
|
*/
|
|
49
44
|
const DEVFLOW_POLICY_LINE = '!.devflow/policy.json';
|
|
45
|
+
/**
|
|
46
|
+
* Re-includes the team-committed project settings file (D-GITIGNORE-V6). The same
|
|
47
|
+
* contract as the policy line: a COMPLETION line, never a presence sentinel — a
|
|
48
|
+
* user may author it before devflow ever runs, so its presence proves nothing about
|
|
49
|
+
* the block (avoids PF-059). It sits after the policy line and before `.claudeignore`,
|
|
50
|
+
* so a v5 block, which ends in `.claudeignore`, gains it just before that line
|
|
51
|
+
* (D-GITIGNORE-IN-BLOCK, computeDevflowGitignore). Without it
|
|
52
|
+
* `.devflow/*` ignores `.devflow/project.json`, and a team could only commit it with
|
|
53
|
+
* `git add -f`. Devflow never writes the file itself (ADR-024).
|
|
54
|
+
*/
|
|
55
|
+
const DEVFLOW_PROJECT_LINE = '!.devflow/project.json';
|
|
50
56
|
/**
|
|
51
57
|
* The shared .devflow/ gitignore block. Everything under .devflow/ is local
|
|
52
58
|
* (memory, learning, docs, locks) EXCEPT:
|
|
@@ -54,9 +60,14 @@ const DEVFLOW_POLICY_LINE = '!.devflow/policy.json';
|
|
|
54
60
|
* + committed (the Knowledge agent commits them at workflow end).
|
|
55
61
|
* - conventions.md: naming-convention authority written by the Git learn-conventions
|
|
56
62
|
* operation; GIT-TRACKED so the team shares a single naming source.
|
|
57
|
-
* - policy.json: the
|
|
58
|
-
*
|
|
59
|
-
*
|
|
63
|
+
* - policy.json: the retired evidence-policy file. resolve-evidence-policy.cjs never
|
|
64
|
+
* parses it, but where project.json has no `evidence` its presence holds the
|
|
65
|
+
* repository at `required` (D-POLICY-JSON-RETIRED); GIT-TRACKED so a team's
|
|
66
|
+
* committed copy stays shared until its value moves into project.json. Devflow
|
|
67
|
+
* never writes it.
|
|
68
|
+
* - project.json: the team-committed settings (evidence, compliance, tracker, review
|
|
69
|
+
* publication, narrow-only feature switches) both resolvers read; GIT-TRACKED for
|
|
70
|
+
* the same reason (D-GITIGNORE-V6). Devflow never writes it.
|
|
60
71
|
*
|
|
61
72
|
* Re-including files under an ignored tree needs a `dir/*` + `!dir/keep` pair at
|
|
62
73
|
* each level — a bare `.devflow/` excludes the directory so git never descends and
|
|
@@ -68,9 +79,10 @@ const DEVFLOW_POLICY_LINE = '!.devflow/policy.json';
|
|
|
68
79
|
const DEVFLOW_GITIGNORE_BLOCK_LINES = [
|
|
69
80
|
'# Devflow runtime data — local by default (memory, learning, docs, locks).',
|
|
70
81
|
'# Shared via git: feature knowledge bases under .devflow/features/ (index.md and',
|
|
71
|
-
'# every {slug}/KNOWLEDGE.md), .devflow/conventions.md (naming authority)
|
|
72
|
-
'# .devflow/policy.json (
|
|
73
|
-
'# `.devflow/features/` or
|
|
82
|
+
'# every {slug}/KNOWLEDGE.md), .devflow/conventions.md (naming authority),',
|
|
83
|
+
'# .devflow/policy.json (retired; presence only) and .devflow/project.json (team settings).',
|
|
84
|
+
'# To stop sharing the first two, re-add `.devflow/features/` or',
|
|
85
|
+
'# `.devflow/conventions.md` to your own .gitignore.',
|
|
74
86
|
'.devflow/*',
|
|
75
87
|
'!.devflow/features/',
|
|
76
88
|
'.devflow/features/*',
|
|
@@ -80,10 +92,29 @@ const DEVFLOW_GITIGNORE_BLOCK_LINES = [
|
|
|
80
92
|
DEVFLOW_GITIGNORE_SENTINEL_V2,
|
|
81
93
|
DEVFLOW_GITIGNORE_SENTINEL_V3,
|
|
82
94
|
DEVFLOW_POLICY_LINE,
|
|
95
|
+
DEVFLOW_PROJECT_LINE,
|
|
83
96
|
CLAUDEIGNORE_LINE,
|
|
84
97
|
];
|
|
85
98
|
/** The full carve-out block, `.claudeignore` line included. */
|
|
86
99
|
export const DEVFLOW_GITIGNORE_BLOCK = DEVFLOW_GITIGNORE_BLOCK_LINES.join('\n');
|
|
100
|
+
/**
|
|
101
|
+
* The entries directly under a repository's `.devflow/` that the team shares
|
|
102
|
+
* through git (a trailing `/` marks a directory): the feature knowledge bases,
|
|
103
|
+
* the naming conventions, the retired evidence-policy file and the committed
|
|
104
|
+
* project settings.
|
|
105
|
+
*
|
|
106
|
+
* D-UNINSTALL-CARVE-OUT: uninstall's project-data step never deletes these — a
|
|
107
|
+
* confirmed removal takes everything else under `.devflow/` and keeps them
|
|
108
|
+
* byte-identical, because an uncommitted edit to a tracked file is not
|
|
109
|
+
* recoverable from git. Kept next to the gitignore block it mirrors: every path
|
|
110
|
+
* the block re-includes must appear here (pinned by tests/uninstall-logic.test.ts).
|
|
111
|
+
*/
|
|
112
|
+
export const DEVFLOW_TRACKED_PATHS = Object.freeze([
|
|
113
|
+
'features/',
|
|
114
|
+
'conventions.md',
|
|
115
|
+
'policy.json',
|
|
116
|
+
'project.json',
|
|
117
|
+
]);
|
|
87
118
|
/**
|
|
88
119
|
* The carve-out block without its final `.claudeignore` line — emitted instead of
|
|
89
120
|
* the full block when the target .gitignore already carries a `.claudeignore` or
|
|
@@ -92,15 +123,23 @@ export const DEVFLOW_GITIGNORE_BLOCK = DEVFLOW_GITIGNORE_BLOCK_LINES.join('\n');
|
|
|
92
123
|
export const DEVFLOW_GITIGNORE_BLOCK_WITHOUT_CLAUDEIGNORE = DEVFLOW_GITIGNORE_BLOCK_LINES.slice(0, -1).join('\n');
|
|
93
124
|
/** The legacy wholesale comment our pre-carve-out writers emitted. */
|
|
94
125
|
const LEGACY_DEVFLOW_COMMENT = '# Devflow runtime data (local by default; remove to share via git)';
|
|
126
|
+
/**
|
|
127
|
+
* The most lines a top-up run extends below its anchor — one per line the run may
|
|
128
|
+
* hold (the sentinel, the policy line, the project line). The shell twin's loop has
|
|
129
|
+
* the same bound.
|
|
130
|
+
*/
|
|
131
|
+
const BLOCK_RUN_MAX = 3;
|
|
95
132
|
/**
|
|
96
133
|
* PURE: given existing .gitignore content, return the content that ignores
|
|
97
|
-
* `.devflow/` with the feature-knowledge + conventions.md + policy.json
|
|
98
|
-
* or `null` when no change is needed. Idempotent: feeding its own output
|
|
134
|
+
* `.devflow/` with the feature-knowledge + conventions.md + policy.json + project.json
|
|
135
|
+
* carve-out — or `null` when no change is needed. Idempotent: feeding its own output
|
|
136
|
+
* back returns `null`.
|
|
99
137
|
*
|
|
100
|
-
* D-GITIGNORE-V5: the block is detected ONLY by its own
|
|
101
|
-
* (`!.devflow/conventions.md`). `.claudeignore
|
|
102
|
-
*
|
|
103
|
-
*
|
|
138
|
+
* D-GITIGNORE-V5 / D-GITIGNORE-V6: the block is detected ONLY by its own
|
|
139
|
+
* devflow-unique sentinel (`!.devflow/conventions.md`). `.claudeignore`,
|
|
140
|
+
* `!.devflow/policy.json` and `!.devflow/project.json` are COMPLETION lines — users
|
|
141
|
+
* legitimately author all three themselves — so each is topped up when missing and
|
|
142
|
+
* never read as proof the block exists. A presence check on a
|
|
104
143
|
* user-authored line inverts both halves of the contract: projects that already carry
|
|
105
144
|
* that line are told the block is installed when it is not, and a user's
|
|
106
145
|
* `!.claudeignore` un-ignore is silently reversed by re-appending `.claudeignore`
|
|
@@ -108,26 +147,36 @@ const LEGACY_DEVFLOW_COMMENT = '# Devflow runtime data (local by default; remove
|
|
|
108
147
|
*
|
|
109
148
|
* `hasClaudeignoreEntry` is true when some whole line, trimmed, is exactly
|
|
110
149
|
* `.claudeignore` OR `!.claudeignore`. Treating both forms as "present" both honours
|
|
111
|
-
* an un-ignore and makes every branch converge on re-run. `hasPolicyLine`
|
|
112
|
-
* some whole line, trimmed, is exactly
|
|
113
|
-
*
|
|
150
|
+
* an un-ignore and makes every branch converge on re-run. `hasPolicyLine` and
|
|
151
|
+
* `hasProjectLine` are true when some whole line, trimmed, is exactly
|
|
152
|
+
* `!.devflow/policy.json` / `!.devflow/project.json`. The missing completion lines, in
|
|
153
|
+
* block order, are [policy line, project line, `.claudeignore` (only when
|
|
114
154
|
* `!hasClaudeignoreEntry`)].
|
|
115
155
|
*
|
|
116
156
|
* 1. A `/.devflow/` line present → `null` (user opt-out; respect manual config).
|
|
117
|
-
* 2. v3 sentinel present →
|
|
118
|
-
* missing. This is the v4→v5 upgrade: a
|
|
119
|
-
* its `.claudeignore` line, and
|
|
120
|
-
*
|
|
121
|
-
*
|
|
157
|
+
* 2. v3 sentinel present → insert the missing completion lines into the block;
|
|
158
|
+
* `null` when none are missing. This is the v4→v6 and v5→v6 upgrade: a v5 block
|
|
159
|
+
* gains only the project line, just before its `.claudeignore` line, and a v4
|
|
160
|
+
* block the policy and project lines, each keeping its old comment.
|
|
161
|
+
* 3. v2 sentinel present, no v3 → insert `!.devflow/conventions.md` followed by the
|
|
162
|
+
* missing completion lines right after the v2 sentinel.
|
|
122
163
|
* 4. Legacy bare `.devflow/` present → strip it (+ our old comment), then append the
|
|
123
164
|
* 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
|
|
125
|
-
* duplicated harmlessly here, and the re-run is a no-op.
|
|
126
|
-
* 5.
|
|
127
|
-
* (`.devflow/.root-gitignore-configured-
|
|
165
|
+
* `.claudeignore` line when `hasClaudeignoreEntry`. A user's own policy or project
|
|
166
|
+
* line is duplicated harmlessly here, and the re-run is a no-op.
|
|
167
|
+
* 5. No completion line is ever a sentinel. The marker file
|
|
168
|
+
* (`.devflow/.root-gitignore-configured-v6`) is a fast-path claim, never proof.
|
|
169
|
+
*
|
|
170
|
+
* D-GITIGNORE-IN-BLOCK: lines topped up into an existing block (2 and 3) go INSIDE
|
|
171
|
+
* it, where a fresh block holds them — never at the end of the file. gitignore is
|
|
172
|
+
* last-match-wins, so a `!.devflow/project.json` appended after a user's own later
|
|
173
|
+
* `.devflow/project.json` would silently override their re-ignore. The missing
|
|
174
|
+
* lines are inserted as one run, in block order, after the first sentinel line and
|
|
175
|
+
* the lines right after it that a fresh block places before the first missing line
|
|
176
|
+
* (blockRunBefore; at most three). Every other byte of the file is kept.
|
|
128
177
|
*
|
|
129
178
|
* Line matching is whole-line, whitespace-tolerant, exact text — never substring.
|
|
130
|
-
*
|
|
179
|
+
* The insert and the append form are mirrored byte-for-byte in the shell twin
|
|
131
180
|
* (src/assets/scripts/hooks/ensure-root-gitignore), which is what the cross-implementation
|
|
132
181
|
* parity table in tests/shell-hooks.test.ts pins.
|
|
133
182
|
*/
|
|
@@ -136,16 +185,39 @@ export function computeDevflowGitignore(existingContent) {
|
|
|
136
185
|
const trimmed = lines.map(l => l.trim());
|
|
137
186
|
const hasClaudeignoreEntry = trimmed.some(l => l === CLAUDEIGNORE_LINE || l === CLAUDEIGNORE_NEGATION);
|
|
138
187
|
const hasPolicyLine = trimmed.includes(DEVFLOW_POLICY_LINE);
|
|
188
|
+
const hasProjectLine = trimmed.includes(DEVFLOW_PROJECT_LINE);
|
|
139
189
|
/** The block-completing lines this file lacks, in block order. */
|
|
140
190
|
const missingCompletionLines = [
|
|
141
191
|
...(hasPolicyLine ? [] : [DEVFLOW_POLICY_LINE]),
|
|
192
|
+
...(hasProjectLine ? [] : [DEVFLOW_PROJECT_LINE]),
|
|
142
193
|
...(hasClaudeignoreEntry ? [] : [CLAUDEIGNORE_LINE]),
|
|
143
194
|
];
|
|
144
195
|
/**
|
|
145
|
-
*
|
|
146
|
-
*
|
|
196
|
+
* The block lines a top-up run follows: the v3 sentinel, then whichever of the
|
|
197
|
+
* policy and project lines a fresh block places before the first missing line.
|
|
198
|
+
* Mirrors `_ERG_RUN_RE` in the shell twin.
|
|
199
|
+
*/
|
|
200
|
+
const blockRunBefore = !hasPolicyLine
|
|
201
|
+
? [DEVFLOW_GITIGNORE_SENTINEL_V3]
|
|
202
|
+
: !hasProjectLine
|
|
203
|
+
? [DEVFLOW_GITIGNORE_SENTINEL_V3, DEVFLOW_POLICY_LINE]
|
|
204
|
+
: [DEVFLOW_GITIGNORE_SENTINEL_V3, DEVFLOW_POLICY_LINE, DEVFLOW_PROJECT_LINE];
|
|
205
|
+
/**
|
|
206
|
+
* Insert `inserted` into an existing devflow block: after the first line whose
|
|
207
|
+
* trimmed text is `anchor`, and after up to BLOCK_RUN_MAX lines right below it
|
|
208
|
+
* whose trimmed text is in `run`. Every other byte is kept. A run that ends on a
|
|
209
|
+
* last line with no newline gets one first, so the inserted lines never fuse onto
|
|
210
|
+
* it. Mirrors `_erg_insert_in_block` in the shell twin (D-GITIGNORE-IN-BLOCK).
|
|
147
211
|
*/
|
|
148
|
-
const
|
|
212
|
+
const insertInBlock = (anchor, run, inserted) => {
|
|
213
|
+
let end = trimmed.indexOf(anchor);
|
|
214
|
+
for (let k = 0; k < BLOCK_RUN_MAX && end + 1 < lines.length && run.includes(trimmed[end + 1]); k++) {
|
|
215
|
+
end++;
|
|
216
|
+
}
|
|
217
|
+
if (end === lines.length - 1)
|
|
218
|
+
return `${existingContent}\n${inserted.join('\n')}\n`;
|
|
219
|
+
return [...lines.slice(0, end + 1), ...inserted, ...lines.slice(end + 1)].join('\n');
|
|
220
|
+
};
|
|
149
221
|
/**
|
|
150
222
|
* Start a new block after unrelated content: one blank separator line. Existing
|
|
151
223
|
* trailing newlines are preserved verbatim (no trimEnd, no blank-line dedupe) so
|
|
@@ -161,11 +233,12 @@ export function computeDevflowGitignore(existingContent) {
|
|
|
161
233
|
if (trimmed.includes(DEVFLOW_GITIGNORE_SENTINEL_V3)) {
|
|
162
234
|
return missingCompletionLines.length === 0
|
|
163
235
|
? null
|
|
164
|
-
:
|
|
236
|
+
: insertInBlock(DEVFLOW_GITIGNORE_SENTINEL_V3, blockRunBefore, missingCompletionLines);
|
|
165
237
|
}
|
|
166
|
-
// 3. v2 block installed —
|
|
238
|
+
// 3. v2 block installed — insert the lines it lacks, in block order, right after
|
|
239
|
+
// its sentinel, the last carve-out line it has.
|
|
167
240
|
if (trimmed.includes(DEVFLOW_GITIGNORE_SENTINEL_V2)) {
|
|
168
|
-
return
|
|
241
|
+
return insertInBlock(DEVFLOW_GITIGNORE_SENTINEL_V2, [], [DEVFLOW_GITIGNORE_SENTINEL_V3, ...missingCompletionLines]);
|
|
169
242
|
}
|
|
170
243
|
// 4. No devflow block — install one, respecting any .claudeignore entry of the user's own.
|
|
171
244
|
const block = hasClaudeignoreEntry
|
|
@@ -182,8 +255,12 @@ export function computeDevflowGitignore(existingContent) {
|
|
|
182
255
|
}
|
|
183
256
|
/**
|
|
184
257
|
* Merge Devflow deny entries into an existing settings JSON object.
|
|
185
|
-
* Preserves existing entries (including allow and sibling keys)
|
|
186
|
-
* and returns the merged JSON string with trailing newline.
|
|
258
|
+
* Preserves existing entries (including allow and sibling keys) except those named
|
|
259
|
+
* in `retired`, deduplicates, and returns the merged JSON string with trailing newline.
|
|
260
|
+
*
|
|
261
|
+
* `retired` is how an install converges an older one: pass retiredDenyEntries(template)
|
|
262
|
+
* so entries Devflow once shipped and has since dropped do not linger. An entry that
|
|
263
|
+
* `newDenyEntries` carries is never dropped, whatever `retired` says.
|
|
187
264
|
*
|
|
188
265
|
* PURE + idempotent: calling with the same inputs always yields byte-equal output.
|
|
189
266
|
* Non-array `deny` (e.g. a string, null) is treated as empty — neither throws nor spreads chars.
|
|
@@ -191,22 +268,33 @@ export function computeDevflowGitignore(existingContent) {
|
|
|
191
268
|
* @throws {SyntaxError} on malformed JSON — callers must pre-validate (e.g. via detectDenyState)
|
|
192
269
|
* or wrap in try/catch.
|
|
193
270
|
*/
|
|
194
|
-
export function mergeDenyList(existingJson, newDenyEntries) {
|
|
271
|
+
export function mergeDenyList(existingJson, newDenyEntries, retired = new Set()) {
|
|
195
272
|
const existing = JSON.parse(existingJson);
|
|
196
273
|
const rawDeny = existing.permissions?.deny;
|
|
197
274
|
const currentDeny = Array.isArray(rawDeny) ? rawDeny : [];
|
|
198
|
-
const
|
|
275
|
+
const kept = currentDeny.filter(e => !retired.has(e));
|
|
276
|
+
const merged = [...new Set([...kept, ...newDenyEntries])];
|
|
199
277
|
existing.permissions = { ...(existing.permissions ?? {}), deny: merged };
|
|
200
278
|
return JSON.stringify(existing, null, 2) + '\n';
|
|
201
279
|
}
|
|
202
280
|
/**
|
|
203
281
|
* Historical superset of every deny entry Devflow has ever shipped.
|
|
204
|
-
* Append every future entry here; never remove entries
|
|
205
|
-
*
|
|
282
|
+
* Append every future entry here; never remove entries — a retired template entry
|
|
283
|
+
* stays here so removal (stripUserDenyList, removeManagedSettings) and install
|
|
284
|
+
* convergence (retiredDenyEntries) still recognise it in an older install.
|
|
206
285
|
*
|
|
207
286
|
* Load-time assertion below verifies this is a superset of the current template.
|
|
208
287
|
*/
|
|
209
288
|
// D-SECURITY-01: frozen at module load — any future template entry must appear here too.
|
|
289
|
+
// D-SECURITY-02 (#399): the nine v1 piped rules (`Bash(curl * | bash*)` and kin) are
|
|
290
|
+
// RETIRED — kept here, dropped from the template. Claude Code splits a Bash command at
|
|
291
|
+
// `|` (and `&&`, `||`, `;`, `|&`, `&`, newlines) and matches every rule against each
|
|
292
|
+
// subcommand alone, so a rule holding ` | ` can never match anything. The exact
|
|
293
|
+
// shell-on-stdin denies in the v2 batch (`Bash(bash)`, `Bash(sh -s *)`, ...) match the
|
|
294
|
+
// shell subcommand of such a pipeline instead.
|
|
295
|
+
// Only entries a release actually shipped belong here: removal and install convergence
|
|
296
|
+
// strip every entry this set names that the template does not, so a rule Devflow never
|
|
297
|
+
// shipped would be taken from a user who wrote it (ADR-024, prove-you-wrote-it).
|
|
210
298
|
export const DEVFLOW_HISTORICAL_DENY = Object.freeze(new Set([
|
|
211
299
|
// v1 batch — 154 entries shipped in src/targets/claude-code/templates/managed-settings.json
|
|
212
300
|
'Bash(rm -rf /*)',
|
|
@@ -363,7 +451,54 @@ export const DEVFLOW_HISTORICAL_DENY = Object.freeze(new Set([
|
|
|
363
451
|
'Read(/etc/shadow)',
|
|
364
452
|
'Read(/etc/sudoers)',
|
|
365
453
|
'Read(/etc/passwd)',
|
|
454
|
+
// v2 batch (#399) — 25 template entries: a shell reading its script from stdin,
|
|
455
|
+
// `zsh -c` beside the v1 `sh -c`/`bash -c`, OrbStack VM control, docker
|
|
456
|
+
// pull/delete/prune and whole-disk or privileged runs.
|
|
457
|
+
'Bash(bash)',
|
|
458
|
+
'Bash(sh)',
|
|
459
|
+
'Bash(zsh)',
|
|
460
|
+
'Bash(bash - *)',
|
|
461
|
+
'Bash(sh - *)',
|
|
462
|
+
'Bash(zsh - *)',
|
|
463
|
+
'Bash(bash -s *)',
|
|
464
|
+
'Bash(sh -s *)',
|
|
465
|
+
'Bash(zsh -s *)',
|
|
466
|
+
'Bash(zsh -c *)',
|
|
467
|
+
'Bash(docker run*--privileged*)',
|
|
468
|
+
'Bash(docker run*-v /:*)',
|
|
469
|
+
'Bash(docker run*--volume /:*)',
|
|
470
|
+
'Bash(docker run*--volume=/:*)',
|
|
471
|
+
'Bash(docker pull *)',
|
|
472
|
+
'Bash(docker image pull *)',
|
|
473
|
+
'Bash(docker rm *)',
|
|
474
|
+
'Bash(docker container rm *)',
|
|
475
|
+
'Bash(docker rmi *)',
|
|
476
|
+
'Bash(docker image rm *)',
|
|
477
|
+
'Bash(docker volume rm *)',
|
|
478
|
+
'Bash(docker*prune*)',
|
|
479
|
+
'Bash(orb *)',
|
|
480
|
+
'Bash(orbctl *)',
|
|
481
|
+
'Bash(open *OrbStack*)',
|
|
366
482
|
]));
|
|
483
|
+
/**
|
|
484
|
+
* The Devflow deny entries an older install may carry that the current template no
|
|
485
|
+
* longer ships: DEVFLOW_HISTORICAL_DENY minus the template. PURE.
|
|
486
|
+
*
|
|
487
|
+
* An empty template (loadTemplateDenyEntries' failure value) retires nothing — an
|
|
488
|
+
* unreadable template must never read as "Devflow dropped every entry it ever shipped".
|
|
489
|
+
*
|
|
490
|
+
* Accepted trade-off (ADR-024): a deny entry is a bare string, so a user who typed a
|
|
491
|
+
* retired entry themselves is indistinguishable from Devflow's copy and loses it on the
|
|
492
|
+
* next install, exactly as `security --disable` and uninstall already strip every
|
|
493
|
+
* historical entry. Retire an entry only when losing a user's identical copy is
|
|
494
|
+
* harmless; the #399 piped rules qualify because none could ever match (D-SECURITY-02).
|
|
495
|
+
*/
|
|
496
|
+
export function retiredDenyEntries(templateEntries) {
|
|
497
|
+
if (templateEntries.length === 0)
|
|
498
|
+
return new Set();
|
|
499
|
+
const current = new Set(templateEntries);
|
|
500
|
+
return new Set([...DEVFLOW_HISTORICAL_DENY].filter(e => !current.has(e)));
|
|
501
|
+
}
|
|
367
502
|
/**
|
|
368
503
|
* Assert that DEVFLOW_HISTORICAL_DENY is a superset of the provided template entries.
|
|
369
504
|
* Throws if any template entry is missing from the historical set.
|
|
@@ -539,8 +674,8 @@ export function resolveSecurityAction(flag, manifestMode, detected, isTTY) {
|
|
|
539
674
|
}
|
|
540
675
|
/**
|
|
541
676
|
* Load the deny entry array from the managed-settings.json template.
|
|
542
|
-
* Canonical single-source helper used by installManagedSettings,
|
|
543
|
-
*
|
|
677
|
+
* Canonical single-source helper used by installManagedSettings, init.ts's security
|
|
678
|
+
* step, and security.ts's --enable path. Removal keys on DEVFLOW_HISTORICAL_DENY instead.
|
|
544
679
|
*
|
|
545
680
|
* Defensive read: treats file as `Record<string, unknown>`, guards with Array.isArray,
|
|
546
681
|
* coerces each element to string. Returns [] on any read or parse failure (never throws).
|
|
@@ -584,7 +719,7 @@ export async function installManagedSettings(rootDir, verbose) {
|
|
|
584
719
|
let content;
|
|
585
720
|
try {
|
|
586
721
|
const existing = await fs.readFile(managedPath, 'utf-8');
|
|
587
|
-
content = mergeDenyList(existing, newDenyEntries);
|
|
722
|
+
content = mergeDenyList(existing, newDenyEntries, retiredDenyEntries(newDenyEntries));
|
|
588
723
|
}
|
|
589
724
|
catch {
|
|
590
725
|
// File doesn't exist — use template as-is
|
|
@@ -669,15 +804,11 @@ export async function removeManagedSettings(rootDir, verbose, managedPathOverrid
|
|
|
669
804
|
catch {
|
|
670
805
|
return false; // File doesn't exist
|
|
671
806
|
}
|
|
672
|
-
//
|
|
673
|
-
|
|
674
|
-
if (devflowDenyEntries.length === 0) {
|
|
675
|
-
return false;
|
|
676
|
-
}
|
|
807
|
+
// Key on every entry Devflow has ever shipped, not the current template: an install
|
|
808
|
+
// from an older release carries entries the template has since retired (D-SECURITY-02).
|
|
677
809
|
const existing = JSON.parse(existingContent);
|
|
678
810
|
const currentDeny = existing.permissions?.deny ?? [];
|
|
679
|
-
const
|
|
680
|
-
const remaining = currentDeny.filter(entry => !devflowSet.has(entry));
|
|
811
|
+
const remaining = currentDeny.filter(entry => !DEVFLOW_HISTORICAL_DENY.has(entry));
|
|
681
812
|
// Determine the target action: delete file entirely or write updated content
|
|
682
813
|
let shouldDelete = false;
|
|
683
814
|
let updatedContent = null;
|
|
@@ -771,8 +902,8 @@ export async function applyUserSecurityDenyList(settingsPath, currentTemplateDen
|
|
|
771
902
|
catch {
|
|
772
903
|
existing = '{}';
|
|
773
904
|
}
|
|
774
|
-
const merged = mergeDenyList(existing, currentTemplateDeny);
|
|
775
|
-
await
|
|
905
|
+
const merged = mergeDenyList(existing, currentTemplateDeny, retiredDenyEntries(currentTemplateDeny));
|
|
906
|
+
await writeSettingsFileAtomic(settingsPath, merged);
|
|
776
907
|
return merged;
|
|
777
908
|
}
|
|
778
909
|
/**
|
|
@@ -780,7 +911,7 @@ export async function applyUserSecurityDenyList(settingsPath, currentTemplateDen
|
|
|
780
911
|
* Colocated with applyUserSecurityDenyList — the remove-side counterpart.
|
|
781
912
|
*
|
|
782
913
|
* Sequence: read → stripUserDenyList → guard (stripped !== existing) →
|
|
783
|
-
*
|
|
914
|
+
* writeSettingsFileAtomic → return { removed }.
|
|
784
915
|
* Atomic write (temp+rename) upholds the never-truncate-on-crash invariant.
|
|
785
916
|
* ENOENT is swallowed (file absent = nothing to strip). Other errors propagate.
|
|
786
917
|
*
|
|
@@ -805,7 +936,7 @@ export async function stripUserSecurityDenyList(settingsPath) {
|
|
|
805
936
|
if (stripped === existing) {
|
|
806
937
|
return null;
|
|
807
938
|
}
|
|
808
|
-
await
|
|
939
|
+
await writeSettingsFileAtomic(settingsPath, stripped);
|
|
809
940
|
return { removed };
|
|
810
941
|
}
|
|
811
942
|
/** True for a non-null, non-array object literal. */
|
|
@@ -916,7 +1047,7 @@ export async function installSettings(claudeDir, rootDir, devflowDir, verbose) {
|
|
|
916
1047
|
settingsExists = false;
|
|
917
1048
|
}
|
|
918
1049
|
if (!settingsExists) {
|
|
919
|
-
await
|
|
1050
|
+
await writeSettingsFileAtomic(settingsPath, settingsContent);
|
|
920
1051
|
if (verbose) {
|
|
921
1052
|
p.log.success('Settings configured');
|
|
922
1053
|
}
|
|
@@ -943,7 +1074,7 @@ export async function installSettings(claudeDir, rootDir, devflowDir, verbose) {
|
|
|
943
1074
|
// Already fully configured — nothing to do
|
|
944
1075
|
return;
|
|
945
1076
|
}
|
|
946
|
-
await
|
|
1077
|
+
await writeSettingsFileAtomic(settingsPath, JSON.stringify(existingParsed, null, 2) + '\n');
|
|
947
1078
|
if (verbose) {
|
|
948
1079
|
p.log.success('Settings updated with Devflow hooks and HUD');
|
|
949
1080
|
}
|
|
@@ -981,11 +1112,12 @@ export async function installClaudeignore(gitRoot, rootDir, verbose) {
|
|
|
981
1112
|
}
|
|
982
1113
|
/**
|
|
983
1114
|
* Discover git repository roots from Claude's project history.
|
|
984
|
-
* Parses
|
|
985
|
-
* @param
|
|
1115
|
+
* Parses `<claudeDir>/history.jsonl` for unique project paths that are valid git repos.
|
|
1116
|
+
* @param claudeDir - The Claude Code directory whose history is read — the caller
|
|
1117
|
+
* passes getClaudeDirectory() (D-CLAUDE-CONFIG-DIR), tests a sandbox.
|
|
986
1118
|
*/
|
|
987
|
-
export async function discoverProjectGitRoots(
|
|
988
|
-
const historyPath = path.join(
|
|
1119
|
+
export async function discoverProjectGitRoots(claudeDir) {
|
|
1120
|
+
const historyPath = path.join(claudeDir, 'history.jsonl');
|
|
989
1121
|
let content;
|
|
990
1122
|
try {
|
|
991
1123
|
content = await fs.readFile(historyPath, 'utf-8');
|
|
@@ -1016,53 +1148,31 @@ export async function discoverProjectGitRoots(homeDir) {
|
|
|
1016
1148
|
.map((r) => r.value);
|
|
1017
1149
|
return gitRoots.sort();
|
|
1018
1150
|
}
|
|
1019
|
-
/**
|
|
1020
|
-
* Update .gitignore with Devflow entries (for local scope installs).
|
|
1021
|
-
*/
|
|
1022
|
-
export async function updateGitignore(gitRoot, verbose) {
|
|
1023
|
-
try {
|
|
1024
|
-
const gitignorePath = path.join(gitRoot, '.gitignore');
|
|
1025
|
-
const entriesToAdd = getGitignoreEntries();
|
|
1026
|
-
let gitignoreContent = '';
|
|
1027
|
-
try {
|
|
1028
|
-
gitignoreContent = await fs.readFile(gitignorePath, 'utf-8');
|
|
1029
|
-
}
|
|
1030
|
-
catch { /* doesn't exist */ }
|
|
1031
|
-
const linesToAdd = computeGitignoreAppend(gitignoreContent, entriesToAdd);
|
|
1032
|
-
if (linesToAdd.length > 0) {
|
|
1033
|
-
const newContent = gitignoreContent
|
|
1034
|
-
? `${gitignoreContent.trimEnd()}\n\n# Devflow local installation\n${linesToAdd.join('\n')}\n`
|
|
1035
|
-
: `# Devflow local installation\n${linesToAdd.join('\n')}\n`;
|
|
1036
|
-
await fs.writeFile(gitignorePath, newContent, 'utf-8');
|
|
1037
|
-
if (verbose) {
|
|
1038
|
-
p.log.success('.gitignore updated');
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
catch (error) {
|
|
1043
|
-
if (verbose) {
|
|
1044
|
-
p.log.warn(`Could not update .gitignore: ${error instanceof Error ? error.message : error}`);
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
1151
|
/**
|
|
1049
1152
|
* Current carve-out marker version. Bump when the block format changes — together
|
|
1050
1153
|
* with the shell twin's stamp (ensure-root-gitignore) and the ensure-devflow-init
|
|
1051
|
-
* fast path, in one commit
|
|
1154
|
+
* fast path, in one commit.
|
|
1155
|
+
*
|
|
1156
|
+
* D-GITIGNORE-V6 (#392): v6 adds the `!.devflow/project.json` completion line. A
|
|
1157
|
+
* v5-stamped project misses the v6 fast path once, gains that line (and nothing else
|
|
1158
|
+
* — its comment and every other line stay byte-identical) and is re-stamped v6, so
|
|
1159
|
+
* a team can commit `.devflow/project.json` without `git add -f`. v2–v5 inputs all
|
|
1160
|
+
* converge on the same completion order: policy, project, `.claudeignore`.
|
|
1052
1161
|
*/
|
|
1053
|
-
const
|
|
1162
|
+
const GITIGNORE_MARKER_V6 = '.root-gitignore-configured-v6';
|
|
1054
1163
|
/**
|
|
1055
1164
|
* Earlier markers, the unversioned (v1) one included — every one is removed
|
|
1056
|
-
* whenever the project is
|
|
1057
|
-
* re-stamp one beside
|
|
1165
|
+
* whenever the project is v6-stamped, on the fast path too: an older devflow can
|
|
1166
|
+
* re-stamp one beside v6, and the shell twin drops the same five.
|
|
1058
1167
|
*/
|
|
1059
1168
|
const LEGACY_GITIGNORE_MARKERS = [
|
|
1169
|
+
'.root-gitignore-configured-v5',
|
|
1060
1170
|
'.root-gitignore-configured-v4',
|
|
1061
1171
|
'.root-gitignore-configured-v3',
|
|
1062
1172
|
'.root-gitignore-configured-v2',
|
|
1063
1173
|
'.root-gitignore-configured',
|
|
1064
1174
|
];
|
|
1065
|
-
/** Remove every legacy marker; an absent one is a no-op. Call only once
|
|
1175
|
+
/** Remove every legacy marker; an absent one is a no-op. Call only once v6 is stamped. */
|
|
1066
1176
|
async function removeLegacyGitignoreMarkers(devflowDir) {
|
|
1067
1177
|
for (const legacy of LEGACY_GITIGNORE_MARKERS) {
|
|
1068
1178
|
try {
|
|
@@ -1073,23 +1183,23 @@ async function removeLegacyGitignoreMarkers(devflowDir) {
|
|
|
1073
1183
|
}
|
|
1074
1184
|
/**
|
|
1075
1185
|
* Deterministically ensure the project root .gitignore applies the `.devflow/`
|
|
1076
|
-
* carve-out (local by default; feature knowledge, conventions.md
|
|
1077
|
-
* policy shared via git).
|
|
1186
|
+
* carve-out (local by default; feature knowledge, conventions.md, the evidence
|
|
1187
|
+
* policy and the project settings shared via git).
|
|
1078
1188
|
*
|
|
1079
|
-
* Manages ONLY `.devflow/` — never `.claude/` — because
|
|
1080
|
-
*
|
|
1189
|
+
* Manages ONLY `.devflow/` — never `.claude/` — because a project's `.claude/`
|
|
1190
|
+
* is its own to share or ignore. This is the init-time counterpart to the always-on
|
|
1081
1191
|
* src/assets/scripts/hooks/ensure-root-gitignore shell helper; both resolve the same
|
|
1082
1192
|
* shape for a given .gitignore — DEVFLOW_GITIGNORE_BLOCK, or
|
|
1083
1193
|
* DEVFLOW_GITIGNORE_BLOCK_WITHOUT_CLAUDEIGNORE when the project owns that entry — and
|
|
1084
1194
|
* emit identical bytes, so the two paths are byte-compatible and mutually idempotent.
|
|
1085
|
-
* Called unconditionally (independent of
|
|
1086
|
-
*
|
|
1195
|
+
* Called unconditionally (independent of every feature toggle) whenever a git
|
|
1196
|
+
* root is known.
|
|
1087
1197
|
*
|
|
1088
|
-
* Uses a versioned project-local marker file (`.devflow/.root-gitignore-configured-
|
|
1198
|
+
* Uses a versioned project-local marker file (`.devflow/.root-gitignore-configured-v6`)
|
|
1089
1199
|
* for fast-path detection — the same pattern as the shell twin. The marker is a claim,
|
|
1090
1200
|
* not proof, so even a marked install re-reads .gitignore and re-runs
|
|
1091
1201
|
* computeDevflowGitignore; bumping the version forces a re-run once per install, which
|
|
1092
|
-
* is how a
|
|
1202
|
+
* is how a v5-marked project gains the project line and is re-stamped v6.
|
|
1093
1203
|
*
|
|
1094
1204
|
* Idempotent: computeDevflowGitignore returns null for a converged file, so a
|
|
1095
1205
|
* marked install performs one read and no write. Errors are swallowed
|
|
@@ -1098,20 +1208,20 @@ async function removeLegacyGitignoreMarkers(devflowDir) {
|
|
|
1098
1208
|
export async function ensureDevflowGitignore(gitRoot, verbose) {
|
|
1099
1209
|
try {
|
|
1100
1210
|
const devflowDir = path.join(gitRoot, '.devflow');
|
|
1101
|
-
const
|
|
1211
|
+
const markerV6 = path.join(devflowDir, GITIGNORE_MARKER_V6);
|
|
1102
1212
|
const gitignorePath = path.join(gitRoot, '.gitignore');
|
|
1103
|
-
// Fast-path with verification:
|
|
1213
|
+
// Fast-path with verification: v6 marker normally means the block is installed,
|
|
1104
1214
|
// but the marker is a claim, not proof — a merge-conflict resolution may have
|
|
1105
1215
|
// dropped the block. Even when the marker exists, read .gitignore (one cheap
|
|
1106
1216
|
// read) and run computeDevflowGitignore; write only when it returns non-null.
|
|
1107
1217
|
// Idempotent: converged file → computeDevflowGitignore returns null → no write.
|
|
1108
|
-
let
|
|
1218
|
+
let v6Marked = false;
|
|
1109
1219
|
try {
|
|
1110
|
-
await fs.access(
|
|
1111
|
-
|
|
1220
|
+
await fs.access(markerV6);
|
|
1221
|
+
v6Marked = true;
|
|
1112
1222
|
}
|
|
1113
1223
|
catch { /* absent */ }
|
|
1114
|
-
if (
|
|
1224
|
+
if (v6Marked) {
|
|
1115
1225
|
let existingContent = '';
|
|
1116
1226
|
try {
|
|
1117
1227
|
existingContent = await fs.readFile(gitignorePath, 'utf-8');
|
|
@@ -1121,7 +1231,7 @@ export async function ensureDevflowGitignore(gitRoot, verbose) {
|
|
|
1121
1231
|
if (healContent !== null) {
|
|
1122
1232
|
await fs.writeFile(gitignorePath, healContent, 'utf-8');
|
|
1123
1233
|
if (verbose) {
|
|
1124
|
-
p.log.success('.gitignore configured (.devflow/ local; feature knowledge + conventions +
|
|
1234
|
+
p.log.success('.gitignore configured (.devflow/ local; feature knowledge + conventions + retired policy.json + project settings shared)');
|
|
1125
1235
|
}
|
|
1126
1236
|
}
|
|
1127
1237
|
await removeLegacyGitignoreMarkers(devflowDir);
|
|
@@ -1136,12 +1246,12 @@ export async function ensureDevflowGitignore(gitRoot, verbose) {
|
|
|
1136
1246
|
if (newContent !== null) {
|
|
1137
1247
|
await fs.writeFile(gitignorePath, newContent, 'utf-8');
|
|
1138
1248
|
if (verbose) {
|
|
1139
|
-
p.log.success('.gitignore configured (.devflow/ local; feature knowledge + conventions +
|
|
1249
|
+
p.log.success('.gitignore configured (.devflow/ local; feature knowledge + conventions + retired policy.json + project settings shared)');
|
|
1140
1250
|
}
|
|
1141
1251
|
}
|
|
1142
|
-
// Stamp
|
|
1252
|
+
// Stamp v6 marker so subsequent runs fast-path; drop every legacy marker.
|
|
1143
1253
|
await fs.mkdir(devflowDir, { recursive: true });
|
|
1144
|
-
await fs.writeFile(
|
|
1254
|
+
await fs.writeFile(markerV6, '', 'utf-8');
|
|
1145
1255
|
await removeLegacyGitignoreMarkers(devflowDir);
|
|
1146
1256
|
}
|
|
1147
1257
|
catch (error) {
|
|
@@ -1150,21 +1260,4 @@ export async function ensureDevflowGitignore(gitRoot, verbose) {
|
|
|
1150
1260
|
}
|
|
1151
1261
|
}
|
|
1152
1262
|
}
|
|
1153
|
-
/**
|
|
1154
|
-
* Create .devflow/docs/ directory structure for Devflow artifacts.
|
|
1155
|
-
*/
|
|
1156
|
-
export async function createDocsStructure(verbose) {
|
|
1157
|
-
const docsDir = getDocsDir(process.cwd());
|
|
1158
|
-
try {
|
|
1159
|
-
await Promise.all([
|
|
1160
|
-
fs.mkdir(path.join(docsDir, 'status', 'compact'), { recursive: true }),
|
|
1161
|
-
fs.mkdir(path.join(docsDir, 'reviews'), { recursive: true }),
|
|
1162
|
-
fs.mkdir(path.join(docsDir, 'releases'), { recursive: true }),
|
|
1163
|
-
]);
|
|
1164
|
-
if (verbose) {
|
|
1165
|
-
p.log.success('.devflow/docs/ structure ready');
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
catch { /* may already exist */ }
|
|
1169
|
-
}
|
|
1170
1263
|
//# sourceMappingURL=post-install.js.map
|