devflow-kit 2.4.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 +229 -0
- package/README.md +111 -18
- package/dist/agents/git.md +822 -0
- package/dist/cli/commands/agents.js +6 -1
- package/dist/cli/commands/ambient.js +160 -145
- package/dist/cli/commands/attribution-prompts.js +1 -1
- package/dist/cli/commands/capture.js +29 -55
- package/dist/cli/commands/compliance-prompts.js +1 -1
- package/dist/cli/commands/compliance.js +48 -55
- 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 +61 -27
- package/dist/cli/commands/init.js +649 -240
- package/dist/cli/commands/install-report.js +200 -0
- package/dist/cli/commands/knowledge/index.js +2 -2
- package/dist/cli/commands/knowledge/toggle.js +35 -37
- package/dist/cli/commands/learning.js +79 -57
- package/dist/cli/commands/legacy-hooks.js +11 -14
- package/dist/cli/commands/memory.js +134 -135
- package/dist/cli/commands/prompt-io.js +4 -4
- package/dist/cli/commands/proxy.js +23 -41
- package/dist/cli/commands/security.js +81 -29
- package/dist/cli/commands/skills.js +71 -7
- package/dist/cli/commands/tracker-prompts.js +145 -0
- package/dist/cli/commands/tracker.js +277 -0
- package/dist/cli/commands/uninstall.js +520 -169
- package/dist/cli.js +2 -0
- package/dist/commands/bug-analysis.md +58 -14
- package/dist/commands/code-review.md +110 -32
- package/dist/commands/debug.md +55 -11
- package/dist/commands/dynamic-build.md +344 -73
- package/dist/commands/dynamic-plan.md +77 -27
- package/dist/commands/dynamic-profile.md +25 -11
- package/dist/commands/dynamic-tickets.md +76 -15
- package/dist/commands/explore.md +37 -7
- package/dist/commands/implement.md +314 -62
- package/dist/commands/plan.md +146 -32
- package/dist/commands/release.md +64 -17
- package/dist/commands/research.md +34 -8
- package/dist/commands/resolve.md +196 -68
- package/dist/commands/self-review.md +45 -9
- package/dist/core/agent-models.js +55 -12
- package/dist/core/assets.js +58 -2
- package/dist/core/compliance-compose.js +27 -27
- package/dist/core/evidence-policy.js +363 -0
- package/dist/core/feature-config.js +200 -65
- package/dist/core/feature-switch.js +112 -0
- package/dist/core/flags.js +34 -6
- 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 +38 -10
- package/dist/core/mds-variants.js +798 -0
- package/dist/core/migrations.js +49 -23
- package/dist/core/model-discovery.js +12 -1
- package/dist/core/plugins.js +361 -12
- package/dist/core/project-paths.js +1 -18
- package/dist/core/proxy-log.js +8 -6
- package/dist/core/proxy-state.js +11 -8
- package/dist/core/reference-sweep.js +136 -0
- package/dist/core/same-location.js +25 -0
- package/dist/core/tracker.js +494 -0
- 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/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/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 +1187 -32
- package/dist/targets/claude-code/legacy.js +5 -0
- package/dist/targets/claude-code/post-install.js +366 -151
- package/dist/targets/claude-code/tracker-install.js +134 -0
- package/package.json +8 -6
- package/src/assets/agents/code.md +45 -6
- package/src/assets/agents/design.md +2 -1
- package/src/assets/agents/git.mds +825 -0
- package/src/assets/agents/knowledge.md +3 -3
- package/src/assets/agents/learning.md +11 -0
- package/src/assets/agents/review.md +3 -1
- package/src/assets/agents/synthesize.md +1 -1
- package/src/assets/agents/test.md +16 -5
- package/src/assets/agents/tracker.md +474 -0
- package/src/assets/agents/validate.md +7 -5
- 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 +13 -11
- package/src/assets/commands/_partials/_evidence_policy.mds +30 -0
- 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 +22 -7
- package/src/assets/commands/_partials/_preamble.mds +2 -2
- package/src/assets/commands/_partials/_publication.mds +8 -2
- package/src/assets/commands/_partials/_settings.mds +28 -0
- 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 +31 -19
- package/src/assets/commands/code-review.mds +67 -41
- package/src/assets/commands/debug.mds +13 -7
- package/src/assets/commands/dynamic-build.mds +274 -66
- package/src/assets/commands/dynamic-plan.mds +50 -23
- package/src/assets/commands/dynamic-profile.mds +24 -11
- package/src/assets/commands/dynamic-tickets.mds +63 -16
- package/src/assets/commands/explore.mds +4 -5
- package/src/assets/commands/implement.mds +234 -67
- package/src/assets/commands/plan.mds +91 -33
- package/src/assets/commands/release.md +64 -17
- package/src/assets/commands/research.mds +11 -9
- package/src/assets/commands/resolve.mds +150 -78
- package/src/assets/commands/self-review.mds +24 -25
- 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 +305 -0
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +5 -8
- package/src/assets/scripts/hooks/background-memory-update +40 -19
- package/src/assets/scripts/hooks/capture-prompt +18 -8
- package/src/assets/scripts/hooks/capture-question +18 -8
- package/src/assets/scripts/hooks/capture-turn +27 -13
- 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 +236 -60
- package/src/assets/scripts/hooks/git-marker +48 -0
- package/src/assets/scripts/hooks/hook-log-init +3 -1
- package/src/assets/scripts/hooks/json-helper.cjs +228 -5
- package/src/assets/scripts/hooks/lib/project-paths.cjs +1 -20
- package/src/assets/scripts/hooks/log-paths +80 -0
- package/src/assets/scripts/hooks/memory-worker +22 -13
- package/src/assets/scripts/hooks/pre-compact-memory +44 -15
- package/src/assets/scripts/hooks/preamble +1 -4
- package/src/assets/scripts/hooks/queue-append +146 -28
- package/src/assets/scripts/hooks/resolve-project-root +101 -7
- package/src/assets/scripts/hooks/session-start-context +534 -20
- package/src/assets/scripts/hooks/session-start-memory +38 -15
- package/src/assets/scripts/lib/project-config.cjs +633 -0
- 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 +1145 -0
- package/src/assets/scripts/resolve-settings.cjs +1054 -0
- package/src/assets/scripts/verify-evidence.cjs +1822 -0
- package/src/assets/skills/compliance/SKILL.md +4 -2
- package/src/assets/skills/docs-framework/SKILL.md +11 -10
- 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/SKILL.md +8 -78
- package/src/assets/skills/git/references/github-api.md +179 -141
- package/src/assets/skills/git/references/patterns.md +11 -6
- package/src/assets/skills/review-methodology/SKILL.md +1 -1
- package/src/assets/skills/review-methodology/references/patterns.md +6 -61
- package/src/assets/skills/review-methodology/references/violations.md +14 -22
- package/src/assets/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
- package/src/assets/agents/git.md +0 -938
|
@@ -1,11 +1,9 @@
|
|
|
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
|
-
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,48 @@ 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
23
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
24
|
+
* Sentinel line whose presence means the current (v3-and-later) carve-out block is
|
|
25
|
+
* installed. Devflow-unique: no user writes `!.devflow/conventions.md` by hand.
|
|
23
26
|
*/
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
const DEVFLOW_GITIGNORE_SENTINEL_V3 = '!.devflow/conventions.md';
|
|
28
|
+
/** Sentinel line whose presence means the v2 carve-out block is installed (no conventions.md line). */
|
|
29
|
+
const DEVFLOW_GITIGNORE_SENTINEL_V2 = '!.devflow/features/*/KNOWLEDGE.md';
|
|
30
|
+
/**
|
|
31
|
+
* The block's final line: ignore the devflow-managed `.claudeignore` file.
|
|
32
|
+
* NOT a sentinel — users legitimately author this line themselves.
|
|
33
|
+
*/
|
|
34
|
+
const CLAUDEIGNORE_LINE = '.claudeignore';
|
|
35
|
+
/** A user's explicit un-ignore of `.claudeignore`; never overridden. */
|
|
36
|
+
const CLAUDEIGNORE_NEGATION = '!.claudeignore';
|
|
37
|
+
/**
|
|
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
|
|
40
|
+
* proves nothing about the devflow block (avoids PF-059). It sits after `.devflow/*`
|
|
41
|
+
* (which it overrides under last-match-wins) and before `.claudeignore`, so the
|
|
42
|
+
* block's final line stays `.claudeignore`.
|
|
43
|
+
*/
|
|
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';
|
|
28
56
|
/**
|
|
29
57
|
* The shared .devflow/ gitignore block. Everything under .devflow/ is local
|
|
30
58
|
* (memory, learning, docs, locks) EXCEPT:
|
|
@@ -32,6 +60,14 @@ export function computeGitignoreAppend(existingContent, entries) {
|
|
|
32
60
|
* + committed (the Knowledge agent commits them at workflow end).
|
|
33
61
|
* - conventions.md: naming-convention authority written by the Git learn-conventions
|
|
34
62
|
* operation; GIT-TRACKED so the team shares a single naming source.
|
|
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.
|
|
35
71
|
*
|
|
36
72
|
* Re-including files under an ignored tree needs a `dir/*` + `!dir/keep` pair at
|
|
37
73
|
* each level — a bare `.devflow/` excludes the directory so git never descends and
|
|
@@ -39,70 +75,192 @@ export function computeGitignoreAppend(existingContent, entries) {
|
|
|
39
75
|
*
|
|
40
76
|
* Kept BYTE-IDENTICAL to the block emitted by src/assets/scripts/hooks/ensure-root-gitignore
|
|
41
77
|
* 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
78
|
*/
|
|
45
|
-
|
|
79
|
+
const DEVFLOW_GITIGNORE_BLOCK_LINES = [
|
|
46
80
|
'# Devflow runtime data — local by default (memory, learning, docs, locks).',
|
|
47
|
-
'#
|
|
48
|
-
'#
|
|
49
|
-
'#
|
|
50
|
-
'#
|
|
81
|
+
'# Shared via git: feature knowledge bases under .devflow/features/ (index.md and',
|
|
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.',
|
|
51
86
|
'.devflow/*',
|
|
52
87
|
'!.devflow/features/',
|
|
53
88
|
'.devflow/features/*',
|
|
54
89
|
'!.devflow/features/index.md',
|
|
55
90
|
'!.devflow/features/*/',
|
|
56
91
|
'.devflow/features/*/*',
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
92
|
+
DEVFLOW_GITIGNORE_SENTINEL_V2,
|
|
93
|
+
DEVFLOW_GITIGNORE_SENTINEL_V3,
|
|
94
|
+
DEVFLOW_POLICY_LINE,
|
|
95
|
+
DEVFLOW_PROJECT_LINE,
|
|
96
|
+
CLAUDEIGNORE_LINE,
|
|
97
|
+
];
|
|
98
|
+
/** The full carve-out block, `.claudeignore` line included. */
|
|
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
|
+
]);
|
|
118
|
+
/**
|
|
119
|
+
* The carve-out block without its final `.claudeignore` line — emitted instead of
|
|
120
|
+
* the full block when the target .gitignore already carries a `.claudeignore` or
|
|
121
|
+
* `!.claudeignore` entry of the user's own.
|
|
122
|
+
*/
|
|
123
|
+
export const DEVFLOW_GITIGNORE_BLOCK_WITHOUT_CLAUDEIGNORE = DEVFLOW_GITIGNORE_BLOCK_LINES.slice(0, -1).join('\n');
|
|
64
124
|
/** The legacy wholesale comment our pre-carve-out writers emitted. */
|
|
65
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;
|
|
66
132
|
/**
|
|
67
133
|
* PURE: given existing .gitignore content, return the content that ignores
|
|
68
|
-
* `.devflow/` with the feature-knowledge + conventions.md
|
|
69
|
-
* 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`.
|
|
137
|
+
*
|
|
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
|
|
143
|
+
* user-authored line inverts both halves of the contract: projects that already carry
|
|
144
|
+
* that line are told the block is installed when it is not, and a user's
|
|
145
|
+
* `!.claudeignore` un-ignore is silently reversed by re-appending `.claudeignore`
|
|
146
|
+
* under last-match-wins (avoids PF-059).
|
|
147
|
+
*
|
|
148
|
+
* `hasClaudeignoreEntry` is true when some whole line, trimmed, is exactly
|
|
149
|
+
* `.claudeignore` OR `!.claudeignore`. Treating both forms as "present" both honours
|
|
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
|
|
154
|
+
* `!hasClaudeignoreEntry`)].
|
|
70
155
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
156
|
+
* 1. A `/.devflow/` line present → `null` (user opt-out; respect manual config).
|
|
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.
|
|
163
|
+
* 4. Legacy bare `.devflow/` present → strip it (+ our old comment), then append the
|
|
164
|
+
* block; no block at all → append the block. The block is emitted MINUS its final
|
|
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.
|
|
177
|
+
*
|
|
178
|
+
* Line matching is whole-line, whitespace-tolerant, exact text — never substring.
|
|
179
|
+
* The insert and the append form are mirrored byte-for-byte in the shell twin
|
|
180
|
+
* (src/assets/scripts/hooks/ensure-root-gitignore), which is what the cross-implementation
|
|
181
|
+
* parity table in tests/shell-hooks.test.ts pins.
|
|
76
182
|
*/
|
|
77
183
|
export function computeDevflowGitignore(existingContent) {
|
|
78
184
|
const lines = existingContent.split('\n');
|
|
79
185
|
const trimmed = lines.map(l => l.trim());
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
186
|
+
const hasClaudeignoreEntry = trimmed.some(l => l === CLAUDEIGNORE_LINE || l === CLAUDEIGNORE_NEGATION);
|
|
187
|
+
const hasPolicyLine = trimmed.includes(DEVFLOW_POLICY_LINE);
|
|
188
|
+
const hasProjectLine = trimmed.includes(DEVFLOW_PROJECT_LINE);
|
|
189
|
+
/** The block-completing lines this file lacks, in block order. */
|
|
190
|
+
const missingCompletionLines = [
|
|
191
|
+
...(hasPolicyLine ? [] : [DEVFLOW_POLICY_LINE]),
|
|
192
|
+
...(hasProjectLine ? [] : [DEVFLOW_PROJECT_LINE]),
|
|
193
|
+
...(hasClaudeignoreEntry ? [] : [CLAUDEIGNORE_LINE]),
|
|
194
|
+
];
|
|
195
|
+
/**
|
|
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).
|
|
211
|
+
*/
|
|
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
|
+
};
|
|
221
|
+
/**
|
|
222
|
+
* Start a new block after unrelated content: one blank separator line. Existing
|
|
223
|
+
* trailing newlines are preserved verbatim (no trimEnd, no blank-line dedupe) so
|
|
224
|
+
* the shell twin's `tail -c 1` guard produces the identical bytes.
|
|
225
|
+
*/
|
|
226
|
+
const appendBlock = (body, block) => body.length === 0
|
|
227
|
+
? `${block}\n`
|
|
228
|
+
: `${body}${body.endsWith('\n') ? '' : '\n'}\n${block}\n`;
|
|
229
|
+
// 1. User opt-out wins over every sentinel.
|
|
230
|
+
if (trimmed.includes('/.devflow/'))
|
|
83
231
|
return null;
|
|
84
|
-
//
|
|
85
|
-
|
|
232
|
+
// 2. Block installed (v3 and later) — top up only the completion lines it lacks.
|
|
233
|
+
if (trimmed.includes(DEVFLOW_GITIGNORE_SENTINEL_V3)) {
|
|
234
|
+
return missingCompletionLines.length === 0
|
|
235
|
+
? null
|
|
236
|
+
: insertInBlock(DEVFLOW_GITIGNORE_SENTINEL_V3, blockRunBefore, missingCompletionLines);
|
|
237
|
+
}
|
|
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.
|
|
86
240
|
if (trimmed.includes(DEVFLOW_GITIGNORE_SENTINEL_V2)) {
|
|
87
|
-
|
|
88
|
-
return `${existingContent}${sep}${DEVFLOW_GITIGNORE_SENTINEL_V3}\n`;
|
|
241
|
+
return insertInBlock(DEVFLOW_GITIGNORE_SENTINEL_V2, [], [DEVFLOW_GITIGNORE_SENTINEL_V3, ...missingCompletionLines]);
|
|
89
242
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
243
|
+
// 4. No devflow block — install one, respecting any .claudeignore entry of the user's own.
|
|
244
|
+
const block = hasClaudeignoreEntry
|
|
245
|
+
? DEVFLOW_GITIGNORE_BLOCK_WITHOUT_CLAUDEIGNORE
|
|
246
|
+
: DEVFLOW_GITIGNORE_BLOCK;
|
|
247
|
+
if (trimmed.includes('.devflow/')) {
|
|
94
248
|
// Upgrade our legacy wholesale entry: drop the bare line + old comment, append block.
|
|
95
249
|
const kept = lines
|
|
96
250
|
.filter(l => l.trim() !== '.devflow/' && l.trim() !== LEGACY_DEVFLOW_COMMENT)
|
|
97
251
|
.join('\n');
|
|
98
|
-
return
|
|
252
|
+
return appendBlock(kept, block);
|
|
99
253
|
}
|
|
100
|
-
return
|
|
254
|
+
return appendBlock(existingContent, block);
|
|
101
255
|
}
|
|
102
256
|
/**
|
|
103
257
|
* Merge Devflow deny entries into an existing settings JSON object.
|
|
104
|
-
* Preserves existing entries (including allow and sibling keys)
|
|
105
|
-
* 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.
|
|
106
264
|
*
|
|
107
265
|
* PURE + idempotent: calling with the same inputs always yields byte-equal output.
|
|
108
266
|
* Non-array `deny` (e.g. a string, null) is treated as empty — neither throws nor spreads chars.
|
|
@@ -110,22 +268,33 @@ export function computeDevflowGitignore(existingContent) {
|
|
|
110
268
|
* @throws {SyntaxError} on malformed JSON — callers must pre-validate (e.g. via detectDenyState)
|
|
111
269
|
* or wrap in try/catch.
|
|
112
270
|
*/
|
|
113
|
-
export function mergeDenyList(existingJson, newDenyEntries) {
|
|
271
|
+
export function mergeDenyList(existingJson, newDenyEntries, retired = new Set()) {
|
|
114
272
|
const existing = JSON.parse(existingJson);
|
|
115
273
|
const rawDeny = existing.permissions?.deny;
|
|
116
274
|
const currentDeny = Array.isArray(rawDeny) ? rawDeny : [];
|
|
117
|
-
const
|
|
275
|
+
const kept = currentDeny.filter(e => !retired.has(e));
|
|
276
|
+
const merged = [...new Set([...kept, ...newDenyEntries])];
|
|
118
277
|
existing.permissions = { ...(existing.permissions ?? {}), deny: merged };
|
|
119
278
|
return JSON.stringify(existing, null, 2) + '\n';
|
|
120
279
|
}
|
|
121
280
|
/**
|
|
122
281
|
* Historical superset of every deny entry Devflow has ever shipped.
|
|
123
|
-
* Append every future entry here; never remove entries
|
|
124
|
-
*
|
|
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.
|
|
125
285
|
*
|
|
126
286
|
* Load-time assertion below verifies this is a superset of the current template.
|
|
127
287
|
*/
|
|
128
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).
|
|
129
298
|
export const DEVFLOW_HISTORICAL_DENY = Object.freeze(new Set([
|
|
130
299
|
// v1 batch — 154 entries shipped in src/targets/claude-code/templates/managed-settings.json
|
|
131
300
|
'Bash(rm -rf /*)',
|
|
@@ -282,7 +451,54 @@ export const DEVFLOW_HISTORICAL_DENY = Object.freeze(new Set([
|
|
|
282
451
|
'Read(/etc/shadow)',
|
|
283
452
|
'Read(/etc/sudoers)',
|
|
284
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*)',
|
|
285
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
|
+
}
|
|
286
502
|
/**
|
|
287
503
|
* Assert that DEVFLOW_HISTORICAL_DENY is a superset of the provided template entries.
|
|
288
504
|
* Throws if any template entry is missing from the historical set.
|
|
@@ -458,8 +674,8 @@ export function resolveSecurityAction(flag, manifestMode, detected, isTTY) {
|
|
|
458
674
|
}
|
|
459
675
|
/**
|
|
460
676
|
* Load the deny entry array from the managed-settings.json template.
|
|
461
|
-
* Canonical single-source helper used by installManagedSettings,
|
|
462
|
-
*
|
|
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.
|
|
463
679
|
*
|
|
464
680
|
* Defensive read: treats file as `Record<string, unknown>`, guards with Array.isArray,
|
|
465
681
|
* coerces each element to string. Returns [] on any read or parse failure (never throws).
|
|
@@ -503,7 +719,7 @@ export async function installManagedSettings(rootDir, verbose) {
|
|
|
503
719
|
let content;
|
|
504
720
|
try {
|
|
505
721
|
const existing = await fs.readFile(managedPath, 'utf-8');
|
|
506
|
-
content = mergeDenyList(existing, newDenyEntries);
|
|
722
|
+
content = mergeDenyList(existing, newDenyEntries, retiredDenyEntries(newDenyEntries));
|
|
507
723
|
}
|
|
508
724
|
catch {
|
|
509
725
|
// File doesn't exist — use template as-is
|
|
@@ -531,11 +747,14 @@ export async function installManagedSettings(rootDir, verbose) {
|
|
|
531
747
|
return false;
|
|
532
748
|
}
|
|
533
749
|
try {
|
|
534
|
-
|
|
535
|
-
//
|
|
750
|
+
execFileSync('sudo', ['mkdir', '-p', managedDir], { stdio: 'inherit' });
|
|
751
|
+
// Stage the JSON in a file and copy it with sudo. Every sudo call takes an
|
|
752
|
+
// argv (execFileSync, no shell), so no path is ever re-parsed by a shell — a
|
|
753
|
+
// package root under a home directory holding a quote cannot break or extend
|
|
754
|
+
// the root command.
|
|
536
755
|
const tmpFile = path.join(rootDir, '.managed-settings-tmp.json');
|
|
537
756
|
await fs.writeFile(tmpFile, content, 'utf-8');
|
|
538
|
-
|
|
757
|
+
execFileSync('sudo', ['cp', tmpFile, managedPath], { stdio: 'inherit' });
|
|
539
758
|
await fs.rm(tmpFile, { force: true });
|
|
540
759
|
if (verbose) {
|
|
541
760
|
p.log.success(`Managed settings written to ${managedPath} (via sudo)`);
|
|
@@ -557,14 +776,26 @@ export async function installManagedSettings(rootDir, verbose) {
|
|
|
557
776
|
* 1. Try direct write/delete
|
|
558
777
|
* 2. If EACCES and TTY, ask user before sudo
|
|
559
778
|
* 3. Non-TTY: return false (caller logs preservation message)
|
|
779
|
+
*
|
|
780
|
+
* `managedPathOverride` exists so a test can point the removal at a temp file;
|
|
781
|
+
* production callers omit it and get the platform's system path. It is a
|
|
782
|
+
* parameter, deliberately not an environment variable: this function may run
|
|
783
|
+
* `sudo rm` / `sudo cp` on the path, and an env-selectable target would let
|
|
784
|
+
* whoever controls the environment aim a root write the user consented to for
|
|
785
|
+
* "managed settings" at any file.
|
|
560
786
|
*/
|
|
561
|
-
export async function removeManagedSettings(rootDir, verbose) {
|
|
787
|
+
export async function removeManagedSettings(rootDir, verbose, managedPathOverride) {
|
|
562
788
|
let managedPath;
|
|
563
|
-
|
|
564
|
-
managedPath =
|
|
789
|
+
if (managedPathOverride !== undefined) {
|
|
790
|
+
managedPath = managedPathOverride;
|
|
565
791
|
}
|
|
566
|
-
|
|
567
|
-
|
|
792
|
+
else {
|
|
793
|
+
try {
|
|
794
|
+
managedPath = getManagedSettingsPath();
|
|
795
|
+
}
|
|
796
|
+
catch {
|
|
797
|
+
return false;
|
|
798
|
+
}
|
|
568
799
|
}
|
|
569
800
|
let existingContent;
|
|
570
801
|
try {
|
|
@@ -573,15 +804,11 @@ export async function removeManagedSettings(rootDir, verbose) {
|
|
|
573
804
|
catch {
|
|
574
805
|
return false; // File doesn't exist
|
|
575
806
|
}
|
|
576
|
-
//
|
|
577
|
-
|
|
578
|
-
if (devflowDenyEntries.length === 0) {
|
|
579
|
-
return false;
|
|
580
|
-
}
|
|
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).
|
|
581
809
|
const existing = JSON.parse(existingContent);
|
|
582
810
|
const currentDeny = existing.permissions?.deny ?? [];
|
|
583
|
-
const
|
|
584
|
-
const remaining = currentDeny.filter(entry => !devflowSet.has(entry));
|
|
811
|
+
const remaining = currentDeny.filter(entry => !DEVFLOW_HISTORICAL_DENY.has(entry));
|
|
585
812
|
// Determine the target action: delete file entirely or write updated content
|
|
586
813
|
let shouldDelete = false;
|
|
587
814
|
let updatedContent = null;
|
|
@@ -639,12 +866,12 @@ export async function removeManagedSettings(rootDir, verbose) {
|
|
|
639
866
|
}
|
|
640
867
|
try {
|
|
641
868
|
if (shouldDelete) {
|
|
642
|
-
|
|
869
|
+
execFileSync('sudo', ['rm', managedPath], { stdio: 'inherit' });
|
|
643
870
|
}
|
|
644
871
|
else {
|
|
645
872
|
const tmpFile = path.join(rootDir, '.managed-settings-tmp.json');
|
|
646
873
|
await fs.writeFile(tmpFile, updatedContent, 'utf-8');
|
|
647
|
-
|
|
874
|
+
execFileSync('sudo', ['cp', tmpFile, managedPath], { stdio: 'inherit' });
|
|
648
875
|
await fs.rm(tmpFile, { force: true });
|
|
649
876
|
}
|
|
650
877
|
if (verbose) {
|
|
@@ -675,8 +902,8 @@ export async function applyUserSecurityDenyList(settingsPath, currentTemplateDen
|
|
|
675
902
|
catch {
|
|
676
903
|
existing = '{}';
|
|
677
904
|
}
|
|
678
|
-
const merged = mergeDenyList(existing, currentTemplateDeny);
|
|
679
|
-
await
|
|
905
|
+
const merged = mergeDenyList(existing, currentTemplateDeny, retiredDenyEntries(currentTemplateDeny));
|
|
906
|
+
await writeSettingsFileAtomic(settingsPath, merged);
|
|
680
907
|
return merged;
|
|
681
908
|
}
|
|
682
909
|
/**
|
|
@@ -684,7 +911,7 @@ export async function applyUserSecurityDenyList(settingsPath, currentTemplateDen
|
|
|
684
911
|
* Colocated with applyUserSecurityDenyList — the remove-side counterpart.
|
|
685
912
|
*
|
|
686
913
|
* Sequence: read → stripUserDenyList → guard (stripped !== existing) →
|
|
687
|
-
*
|
|
914
|
+
* writeSettingsFileAtomic → return { removed }.
|
|
688
915
|
* Atomic write (temp+rename) upholds the never-truncate-on-crash invariant.
|
|
689
916
|
* ENOENT is swallowed (file absent = nothing to strip). Other errors propagate.
|
|
690
917
|
*
|
|
@@ -709,7 +936,7 @@ export async function stripUserSecurityDenyList(settingsPath) {
|
|
|
709
936
|
if (stripped === existing) {
|
|
710
937
|
return null;
|
|
711
938
|
}
|
|
712
|
-
await
|
|
939
|
+
await writeSettingsFileAtomic(settingsPath, stripped);
|
|
713
940
|
return { removed };
|
|
714
941
|
}
|
|
715
942
|
/** True for a non-null, non-array object literal. */
|
|
@@ -820,7 +1047,7 @@ export async function installSettings(claudeDir, rootDir, devflowDir, verbose) {
|
|
|
820
1047
|
settingsExists = false;
|
|
821
1048
|
}
|
|
822
1049
|
if (!settingsExists) {
|
|
823
|
-
await
|
|
1050
|
+
await writeSettingsFileAtomic(settingsPath, settingsContent);
|
|
824
1051
|
if (verbose) {
|
|
825
1052
|
p.log.success('Settings configured');
|
|
826
1053
|
}
|
|
@@ -847,7 +1074,7 @@ export async function installSettings(claudeDir, rootDir, devflowDir, verbose) {
|
|
|
847
1074
|
// Already fully configured — nothing to do
|
|
848
1075
|
return;
|
|
849
1076
|
}
|
|
850
|
-
await
|
|
1077
|
+
await writeSettingsFileAtomic(settingsPath, JSON.stringify(existingParsed, null, 2) + '\n');
|
|
851
1078
|
if (verbose) {
|
|
852
1079
|
p.log.success('Settings updated with Devflow hooks and HUD');
|
|
853
1080
|
}
|
|
@@ -885,11 +1112,12 @@ export async function installClaudeignore(gitRoot, rootDir, verbose) {
|
|
|
885
1112
|
}
|
|
886
1113
|
/**
|
|
887
1114
|
* Discover git repository roots from Claude's project history.
|
|
888
|
-
* Parses
|
|
889
|
-
* @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.
|
|
890
1118
|
*/
|
|
891
|
-
export async function discoverProjectGitRoots(
|
|
892
|
-
const historyPath = path.join(
|
|
1119
|
+
export async function discoverProjectGitRoots(claudeDir) {
|
|
1120
|
+
const historyPath = path.join(claudeDir, 'history.jsonl');
|
|
893
1121
|
let content;
|
|
894
1122
|
try {
|
|
895
1123
|
content = await fs.readFile(historyPath, 'utf-8');
|
|
@@ -921,73 +1149,79 @@ export async function discoverProjectGitRoots(homeDir) {
|
|
|
921
1149
|
return gitRoots.sort();
|
|
922
1150
|
}
|
|
923
1151
|
/**
|
|
924
|
-
*
|
|
1152
|
+
* Current carve-out marker version. Bump when the block format changes — together
|
|
1153
|
+
* with the shell twin's stamp (ensure-root-gitignore) and the ensure-devflow-init
|
|
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`.
|
|
925
1161
|
*/
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1162
|
+
const GITIGNORE_MARKER_V6 = '.root-gitignore-configured-v6';
|
|
1163
|
+
/**
|
|
1164
|
+
* Earlier markers, the unversioned (v1) one included — every one is removed
|
|
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.
|
|
1167
|
+
*/
|
|
1168
|
+
const LEGACY_GITIGNORE_MARKERS = [
|
|
1169
|
+
'.root-gitignore-configured-v5',
|
|
1170
|
+
'.root-gitignore-configured-v4',
|
|
1171
|
+
'.root-gitignore-configured-v3',
|
|
1172
|
+
'.root-gitignore-configured-v2',
|
|
1173
|
+
'.root-gitignore-configured',
|
|
1174
|
+
];
|
|
1175
|
+
/** Remove every legacy marker; an absent one is a no-op. Call only once v6 is stamped. */
|
|
1176
|
+
async function removeLegacyGitignoreMarkers(devflowDir) {
|
|
1177
|
+
for (const legacy of LEGACY_GITIGNORE_MARKERS) {
|
|
931
1178
|
try {
|
|
932
|
-
|
|
933
|
-
}
|
|
934
|
-
catch { /* doesn't exist */ }
|
|
935
|
-
const linesToAdd = computeGitignoreAppend(gitignoreContent, entriesToAdd);
|
|
936
|
-
if (linesToAdd.length > 0) {
|
|
937
|
-
const newContent = gitignoreContent
|
|
938
|
-
? `${gitignoreContent.trimEnd()}\n\n# Devflow local installation\n${linesToAdd.join('\n')}\n`
|
|
939
|
-
: `# Devflow local installation\n${linesToAdd.join('\n')}\n`;
|
|
940
|
-
await fs.writeFile(gitignorePath, newContent, 'utf-8');
|
|
941
|
-
if (verbose) {
|
|
942
|
-
p.log.success('.gitignore updated');
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
catch (error) {
|
|
947
|
-
if (verbose) {
|
|
948
|
-
p.log.warn(`Could not update .gitignore: ${error instanceof Error ? error.message : error}`);
|
|
1179
|
+
await fs.rm(path.join(devflowDir, legacy), { force: true });
|
|
949
1180
|
}
|
|
1181
|
+
catch { /* ok if absent */ }
|
|
950
1182
|
}
|
|
951
1183
|
}
|
|
952
|
-
/** Current carve-out marker version. Bump when the block format changes. */
|
|
953
|
-
const GITIGNORE_MARKER_V3 = '.root-gitignore-configured-v3';
|
|
954
|
-
/** Previous marker — removed when upgrading to v3. */
|
|
955
|
-
const GITIGNORE_MARKER_V2 = '.root-gitignore-configured-v2';
|
|
956
1184
|
/**
|
|
957
1185
|
* Deterministically ensure the project root .gitignore applies the `.devflow/`
|
|
958
|
-
* carve-out (local by default
|
|
1186
|
+
* carve-out (local by default; feature knowledge, conventions.md, the evidence
|
|
1187
|
+
* policy and the project settings shared via git).
|
|
959
1188
|
*
|
|
960
|
-
* Manages ONLY `.devflow/` — never `.claude/` — because
|
|
961
|
-
*
|
|
962
|
-
* src/assets/scripts/hooks/ensure-root-gitignore shell helper; both
|
|
963
|
-
*
|
|
964
|
-
*
|
|
965
|
-
*
|
|
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
|
|
1191
|
+
* src/assets/scripts/hooks/ensure-root-gitignore shell helper; both resolve the same
|
|
1192
|
+
* shape for a given .gitignore — DEVFLOW_GITIGNORE_BLOCK, or
|
|
1193
|
+
* DEVFLOW_GITIGNORE_BLOCK_WITHOUT_CLAUDEIGNORE when the project owns that entry — and
|
|
1194
|
+
* emit identical bytes, so the two paths are byte-compatible and mutually idempotent.
|
|
1195
|
+
* Called unconditionally (independent of every feature toggle) whenever a git
|
|
1196
|
+
* root is known.
|
|
966
1197
|
*
|
|
967
|
-
* Uses a versioned marker file (`.devflow/.root-gitignore-configured-
|
|
968
|
-
* detection — the same pattern as the shell twin.
|
|
969
|
-
*
|
|
1198
|
+
* Uses a versioned project-local marker file (`.devflow/.root-gitignore-configured-v6`)
|
|
1199
|
+
* for fast-path detection — the same pattern as the shell twin. The marker is a claim,
|
|
1200
|
+
* not proof, so even a marked install re-reads .gitignore and re-runs
|
|
1201
|
+
* computeDevflowGitignore; bumping the version forces a re-run once per install, which
|
|
1202
|
+
* is how a v5-marked project gains the project line and is re-stamped v6.
|
|
970
1203
|
*
|
|
971
|
-
* Idempotent:
|
|
972
|
-
*
|
|
1204
|
+
* Idempotent: computeDevflowGitignore returns null for a converged file, so a
|
|
1205
|
+
* marked install performs one read and no write. Errors are swallowed
|
|
1206
|
+
* (verbose-logged) — a gitignore write must never abort init.
|
|
973
1207
|
*/
|
|
974
1208
|
export async function ensureDevflowGitignore(gitRoot, verbose) {
|
|
975
1209
|
try {
|
|
976
1210
|
const devflowDir = path.join(gitRoot, '.devflow');
|
|
977
|
-
const
|
|
1211
|
+
const markerV6 = path.join(devflowDir, GITIGNORE_MARKER_V6);
|
|
978
1212
|
const gitignorePath = path.join(gitRoot, '.gitignore');
|
|
979
|
-
// Fast-path with verification:
|
|
1213
|
+
// Fast-path with verification: v6 marker normally means the block is installed,
|
|
980
1214
|
// but the marker is a claim, not proof — a merge-conflict resolution may have
|
|
981
1215
|
// dropped the block. Even when the marker exists, read .gitignore (one cheap
|
|
982
1216
|
// read) and run computeDevflowGitignore; write only when it returns non-null.
|
|
983
|
-
// Idempotent:
|
|
984
|
-
let
|
|
1217
|
+
// Idempotent: converged file → computeDevflowGitignore returns null → no write.
|
|
1218
|
+
let v6Marked = false;
|
|
985
1219
|
try {
|
|
986
|
-
await fs.access(
|
|
987
|
-
|
|
1220
|
+
await fs.access(markerV6);
|
|
1221
|
+
v6Marked = true;
|
|
988
1222
|
}
|
|
989
1223
|
catch { /* absent */ }
|
|
990
|
-
if (
|
|
1224
|
+
if (v6Marked) {
|
|
991
1225
|
let existingContent = '';
|
|
992
1226
|
try {
|
|
993
1227
|
existingContent = await fs.readFile(gitignorePath, 'utf-8');
|
|
@@ -997,9 +1231,10 @@ export async function ensureDevflowGitignore(gitRoot, verbose) {
|
|
|
997
1231
|
if (healContent !== null) {
|
|
998
1232
|
await fs.writeFile(gitignorePath, healContent, 'utf-8');
|
|
999
1233
|
if (verbose) {
|
|
1000
|
-
p.log.success('.gitignore configured (.devflow/ local; feature knowledge + conventions shared)');
|
|
1234
|
+
p.log.success('.gitignore configured (.devflow/ local; feature knowledge + conventions + retired policy.json + project settings shared)');
|
|
1001
1235
|
}
|
|
1002
1236
|
}
|
|
1237
|
+
await removeLegacyGitignoreMarkers(devflowDir);
|
|
1003
1238
|
return;
|
|
1004
1239
|
}
|
|
1005
1240
|
let gitignoreContent = '';
|
|
@@ -1011,16 +1246,13 @@ export async function ensureDevflowGitignore(gitRoot, verbose) {
|
|
|
1011
1246
|
if (newContent !== null) {
|
|
1012
1247
|
await fs.writeFile(gitignorePath, newContent, 'utf-8');
|
|
1013
1248
|
if (verbose) {
|
|
1014
|
-
p.log.success('.gitignore configured (.devflow/ local; feature knowledge + conventions shared)');
|
|
1249
|
+
p.log.success('.gitignore configured (.devflow/ local; feature knowledge + conventions + retired policy.json + project settings shared)');
|
|
1015
1250
|
}
|
|
1016
1251
|
}
|
|
1017
|
-
// Stamp
|
|
1252
|
+
// Stamp v6 marker so subsequent runs fast-path; drop every legacy marker.
|
|
1018
1253
|
await fs.mkdir(devflowDir, { recursive: true });
|
|
1019
|
-
await fs.writeFile(
|
|
1020
|
-
|
|
1021
|
-
await fs.rm(path.join(devflowDir, GITIGNORE_MARKER_V2), { force: true });
|
|
1022
|
-
}
|
|
1023
|
-
catch { /* ok if absent */ }
|
|
1254
|
+
await fs.writeFile(markerV6, '', 'utf-8');
|
|
1255
|
+
await removeLegacyGitignoreMarkers(devflowDir);
|
|
1024
1256
|
}
|
|
1025
1257
|
catch (error) {
|
|
1026
1258
|
if (verbose) {
|
|
@@ -1028,21 +1260,4 @@ export async function ensureDevflowGitignore(gitRoot, verbose) {
|
|
|
1028
1260
|
}
|
|
1029
1261
|
}
|
|
1030
1262
|
}
|
|
1031
|
-
/**
|
|
1032
|
-
* Create .devflow/docs/ directory structure for Devflow artifacts.
|
|
1033
|
-
*/
|
|
1034
|
-
export async function createDocsStructure(verbose) {
|
|
1035
|
-
const docsDir = getDocsDir(process.cwd());
|
|
1036
|
-
try {
|
|
1037
|
-
await Promise.all([
|
|
1038
|
-
fs.mkdir(path.join(docsDir, 'status', 'compact'), { recursive: true }),
|
|
1039
|
-
fs.mkdir(path.join(docsDir, 'reviews'), { recursive: true }),
|
|
1040
|
-
fs.mkdir(path.join(docsDir, 'releases'), { recursive: true }),
|
|
1041
|
-
]);
|
|
1042
|
-
if (verbose) {
|
|
1043
|
-
p.log.success('.devflow/docs/ structure ready');
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
catch { /* may already exist */ }
|
|
1047
|
-
}
|
|
1048
1263
|
//# sourceMappingURL=post-install.js.map
|