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
|
@@ -39,16 +39,20 @@ devflow_debug_set_cwd "$CWD"
|
|
|
39
39
|
# Anchor .devflow/ to the project root (prevents a stray nested .devflow/ when this
|
|
40
40
|
# hook runs with a CWD inside .devflow/...). Empty → fall back to CWD.
|
|
41
41
|
source "$SCRIPT_DIR/resolve-project-root" 2>/dev/null || true
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
42
|
+
df_resolve_roots "$CWD" 2>/dev/null || true
|
|
43
|
+
PROJECT_ROOT="${DF_ROOT:-$CWD}"
|
|
44
|
+
# The learning queue is the repository's, not the checkout's: in a linked worktree
|
|
45
|
+
# it is the main worktree's queue (D-LEDGER-MAIN-WORKTREE, resolve-project-root),
|
|
46
|
+
# the one the Learning agent drains into the shared ledger. Memory stays per checkout.
|
|
47
|
+
LEDGER_ROOT="${DF_LEDGER_ROOT:-$PROJECT_ROOT}"
|
|
48
|
+
|
|
49
|
+
# The machine-wide manifest (the memory/learning switches, D-FEATURES-NARROW-ONLY
|
|
50
|
+
# in queue-append) lives at the machine root,
|
|
51
|
+
# $HOME/.devflow (D-ONE-HOME).
|
|
52
|
+
DEVFLOW_MANIFEST="$HOME/.devflow/manifest.json"
|
|
53
|
+
PROJECT_DEVFLOW_DIR="$PROJECT_ROOT/.devflow"
|
|
54
|
+
MEMORY_DIR="$PROJECT_DEVFLOW_DIR/memory"
|
|
55
|
+
LEARNING_DIR="$LEDGER_ROOT/.devflow/learning"
|
|
52
56
|
|
|
53
57
|
if [ -z "$PROMPT" ]; then
|
|
54
58
|
dbg "EXIT: empty PROMPT"
|
|
@@ -57,8 +61,10 @@ fi
|
|
|
57
61
|
|
|
58
62
|
source "$SCRIPT_DIR/queue-append" || { echo "capture-prompt: failed to source queue-append" >&2; exit 1; }
|
|
59
63
|
|
|
60
|
-
# --- AC-P1:
|
|
61
|
-
|
|
64
|
+
# --- AC-P1: at most ONE gate-read fork for memory + learning ---
|
|
65
|
+
# The machine switch, narrowed by this checkout's project.json/config.json
|
|
66
|
+
# `features` (D-FEATURES-NARROW-ONLY, see queue-append).
|
|
67
|
+
queue_read_gates "$DEVFLOW_MANIFEST" "$PROJECT_ROOT"
|
|
62
68
|
MEMORY_ENABLED="$_QG_MEMORY"
|
|
63
69
|
LEARNING_ENABLED="$_QG_LEARNING"
|
|
64
70
|
|
|
@@ -58,16 +58,20 @@ if [ -z "$CWD" ] || [ ! -d "$CWD" ]; then dbg "EXIT: bad CWD"; exit 0; fi
|
|
|
58
58
|
devflow_debug_set_cwd "$CWD"
|
|
59
59
|
|
|
60
60
|
source "$SCRIPT_DIR/resolve-project-root" 2>/dev/null || true
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
61
|
+
df_resolve_roots "$CWD" 2>/dev/null || true
|
|
62
|
+
PROJECT_ROOT="${DF_ROOT:-$CWD}"
|
|
63
|
+
# The learning queue is the repository's, not the checkout's: in a linked worktree
|
|
64
|
+
# it is the main worktree's queue (D-LEDGER-MAIN-WORKTREE, resolve-project-root),
|
|
65
|
+
# the one the Learning agent drains into the shared ledger. Memory stays per checkout.
|
|
66
|
+
LEDGER_ROOT="${DF_LEDGER_ROOT:-$PROJECT_ROOT}"
|
|
67
|
+
|
|
68
|
+
# The machine-wide manifest (the memory/learning switches, D-FEATURES-NARROW-ONLY
|
|
69
|
+
# in queue-append) lives at the machine root,
|
|
70
|
+
# $HOME/.devflow (D-ONE-HOME).
|
|
71
|
+
DEVFLOW_MANIFEST="$HOME/.devflow/manifest.json"
|
|
72
|
+
PROJECT_DEVFLOW_DIR="$PROJECT_ROOT/.devflow"
|
|
73
|
+
MEMORY_DIR="$PROJECT_DEVFLOW_DIR/memory"
|
|
74
|
+
LEARNING_DIR="$LEDGER_ROOT/.devflow/learning"
|
|
71
75
|
|
|
72
76
|
# --- Parse questions + answers into "question<TAB>answer" rows (one subprocess) ---
|
|
73
77
|
# Defensive against: tool_response absent or a plain string (error case), missing
|
|
@@ -119,8 +123,10 @@ fi
|
|
|
119
123
|
|
|
120
124
|
source "$SCRIPT_DIR/queue-append" || { echo "capture-question: failed to source queue-append" >&2; exit 1; }
|
|
121
125
|
|
|
122
|
-
# --- AC-P1:
|
|
123
|
-
|
|
126
|
+
# --- AC-P1: at most ONE gate-read fork for memory + learning ---
|
|
127
|
+
# The machine switch, narrowed by this checkout's project.json/config.json
|
|
128
|
+
# `features` (D-FEATURES-NARROW-ONLY, see queue-append).
|
|
129
|
+
queue_read_gates "$DEVFLOW_MANIFEST" "$PROJECT_ROOT"
|
|
124
130
|
MEMORY_ENABLED="$_QG_MEMORY"
|
|
125
131
|
LEARNING_ENABLED="$_QG_LEARNING"
|
|
126
132
|
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
# learning queue, each independently gated by its own feature flag (AC-F4). Runs
|
|
6
6
|
# the decisions usage scanner (D29 grep-first) regardless of which queue is
|
|
7
7
|
# gated on/off -- memory-disabled projects still run the usage scanner. The
|
|
8
|
-
# 120s-throttle/nohup-spawn block lives in memory-worker,
|
|
9
|
-
#
|
|
10
|
-
#
|
|
8
|
+
# 120s-throttle/nohup-spawn block lives in memory-worker, a separate Stop
|
|
9
|
+
# hook that Claude Code runs in parallel with this one; the worker tolerates a
|
|
10
|
+
# queue this hook has not appended to yet (D-QUEUE-NO-ORPHAN-DELETE).
|
|
11
11
|
# This hook never spawns a process (AC-F5).
|
|
12
12
|
|
|
13
13
|
# Safe no-op fallback: must exist before set -e and before hook-bootstrap is sourced.
|
|
@@ -45,16 +45,20 @@ dbg "ASSISTANT_MSG length=${#ASSISTANT_MSG}"
|
|
|
45
45
|
# Anchor .devflow/ to the project root (prevents a stray nested .devflow/ when this
|
|
46
46
|
# hook runs with a CWD inside .devflow/...). Empty → fall back to CWD.
|
|
47
47
|
source "$SCRIPT_DIR/resolve-project-root" 2>/dev/null || true
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
48
|
+
df_resolve_roots "$CWD" 2>/dev/null || true
|
|
49
|
+
PROJECT_ROOT="${DF_ROOT:-$CWD}"
|
|
50
|
+
# The learning queue is the repository's, not the checkout's: in a linked worktree
|
|
51
|
+
# it is the main worktree's queue (D-LEDGER-MAIN-WORKTREE, resolve-project-root),
|
|
52
|
+
# the one the Learning agent drains into the shared ledger. Memory stays per checkout.
|
|
53
|
+
LEDGER_ROOT="${DF_LEDGER_ROOT:-$PROJECT_ROOT}"
|
|
54
|
+
|
|
55
|
+
# The machine-wide manifest (the memory/learning switches, D-FEATURES-NARROW-ONLY
|
|
56
|
+
# in queue-append) lives at the machine root,
|
|
57
|
+
# $HOME/.devflow (D-ONE-HOME).
|
|
58
|
+
DEVFLOW_MANIFEST="$HOME/.devflow/manifest.json"
|
|
59
|
+
PROJECT_DEVFLOW_DIR="$PROJECT_ROOT/.devflow"
|
|
60
|
+
MEMORY_DIR="$PROJECT_DEVFLOW_DIR/memory"
|
|
61
|
+
LEARNING_DIR="$LEDGER_ROOT/.devflow/learning"
|
|
58
62
|
|
|
59
63
|
# Skip if empty response
|
|
60
64
|
if [ -z "$ASSISTANT_MSG" ]; then
|
|
@@ -64,8 +68,10 @@ fi
|
|
|
64
68
|
|
|
65
69
|
source "$SCRIPT_DIR/queue-append" || { echo "capture-turn: failed to source queue-append" >&2; exit 1; }
|
|
66
70
|
|
|
67
|
-
# --- AC-P1:
|
|
68
|
-
|
|
71
|
+
# --- AC-P1: at most ONE gate-read fork for memory + learning ---
|
|
72
|
+
# The machine switch, narrowed by this checkout's project.json/config.json
|
|
73
|
+
# `features` (D-FEATURES-NARROW-ONLY, see queue-append).
|
|
74
|
+
queue_read_gates "$DEVFLOW_MANIFEST" "$PROJECT_ROOT"
|
|
69
75
|
MEMORY_ENABLED="$_QG_MEMORY"
|
|
70
76
|
LEARNING_ENABLED="$_QG_LEARNING"
|
|
71
77
|
|
|
@@ -73,11 +79,15 @@ dbg "MEMORY_ENABLED=$MEMORY_ENABLED LEARNING_ENABLED=$LEARNING_ENABLED"
|
|
|
73
79
|
|
|
74
80
|
# --- Decisions usage scanner (independent of the memory/learning queue gates below) ---
|
|
75
81
|
# D29: Grep-first reorder -- cheap in-process citation check gates the scanner call.
|
|
82
|
+
# The scanner writes the ledger's usage file, and it runs before ensure-devflow-init,
|
|
83
|
+
# so it takes the D-HOOKS-GIT-ONLY gate itself (df_is_project_root, git-marker via
|
|
84
|
+
# resolve-project-root; zero forks): a `.devflow/` an older devflow left in a
|
|
85
|
+
# non-git directory, or at HOME, is not a project's ledger.
|
|
76
86
|
SCANNER="$SCRIPT_DIR/decisions-usage-scan.cjs"
|
|
77
87
|
if [ -f "$SCANNER" ] && printf '%s' "$ASSISTANT_MSG" | grep -qE 'ADR-[0-9]+|PF-[0-9]+'; then
|
|
78
|
-
if [ "$LEARNING_ENABLED" = "true" ]; then
|
|
88
|
+
if [ "$LEARNING_ENABLED" = "true" ] && df_is_project_root "$PROJECT_ROOT" 2>/dev/null; then
|
|
79
89
|
dbg "Running decisions usage scanner"
|
|
80
|
-
printf '%s' "$ASSISTANT_MSG" | node "$SCANNER" --cwd "$
|
|
90
|
+
printf '%s' "$ASSISTANT_MSG" | node "$SCANNER" --cwd "$LEDGER_ROOT" 2>/dev/null || true
|
|
81
91
|
fi
|
|
82
92
|
fi
|
|
83
93
|
|
|
@@ -61,12 +61,17 @@ devflow_debug_init() {
|
|
|
61
61
|
devflow_debug_set_cwd() {
|
|
62
62
|
local cwd="$1"
|
|
63
63
|
if [ -z "$cwd" ] || [ "${DEVFLOW_HOOK_DEBUG:-}" != "1" ]; then return; fi
|
|
64
|
-
# Phase 2: switch to per-project log (only when debug is active)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
64
|
+
# Phase 2: switch to per-project log (only when debug is active). The folder
|
|
65
|
+
# comes from log-paths' devflow_log_dir, so a folder created here goes through
|
|
66
|
+
# the same D-LOG-DIR-CAP prune as every other hook log folder. log-paths sits
|
|
67
|
+
# beside this file; a hook that already sourced it is not re-sourced (that
|
|
68
|
+
# would reset its cached log dir).
|
|
69
|
+
if ! declare -F devflow_log_dir >/dev/null 2>&1; then
|
|
70
|
+
source "$(dirname "${BASH_SOURCE[0]}")/log-paths" 2>/dev/null || return 0
|
|
71
|
+
fi
|
|
72
|
+
local project_log_dir
|
|
73
|
+
project_log_dir=$(devflow_log_dir "$cwd" 2>/dev/null) || project_log_dir=""
|
|
74
|
+
[ -n "$project_log_dir" ] || return 0
|
|
70
75
|
_DEVFLOW_DBG_LOG="$project_log_dir/.hook-debug.log"
|
|
71
76
|
_devflow_dbg_size_guard "$_DEVFLOW_DBG_LOG"
|
|
72
77
|
dbg() {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# Ensures .devflow/ and all subdirectories exist and the project root .gitignore
|
|
3
3
|
# ignores .devflow/. Called from capture-prompt, capture-turn, capture-question,
|
|
4
4
|
# memory-worker, and pre-compact-memory. Idempotent.
|
|
5
|
+
# Returns non-zero, having written nothing, outside a git project or at HOME.
|
|
5
6
|
# Usage: source ensure-devflow-init "$CWD"
|
|
6
7
|
|
|
7
8
|
[ -z "$1" ] && return 1
|
|
@@ -11,23 +12,49 @@
|
|
|
11
12
|
# by a hook or directly by tests). Empty → fall back to "$1" (old behavior).
|
|
12
13
|
_EDI_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
13
14
|
source "$_EDI_DIR/resolve-project-root" 2>/dev/null || true
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
df_resolve_roots "$1" 2>/dev/null || true
|
|
16
|
+
_EDI_ROOT="${DF_ROOT:-$1}"
|
|
17
|
+
_EDI_LEDGER="${DF_LEDGER_ROOT:-$_EDI_ROOT}"
|
|
18
|
+
|
|
19
|
+
# D-HOOKS-GIT-ONLY (git-marker): scaffold nothing outside a git project, and
|
|
20
|
+
# nothing at a repository rooted at HOME. D-HOOKS-TOPLEVEL-ONLY (git-marker): nor
|
|
21
|
+
# below a checkout's toplevel — when git refuses the cwd (dubious ownership,
|
|
22
|
+
# GIT_CEILING_DIRECTORIES) the root above is that cwd, and a subdirectory holds no
|
|
23
|
+
# `.git` of its own, so it is refused rather than scaffolded. This is the one gate every capture and
|
|
24
|
+
# memory caller (capture-prompt, capture-turn, capture-question, memory-worker,
|
|
25
|
+
# pre-compact-memory) passes through before it writes, and each of them treats a
|
|
26
|
+
# non-zero return as "stop" (`|| exit 0`), so memory and learning stop together
|
|
27
|
+
# in a non-git directory. ensure-root-gitignore itself stays ungated: it is a
|
|
28
|
+
# pure writer its parity suite runs in plain directories (PF-059), and both of
|
|
29
|
+
# its callers now decide before reaching it. A helper that failed to source is a
|
|
30
|
+
# command not found — the refusing branch, so the failure mode writes nothing.
|
|
31
|
+
source "$_EDI_DIR/git-marker" 2>/dev/null || true
|
|
32
|
+
df_is_project_root "$_EDI_ROOT" 2>/dev/null || return 1
|
|
16
33
|
|
|
17
34
|
_DEVFLOW_DIR="$_EDI_ROOT/.devflow"
|
|
18
35
|
|
|
19
|
-
#
|
|
36
|
+
# learning/ is scaffolded only where the ledger lives. In a linked worktree whose
|
|
37
|
+
# main checkout holds the ledger (D-LEDGER-MAIN-WORKTREE, resolve-project-root) a
|
|
38
|
+
# learning/ here would be an empty directory nothing writes to; the capture hooks
|
|
39
|
+
# create the ledger's own learning/ at the ledger root when they append to it.
|
|
40
|
+
_EDI_LEARNING="$_DEVFLOW_DIR/learning"
|
|
41
|
+
[ "$_EDI_LEDGER" = "$_EDI_ROOT" ] || _EDI_LEARNING=""
|
|
42
|
+
|
|
43
|
+
# Fast-path: if all subdirectories already exist, skip mkdir and gitignore setup.
|
|
44
|
+
# The marker is the CURRENT carve-out version (D-GITIGNORE-V6), bumped with both
|
|
45
|
+
# writers' stamps, so a v5-stamped project misses this once and gains the
|
|
46
|
+
# `!.devflow/project.json` line.
|
|
20
47
|
if [ -d "$_DEVFLOW_DIR/memory" ] && [ -d "$_DEVFLOW_DIR/docs" ] && \
|
|
21
|
-
[ -d "$
|
|
48
|
+
{ [ -z "$_EDI_LEARNING" ] || [ -d "$_EDI_LEARNING" ]; } && \
|
|
22
49
|
[ -d "$_DEVFLOW_DIR/features" ] && \
|
|
23
|
-
[ -f "$_DEVFLOW_DIR/.root-gitignore-configured-
|
|
50
|
+
[ -f "$_DEVFLOW_DIR/.root-gitignore-configured-v6" ]; then
|
|
24
51
|
return 0
|
|
25
52
|
fi
|
|
26
53
|
|
|
27
54
|
# Create all subdirectories
|
|
28
55
|
mkdir -p \
|
|
29
56
|
"$_DEVFLOW_DIR/memory" \
|
|
30
|
-
"$
|
|
57
|
+
${_EDI_LEARNING:+"$_EDI_LEARNING"} \
|
|
31
58
|
"$_DEVFLOW_DIR/features" \
|
|
32
59
|
"$_DEVFLOW_DIR/docs" \
|
|
33
60
|
2>/dev/null || return 1
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# ensure-proxy — SessionStart + UserPromptSubmit hook
|
|
4
4
|
# Ensures the Devflow proxy relay is running when external model routing is enabled.
|
|
5
5
|
# NOT git-gated (proxy is a global user-scope feature, not per-project).
|
|
6
|
-
# NOT project-scoped: proxy state lives at $
|
|
6
|
+
# NOT project-scoped: proxy state lives at $HOME/.devflow/proxy.json (machine-wide).
|
|
7
7
|
#
|
|
8
8
|
# SessionStart: probe port → if DOWN attempt spawn → inject additionalContext warning if still down
|
|
9
9
|
# UserPromptSubmit: fast exit (both port-up and port-down) — silent; SessionStart handles all warnings
|
|
@@ -55,9 +55,10 @@ if [ "$HOOK_EVENT" = "UserPromptSubmit" ]; then
|
|
|
55
55
|
fi
|
|
56
56
|
|
|
57
57
|
# ── Proxy state ────────────────────────────────────────────────────────────────
|
|
58
|
-
# Proxy is
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
# Proxy is machine-wide, not project-scoped: state lives at the machine root,
|
|
59
|
+
# always $HOME/.devflow (D-ONE-HOME).
|
|
60
|
+
MACHINE_DEVFLOW_DIR="$HOME/.devflow"
|
|
61
|
+
PROXY_STATE_FILE="$MACHINE_DEVFLOW_DIR/proxy.json"
|
|
61
62
|
|
|
62
63
|
if [ ! -f "$PROXY_STATE_FILE" ]; then
|
|
63
64
|
dbg "EXIT: no proxy state file"
|
|
@@ -93,7 +94,7 @@ if [ "${DEVFLOW_HOOK_DEBUG:-}" = "1" ]; then
|
|
|
93
94
|
fi
|
|
94
95
|
|
|
95
96
|
# ── Log setup (SessionStart only) ──────────────────────────────────────────────
|
|
96
|
-
LOG_DIR="$
|
|
97
|
+
LOG_DIR="$MACHINE_DEVFLOW_DIR/logs"
|
|
97
98
|
mkdir -p "$LOG_DIR" 2>/dev/null || true
|
|
98
99
|
# SEC-2: harden log directory to 0700 (best-effort; pre-existing dirs may be wider).
|
|
99
100
|
chmod 700 "$LOG_DIR" 2>/dev/null || true
|
|
@@ -101,7 +102,7 @@ LOG_FILE="$LOG_DIR/proxy.log"
|
|
|
101
102
|
|
|
102
103
|
# Size guard: 2MB max → truncate to 1MB tail (matches hook-log-init guard pattern).
|
|
103
104
|
# Sharing hook-log-init is not feasible here: hook-log-init requires $CWD and targets
|
|
104
|
-
# the per-project log path; ensure-proxy uses the
|
|
105
|
+
# the per-project log path; ensure-proxy uses the machine-wide $MACHINE_DEVFLOW_DIR/logs path.
|
|
105
106
|
# The existence guard is required: wc -c uses a shell redirect whose failure is emitted
|
|
106
107
|
# by bash itself (before wc starts), bypassing 2>/dev/null.
|
|
107
108
|
_LOG_MAX_BYTES=2097152
|
|
@@ -268,7 +269,7 @@ fi
|
|
|
268
269
|
source "$SCRIPT_DIR/get-mtime" 2>/dev/null || true
|
|
269
270
|
source "$SCRIPT_DIR/learning-lock" 2>/dev/null || true
|
|
270
271
|
|
|
271
|
-
SPAWN_LOCK="$
|
|
272
|
+
SPAWN_LOCK="$MACHINE_DEVFLOW_DIR/.proxy-spawn.lock"
|
|
272
273
|
|
|
273
274
|
if ! learning_lock_acquire "$SPAWN_LOCK" 2 2>/dev/null; then
|
|
274
275
|
# Another session is racing to start the proxy — give it a moment then re-probe
|
|
@@ -324,7 +325,7 @@ log "relay spawned with pid $_RELAY_PID"
|
|
|
324
325
|
# Best-effort pid record for `devflow proxy --status` — mirrors the CLI enable path.
|
|
325
326
|
# Written unconditionally after spawn (like the CLI): a stale pid from a relay that
|
|
326
327
|
# never came up is harmless, since --status liveness-checks it before display.
|
|
327
|
-
printf '%s' "$_RELAY_PID" > "$
|
|
328
|
+
printf '%s' "$_RELAY_PID" > "$MACHINE_DEVFLOW_DIR/proxy.pid" 2>/dev/null || \
|
|
328
329
|
log "warn: could not write proxy.pid (non-fatal)"
|
|
329
330
|
|
|
330
331
|
# Bounded wait: 80×0.1s = 8s maximum (well within 15s hook timeout)
|
|
@@ -3,13 +3,16 @@
|
|
|
3
3
|
# rules that govern .devflow/.
|
|
4
4
|
#
|
|
5
5
|
# .devflow/ holds per-developer runtime state (memory, learning, docs, locks) —
|
|
6
|
-
# local by default.
|
|
6
|
+
# local by default. FOUR exceptions are shared via git:
|
|
7
7
|
# 1. Feature knowledge bases under .devflow/features/: index.md and every
|
|
8
8
|
# {slug}/KNOWLEDGE.md (tracked, committed by the Knowledge agent at workflow end).
|
|
9
9
|
# 2. .devflow/conventions.md — naming-convention authority written by the Git
|
|
10
10
|
# learn-conventions operation; shared so the team uses one naming source.
|
|
11
|
-
# 3. .devflow/policy.json — the
|
|
12
|
-
#
|
|
11
|
+
# 3. .devflow/policy.json — the retired evidence-policy file: never parsed, but
|
|
12
|
+
# where project.json has no `evidence` its presence holds the repository at
|
|
13
|
+
# required (D-POLICY-JSON-RETIRED). Devflow never writes it.
|
|
14
|
+
# 4. .devflow/project.json — the team-committed settings both resolvers read
|
|
15
|
+
# (D-GITIGNORE-V6). Devflow never writes it.
|
|
13
16
|
#
|
|
14
17
|
# Everything else under .devflow/ stays local. A user opts back out of the first two
|
|
15
18
|
# by re-adding `.devflow/features/` or `.devflow/conventions.md` to their own .gitignore.
|
|
@@ -25,17 +28,29 @@
|
|
|
25
28
|
# Both reach this one writer so the rule is applied identically everywhere; this
|
|
26
29
|
# decouples git-tracking of .devflow/ from any single feature toggle (avoids PF-014).
|
|
27
30
|
#
|
|
28
|
-
# Idempotent and O(1) after the first run via the .root-gitignore-configured-
|
|
31
|
+
# Idempotent and O(1) after the first run via the .root-gitignore-configured-v6
|
|
29
32
|
# marker under .devflow/ (project-local). The marker is a claim, not proof, so the
|
|
30
|
-
# fast path also requires the block's own sentinel, the policy line
|
|
31
|
-
# .claudeignore entry to be present; a .gitignore that lost any of them (a
|
|
32
|
-
# resolution, a hand edit) is healed on the next run. A
|
|
33
|
-
# fast path, gains the
|
|
33
|
+
# fast path also requires the block's own sentinel, the policy line, the project
|
|
34
|
+
# line AND a .claudeignore entry to be present; a .gitignore that lost any of them (a
|
|
35
|
+
# merge resolution, a hand edit) is healed on the next run. A v5-marked project
|
|
36
|
+
# misses the fast path, gains the project line just before its .claudeignore line
|
|
37
|
+
# and is re-stamped v6 (D-GITIGNORE-V6); a v4-marked one gains the policy and
|
|
38
|
+
# project lines.
|
|
34
39
|
#
|
|
35
|
-
# D-GITIGNORE-
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
40
|
+
# D-GITIGNORE-IN-BLOCK: a line topped up into an existing block goes INSIDE the
|
|
41
|
+
# block, where a fresh block holds it, never at the end of the file. gitignore is
|
|
42
|
+
# last-match-wins, so a `!.devflow/project.json` appended after a user's own later
|
|
43
|
+
# `.devflow/project.json` would silently override their re-ignore. The missing
|
|
44
|
+
# lines are inserted as one run, in block order, right after the block sentinel
|
|
45
|
+
# and the block lines that precede the first missing one in a fresh block (at most
|
|
46
|
+
# three: the sentinel, the policy line, the project line). Every other byte of the
|
|
47
|
+
# file is kept.
|
|
48
|
+
#
|
|
49
|
+
# D-GITIGNORE-V5 / D-GITIGNORE-V6: the block is detected ONLY by its devflow-unique
|
|
50
|
+
# sentinel `!.devflow/conventions.md` — NEVER by `.claudeignore`,
|
|
51
|
+
# `!.devflow/policy.json` or `!.devflow/project.json`, which users legitimately author
|
|
52
|
+
# themselves. Those three are completion lines: each is topped up when missing, in
|
|
53
|
+
# block order (policy, project, then .claudeignore). Keying presence
|
|
39
54
|
# off a user-authored line inverts both halves of the contract: projects that already
|
|
40
55
|
# carry that line never receive the carve-out (so .devflow/ runtime files leak into
|
|
41
56
|
# git), and re-appending `.claudeignore` after a user's `!.claudeignore` reverses
|
|
@@ -50,7 +65,7 @@
|
|
|
50
65
|
[ -z "$1" ] && return 1
|
|
51
66
|
|
|
52
67
|
_ERG_DEVFLOW_DIR="$1/.devflow"
|
|
53
|
-
_ERG_MARKER="$_ERG_DEVFLOW_DIR/.root-gitignore-configured-
|
|
68
|
+
_ERG_MARKER="$_ERG_DEVFLOW_DIR/.root-gitignore-configured-v6"
|
|
54
69
|
_ERG_GITIGNORE="$1/.gitignore"
|
|
55
70
|
|
|
56
71
|
# Whole-line, whitespace-tolerant matchers. `*` and `.` are escaped so the ERE
|
|
@@ -60,9 +75,10 @@ _ERG_RE_LEGACY='^[[:space:]]*\.devflow/[[:space:]]*$'
|
|
|
60
75
|
_ERG_RE_SENTINEL='^[[:space:]]*!\.devflow/conventions\.md[[:space:]]*$'
|
|
61
76
|
_ERG_RE_SENTINEL_V2='^[[:space:]]*!\.devflow/features/\*/KNOWLEDGE\.md[[:space:]]*$'
|
|
62
77
|
_ERG_RE_POLICY='^[[:space:]]*!\.devflow/policy\.json[[:space:]]*$'
|
|
78
|
+
_ERG_RE_PROJECT='^[[:space:]]*!\.devflow/project\.json[[:space:]]*$'
|
|
63
79
|
_ERG_RE_CLAUDEIGNORE='^[[:space:]]*!?\.claudeignore[[:space:]]*$'
|
|
64
80
|
|
|
65
|
-
# Drop every legacy marker beside
|
|
81
|
+
# Drop every legacy marker beside v6 — builtin `[ -e ]` tests, so rm forks only for a
|
|
66
82
|
# marker that exists. Called from the fast path and from the on-success stamp below,
|
|
67
83
|
# so an older devflow's marker is dropped whichever path a run takes.
|
|
68
84
|
# Always returns 0 (never fails the caller under `set -e`): the last iteration's
|
|
@@ -70,7 +86,7 @@ _ERG_RE_CLAUDEIGNORE='^[[:space:]]*!?\.claudeignore[[:space:]]*$'
|
|
|
70
86
|
# and a function call's own exit status is NOT exempt from errexit the way an inlined
|
|
71
87
|
# for-loop's is — unlike this same loop when it sat directly inside the caller's `{ }`.
|
|
72
88
|
_erg_drop_legacy_markers() {
|
|
73
|
-
for _ERG_OLD in -v4 -v3 -v2 ''; do
|
|
89
|
+
for _ERG_OLD in -v5 -v4 -v3 -v2 ''; do
|
|
74
90
|
[ -e "$_ERG_DEVFLOW_DIR/.root-gitignore-configured$_ERG_OLD" ] \
|
|
75
91
|
&& rm -f "$_ERG_DEVFLOW_DIR/.root-gitignore-configured$_ERG_OLD" 2>/dev/null
|
|
76
92
|
done
|
|
@@ -78,11 +94,12 @@ _erg_drop_legacy_markers() {
|
|
|
78
94
|
}
|
|
79
95
|
|
|
80
96
|
# Fast path: converged only when the marker is stamped AND the block sentinel is
|
|
81
|
-
# present AND the policy
|
|
82
|
-
# other state falls through and recomputes.
|
|
97
|
+
# present AND the policy and project lines are present AND a .claudeignore entry
|
|
98
|
+
# exists. Every other state falls through and recomputes.
|
|
83
99
|
if [ -f "$_ERG_MARKER" ] \
|
|
84
100
|
&& grep -qE "$_ERG_RE_SENTINEL" "$_ERG_GITIGNORE" 2>/dev/null \
|
|
85
101
|
&& grep -qE "$_ERG_RE_POLICY" "$_ERG_GITIGNORE" 2>/dev/null \
|
|
102
|
+
&& grep -qE "$_ERG_RE_PROJECT" "$_ERG_GITIGNORE" 2>/dev/null \
|
|
86
103
|
&& grep -qE "$_ERG_RE_CLAUDEIGNORE" "$_ERG_GITIGNORE" 2>/dev/null; then
|
|
87
104
|
_erg_drop_legacy_markers
|
|
88
105
|
return 0
|
|
@@ -100,9 +117,10 @@ mkdir -p "$_ERG_DEVFLOW_DIR" 2>/dev/null || return 1
|
|
|
100
117
|
printf -v _ERG_BLOCK_NO_CI '%s\n' \
|
|
101
118
|
'# Devflow runtime data — local by default (memory, learning, docs, locks).' \
|
|
102
119
|
'# Shared via git: feature knowledge bases under .devflow/features/ (index.md and' \
|
|
103
|
-
'# every {slug}/KNOWLEDGE.md), .devflow/conventions.md (naming authority)
|
|
104
|
-
'# .devflow/policy.json (
|
|
105
|
-
'# `.devflow/features/` or
|
|
120
|
+
'# every {slug}/KNOWLEDGE.md), .devflow/conventions.md (naming authority),' \
|
|
121
|
+
'# .devflow/policy.json (retired; presence only) and .devflow/project.json (team settings).' \
|
|
122
|
+
'# To stop sharing the first two, re-add `.devflow/features/` or' \
|
|
123
|
+
'# `.devflow/conventions.md` to your own .gitignore.' \
|
|
106
124
|
'.devflow/*' \
|
|
107
125
|
'!.devflow/features/' \
|
|
108
126
|
'.devflow/features/*' \
|
|
@@ -111,33 +129,55 @@ printf -v _ERG_BLOCK_NO_CI '%s\n' \
|
|
|
111
129
|
'.devflow/features/*/*' \
|
|
112
130
|
'!.devflow/features/*/KNOWLEDGE.md' \
|
|
113
131
|
'!.devflow/conventions.md' \
|
|
114
|
-
'!.devflow/policy.json'
|
|
132
|
+
'!.devflow/policy.json' \
|
|
133
|
+
'!.devflow/project.json'
|
|
115
134
|
printf -v _ERG_LINE_SENTINEL '%s\n' '!.devflow/conventions.md'
|
|
116
135
|
printf -v _ERG_LINE_POLICY '%s\n' '!.devflow/policy.json'
|
|
136
|
+
printf -v _ERG_LINE_PROJECT '%s\n' '!.devflow/project.json'
|
|
117
137
|
printf -v _ERG_LINE_CLAUDEIGNORE '%s\n' '.claudeignore'
|
|
118
138
|
_ERG_BLOCK="$_ERG_BLOCK_NO_CI$_ERG_LINE_CLAUDEIGNORE"
|
|
119
139
|
|
|
120
|
-
# Does the file already carry the policy line, or a .claudeignore
|
|
121
|
-
# own (either form)? Computed BEFORE the legacy filter below,
|
|
140
|
+
# Does the file already carry the policy line, the project line, or a .claudeignore
|
|
141
|
+
# entry of the user's own (either form)? Computed BEFORE the legacy filter below,
|
|
142
|
+
# which rewrites the file.
|
|
122
143
|
_ERG_HAS_POLICY=0
|
|
123
144
|
grep -qE "$_ERG_RE_POLICY" "$_ERG_GITIGNORE" 2>/dev/null && _ERG_HAS_POLICY=1
|
|
145
|
+
_ERG_HAS_PROJECT=0
|
|
146
|
+
grep -qE "$_ERG_RE_PROJECT" "$_ERG_GITIGNORE" 2>/dev/null && _ERG_HAS_PROJECT=1
|
|
124
147
|
_ERG_HAS_CI=0
|
|
125
148
|
grep -qE "$_ERG_RE_CLAUDEIGNORE" "$_ERG_GITIGNORE" 2>/dev/null && _ERG_HAS_CI=1
|
|
126
149
|
|
|
127
|
-
# The completion lines this file lacks, in block order (policy, then
|
|
128
|
-
# Mirrors missingCompletionLines in the TS twin.
|
|
150
|
+
# The completion lines this file lacks, in block order (policy, project, then
|
|
151
|
+
# .claudeignore). Mirrors missingCompletionLines in the TS twin.
|
|
129
152
|
_ERG_COMPLETION=''
|
|
130
153
|
if [ "$_ERG_HAS_POLICY" = 0 ]; then
|
|
131
154
|
_ERG_COMPLETION="$_ERG_LINE_POLICY"
|
|
132
155
|
fi
|
|
156
|
+
if [ "$_ERG_HAS_PROJECT" = 0 ]; then
|
|
157
|
+
_ERG_COMPLETION="$_ERG_COMPLETION$_ERG_LINE_PROJECT"
|
|
158
|
+
fi
|
|
133
159
|
if [ "$_ERG_HAS_CI" = 0 ]; then
|
|
134
160
|
_ERG_COMPLETION="$_ERG_COMPLETION$_ERG_LINE_CLAUDEIGNORE"
|
|
135
161
|
fi
|
|
136
162
|
|
|
163
|
+
# The block lines a top-up run is inserted after: the sentinel, then whichever of
|
|
164
|
+
# the policy and project lines precede the first missing line in a fresh block.
|
|
165
|
+
# Mirrors blockRunBefore in the TS twin.
|
|
166
|
+
if [ "$_ERG_HAS_POLICY" = 0 ]; then
|
|
167
|
+
_ERG_RUN_RE='^[[:space:]]*(!\.devflow/conventions\.md)[[:space:]]*$'
|
|
168
|
+
elif [ "$_ERG_HAS_PROJECT" = 0 ]; then
|
|
169
|
+
_ERG_RUN_RE='^[[:space:]]*(!\.devflow/conventions\.md|!\.devflow/policy\.json)[[:space:]]*$'
|
|
170
|
+
else
|
|
171
|
+
_ERG_RUN_RE='^[[:space:]]*(!\.devflow/conventions\.md|!\.devflow/policy\.json|!\.devflow/project\.json)[[:space:]]*$'
|
|
172
|
+
fi
|
|
173
|
+
|
|
137
174
|
# Classify: noop (nothing to do) | continue (extend an existing block) |
|
|
138
175
|
# block (install a fresh block) | fail (a rewrite failed; leave the file alone).
|
|
176
|
+
# continue inserts _ERG_APPEND after the first line matching _ERG_ANCHOR_RE and the
|
|
177
|
+
# run of lines matching _ERG_RUN_RE right after it (empty: no run).
|
|
139
178
|
_ERG_MODE=noop
|
|
140
179
|
_ERG_APPEND=''
|
|
180
|
+
_ERG_ANCHOR_RE=''
|
|
141
181
|
if grep -qE "$_ERG_RE_OPTOUT" "$_ERG_GITIGNORE" 2>/dev/null; then
|
|
142
182
|
# User-authored `/.devflow/` (leading slash) — respect it; don't force the carve-out.
|
|
143
183
|
# Checked FIRST so a file containing both it and a sentinel keeps the user's entry.
|
|
@@ -147,11 +187,15 @@ elif grep -qE "$_ERG_RE_SENTINEL" "$_ERG_GITIGNORE" 2>/dev/null; then
|
|
|
147
187
|
if [ -n "$_ERG_COMPLETION" ]; then
|
|
148
188
|
_ERG_MODE=continue
|
|
149
189
|
_ERG_APPEND="$_ERG_COMPLETION"
|
|
190
|
+
_ERG_ANCHOR_RE="$_ERG_RE_SENTINEL"
|
|
150
191
|
fi
|
|
151
192
|
elif grep -qE "$_ERG_RE_SENTINEL_V2" "$_ERG_GITIGNORE" 2>/dev/null; then
|
|
152
|
-
# v2 block installed —
|
|
193
|
+
# v2 block installed — insert the lines it lacks, in block order, right after
|
|
194
|
+
# its sentinel, the last carve-out line it has.
|
|
153
195
|
_ERG_MODE=continue
|
|
154
196
|
_ERG_APPEND="$_ERG_LINE_SENTINEL$_ERG_COMPLETION"
|
|
197
|
+
_ERG_ANCHOR_RE="$_ERG_RE_SENTINEL_V2"
|
|
198
|
+
_ERG_RUN_RE=''
|
|
155
199
|
else
|
|
156
200
|
# No devflow block — install one, omitting the .claudeignore line the user owns.
|
|
157
201
|
_ERG_MODE=block
|
|
@@ -180,21 +224,52 @@ else
|
|
|
180
224
|
fi
|
|
181
225
|
fi
|
|
182
226
|
|
|
183
|
-
#
|
|
184
|
-
#
|
|
185
|
-
#
|
|
186
|
-
#
|
|
187
|
-
#
|
|
227
|
+
# _erg_insert_in_block <anchor-ERE> <run-ERE or ''> <text> — rewrite the file with
|
|
228
|
+
# <text> inserted after the first line matching <anchor-ERE> and the run of up to
|
|
229
|
+
# three lines matching <run-ERE> right after it (D-GITIGNORE-IN-BLOCK). Mirrors
|
|
230
|
+
# insertInBlock in the TS twin byte for byte: head and tail copy every other byte
|
|
231
|
+
# unchanged, and when the run ends on a last line with no newline, one is added
|
|
232
|
+
# before <text>. Each read ends in a trailing `x`, so command substitution cannot
|
|
233
|
+
# strip the file's own trailing newlines. The file is rewritten in place with one
|
|
234
|
+
# `>`, as the TS twin's writeFile does, so a symlinked .gitignore keeps its link.
|
|
235
|
+
# Returns non-zero, having written nothing, when the anchor line cannot be found.
|
|
236
|
+
_erg_insert_in_block() {
|
|
237
|
+
local _anchor="$1" _run="$2" _text="$3" _hit="" _n="" _next="" _line="" _i=0 _head="" _tail=""
|
|
238
|
+
_hit="$(grep -n -m 1 -E "$_anchor" "$_ERG_GITIGNORE" 2>/dev/null)" || return 1
|
|
239
|
+
_n="${_hit%%:*}"
|
|
240
|
+
case "$_n" in ''|*[!0-9]*) return 1 ;; esac
|
|
241
|
+
if [ -n "$_run" ]; then
|
|
242
|
+
_next="$(sed -n "$((_n + 1)),$((_n + 3))p" "$_ERG_GITIGNORE" 2>/dev/null)"
|
|
243
|
+
while [ "$_i" -lt 3 ] && [ -n "$_next" ]; do
|
|
244
|
+
_line="${_next%%$'\n'*}"
|
|
245
|
+
if [ "$_line" = "$_next" ]; then _next=''; else _next="${_next#*$'\n'}"; fi
|
|
246
|
+
[[ $_line =~ $_run ]] || break
|
|
247
|
+
_n=$((_n + 1))
|
|
248
|
+
_i=$((_i + 1))
|
|
249
|
+
done
|
|
250
|
+
fi
|
|
251
|
+
# The `x` is printed only when the read succeeded, so a failed read leaves none
|
|
252
|
+
# and nothing is written — never a truncated file.
|
|
253
|
+
_head="$(head -n "$_n" "$_ERG_GITIGNORE" 2>/dev/null && printf x)"
|
|
254
|
+
case "$_head" in *x) _head="${_head%x}" ;; *) return 1 ;; esac
|
|
255
|
+
_tail="$(tail -n "+$((_n + 1))" "$_ERG_GITIGNORE" 2>/dev/null && printf x)"
|
|
256
|
+
case "$_tail" in *x) _tail="${_tail%x}" ;; *) return 1 ;; esac
|
|
257
|
+
case "$_head" in *$'\n') ;; *) _head="$_head"$'\n' ;; esac
|
|
258
|
+
printf '%s%s%s' "$_head" "$_text" "$_tail" > "$_ERG_GITIGNORE"
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
# The fresh-block append shape, mirrored byte-for-byte by the TS twin's appendBlock.
|
|
262
|
+
# `tail -c 1` yields the empty string when the file already ends in a newline
|
|
263
|
+
# (command substitution strips it), so a file whose last byte is not a newline gets
|
|
264
|
+
# one before the appended text fuses onto its last line. Existing trailing newlines
|
|
265
|
+
# are preserved verbatim — no trimming, no blank-line dedupe.
|
|
188
266
|
_ERG_OK=0
|
|
189
267
|
case "$_ERG_MODE" in
|
|
190
268
|
noop)
|
|
191
269
|
_ERG_OK=1
|
|
192
270
|
;;
|
|
193
271
|
continue)
|
|
194
|
-
|
|
195
|
-
printf '\n' >> "$_ERG_GITIGNORE"
|
|
196
|
-
fi
|
|
197
|
-
printf '%s' "$_ERG_APPEND" >> "$_ERG_GITIGNORE" && _ERG_OK=1
|
|
272
|
+
_erg_insert_in_block "$_ERG_ANCHOR_RE" "$_ERG_RUN_RE" "$_ERG_APPEND" && _ERG_OK=1
|
|
198
273
|
;;
|
|
199
274
|
block)
|
|
200
275
|
if [ ! -s "$_ERG_GITIGNORE" ]; then
|
|
@@ -209,7 +284,7 @@ case "$_ERG_MODE" in
|
|
|
209
284
|
;;
|
|
210
285
|
esac
|
|
211
286
|
|
|
212
|
-
# On success, stamp the current-format
|
|
287
|
+
# On success, stamp the current-format v6 marker and drop legacy markers.
|
|
213
288
|
[ "$_ERG_OK" = 1 ] && {
|
|
214
289
|
touch "$_ERG_MARKER"
|
|
215
290
|
_erg_drop_legacy_markers
|
|
@@ -14,3 +14,51 @@ df_has_git_marker() {
|
|
|
14
14
|
done
|
|
15
15
|
return 1
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
# df_is_project_root <root> — returns 0 when <root> may hold per-project devflow
|
|
19
|
+
# data: it is a checkout's toplevel — it holds its own `.git` entry — AND it is not
|
|
20
|
+
# the home directory.
|
|
21
|
+
#
|
|
22
|
+
# D-HOOKS-GIT-ONLY: the hooks that scaffold `.devflow/` and the root `.gitignore`
|
|
23
|
+
# carve-out — session-start-context and ensure-devflow-init, which every capture
|
|
24
|
+
# and memory hook reaches — run only for a git project. Outside one there is no
|
|
25
|
+
# repository for the carve-out to protect and no branch for memory to describe,
|
|
26
|
+
# and scaffolding there would leave `.devflow/` and a `.gitignore` block in `~`,
|
|
27
|
+
# Downloads or a folder of repos — wherever Claude Code happened to start.
|
|
28
|
+
# A repository rooted at HOME (a dotfiles repo) is refused too: its `.devflow` IS
|
|
29
|
+
# the machine root, so per-project data would land inside `~/.devflow` and its
|
|
30
|
+
# carve-out in the user's home `.gitignore`.
|
|
31
|
+
#
|
|
32
|
+
# D-HOOKS-TOPLEVEL-ONLY: the `.git` entry must sit AT <root>, not merely above it.
|
|
33
|
+
# Callers pass the root df_resolve_roots resolved, and when `git rev-parse` fails
|
|
34
|
+
# from inside a checkout — the repository has dubious ownership, or
|
|
35
|
+
# GIT_CEILING_DIRECTORIES stops git's search — that root is the raw cwd, possibly a
|
|
36
|
+
# subdirectory. An ancestor walk would still find the checkout's `.git` and admit
|
|
37
|
+
# it, and `.devflow/` plus a `.gitignore` block would land in that subdirectory.
|
|
38
|
+
# Requiring the marker at the root makes the gate and the resolver agree: git's
|
|
39
|
+
# toplevel always holds `.git` (a directory, or the file a linked worktree or
|
|
40
|
+
# submodule carries), a subdirectory never does. A directory that holds its own
|
|
41
|
+
# `.git` still passes when git cannot read it — the marker contract is the zero-fork
|
|
42
|
+
# one above, and writing there scatters nothing, because it is the checkout's root.
|
|
43
|
+
#
|
|
44
|
+
# The comparison is between PHYSICAL paths, because the two sides reach it by
|
|
45
|
+
# different routes: git reports a toplevel with every symlink resolved, while
|
|
46
|
+
# $HOME is whatever the login shell was handed — on macOS the temp tree is
|
|
47
|
+
# /var → /private/var, and a home directory can itself sit behind a symlink. A
|
|
48
|
+
# textual compare would miss exactly the case it exists for.
|
|
49
|
+
#
|
|
50
|
+
# Zero forks, like the marker walk above: a builtin `-e` test, then `cd -P`, which
|
|
51
|
+
# resolves a path physically and publishes it in $PWD — both shell builtins, so no
|
|
52
|
+
# `$(...)` subshell and no realpath/readlink binary is involved. The caller's directory is restored before
|
|
53
|
+
# returning. An unresolvable <root> is refused (fail closed: nothing is written);
|
|
54
|
+
# an unset or unresolvable HOME cannot equal a resolved root, so it refuses nothing.
|
|
55
|
+
df_is_project_root() {
|
|
56
|
+
local _root="$1" _back="$PWD" _root_real="" _home_real=""
|
|
57
|
+
[ -n "$_root" ] || return 1
|
|
58
|
+
[ -e "$_root/.git" ] || return 1
|
|
59
|
+
if cd -P "$_root" 2>/dev/null; then _root_real="$PWD"; fi
|
|
60
|
+
if [ -n "${HOME:-}" ] && cd -P "$HOME" 2>/dev/null; then _home_real="$PWD"; fi
|
|
61
|
+
cd "$_back" 2>/dev/null || true
|
|
62
|
+
[ -n "$_root_real" ] || return 1
|
|
63
|
+
[ "$_root_real" != "$_home_real" ]
|
|
64
|
+
}
|