devflow-kit 2.5.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +82 -0
- package/README.md +44 -19
- package/dist/agents/git.md +13 -15
- package/dist/cli/commands/ambient.js +160 -145
- package/dist/cli/commands/capture.js +29 -55
- package/dist/cli/commands/compliance.js +32 -61
- package/dist/cli/commands/context.js +17 -32
- package/dist/cli/commands/debug.js +65 -26
- package/dist/cli/commands/flags.js +3 -3
- package/dist/cli/commands/hud.js +34 -10
- package/dist/cli/commands/init-seed.js +40 -4
- package/dist/cli/commands/init.js +249 -271
- package/dist/cli/commands/install-report.js +10 -15
- package/dist/cli/commands/knowledge/index.js +1 -1
- package/dist/cli/commands/knowledge/toggle.js +11 -3
- package/dist/cli/commands/learning.js +52 -37
- package/dist/cli/commands/legacy-hooks.js +11 -14
- package/dist/cli/commands/memory.js +67 -78
- package/dist/cli/commands/proxy.js +23 -41
- package/dist/cli/commands/security.js +5 -13
- package/dist/cli/commands/skills.js +21 -3
- package/dist/cli/commands/tracker.js +100 -228
- package/dist/cli/commands/uninstall.js +343 -138
- package/dist/commands/bug-analysis.md +38 -12
- package/dist/commands/code-review.md +70 -21
- package/dist/commands/debug.md +37 -7
- package/dist/commands/dynamic-build.md +66 -17
- package/dist/commands/dynamic-plan.md +19 -8
- package/dist/commands/dynamic-profile.md +24 -10
- package/dist/commands/dynamic-tickets.md +22 -11
- package/dist/commands/explore.md +37 -7
- package/dist/commands/implement.md +96 -32
- package/dist/commands/plan.md +62 -19
- package/dist/commands/release.md +2 -2
- package/dist/commands/research.md +34 -8
- package/dist/commands/resolve.md +65 -17
- package/dist/commands/self-review.md +45 -9
- package/dist/core/compliance-compose.js +27 -27
- package/dist/core/evidence-policy.js +240 -24
- package/dist/core/feature-config.js +94 -25
- package/dist/core/feature-switch.js +1 -1
- package/dist/core/flags.js +30 -2
- package/dist/core/fs-atomic.js +27 -0
- package/dist/core/hook-log-dirs.js +104 -0
- package/dist/core/learning-tuning-config.js +5 -3
- package/dist/core/ledger-root.js +102 -0
- package/dist/core/manifest.js +6 -4
- package/dist/core/mds-variants.js +34 -97
- package/dist/core/migrations.js +49 -23
- package/dist/core/plugins.js +5 -4
- package/dist/core/project-paths.js +0 -17
- package/dist/core/same-location.js +25 -0
- package/dist/core/tracker.js +226 -139
- package/dist/hud/components/config-counts.js +15 -4
- package/dist/hud/components/learning-counts.js +14 -0
- package/dist/hud/config.js +2 -1
- package/dist/hud/cost-history.js +2 -4
- package/dist/hud/git.js +52 -7
- package/dist/hud/index.js +7 -9
- package/dist/skills/git/references/pr/check-merge-readiness.md +1 -1
- package/dist/skills/git/references/pr/ensure-pr-ready.md +1 -1
- package/dist/skills/git/references/pr/update-pr-evidence.md +1 -1
- package/dist/skills/git/references/tracker/_mcp.md +1 -1
- package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +1 -1
- package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +1 -1
- package/dist/skills/git/references/tracker/github/gather-release-evidence.md +2 -2
- package/dist/skills/git/references/tracker/github/manage-debt.md +3 -3
- package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +1 -1
- package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +1 -1
- package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +2 -2
- package/dist/skills/git/references/tracker/jira/manage-debt.md +1 -1
- package/dist/skills/git/references/tracker/jira/post-wave-report.md +1 -1
- package/dist/skills/git/references/tracker/jira/setup-task.md +1 -1
- package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +1 -1
- package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +1 -1
- package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +2 -2
- package/dist/skills/git/references/tracker/linear/manage-debt.md +1 -1
- package/dist/skills/git/references/tracker/linear/post-wave-report.md +1 -1
- package/dist/skills/git/references/tracker/linear/setup-task.md +1 -1
- package/dist/targets/claude-code/claude-paths.js +59 -57
- package/dist/targets/claude-code/compliance-install.js +49 -65
- package/dist/targets/claude-code/hooks.js +108 -3
- package/dist/targets/claude-code/installer.js +30 -57
- package/dist/targets/claude-code/post-install.js +246 -139
- package/dist/targets/claude-code/tracker-install.js +38 -65
- package/package.json +5 -4
- package/src/assets/agents/code.md +4 -3
- package/src/assets/agents/design.md +1 -0
- package/src/assets/agents/git.mds +55 -57
- package/src/assets/agents/knowledge.md +2 -2
- package/src/assets/agents/review.md +3 -1
- package/src/assets/agents/tracker.md +37 -30
- package/src/assets/commands/_partials/_compliance.mds +19 -1
- package/src/assets/commands/_partials/_decisions.mds +15 -3
- package/src/assets/commands/_partials/_docs_root.mds +35 -0
- package/src/assets/commands/_partials/_engine.mds +2 -2
- package/src/assets/commands/_partials/_evidence_policy.mds +3 -3
- package/src/assets/commands/_partials/_factory.mds +1 -1
- package/src/assets/commands/_partials/_knowledge.mds +27 -9
- package/src/assets/commands/_partials/_plan_contract.mds +2 -2
- package/src/assets/commands/_partials/_preamble.mds +1 -1
- package/src/assets/commands/_partials/_publication.mds +6 -2
- package/src/assets/commands/_partials/_settings.mds +28 -0
- package/src/assets/commands/_partials/_ticket_template.mds +3 -3
- package/src/assets/commands/_partials/_tracker.mds +4 -4
- package/src/assets/commands/_partials/_wave.mds +4 -4
- package/src/assets/commands/bug-analysis.mds +19 -17
- package/src/assets/commands/code-review.mds +39 -33
- package/src/assets/commands/debug.mds +4 -5
- package/src/assets/commands/dynamic-build.mds +75 -53
- package/src/assets/commands/dynamic-plan.mds +20 -15
- package/src/assets/commands/dynamic-profile.mds +24 -11
- package/src/assets/commands/dynamic-tickets.mds +25 -20
- package/src/assets/commands/explore.mds +4 -5
- package/src/assets/commands/implement.mds +58 -45
- package/src/assets/commands/plan.mds +34 -29
- package/src/assets/commands/release.md +2 -2
- package/src/assets/commands/research.mds +11 -9
- package/src/assets/commands/resolve.mds +41 -39
- package/src/assets/commands/self-review.mds +24 -25
- package/src/assets/mds/git/_pr.mds +61 -61
- package/src/assets/mds/git/_references.mds +19 -19
- package/src/assets/mds/tracker/_common.mds +8 -8
- package/src/assets/mds/tracker/_github.mds +71 -71
- package/src/assets/mds/tracker/_jira.mds +74 -74
- package/src/assets/mds/tracker/_linear.mds +75 -75
- package/src/assets/mds/tracker/_mcp.mds +23 -17
- package/src/assets/scripts/hooks/background-memory-update +35 -19
- package/src/assets/scripts/hooks/capture-prompt +18 -12
- package/src/assets/scripts/hooks/capture-question +18 -12
- package/src/assets/scripts/hooks/capture-turn +27 -17
- package/src/assets/scripts/hooks/debug-trace +11 -6
- package/src/assets/scripts/hooks/ensure-devflow-init +33 -6
- package/src/assets/scripts/hooks/ensure-proxy +9 -8
- package/src/assets/scripts/hooks/ensure-root-gitignore +111 -36
- package/src/assets/scripts/hooks/git-marker +48 -0
- package/src/assets/scripts/hooks/json-helper.cjs +6 -1
- package/src/assets/scripts/hooks/lib/project-paths.cjs +0 -19
- package/src/assets/scripts/hooks/log-paths +80 -0
- package/src/assets/scripts/hooks/memory-worker +17 -15
- package/src/assets/scripts/hooks/pre-compact-memory +41 -16
- package/src/assets/scripts/hooks/queue-append +104 -30
- package/src/assets/scripts/hooks/resolve-project-root +101 -7
- package/src/assets/scripts/hooks/session-start-context +289 -122
- package/src/assets/scripts/hooks/session-start-memory +35 -16
- package/src/assets/scripts/lib/project-config.cjs +633 -0
- package/src/assets/scripts/resolve-evidence-policy.cjs +300 -220
- package/src/assets/scripts/resolve-settings.cjs +1054 -0
- package/src/assets/scripts/verify-evidence.cjs +1 -1
- package/src/assets/skills/compliance/SKILL.md +2 -2
- package/src/assets/skills/docs-framework/SKILL.md +6 -7
- package/src/assets/skills/docs-framework/references/patterns.md +10 -17
- package/src/assets/skills/gap-analysis/SKILL.md +2 -2
- package/src/assets/skills/git/references/github-api.md +9 -9
- package/src/assets/skills/git/references/patterns.md +1 -1
- package/src/assets/skills/worktree-support/SKILL.md +1 -1
- package/src/assets/skills/worktree-support/references/roots.md +29 -0
- package/src/targets/claude-code/templates/managed-settings.json +25 -9
|
@@ -199,11 +199,16 @@ function isCollisionScanExcluded(relPath) {
|
|
|
199
199
|
* project root is not a git working tree or the `git` binary is unavailable;
|
|
200
200
|
* callers fall back to `listFsWalkFiles`.
|
|
201
201
|
*
|
|
202
|
+
* D-NO-FSMONITOR: `ls-files` reads the index, and reading the index runs the
|
|
203
|
+
* command a repository's config names in `core.fsmonitor` — code chosen by the
|
|
204
|
+
* repository this hook runs inside. The call turns it off for itself
|
|
205
|
+
* (`-c core.fsmonitor=false`), so the listing stays a pure read.
|
|
206
|
+
*
|
|
202
207
|
* @param {string} projectRoot
|
|
203
208
|
* @returns {string[]} project-relative paths
|
|
204
209
|
*/
|
|
205
210
|
function listGitTrackedFiles(projectRoot) {
|
|
206
|
-
const out = execFileSync('git', ['ls-files', '-z'], {
|
|
211
|
+
const out = execFileSync('git', ['-c', 'core.fsmonitor=false', 'ls-files', '-z'], {
|
|
207
212
|
cwd: projectRoot,
|
|
208
213
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
209
214
|
});
|
|
@@ -174,23 +174,6 @@ function getHandoffPath(projectRoot, branchSlug) {
|
|
|
174
174
|
return path.join(projectRoot, '.devflow', 'docs', `handoff-${branchSlug}.md`);
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
// ---------------------------------------------------------------------------
|
|
178
|
-
// Gitignore entries
|
|
179
|
-
// ---------------------------------------------------------------------------
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* The canonical list of generic gitignore entries Devflow adds to a project's
|
|
183
|
-
* root .gitignore for LOCAL-scope installs. Currently just `.claude/`.
|
|
184
|
-
*
|
|
185
|
-
* `.devflow/` is intentionally NOT here: it is managed by ensureDevflowGitignore
|
|
186
|
-
* (TS) / ensure-root-gitignore (hook), which write the feature-knowledge carve-out
|
|
187
|
-
* for ALL scopes. Adding a bare `.devflow/` here would append a wholesale-ignore
|
|
188
|
-
* line after the carve-out and re-bury it (last match wins in .gitignore).
|
|
189
|
-
*/
|
|
190
|
-
function getGitignoreEntries() {
|
|
191
|
-
return ['.claude/'];
|
|
192
|
-
}
|
|
193
|
-
|
|
194
177
|
module.exports = {
|
|
195
178
|
// Core directories
|
|
196
179
|
getMemoryDir,
|
|
@@ -225,6 +208,4 @@ module.exports = {
|
|
|
225
208
|
getDesignDir,
|
|
226
209
|
getResearchDir,
|
|
227
210
|
getHandoffPath,
|
|
228
|
-
// Gitignore entries
|
|
229
|
-
getGitignoreEntries,
|
|
230
211
|
};
|
|
@@ -2,11 +2,88 @@
|
|
|
2
2
|
# Shared log path computation for Devflow hooks.
|
|
3
3
|
# Source this file to get devflow_log_dir function.
|
|
4
4
|
|
|
5
|
+
# D-LOG-DIR-CAP (hook side). Every hook logs under ~/.devflow/logs/<cwd-slug>/,
|
|
6
|
+
# one folder per working directory, and `devflow init` prunes them to the
|
|
7
|
+
# MAX_HOOK_LOG_DIRS most recent (src/core/hook-log-dirs.ts). A machine that never
|
|
8
|
+
# re-inits would still grow without bound, so devflow_log_dir also prunes — but
|
|
9
|
+
# ONLY when it creates a new folder, which is rare: the common path (the folder
|
|
10
|
+
# already exists) pays nothing. The pass is bounded (one `ls`, at most
|
|
11
|
+
# _DF_LOG_DIRS_SCANNED_MAX entries read, _DF_LOG_DIRS_PRUNED_PER_CALL removed in
|
|
12
|
+
# one `rm`), bash 3.2 compatible, and never spawns node. Only directories are
|
|
13
|
+
# counted or removed: root files (proxy.log) and symbolic links are left alone.
|
|
14
|
+
#
|
|
15
|
+
# Recency: folders are ranked by their own mtime (`ls -t`), which an append to a
|
|
16
|
+
# log inside does not move, so a candidate is spared when any of its first
|
|
17
|
+
# _DF_LOG_FILES_READ_PER_DIR entries is newer than the oldest folder the cap
|
|
18
|
+
# keeps (`-nt`, a shell builtin). A spared folder can leave the count briefly
|
|
19
|
+
# above the cap; init's exact prune settles it.
|
|
20
|
+
#
|
|
21
|
+
# _DF_MAX_HOOK_LOG_DIRS must equal MAX_HOOK_LOG_DIRS (tests/hook-log-paths.test.ts).
|
|
22
|
+
_DF_MAX_HOOK_LOG_DIRS=200
|
|
23
|
+
_DF_LOG_DIRS_PRUNED_PER_CALL=50
|
|
24
|
+
_DF_LOG_DIRS_SCANNED_MAX=100000
|
|
25
|
+
_DF_LOG_FILES_READ_PER_DIR=64
|
|
26
|
+
|
|
5
27
|
# Cache the computed log dir path to avoid spawning 4 subprocesses (sed+tr+mkdir+chmod)
|
|
6
28
|
# on every call within the same hook process.
|
|
7
29
|
_LOG_DIR_CACHED=""
|
|
8
30
|
_LOG_DIR_CACHED_CWD=""
|
|
9
31
|
|
|
32
|
+
# _df_log_dir_is_recent <dir> <cutoff>
|
|
33
|
+
# True when one of the first _DF_LOG_FILES_READ_PER_DIR entries in <dir> is newer
|
|
34
|
+
# than <cutoff>. Globbing must be on.
|
|
35
|
+
_df_log_dir_is_recent() {
|
|
36
|
+
local _dir="$1" _cutoff="$2" _f _read=0
|
|
37
|
+
for _f in "$_dir"/.[!.]* "$_dir"/..?* "$_dir"/*; do
|
|
38
|
+
[ "$_read" -lt "$_DF_LOG_FILES_READ_PER_DIR" ] || break
|
|
39
|
+
[ -e "$_f" ] || [ -L "$_f" ] || continue
|
|
40
|
+
_read=$((_read + 1))
|
|
41
|
+
if [ "$_f" -nt "$_cutoff" ]; then return 0; fi
|
|
42
|
+
done
|
|
43
|
+
return 1
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# _df_prune_log_dirs <logs-root> <just-created-dir>
|
|
47
|
+
# Remove the oldest folders beyond _DF_MAX_HOOK_LOG_DIRS, at most
|
|
48
|
+
# _DF_LOG_DIRS_PRUNED_PER_CALL of them. Never fails the caller.
|
|
49
|
+
_df_prune_log_dirs() {
|
|
50
|
+
local _logs="$1" _new="$2"
|
|
51
|
+
local _listing _name _n=0 _i _removed=0 _cutoff _d _glob_off=0
|
|
52
|
+
local -a _dirs _batch
|
|
53
|
+
_listing=$(ls -1At "$_logs" 2>/dev/null) || return 0
|
|
54
|
+
|
|
55
|
+
case $- in *f*) _glob_off=1 ;; esac
|
|
56
|
+
local IFS='
|
|
57
|
+
'
|
|
58
|
+
set -f
|
|
59
|
+
for _name in $_listing; do
|
|
60
|
+
[ "$_n" -lt "$_DF_LOG_DIRS_SCANNED_MAX" ] || break
|
|
61
|
+
case "$_name" in */*) continue ;; esac
|
|
62
|
+
[ -L "$_logs/$_name" ] && continue
|
|
63
|
+
[ -d "$_logs/$_name" ] || continue
|
|
64
|
+
_dirs[_n]="$_logs/$_name"
|
|
65
|
+
_n=$((_n + 1))
|
|
66
|
+
done
|
|
67
|
+
set +f
|
|
68
|
+
|
|
69
|
+
if [ "$_n" -gt "$_DF_MAX_HOOK_LOG_DIRS" ]; then
|
|
70
|
+
_cutoff="${_dirs[_DF_MAX_HOOK_LOG_DIRS - 1]}"
|
|
71
|
+
_i=$((_n - 1))
|
|
72
|
+
while [ "$_i" -ge "$_DF_MAX_HOOK_LOG_DIRS" ] && [ "$_removed" -lt "$_DF_LOG_DIRS_PRUNED_PER_CALL" ]; do
|
|
73
|
+
_d="${_dirs[_i]}"
|
|
74
|
+
if [ "$_d" != "$_new" ] && ! _df_log_dir_is_recent "$_d" "$_cutoff"; then
|
|
75
|
+
_batch[_removed]="$_d"
|
|
76
|
+
_removed=$((_removed + 1))
|
|
77
|
+
fi
|
|
78
|
+
_i=$((_i - 1))
|
|
79
|
+
done
|
|
80
|
+
if [ "$_removed" -gt 0 ]; then rm -rf -- "${_batch[@]}" 2>/dev/null || true; fi
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
if [ "$_glob_off" = 1 ]; then set -f; fi
|
|
84
|
+
return 0
|
|
85
|
+
}
|
|
86
|
+
|
|
10
87
|
devflow_log_dir() {
|
|
11
88
|
local cwd="$1"
|
|
12
89
|
if [ "$cwd" = "$_LOG_DIR_CACHED_CWD" ] && [ -n "$_LOG_DIR_CACHED" ]; then
|
|
@@ -16,8 +93,11 @@ devflow_log_dir() {
|
|
|
16
93
|
local slug
|
|
17
94
|
slug=$(echo "$cwd" | sed 's|^/||' | tr '/' '-')
|
|
18
95
|
local dir="$HOME/.devflow/logs/$slug"
|
|
96
|
+
local created=0
|
|
97
|
+
[ -d "$dir" ] || created=1
|
|
19
98
|
mkdir -p "$dir"
|
|
20
99
|
chmod 700 "$dir"
|
|
100
|
+
if [ "$created" = 1 ]; then _df_prune_log_dirs "$HOME/.devflow/logs" "$dir" || true; fi
|
|
21
101
|
_LOG_DIR_CACHED="$dir"
|
|
22
102
|
_LOG_DIR_CACHED_CWD="$cwd"
|
|
23
103
|
echo "$dir"
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
# Memory pipeline: memory-worker (Stop Hook)
|
|
4
4
|
# Owns the 120s-throttle + nohup-spawn logic for background-memory-update.
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
5
|
+
# Claude Code runs a Stop event's hooks in parallel, so this hook may fire
|
|
6
|
+
# before capture-turn has appended this turn's assistant row. That is safe:
|
|
7
|
+
# background-memory-update leaves a user-only queue in place and skips the LLM
|
|
8
|
+
# run (D-QUEUE-NO-ORPHAN-DELETE), and the next run takes the whole turn. This
|
|
9
|
+
# hook does NOT append to any queue itself.
|
|
8
10
|
|
|
9
11
|
# Safe no-op fallback: must exist before set -e and before hook-bootstrap is sourced.
|
|
10
12
|
dbg() { :; }
|
|
@@ -32,18 +34,18 @@ source "$SCRIPT_DIR/resolve-project-root" 2>/dev/null || true
|
|
|
32
34
|
PROJECT_ROOT="$(df_resolve_root "$CWD" 2>/dev/null || true)"
|
|
33
35
|
[ -n "$PROJECT_ROOT" ] || PROJECT_ROOT="$CWD"
|
|
34
36
|
|
|
35
|
-
# The machine-wide manifest (the memory switch, D-FEATURES-
|
|
36
|
-
# queue-append)
|
|
37
|
-
#
|
|
38
|
-
DEVFLOW_MANIFEST="$
|
|
39
|
-
|
|
40
|
-
MEMORY_DIR="$
|
|
37
|
+
# The machine-wide manifest (the memory switch, D-FEATURES-NARROW-ONLY in
|
|
38
|
+
# queue-append) lives at the machine root,
|
|
39
|
+
# $HOME/.devflow (D-ONE-HOME).
|
|
40
|
+
DEVFLOW_MANIFEST="$HOME/.devflow/manifest.json"
|
|
41
|
+
PROJECT_DEVFLOW_DIR="$PROJECT_ROOT/.devflow"
|
|
42
|
+
MEMORY_DIR="$PROJECT_DEVFLOW_DIR/memory"
|
|
41
43
|
|
|
42
|
-
# Memory gate: the machine
|
|
43
|
-
#
|
|
44
|
-
#
|
|
44
|
+
# Memory gate: the machine switch, narrowed by this checkout's project.json /
|
|
45
|
+
# config.json `features.memory`, gates this hook entirely -- read by the helper
|
|
46
|
+
# every memory/learning gate shares (D-FEATURES-NARROW-ONLY, see queue-append).
|
|
45
47
|
source "$SCRIPT_DIR/queue-append" || { echo "memory-worker: failed to source queue-append" >&2; exit 1; }
|
|
46
|
-
queue_read_gates "$DEVFLOW_MANIFEST"
|
|
48
|
+
queue_read_gates "$DEVFLOW_MANIFEST" "$PROJECT_ROOT"
|
|
47
49
|
MEMORY_ENABLED="$_QG_MEMORY"
|
|
48
50
|
|
|
49
51
|
dbg "MEMORY_ENABLED=$MEMORY_ENABLED"
|
|
@@ -99,8 +101,8 @@ touch "$TRIGGER_FILE" 2>/dev/null || true
|
|
|
99
101
|
|
|
100
102
|
# Spawn detached worker — nohup + disown so it survives the Stop hook process exit.
|
|
101
103
|
# The manifest path is handed over explicitly: the worker re-checks the memory
|
|
102
|
-
# switch after spawn
|
|
103
|
-
#
|
|
104
|
+
# switch after spawn against the same manifest this hook gated on (and the same
|
|
105
|
+
# checkout's repository files, from the project root it resolves itself).
|
|
104
106
|
nohup "$UPDATER" "$CWD" "$DEVFLOW_MANIFEST" </dev/null >>/dev/null 2>&1 & disown
|
|
105
107
|
|
|
106
108
|
log "Spawned background-memory-update worker (CWD=$CWD)"
|
|
@@ -43,23 +43,23 @@ source "$SCRIPT_DIR/resolve-project-root" 2>/dev/null || true
|
|
|
43
43
|
PROJECT_ROOT="$(df_resolve_root "$CWD" 2>/dev/null || true)"
|
|
44
44
|
[ -n "$PROJECT_ROOT" ] || PROJECT_ROOT="$CWD"
|
|
45
45
|
|
|
46
|
-
# The machine-wide manifest (the memory switch, D-FEATURES-
|
|
47
|
-
# queue-append)
|
|
48
|
-
#
|
|
49
|
-
DEVFLOW_MANIFEST="$
|
|
50
|
-
|
|
51
|
-
MEMORY_DIR="$
|
|
46
|
+
# The machine-wide manifest (the memory switch, D-FEATURES-NARROW-ONLY in
|
|
47
|
+
# queue-append) lives at the machine root,
|
|
48
|
+
# $HOME/.devflow (D-ONE-HOME).
|
|
49
|
+
DEVFLOW_MANIFEST="$HOME/.devflow/manifest.json"
|
|
50
|
+
PROJECT_DEVFLOW_DIR="$PROJECT_ROOT/.devflow"
|
|
51
|
+
MEMORY_DIR="$PROJECT_DEVFLOW_DIR/memory"
|
|
52
52
|
|
|
53
53
|
# Normal logging
|
|
54
54
|
source "$SCRIPT_DIR/hook-log-init" "pre-compact-memory"
|
|
55
55
|
|
|
56
|
-
# Memory gate: the machine
|
|
57
|
-
#
|
|
58
|
-
#
|
|
56
|
+
# Memory gate: the machine switch, narrowed by this checkout's project.json /
|
|
57
|
+
# config.json `features.memory`, read by the helper every memory/learning gate
|
|
58
|
+
# shares (D-FEATURES-NARROW-ONLY, see queue-append).
|
|
59
59
|
source "$SCRIPT_DIR/queue-append" || { echo "pre-compact-memory: failed to source queue-append" >&2; exit 1; }
|
|
60
|
-
queue_read_gates "$DEVFLOW_MANIFEST"
|
|
60
|
+
queue_read_gates "$DEVFLOW_MANIFEST" "$PROJECT_ROOT"
|
|
61
61
|
if [ "$_QG_MEMORY" != "true" ]; then
|
|
62
|
-
dbg "EXIT: memory disabled
|
|
62
|
+
dbg "EXIT: memory disabled"
|
|
63
63
|
exit 0
|
|
64
64
|
fi
|
|
65
65
|
|
|
@@ -79,9 +79,26 @@ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
|
79
79
|
if cd "$CWD" 2>/dev/null && git rev-parse --git-dir >/dev/null 2>&1; then
|
|
80
80
|
GIT_HEAD_SHA=$(git rev-parse HEAD 2>/dev/null || echo "")
|
|
81
81
|
GIT_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
|
|
82
|
-
|
|
82
|
+
# D-DETACHED-HEAD: a checkout of a commit rather than a branch (`git checkout
|
|
83
|
+
# <sha>`, bisect, a rebase stop, `git worktree add --detach`, a CI checkout)
|
|
84
|
+
# prints no branch name. It is labelled `(detached)`, echoing git's own
|
|
85
|
+
# `(HEAD detached at …)` display and holding no space, so the stamp's
|
|
86
|
+
# one-token `branch:` field still parses; the backup names it and the bootstrap
|
|
87
|
+
# below still stamps memory, keyed on the commit it describes. (A branch really
|
|
88
|
+
# named `(detached)` is legal git and would read the same — accepted: nothing
|
|
89
|
+
# decides on the label, it only names.) An unborn branch has no HEAD commit,
|
|
90
|
+
# keeps the empty label, and is still skipped.
|
|
91
|
+
if [ -z "$GIT_BRANCH" ] && is_hex_sha "$GIT_HEAD_SHA" 40 40; then
|
|
92
|
+
GIT_BRANCH="(detached)"
|
|
93
|
+
fi
|
|
94
|
+
# D-NO-FSMONITOR: `status` and `diff` read the index, and reading the index
|
|
95
|
+
# runs the command the repository's config names in `core.fsmonitor` — code
|
|
96
|
+
# chosen by the repository this hook runs inside. Each index read turns it
|
|
97
|
+
# off for itself (`-c core.fsmonitor=false`); rev-parse, branch and log never
|
|
98
|
+
# read the index.
|
|
99
|
+
GIT_STATUS=$(git -c core.fsmonitor=false status --porcelain 2>/dev/null | head -30 || echo "")
|
|
83
100
|
GIT_LOG=$(git log --oneline -10 2>/dev/null || echo "")
|
|
84
|
-
GIT_DIFF_STAT=$(git diff --stat HEAD 2>/dev/null || echo "")
|
|
101
|
+
GIT_DIFF_STAT=$(git -c core.fsmonitor=false diff --stat HEAD 2>/dev/null || echo "")
|
|
85
102
|
dbg "GIT_BRANCH=$GIT_BRANCH HEAD=$GIT_HEAD_SHA"
|
|
86
103
|
fi
|
|
87
104
|
|
|
@@ -105,8 +122,12 @@ json_backup_construct \
|
|
|
105
122
|
log "Wrote backup: $BACKUP_FILE"
|
|
106
123
|
dbg "Wrote backup: $BACKUP_FILE"
|
|
107
124
|
|
|
108
|
-
# Bootstrap minimal WORKING-MEMORY.md if absent; skip on
|
|
109
|
-
#
|
|
125
|
+
# Bootstrap minimal WORKING-MEMORY.md if absent; skip on an unborn branch or a
|
|
126
|
+
# malformed SHA. is_hex_sha 40 40: exactly 40 lowercase hex chars required. A detached
|
|
127
|
+
# HEAD bootstraps with a `branch: (detached)` stamp (D-DETACHED-HEAD, above): the
|
|
128
|
+
# stamp's memory-head SHA is what drift detection keys on, and the Context line names
|
|
129
|
+
# the commit by its short SHA, so the first compaction there no longer leaves the
|
|
130
|
+
# session with nothing to restore.
|
|
110
131
|
# avoids REL-5: O_EXCL-style atomic create via noclobber so the existence test and the
|
|
111
132
|
# create are one operation — if the worker's CAS mv lands in the window, noclobber fails
|
|
112
133
|
# (file already exists) and we skip the bootstrap rather than truncating fresh memory.
|
|
@@ -126,7 +147,11 @@ if [ -n "$GIT_BRANCH" ] && is_hex_sha "$GIT_HEAD_SHA" 40 40; then
|
|
|
126
147
|
echo "- (none recorded)"
|
|
127
148
|
echo ""
|
|
128
149
|
echo "## Context"
|
|
129
|
-
|
|
150
|
+
if [ "$GIT_BRANCH" = "(detached)" ]; then
|
|
151
|
+
echo "- Branch: (detached) @ ${GIT_HEAD_SHA:0:7}"
|
|
152
|
+
else
|
|
153
|
+
echo "- Branch: $GIT_BRANCH"
|
|
154
|
+
fi
|
|
130
155
|
echo "$GIT_LOG" | head -3 | while IFS= read -r line; do
|
|
131
156
|
[ -n "$line" ] && echo "- $line"
|
|
132
157
|
done
|
|
@@ -24,39 +24,53 @@
|
|
|
24
24
|
# queue is gated independently -- callers compute memory_enabled/learning_enabled
|
|
25
25
|
# themselves (see queue_read_gates below for the at-most-one-fork read of both).
|
|
26
26
|
#
|
|
27
|
-
# queue_read_gates <manifest_path>
|
|
28
|
-
# Reads BOTH `
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
# caller's scope. The two values are newline-separated
|
|
27
|
+
# queue_read_gates <manifest_path> [<root>]
|
|
28
|
+
# Reads BOTH switches, `memory` and `learning`, in at most ONE subprocess fork
|
|
29
|
+
# (AC-P1 -- at most one gate-read fork per capture hook, not two; none when
|
|
30
|
+
# the fast paths settle it). Sets _QG_MEMORY and _QG_LEARNING ("true"/"false")
|
|
31
|
+
# in the caller's scope. The two values are newline-separated
|
|
33
32
|
# rather than using a control-character delimiter: they are always the literal
|
|
34
33
|
# strings "true"/"false", never arbitrary content, so a plain newline split is
|
|
35
34
|
# unambiguous and easy to review (no invisible bytes hiding in the source).
|
|
36
35
|
#
|
|
37
|
-
# D-FEATURES-
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
36
|
+
# D-FEATURES-NARROW-ONLY (src/core/feature-switch.ts): a switch is on iff the
|
|
37
|
+
# machine switch is on AND neither repository file sets `features.<name>` to
|
|
38
|
+
# the literal `false` -- the rule resolve-settings.cjs folds, held to the same
|
|
39
|
+
# answers by the shared table tests/fixtures/settings-switch-table.ts (TP-49).
|
|
40
|
+
# A repository can switch memory or learning off for itself, never back on.
|
|
41
|
+
# machine <manifest_path>, the ~/.devflow/manifest.json `devflow init` and
|
|
42
|
+
# `devflow memory|learning --enable/--disable` write. Only an
|
|
43
|
+
# explicit boolean `false` switches a feature off -- an absent,
|
|
44
|
+
# unreadable or malformed manifest, a missing key, or a non-boolean
|
|
45
|
+
# value leaves it ON (fail-open, ADR-028), the rule
|
|
46
|
+
# isMachineFeatureOn() applies in the CLI.
|
|
47
|
+
# project <root>/.devflow/project.json (team-committed) and
|
|
48
|
+
# personal <root>/.devflow/config.json (this worktree's own), read ONLY under
|
|
49
|
+
# `features.memory` / `features.learning`. The retired top-level
|
|
50
|
+
# keys (`memory`, `learning`, `decisions`) and `features.decisions`
|
|
51
|
+
# narrow nothing (#378: reading them let a feature run in every repo
|
|
52
|
+
# but one). An invalid file -- unparseable, over 4096 bytes, a BOM,
|
|
53
|
+
# a symlink, a duplicated key -- narrows nothing.
|
|
54
|
+
# <root> is the checkout's toplevel (DF_ROOT, which every caller holds as
|
|
55
|
+
# $PROJECT_ROOT), for learning too: project.json is committed per branch and
|
|
56
|
+
# config.json lives per worktree, so both are read where the session runs, as
|
|
57
|
+
# resolve-settings.cjs reads them -- never at DF_LEDGER_ROOT, which only says
|
|
58
|
+
# where the queue lands. Without <root> (or with an empty one) no repository
|
|
59
|
+
# file is read.
|
|
47
60
|
#
|
|
48
|
-
# D-LEARNING-LEGACY-DECISIONS:
|
|
49
|
-
# boolean, else the pre-rename
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
# in whichever key decides
|
|
61
|
+
# D-LEARNING-LEGACY-DECISIONS: in the MANIFEST only, learning reads
|
|
62
|
+
# `features.learning` when it is a boolean, else the pre-rename
|
|
63
|
+
# `features.decisions` (ADR-011) -- readManifest's migration precedence
|
|
64
|
+
# exactly, so a legacy `decisions: false` switches learning off before any
|
|
65
|
+
# command has healed the file. Only a boolean `false` in whichever key decides
|
|
66
|
+
# is off, as above.
|
|
53
67
|
#
|
|
54
68
|
# Every hook that gates on memory or learning reads it through here: the
|
|
55
69
|
# capture hooks, session-start-context (Sections 1-2), memory-worker,
|
|
56
70
|
# session-start-memory, pre-compact-memory and background-memory-update. The
|
|
57
|
-
# callers
|
|
58
|
-
#
|
|
59
|
-
#
|
|
71
|
+
# callers pass <manifest_path> as $HOME/.devflow/manifest.json, the machine
|
|
72
|
+
# root (D-ONE-HOME); memory-worker hands its resolved path to the background
|
|
73
|
+
# worker it spawns.
|
|
60
74
|
|
|
61
75
|
queue_append_row() {
|
|
62
76
|
local _qar_file="$1" _qar_role="$2" _qar_content="$3" _qar_ts="$4"
|
|
@@ -106,8 +120,34 @@ queue_append_both() {
|
|
|
106
120
|
fi
|
|
107
121
|
}
|
|
108
122
|
|
|
123
|
+
# _qg_repo_file_can_narrow <file> -- succeeds when <file> COULD set
|
|
124
|
+
# features.memory or features.learning to false, so only the parser can decide
|
|
125
|
+
# it; fails when it certainly cannot. Builtins only: no fork.
|
|
126
|
+
#
|
|
127
|
+
# D-GATES-FAST-PATH (repository files): the read is bounded -- `-n 4097`, which
|
|
128
|
+
# bash 3.2 supports -- and `read -d ''` succeeds only when it stops short of the
|
|
129
|
+
# end of the file: at a NUL byte (never valid in JSON) or at 4097 characters,
|
|
130
|
+
# which is more than MAX_CONFIG_BYTES (4096) bytes. Either way the parser would
|
|
131
|
+
# call the file invalid, and an invalid file narrows nothing. `-f` follows a
|
|
132
|
+
# symlink and refuses a directory or FIFO, so nothing here can block; the parser
|
|
133
|
+
# itself refuses the symlink. A `\u` escape is the only JSON spelling of a
|
|
134
|
+
# letter other than the letter itself, so without one a narrowing file shows
|
|
135
|
+
# `"features"`, then `{`, then `"memory"` or `"learning"`, then `false`,
|
|
136
|
+
# literally and in that order. `.` spans newlines (POSIX ERE without
|
|
137
|
+
# REG_NEWLINE), so the match can only over-report -- an over-report costs one
|
|
138
|
+
# fork and the parser's answer, never a wrong one. No `"decisions"` and no
|
|
139
|
+
# top-level key before `features` is ever matched.
|
|
140
|
+
_qg_repo_file_can_narrow() {
|
|
141
|
+
local _qgf_text=""
|
|
142
|
+
local _qgf_re='"features"[[:space:]]*:[[:space:]]*[{].*"(memory|learning)"[[:space:]]*:[[:space:]]*false'
|
|
143
|
+
[ -f "$1" ] || return 1
|
|
144
|
+
if IFS= read -r -d '' -n 4097 _qgf_text 2>/dev/null < "$1"; then return 1; fi
|
|
145
|
+
case "$_qgf_text" in *'\u'*) return 0 ;; esac
|
|
146
|
+
[[ $_qgf_text =~ $_qgf_re ]]
|
|
147
|
+
}
|
|
148
|
+
|
|
109
149
|
queue_read_gates() {
|
|
110
|
-
local _qg_manifest="${1:-}" _qg_text="" _qg_parse="false"
|
|
150
|
+
local _qg_manifest="${1:-}" _qg_root="${2:-}" _qg_text="" _qg_parse="false" _qg_repo="false" _qg_fields=""
|
|
111
151
|
_QG_MEMORY="true"
|
|
112
152
|
_QG_LEARNING="true"
|
|
113
153
|
|
|
@@ -129,8 +169,38 @@ queue_read_gates() {
|
|
|
129
169
|
if [[ $_qg_text =~ $_qg_re ]]; then _qg_parse="true"; fi
|
|
130
170
|
fi
|
|
131
171
|
|
|
132
|
-
if [ "$
|
|
133
|
-
|
|
172
|
+
if [ -n "$_qg_root" ]; then
|
|
173
|
+
if _qg_repo_file_can_narrow "$_qg_root/.devflow/project.json" \
|
|
174
|
+
|| _qg_repo_file_can_narrow "$_qg_root/.devflow/config.json"; then
|
|
175
|
+
_qg_repo="true"
|
|
176
|
+
fi
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
if [ "$_qg_repo" = "true" ]; then
|
|
180
|
+
# The ONE fork when a repository file can narrow: node on the resolver's own
|
|
181
|
+
# readRepoLayers + foldSettings (resolve-settings.cjs, a sibling of this
|
|
182
|
+
# hooks/ directory), so every file rule -- symlink, size, BOM, fatal UTF-8,
|
|
183
|
+
# duplicate keys -- is the parser's, not a shell copy of it. It folds the
|
|
184
|
+
# manifest too, read only when the fast path above flagged it (an unflagged
|
|
185
|
+
# manifest is both-on), so a false in all three layers still costs one fork.
|
|
186
|
+
# jq cannot see duplicate keys, so this path is node whatever _HAS_JQ says.
|
|
187
|
+
local _qg_scripts="${BASH_SOURCE[0]%/*}/.." _qg_manifest_arg=""
|
|
188
|
+
[ "$_qg_parse" = "true" ] && _qg_manifest_arg="$_qg_manifest"
|
|
189
|
+
_qg_fields=$(node -e "
|
|
190
|
+
const S = require(require('path').resolve(process.argv[1], 'resolve-settings.cjs'));
|
|
191
|
+
let m;
|
|
192
|
+
if (process.argv[2] !== '') {
|
|
193
|
+
try { m = JSON.parse(require('fs').readFileSync(process.argv[2], 'utf8')); } catch (_) { m = undefined; }
|
|
194
|
+
}
|
|
195
|
+
const w = S.foldSettings(Object.assign({ manifest: m }, S.readRepoLayers(process.argv[3]))).switches;
|
|
196
|
+
process.stdout.write(String(w.memory.on) + String.fromCharCode(10) + String(w.learning.on));
|
|
197
|
+
" -- "$_qg_scripts" "$_qg_manifest_arg" "$_qg_root" 2>/dev/null) || _qg_fields=""
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
# The manifest-only parse: when no repository file can narrow, or when the
|
|
201
|
+
# fork above failed (the manifest's answer still stands; the repository's is
|
|
202
|
+
# lost, which fails open like every other unreadable layer).
|
|
203
|
+
if [ -z "$_qg_fields" ] && [ "$_qg_parse" = "true" ]; then
|
|
134
204
|
if [ "$_HAS_JQ" = "true" ]; then
|
|
135
205
|
# `try` covers a manifest whose top level or `features` is not an object; a
|
|
136
206
|
# parse failure empties the output, which reads as "not switched off". The
|
|
@@ -150,11 +220,15 @@ queue_read_gates() {
|
|
|
150
220
|
process.stdout.write(String(on('memory')) + String.fromCharCode(10) + String(on(learnKey)));
|
|
151
221
|
" -- "$_qg_manifest" 2>/dev/null) || _qg_fields=""
|
|
152
222
|
fi
|
|
153
|
-
|
|
223
|
+
fi
|
|
224
|
+
|
|
225
|
+
# Only the four well-formed answers are taken; anything else is fail-open.
|
|
226
|
+
case "$_qg_fields" in
|
|
227
|
+
true$'\n'true|true$'\n'false|false$'\n'true|false$'\n'false)
|
|
154
228
|
_QG_MEMORY="${_qg_fields%%$'\n'*}"
|
|
155
229
|
_QG_LEARNING="${_qg_fields#*$'\n'}"
|
|
156
|
-
|
|
157
|
-
|
|
230
|
+
;;
|
|
231
|
+
esac
|
|
158
232
|
|
|
159
233
|
# Explicit return: this function's meaning is "populate _QG_* outputs," not a
|
|
160
234
|
# success/failure signal, so its exit status must never leak the truthiness of
|
|
@@ -9,13 +9,19 @@
|
|
|
9
9
|
# side anchors identically.
|
|
10
10
|
#
|
|
11
11
|
# Sourced by: the memory/learning/session hooks and ensure-devflow-init.
|
|
12
|
-
# Sourced helper: uses `return`-free pure
|
|
12
|
+
# Sourced helper: uses `return`-free pure functions; _-prefixed locals (never
|
|
13
13
|
# clobbers caller vars). Safe under `set -e` (git failure is guarded with || true).
|
|
14
14
|
#
|
|
15
15
|
# Usage:
|
|
16
16
|
# source resolve-project-root
|
|
17
|
-
# PROJECT_ROOT="$(df_resolve_root "$CWD")"
|
|
17
|
+
# PROJECT_ROOT="$(df_resolve_root "$CWD")" # one root
|
|
18
|
+
# df_resolve_roots "$CWD" # sets DF_ROOT and DF_LEDGER_ROOT
|
|
18
19
|
#
|
|
20
|
+
# df_resolve_roots needs df_is_project_root, so this helper sources its sibling
|
|
21
|
+
# git-marker itself rather than trusting every caller to have done so first.
|
|
22
|
+
|
|
23
|
+
source "${BASH_SOURCE[0]%/*}/git-marker" 2>/dev/null || true
|
|
24
|
+
|
|
19
25
|
# df_resolve_root <cwd> prints the project root for <cwd>:
|
|
20
26
|
# 1. git top-level — git walks up to the real repo root even from a
|
|
21
27
|
# .devflow/-nested subdir, so the nested case is fixed for git repos.
|
|
@@ -27,11 +33,99 @@ df_resolve_root() {
|
|
|
27
33
|
# the caller (e.g. a non-git path). Empty output then routes to the fallback.
|
|
28
34
|
_root="$(git -C "$_cwd" rev-parse --show-toplevel 2>/dev/null || true)"
|
|
29
35
|
if [ -z "$_root" ]; then
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
*/.devflow) _root="${_cwd%/.devflow}" ;;
|
|
33
|
-
*) _root="$_cwd" ;;
|
|
34
|
-
esac
|
|
36
|
+
_df_nongit_root "$_cwd"
|
|
37
|
+
_root="$_DF_NONGIT_ROOT"
|
|
35
38
|
fi
|
|
36
39
|
printf '%s\n' "$_root"
|
|
37
40
|
}
|
|
41
|
+
|
|
42
|
+
# _df_nongit_root <cwd> sets _DF_NONGIT_ROOT to the non-git fallback root: <cwd>
|
|
43
|
+
# with its first /.devflow/ segment onward (or a trailing /.devflow) stripped.
|
|
44
|
+
# A variable rather than stdout, so a caller pays no `$(...)` subshell for it.
|
|
45
|
+
_df_nongit_root() {
|
|
46
|
+
case "$1" in
|
|
47
|
+
*/.devflow/*) _DF_NONGIT_ROOT="${1%%/.devflow/*}" ;;
|
|
48
|
+
*/.devflow) _DF_NONGIT_ROOT="${1%/.devflow}" ;;
|
|
49
|
+
*) _DF_NONGIT_ROOT="$1" ;;
|
|
50
|
+
esac
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# df_resolve_roots <cwd> sets TWO roots in the caller's scope, from ONE git call:
|
|
54
|
+
# DF_ROOT — the checkout's toplevel: where per-checkout data lives
|
|
55
|
+
# (memory, the .gitignore carve-out, the knowledge bases).
|
|
56
|
+
# DF_LEDGER_ROOT — where the learning ledger and its queue live.
|
|
57
|
+
#
|
|
58
|
+
# D-LEDGER-MAIN-WORKTREE: a linked worktree (`git worktree add`, `claude
|
|
59
|
+
# --worktree`) has its own toplevel, so a ledger anchored there restarts at
|
|
60
|
+
# ADR-001 — main's decisions are invisible in it, and the IDs it mints collide
|
|
61
|
+
# with main's (in a repo whose collision guard refuses, the Learning agent stops
|
|
62
|
+
# for good). The ledger is one per REPOSITORY, not per checkout: DF_LEDGER_ROOT
|
|
63
|
+
# is the main worktree — the parent of `--git-common-dir` — when that common dir
|
|
64
|
+
# is a `<dir>/.git` directory AND `<dir>/.devflow` already exists; otherwise it is
|
|
65
|
+
# DF_ROOT. The existence test keeps a main checkout that never ran devflow from
|
|
66
|
+
# being scaffolded by a worktree session. A main worktree at HOME (a dotfiles
|
|
67
|
+
# repository) is refused as well (df_is_project_root, D-HOOKS-GIT-ONLY): its
|
|
68
|
+
# `.devflow` is the machine root, which always exists, so the existence test alone
|
|
69
|
+
# would put the ledger inside `~/.devflow`. A common dir that is not `/.git`
|
|
70
|
+
# (a bare repository, `--separate-git-dir`, a submodule's modules/ dir) has no
|
|
71
|
+
# main checkout to name, so it stays per checkout. Memory stays at DF_ROOT on
|
|
72
|
+
# purpose: working memory describes the branch in front of you. A worktree that
|
|
73
|
+
# already grew its own ledger before this rule keeps it — local data is left in
|
|
74
|
+
# place, and the worktree simply stops appending to it.
|
|
75
|
+
#
|
|
76
|
+
# The one call asks for both answers at once:
|
|
77
|
+
# git rev-parse --path-format=absolute --show-toplevel --git-common-dir
|
|
78
|
+
# and its output is accepted only when it is EXACTLY two lines, each an absolute
|
|
79
|
+
# path. `--path-format` arrived in git 2.31; an older git echoes the unknown
|
|
80
|
+
# flag back as a line of its own (three lines, the first not absolute), and that
|
|
81
|
+
# falls back to df_resolve_root, so the ledger stays at the toplevel exactly as
|
|
82
|
+
# before this rule. A failed call prints nothing: `--show-toplevel` itself failed
|
|
83
|
+
# (not a work tree), so asking git again could only fail the same way, and the
|
|
84
|
+
# non-git fallback applies directly. Inside a checkout git refuses (dubious
|
|
85
|
+
# ownership, GIT_CEILING_DIRECTORIES) that fallback is the raw cwd, which may be a
|
|
86
|
+
# subdirectory; df_is_project_root refuses any root without its own `.git`
|
|
87
|
+
# (D-HOOKS-TOPLEVEL-ONLY), so no hook scaffolds there. Parsing is parameter expansion and `case`
|
|
88
|
+
# only: one git call on the success path and on the non-git path alike.
|
|
89
|
+
df_resolve_roots() {
|
|
90
|
+
local _cwd="$1" _out="" _top="" _common="" _main=""
|
|
91
|
+
DF_ROOT=""
|
|
92
|
+
DF_LEDGER_ROOT=""
|
|
93
|
+
_out="$(git -C "$_cwd" rev-parse --path-format=absolute --show-toplevel --git-common-dir 2>/dev/null || true)"
|
|
94
|
+
_top="${_out%%$'\n'*}"
|
|
95
|
+
if [ "$_top" != "$_out" ]; then
|
|
96
|
+
_common="${_out#*$'\n'}"
|
|
97
|
+
fi
|
|
98
|
+
case "$_top" in /*) ;; *) _top="" ;; esac
|
|
99
|
+
case "$_common" in
|
|
100
|
+
*$'\n'*) _common="" ;;
|
|
101
|
+
/*) ;;
|
|
102
|
+
*) _common="" ;;
|
|
103
|
+
esac
|
|
104
|
+
if [ -z "$_out" ]; then
|
|
105
|
+
_df_nongit_root "$_cwd"
|
|
106
|
+
DF_ROOT="$_DF_NONGIT_ROOT"
|
|
107
|
+
[ -n "$DF_ROOT" ] || DF_ROOT="$_cwd"
|
|
108
|
+
DF_LEDGER_ROOT="$DF_ROOT"
|
|
109
|
+
return 0
|
|
110
|
+
fi
|
|
111
|
+
if [ -z "$_top" ] || [ -z "$_common" ]; then
|
|
112
|
+
DF_ROOT="$(df_resolve_root "$_cwd")"
|
|
113
|
+
[ -n "$DF_ROOT" ] || DF_ROOT="$_cwd"
|
|
114
|
+
DF_LEDGER_ROOT="$DF_ROOT"
|
|
115
|
+
return 0
|
|
116
|
+
fi
|
|
117
|
+
DF_ROOT="$_top"
|
|
118
|
+
DF_LEDGER_ROOT="$_top"
|
|
119
|
+
case "$_common" in
|
|
120
|
+
*/.git)
|
|
121
|
+
_main="${_common%/.git}"
|
|
122
|
+
# df_is_project_root forks nothing (builtin `cd -P` walks). Unavailable —
|
|
123
|
+
# git-marker failed to source — it is a command not found, which keeps the
|
|
124
|
+
# ledger in this checkout: the conservative answer.
|
|
125
|
+
if [ -n "$_main" ] && [ -d "$_main/.devflow" ] && df_is_project_root "$_main" 2>/dev/null; then
|
|
126
|
+
DF_LEDGER_ROOT="$_main"
|
|
127
|
+
fi
|
|
128
|
+
;;
|
|
129
|
+
esac
|
|
130
|
+
return 0
|
|
131
|
+
}
|