devflow-kit 3.7.0 → 3.8.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 +359 -1
- package/README.md +11 -11
- package/dist/agents/code.md +2 -4
- package/dist/agents/design.md +1 -1
- package/dist/agents/fix.md +6 -7
- package/dist/agents/git.md +1 -1
- package/dist/agents/harden.md +1 -1
- package/dist/agents/refactor.md +2 -4
- package/dist/agents/review.md +4 -4
- package/dist/agents/skim.md +2 -2
- package/dist/agents/triage.md +2 -2
- package/dist/cli/commands/capture.js +1 -1
- package/dist/cli/commands/curation-move-io.js +102 -0
- package/dist/cli/commands/curation-move-step.js +374 -0
- package/dist/cli/commands/{learning.js → curation.js} +302 -99
- package/dist/cli/commands/gate-level-prompts.js +426 -0
- package/dist/cli/commands/gates.js +34 -11
- package/dist/cli/commands/init-seed.js +11 -8
- package/dist/cli/commands/init.js +240 -54
- package/dist/cli/commands/memory.js +2 -2
- package/dist/cli/commands/prompt-io.js +37 -0
- package/dist/cli/commands/uninstall.js +81 -9
- package/dist/cli.js +2 -2
- package/dist/commands/bug-analysis.md +24 -35
- package/dist/commands/code-review.md +32 -36
- package/dist/commands/debug.md +4 -4
- package/dist/commands/dynamic-build.md +25 -24
- package/dist/commands/dynamic-plan.md +4 -4
- package/dist/commands/dynamic-profile.md +4 -4
- package/dist/commands/dynamic-tickets.md +4 -4
- package/dist/commands/explore.md +5 -5
- package/dist/commands/implement.md +52 -81
- package/dist/commands/plan.md +5 -5
- package/dist/commands/release.md +4 -4
- package/dist/commands/research.md +5 -5
- package/dist/commands/resolve.md +28 -30
- package/dist/commands/self-review.md +4 -12
- package/dist/core/assets.js +20 -20
- package/dist/core/curate-tuning-config.js +5 -6
- package/dist/core/curation-move-notice.js +84 -0
- package/dist/core/curation-move-skip.js +36 -0
- package/dist/core/curation-move.js +35 -0
- package/dist/core/curation-queue-cleanup.js +134 -0
- package/dist/core/curation-store.js +98 -0
- package/dist/core/{learning-variants.js → curation-variants.js} +65 -41
- package/dist/core/echo-value.js +11 -5
- package/dist/core/evidence-policy.js +1 -1
- package/dist/core/feature-switch.js +95 -19
- package/dist/core/fs-atomic.js +1 -1
- package/dist/core/gate-level.js +10 -2
- package/dist/core/git.js +16 -14
- package/dist/core/ledger-root.js +82 -22
- package/dist/core/linked-path.js +4 -4
- package/dist/core/manifest-lock.js +238 -0
- package/dist/core/manifest.js +58 -14
- package/dist/core/migrations.js +1 -13
- package/dist/core/observations.js +2 -2
- package/dist/core/plugins.js +15 -15
- package/dist/core/pool.js +26 -0
- package/dist/core/project-paths.js +103 -34
- package/dist/core/queue-drain.js +2 -2
- package/dist/core/tracked-areas.js +35 -0
- package/dist/core/tracker.js +1 -1
- package/dist/{learning-off → curation-off}/agents/code.md +2 -4
- package/dist/{learning-off → curation-off}/agents/fix.md +6 -7
- package/dist/{learning-off → curation-off}/agents/refactor.md +2 -4
- package/dist/{learning-off → curation-off}/agents/review.md +3 -3
- package/dist/{learning-off → curation-off}/agents/triage.md +1 -1
- package/dist/{learning-off → curation-off}/commands/bug-analysis.md +20 -31
- package/dist/{learning-off → curation-off}/commands/code-review.md +29 -33
- package/dist/{learning-off → curation-off}/commands/debug.md +1 -1
- package/dist/{learning-off → curation-off}/commands/dynamic-build.md +22 -21
- package/dist/{learning-off → curation-off}/commands/explore.md +1 -1
- package/dist/{learning-off → curation-off}/commands/implement.md +49 -78
- package/dist/{learning-off → curation-off}/commands/plan.md +1 -1
- package/dist/{learning-off → curation-off}/commands/resolve.md +25 -27
- package/dist/{learning-off → curation-off}/commands/self-review.md +1 -9
- package/dist/hud/components/{learning-counts.js → curation-counts.js} +36 -17
- package/dist/hud/config.js +1 -1
- package/dist/hud/index.js +6 -6
- package/dist/hud/render.js +3 -3
- package/dist/skills/git/references/pr/validate-branch.md +8 -5
- package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +13 -5
- package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +13 -5
- package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +13 -5
- package/dist/targets/claude-code/{learning-install.js → curation-install.js} +85 -65
- package/dist/targets/claude-code/installer.js +15 -15
- package/dist/targets/claude-code/language-stamp.js +5 -5
- package/dist/targets/claude-code/post-install.js +2 -2
- package/dist/targets/claude-code/tracker-servers.js +1 -1
- package/package.json +3 -2
- package/src/assets/agents/_partials/_code.mds +11 -11
- package/src/assets/agents/code.mds +2 -2
- package/src/assets/agents/curate.md +8 -8
- package/src/assets/agents/debug.mds +6 -6
- package/src/assets/agents/design.mds +18 -18
- package/src/assets/agents/fix.mds +6 -5
- package/src/assets/agents/git.mds +1 -1
- package/src/assets/agents/harden.mds +7 -7
- package/src/assets/agents/knowledge.mds +23 -23
- package/src/assets/agents/refactor.mds +2 -2
- package/src/assets/agents/research.mds +15 -15
- package/src/assets/agents/review.mds +43 -43
- package/src/assets/agents/skim.mds +11 -11
- package/src/assets/agents/triage.mds +22 -22
- package/src/assets/agents/validate.md +13 -26
- package/src/assets/commands/_partials/_decisions.mds +13 -13
- package/src/assets/commands/_partials/_engine.mds +8 -8
- package/src/assets/commands/_partials/_knowledge.mds +29 -15
- package/src/assets/commands/_partials/_preamble.mds +4 -4
- package/src/assets/commands/_partials/_settings.mds +2 -2
- package/src/assets/commands/_partials/_wave.mds +7 -7
- package/src/assets/commands/bug-analysis.mds +37 -48
- package/src/assets/commands/code-review.mds +46 -50
- package/src/assets/commands/debug.mds +14 -14
- package/src/assets/commands/dynamic-build.mds +38 -37
- package/src/assets/commands/dynamic-plan.mds +22 -22
- package/src/assets/commands/dynamic-profile.mds +2 -2
- package/src/assets/commands/dynamic-tickets.mds +8 -8
- package/src/assets/commands/explore.mds +20 -20
- package/src/assets/commands/implement.mds +77 -98
- package/src/assets/commands/plan.mds +27 -27
- package/src/assets/commands/release.mds +12 -12
- package/src/assets/commands/research.mds +19 -19
- package/src/assets/commands/resolve.mds +56 -50
- package/src/assets/commands/self-review.mds +20 -20
- package/src/assets/mds/git/_ops.mds +3 -1
- package/src/assets/mds/git/_ops_pr.mds +1 -1
- package/src/assets/mds/git/_ops_steps.mds +10 -4
- package/src/assets/mds/git/_pr.mds +7 -4
- package/src/assets/rules/quality.md +1 -0
- package/src/assets/scripts/hooks/background-memory-update +2 -2
- package/src/assets/scripts/hooks/capture-prompt +12 -12
- package/src/assets/scripts/hooks/capture-question +11 -11
- package/src/assets/scripts/hooks/capture-turn +12 -12
- package/src/assets/scripts/hooks/{learning-lock → curation-lock} +2 -2
- package/src/assets/scripts/hooks/curation-move +293 -0
- package/src/assets/scripts/hooks/ensure-devflow-init +45 -13
- package/src/assets/scripts/hooks/ensure-proxy +4 -4
- package/src/assets/scripts/hooks/ensure-root-gitignore +4 -4
- package/src/assets/scripts/hooks/git-marker +81 -0
- package/src/assets/scripts/hooks/json-helper.cjs +134 -51
- package/src/assets/scripts/hooks/json-parse +1 -1
- package/src/assets/scripts/hooks/lib/curation-move.cjs +1582 -0
- package/src/assets/scripts/hooks/lib/{learning-store.cjs → curation-store.cjs} +242 -118
- package/src/assets/scripts/hooks/lib/decisions-format.cjs +3 -3
- package/src/assets/scripts/hooks/lib/mkdir-lock.cjs +24 -7
- package/src/assets/scripts/hooks/lib/project-paths.cjs +132 -39
- package/src/assets/scripts/hooks/lib/render-decisions.cjs +34 -22
- package/src/assets/scripts/hooks/lib/tracked-areas.cjs +259 -0
- package/src/assets/scripts/hooks/memory-worker +1 -1
- package/src/assets/scripts/hooks/pre-compact-memory +6 -16
- package/src/assets/scripts/hooks/queue-append +228 -103
- package/src/assets/scripts/hooks/resolve-project-root +2 -2
- package/src/assets/scripts/hooks/session-start-context +143 -69
- package/src/assets/scripts/hooks/session-start-memory +3 -43
- package/src/assets/scripts/lib/project-config.cjs +12 -5
- package/src/assets/scripts/resolve-base.cjs +1043 -0
- package/src/assets/scripts/resolve-settings.cjs +42 -18
- package/src/assets/skills/agent-references/SKILL.md +5 -2
- package/src/assets/skills/agent-references/references/commands/implement-levels.md +27 -0
- package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +6 -4
- package/src/assets/skills/agent-references/references/commands/resolve-levels.md +24 -0
- package/src/assets/skills/apply-decisions/SKILL.md +1 -1
- package/src/assets/skills/docs-framework/SKILL.md +7 -7
- package/src/assets/skills/docs-framework/references/patterns.md +3 -4
- package/src/assets/skills/quality-gates/SKILL.md +1 -1
- package/src/assets/skills/worktree-support/references/roots.md +2 -2
- package/templates/lint-pack/.lint-pack.conf +86 -0
- package/templates/lint-pack/README.md +190 -0
- package/templates/lint-pack/lint-pack.sh +905 -0
- package/dist/core/learning-queue-cleanup.js +0 -27
- package/dist/core/learning-store.js +0 -61
- /package/dist/{learning-off → curation-off}/agents/debug.md +0 -0
- /package/dist/{learning-off → curation-off}/agents/design.md +0 -0
- /package/dist/{learning-off → curation-off}/agents/harden.md +0 -0
- /package/dist/{learning-off → curation-off}/agents/knowledge.md +0 -0
- /package/dist/{learning-off → curation-off}/agents/research.md +0 -0
- /package/dist/{learning-off → curation-off}/agents/skim.md +0 -0
- /package/dist/{learning-off → curation-off}/commands/dynamic-plan.md +0 -0
- /package/dist/{learning-off → curation-off}/commands/dynamic-profile.md +0 -0
- /package/dist/{learning-off → curation-off}/commands/dynamic-tickets.md +0 -0
- /package/dist/{learning-off → curation-off}/commands/release.md +0 -0
- /package/dist/{learning-off → curation-off}/commands/research.md +0 -0
|
@@ -6,35 +6,47 @@
|
|
|
6
6
|
# under a lock. Used by capture-prompt, capture-turn, and capture-question so the
|
|
7
7
|
# per-feature dual-queue-append logic exists in exactly one place.
|
|
8
8
|
#
|
|
9
|
-
# Source-order requirement: source json-parse (for _HAS_JQ) and
|
|
10
|
-
#
|
|
9
|
+
# Source-order requirement: source json-parse (for _HAS_JQ) and curation-lock (for
|
|
10
|
+
# curation_lock_acquire/curation_lock_release, which itself requires get-mtime) BEFORE
|
|
11
11
|
# calling queue_append_row or queue_append_both, and define log/dbg. This file
|
|
12
|
-
# sources its
|
|
13
|
-
#
|
|
12
|
+
# sources its siblings git-marker (for df_no_symlink_below) and curation-move (for the
|
|
13
|
+
# ledger folder's names, df_curation_move_names) itself. Sourcing it only defines
|
|
14
|
+
# functions -- no side effects until they are called.
|
|
14
15
|
#
|
|
15
16
|
# queue_append_row <root> <queue_file> <role> <content> <ts>
|
|
16
17
|
# Appends one JSONL row {role, content, ts} to <queue_file>, which lies below
|
|
17
|
-
# <root>: the project root for the memory queue, the ledger root for the
|
|
18
|
+
# <root>: the project root for the memory queue, the ledger root for the curation
|
|
18
19
|
# queue. When the queue, or a folder between <root> and it, is a symbolic link,
|
|
19
20
|
# nothing is written: the skip is logged once and the call returns 0, as a
|
|
20
|
-
# skipped capture does (D-HOOKS-NO-SYMLINK, git-marker). Otherwise it
|
|
21
|
-
#
|
|
21
|
+
# skipped capture does (D-HOOKS-NO-SYMLINK, git-marker). Otherwise it builds the
|
|
22
|
+
# row first (a row that fails to build writes nothing), then makes one builtin
|
|
23
|
+
# append under umask 077, so a queue it creates has mode 0600. The umask is the
|
|
24
|
+
# caller's again when the append returns, so a folder made later in the same hook
|
|
25
|
+
# has the mode its parent's umask gives. A ledger queue follows the creator rule
|
|
26
|
+
# (git-marker, df_ledger_queue): it is appended to in the
|
|
27
|
+
# old learning/ folder while that exists, which is never created here; the new
|
|
28
|
+
# curation/ folder is created only when the old name is absent. An append that
|
|
29
|
+
# finds the old folder gone (the one-time move ran meanwhile) looks again and goes
|
|
30
|
+
# on in the new folder, so a capture never drops a row and never waits. Any other
|
|
31
|
+
# queue's folder is created if absent.
|
|
22
32
|
# After appending, truncates from 200 to the newest 100 lines under a lock
|
|
23
|
-
# (
|
|
33
|
+
# (curation_lock_acquire on "<queue_file>.lock", 2s timeout), keeping mode 0600.
|
|
24
34
|
# The append itself is intentionally lock-free (accepted-class race shared with
|
|
25
35
|
# the pre-existing memory design -- see the design doc's "Append-vs-claim race"
|
|
26
36
|
# note).
|
|
27
37
|
#
|
|
28
|
-
# queue_append_both <memory_root> <memory_queue> <
|
|
29
|
-
#
|
|
30
|
-
# root that queue lies below. Each queue is gated
|
|
31
|
-
# compute memory_enabled/
|
|
32
|
-
# for the at-most-one-fork read of both).
|
|
38
|
+
# queue_append_both <memory_root> <memory_queue> <curation_root> <curation_queue> <memory_enabled> <curation_enabled> <role> <content> <ts>
|
|
39
|
+
# Builds the row once, then appends it as queue_append_row does to each queue whose
|
|
40
|
+
# *_enabled flag is "true", with the root that queue lies below. Each queue is gated
|
|
41
|
+
# independently -- callers compute memory_enabled/curation_enabled themselves (see
|
|
42
|
+
# queue_read_gates below for the at-most-one-fork read of both). With neither queue
|
|
43
|
+
# enabled it builds nothing.
|
|
33
44
|
#
|
|
34
45
|
# queue_read_gates <manifest_path> [<root>]
|
|
35
|
-
# Reads BOTH switches, `memory` and `
|
|
46
|
+
# Reads BOTH switches, `memory` and `curation` (curation is read under both of
|
|
47
|
+
# its keys, the current one and the pre-rename copy), in at most ONE subprocess fork
|
|
36
48
|
# (AC-P1 -- at most one gate-read fork per capture hook, not two; none when
|
|
37
|
-
# the fast paths settle it). Sets _QG_MEMORY and
|
|
49
|
+
# the fast paths settle it). Sets _QG_MEMORY and _QG_CURATION ("true"/"false")
|
|
38
50
|
# in the caller's scope. The two values are newline-separated
|
|
39
51
|
# rather than using a control-character delimiter: they are always the literal
|
|
40
52
|
# strings "true"/"false", never arbitrary content, so a plain newline split is
|
|
@@ -44,35 +56,43 @@
|
|
|
44
56
|
# machine switch is on AND neither repository file sets `features.<name>` to
|
|
45
57
|
# the literal `false` -- the rule resolve-settings.cjs folds, held to the same
|
|
46
58
|
# answers by the shared table tests/fixtures/settings-switch-table.ts (TP-49).
|
|
47
|
-
# A repository can switch memory or
|
|
59
|
+
# A repository can switch memory or curation off for itself, never back on.
|
|
48
60
|
# machine <manifest_path>, the ~/.devflow/manifest.json `devflow init` and
|
|
49
|
-
# `devflow memory|
|
|
61
|
+
# `devflow memory|curation --enable/--disable` write. Only an
|
|
50
62
|
# explicit boolean `false` switches a feature off -- an absent,
|
|
51
63
|
# unreadable or malformed manifest, a missing key, or a non-boolean
|
|
52
64
|
# value leaves it ON (fail-open), the rule
|
|
53
65
|
# isMachineFeatureOn() applies in the CLI.
|
|
54
66
|
# project <root>/.devflow/project.json (team-committed) and
|
|
55
67
|
# personal <root>/.devflow/config.json (this worktree's own), read ONLY under
|
|
56
|
-
# `features.memory`
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
#
|
|
68
|
+
# `features.memory` and, for curation, `features.curation` or
|
|
69
|
+
# `features.learning` (a narrow-only union: a literal false in
|
|
70
|
+
# either key turns curation off, and each key is parsed on its
|
|
71
|
+
# own). The retired top-level keys (`memory`, `curation`,
|
|
72
|
+
# `learning`, `decisions`) and `features.decisions` narrow nothing
|
|
73
|
+
# (#378: reading them let a feature run in every repo but one). An
|
|
74
|
+
# invalid file -- unparseable, over 4096 bytes, a BOM, a symlink, a
|
|
75
|
+
# duplicated key -- narrows nothing.
|
|
61
76
|
# <root> is the checkout's toplevel (DF_ROOT, which every caller holds as
|
|
62
|
-
# $PROJECT_ROOT), for
|
|
77
|
+
# $PROJECT_ROOT), for curation too: project.json is committed per branch and
|
|
63
78
|
# config.json lives per worktree, so both are read where the session runs, as
|
|
64
79
|
# resolve-settings.cjs reads them -- never at DF_LEDGER_ROOT, which only says
|
|
65
80
|
# where the queue lands. Without <root> (or with an empty one) no repository
|
|
66
81
|
# file is read.
|
|
67
82
|
#
|
|
68
|
-
#
|
|
69
|
-
# `features.
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
#
|
|
83
|
+
# The machine rule for curation (#529), the one isMachineFeatureOn() applies:
|
|
84
|
+
# off when `features.curation` or `features.learning` is a boolean false; else
|
|
85
|
+
# on when either is a boolean true; else, when neither is a boolean, the
|
|
86
|
+
# pre-rename `features.decisions` decides if it is a boolean; else on. A
|
|
87
|
+
# non-boolean value is ignored for that key only, so a disagreeing pair
|
|
88
|
+
# resolves to off.
|
|
74
89
|
#
|
|
75
|
-
#
|
|
90
|
+
# D-LEARNING-LEGACY-DECISIONS: in the MANIFEST only, the pre-rename
|
|
91
|
+
# `features.decisions` is read when neither current key is a boolean --
|
|
92
|
+
# readManifest's migration precedence exactly, so a legacy `decisions: false`
|
|
93
|
+
# switches curation off before any command has healed the file.
|
|
94
|
+
#
|
|
95
|
+
# Every hook that gates on memory or curation reads it through here: the
|
|
76
96
|
# capture hooks, session-start-context (Sections 1-2), memory-worker,
|
|
77
97
|
# session-start-memory, pre-compact-memory and background-memory-update. The
|
|
78
98
|
# callers pass <manifest_path> as $HOME/.devflow/manifest.json, the machine
|
|
@@ -81,80 +101,179 @@
|
|
|
81
101
|
|
|
82
102
|
# queue_append_row checks its queue with df_no_symlink_below, so this helper sources
|
|
83
103
|
# git-marker itself rather than trusting every caller to have done so first. One that
|
|
84
|
-
#
|
|
85
|
-
|
|
104
|
+
# is missing is a command not found: the refusing branch, so nothing is written.
|
|
105
|
+
# The ledger folder's names come from the move shim (df_curation_move_names), sourced
|
|
106
|
+
# the same way and refused the same way.
|
|
107
|
+
#
|
|
108
|
+
# Each source is guarded by a builtin readability test, because bash 3.2 under `set -e`
|
|
109
|
+
# ends the shell at a `source` of a missing file, even inside `|| true`. Without the test
|
|
110
|
+
# the refusing branch would never run, and the capture hooks would stop here.
|
|
111
|
+
[ -r "${BASH_SOURCE[0]%/*}/git-marker" ] && source "${BASH_SOURCE[0]%/*}/git-marker" 2>/dev/null || true
|
|
112
|
+
[ -r "${BASH_SOURCE[0]%/*}/curation-move" ] && source "${BASH_SOURCE[0]%/*}/curation-move" 2>/dev/null || true
|
|
86
113
|
|
|
87
|
-
|
|
88
|
-
|
|
114
|
+
# The caller's umask, read once per process. A queue is created 0600, so the append
|
|
115
|
+
# runs under umask 077 -- and the rest of the hook must run under the umask its parent
|
|
116
|
+
# gave it, or a folder the next append makes would be 0700. `$(umask)` is one subshell
|
|
117
|
+
# with no exec, and it runs on the first append only; each append gives this value
|
|
118
|
+
# back, so the umask the process holds when it first appends is the one it keeps.
|
|
119
|
+
_QAR_UMASK=""
|
|
120
|
+
|
|
121
|
+
# _qar_build_row <role> <content> <ts> -- sets _QAR_ROW to the JSONL row, built by jq
|
|
122
|
+
# (--arg, --argjson) or node (JSON.stringify), never by string concatenation. A
|
|
123
|
+
# predicate: it succeeds only for a row that built, and a row that fails to build is
|
|
124
|
+
# empty, so nothing partial can reach a queue. Call it in an `if` or an `||` list.
|
|
125
|
+
_qar_build_row() {
|
|
126
|
+
_QAR_ROW=""
|
|
127
|
+
if [ "$_HAS_JQ" = "true" ]; then
|
|
128
|
+
_QAR_ROW=$(jq -n -c --arg role "$1" --arg content "$2" --argjson ts "$3" \
|
|
129
|
+
'{role: $role, content: $content, ts: $ts}') || _QAR_ROW=""
|
|
130
|
+
else
|
|
131
|
+
_QAR_ROW=$(node -e "process.stdout.write(JSON.stringify({role:process.argv[1],content:process.argv[2],ts:parseInt(process.argv[3])}))" \
|
|
132
|
+
-- "$1" "$2" "$3") || _QAR_ROW=""
|
|
133
|
+
fi
|
|
134
|
+
if [ -z "$_QAR_ROW" ]; then
|
|
135
|
+
log "Queue skipped: the row could not be built; nothing written"
|
|
136
|
+
return 1
|
|
137
|
+
fi
|
|
138
|
+
return 0
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# _qar_write <queue_file> <row> -- one builtin append, under umask 077 so a queue it
|
|
142
|
+
# creates has mode 0600 (SEC-2). The umask is the caller's again before it returns,
|
|
143
|
+
# whether the append worked or not. The redirection error is silenced by 2>/dev/null
|
|
144
|
+
# written BEFORE it. A predicate: it fails when the row was not written.
|
|
145
|
+
_qar_write() {
|
|
146
|
+
local _qar_status=0
|
|
147
|
+
umask 077
|
|
148
|
+
printf '%s\n' "$2" 2>/dev/null >> "$1" || _qar_status=1
|
|
149
|
+
umask "$_QAR_UMASK" 2>/dev/null || true
|
|
150
|
+
return "$_qar_status"
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
# _qar_append <root> <queue_file> <row> -- appends <row> to <queue_file>, which lies
|
|
154
|
+
# below <root>, and sets _QAR_FILE to the queue it reached (the old ledger folder, or
|
|
155
|
+
# the new one after a move). A predicate: it fails when nothing was written -- a link on
|
|
156
|
+
# the path (D-HOOKS-NO-SYMLINK: the skip is logged once) or a queue that could not be
|
|
157
|
+
# written -- and a skipped capture is not an error for the hook.
|
|
158
|
+
_qar_append() {
|
|
159
|
+
local _qar_root="$1" _qar_file="$2" _qar_row="$3"
|
|
160
|
+
local _qar_given="$2" _qar_old="" _qar_new=""
|
|
161
|
+
_QAR_FILE=""
|
|
162
|
+
|
|
163
|
+
# The names the creator rule below needs. A shim that failed to source is a command
|
|
164
|
+
# not found, so nothing is appended: no queue is written without them.
|
|
165
|
+
if ! df_curation_move_names 2>/dev/null; then
|
|
166
|
+
log "Queue skipped: the ledger folder names are unavailable; nothing written"
|
|
167
|
+
return 1
|
|
168
|
+
fi
|
|
169
|
+
_qar_old="$DF_CM_OLD_FOLDER"
|
|
170
|
+
_qar_new="$DF_CM_NEW_FOLDER"
|
|
171
|
+
|
|
172
|
+
# The creator rule (git-marker, df_ledger_queue): a ledger queue is appended to in
|
|
173
|
+
# the old folder while that folder is present, so a new name never hides an old
|
|
174
|
+
# ledger. Any other queue (the memory queue) comes back unchanged. A helper that
|
|
175
|
+
# failed to source is a command not found, so the queue stays as given and the
|
|
176
|
+
# check below, from the same library, refuses it.
|
|
177
|
+
if df_ledger_queue "$_qar_file" "$_qar_old" "$_qar_new"; then _qar_file="$DF_LEDGER_QUEUE"; fi
|
|
89
178
|
|
|
90
179
|
# D-HOOKS-NO-SYMLINK (git-marker): a skipped append is a skipped capture.
|
|
91
180
|
if ! df_no_symlink_below "$_qar_root" "$_qar_file"; then
|
|
92
181
|
log "Queue skipped: a symbolic link sits on the path to $_qar_file; nothing written"
|
|
93
|
-
return
|
|
182
|
+
return 1
|
|
94
183
|
fi
|
|
95
|
-
mkdir -p "${_qar_file%/*}" 2>/dev/null || true
|
|
96
184
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
185
|
+
# A folder is made only where this append is its creator: never the old ledger
|
|
186
|
+
# folder, which a move may rename away at any moment, and which a make-again would
|
|
187
|
+
# bring back beside the new one. An old folder that is gone leaves the append to
|
|
188
|
+
# fail, and the re-probe below decides again. A folder is made under the caller's
|
|
189
|
+
# umask: only the write below runs under 077.
|
|
190
|
+
case "${_qar_file%/*}" in
|
|
191
|
+
*/"$_qar_old") ;;
|
|
192
|
+
*) mkdir -p "${_qar_file%/*}" 2>/dev/null || true ;;
|
|
193
|
+
esac
|
|
100
194
|
|
|
101
|
-
if [ "$
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
195
|
+
if [ -z "$_QAR_UMASK" ]; then _QAR_UMASK=$(umask) || _QAR_UMASK=""; fi
|
|
196
|
+
if ! _qar_write "$_qar_file" "$_qar_row"; then
|
|
197
|
+
# The old folder may have been moved after the probe. Look again: when the old
|
|
198
|
+
# name is gone, the creator makes the new folder once and the row goes there.
|
|
199
|
+
df_ledger_queue "$_qar_given" "$_qar_old" "$_qar_new" || true
|
|
200
|
+
if [ "$DF_LEDGER_QUEUE" != "$_qar_file" ] \
|
|
201
|
+
&& df_no_symlink_below "$_qar_root" "$DF_LEDGER_QUEUE" \
|
|
202
|
+
&& mkdir -p "${DF_LEDGER_QUEUE%/*}" 2>/dev/null \
|
|
203
|
+
&& _qar_write "$DF_LEDGER_QUEUE" "$_qar_row"; then
|
|
204
|
+
_qar_file="$DF_LEDGER_QUEUE"
|
|
205
|
+
else
|
|
206
|
+
log "Queue append failed: $_qar_file could not be written"
|
|
207
|
+
return 1
|
|
208
|
+
fi
|
|
107
209
|
fi
|
|
210
|
+
_QAR_FILE="$_qar_file"
|
|
211
|
+
return 0
|
|
212
|
+
}
|
|
108
213
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
214
|
+
# _qar_trim_overflow <queue_file> -- overflow safety: past 200 lines, keep the newest
|
|
215
|
+
# 100, locked to prevent a multi-session race.
|
|
216
|
+
_qar_trim_overflow() {
|
|
217
|
+
local _qar_file="$1" _qar_lines="" _qar_lock="" _qar_tmp=""
|
|
218
|
+
[ -f "$_qar_file" ] || return 0
|
|
219
|
+
_qar_lines=$(wc -l < "$_qar_file" | tr -d ' ')
|
|
220
|
+
if [ "$_qar_lines" -gt 200 ]; then
|
|
221
|
+
_qar_lock="${_qar_file}.lock"
|
|
222
|
+
if curation_lock_acquire "$_qar_lock" 2; then
|
|
223
|
+
_qar_lines=$(wc -l < "$_qar_file" | tr -d ' ')
|
|
224
|
+
if [ "$_qar_lines" -gt 200 ]; then
|
|
225
|
+
_qar_tmp="${_qar_file}.tmp.$$"
|
|
226
|
+
# SEC-2: the trimmed copy is created under umask 077, so the renamed
|
|
227
|
+
# queue keeps mode 0600. mv replaces the inode, so the queue takes the
|
|
228
|
+
# copy's mode, and a chmod after the mv would leave a window. The copy is
|
|
229
|
+
# created only where nothing stands at its name: builtin tests come first,
|
|
230
|
+
# so an entry already there, a link to a FIFO or a device included, is
|
|
231
|
+
# never opened (noclobber alone would open one, as no regular file stands
|
|
232
|
+
# there), and noclobber then makes the create itself exclusive. Such an
|
|
233
|
+
# entry is removed instead, and the trim waits for a later append.
|
|
234
|
+
if [ ! -e "$_qar_tmp" ] && [ ! -L "$_qar_tmp" ] && (umask 077 && set -o noclobber && tail -100 "$_qar_file" > "$_qar_tmp") && mv "$_qar_tmp" "$_qar_file"; then
|
|
235
|
+
log "Queue overflow: truncated from $_qar_lines to 100 lines ($(basename "$_qar_file"))"
|
|
236
|
+
dbg "Queue overflow: truncated from $_qar_lines to 100 lines ($_qar_file)"
|
|
237
|
+
else
|
|
238
|
+
rm -f "$_qar_tmp" 2>/dev/null || true
|
|
239
|
+
log "Queue overflow: not truncated, its copy could not be written ($(basename "$_qar_file"))"
|
|
134
240
|
fi
|
|
135
|
-
learning_lock_release "$_qar_lock"
|
|
136
241
|
fi
|
|
242
|
+
curation_lock_release "$_qar_lock"
|
|
137
243
|
fi
|
|
138
244
|
fi
|
|
245
|
+
return 0
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
queue_append_row() {
|
|
249
|
+
_qar_build_row "$3" "$4" "$5" || return 0
|
|
250
|
+
if _qar_append "$1" "$2" "$_QAR_ROW"; then _qar_trim_overflow "$_QAR_FILE"; fi
|
|
251
|
+
return 0
|
|
139
252
|
}
|
|
140
253
|
|
|
141
254
|
queue_append_both() {
|
|
142
255
|
local _qab_memory_root="$1" _qab_memory_queue="$2"
|
|
143
|
-
local
|
|
144
|
-
local _qab_memory_enabled="$5"
|
|
145
|
-
local _qab_role="$7" _qab_content="$8" _qab_ts="$9"
|
|
256
|
+
local _qab_curation_root="$3" _qab_curation_queue="$4"
|
|
257
|
+
local _qab_memory_enabled="$5" _qab_curation_enabled="$6"
|
|
258
|
+
local _qab_role="$7" _qab_content="$8" _qab_ts="$9" _qab_row=""
|
|
259
|
+
|
|
260
|
+
if [ "$_qab_memory_enabled" != "true" ] && [ "$_qab_curation_enabled" != "true" ]; then return 0; fi
|
|
261
|
+
# One row for both queues: it is built once, and each enabled queue takes the same line.
|
|
262
|
+
_qar_build_row "$_qab_role" "$_qab_content" "$_qab_ts" || return 0
|
|
263
|
+
_qab_row="$_QAR_ROW"
|
|
146
264
|
|
|
147
265
|
if [ "$_qab_memory_enabled" = "true" ]; then
|
|
148
|
-
|
|
266
|
+
if _qar_append "$_qab_memory_root" "$_qab_memory_queue" "$_qab_row"; then _qar_trim_overflow "$_QAR_FILE"; fi
|
|
149
267
|
fi
|
|
150
|
-
if [ "$
|
|
151
|
-
|
|
268
|
+
if [ "$_qab_curation_enabled" = "true" ]; then
|
|
269
|
+
if _qar_append "$_qab_curation_root" "$_qab_curation_queue" "$_qab_row"; then _qar_trim_overflow "$_QAR_FILE"; fi
|
|
152
270
|
fi
|
|
271
|
+
return 0
|
|
153
272
|
}
|
|
154
273
|
|
|
155
274
|
# _qg_repo_file_can_narrow <file> -- succeeds when <file> COULD set
|
|
156
|
-
# features.memory or features.learning to false, so only the
|
|
157
|
-
# it; fails when it certainly cannot. Builtins only: no fork.
|
|
275
|
+
# features.memory, features.curation or features.learning to false, so only the
|
|
276
|
+
# parser can decide it; fails when it certainly cannot. Builtins only: no fork.
|
|
158
277
|
#
|
|
159
278
|
# D-GATES-FAST-PATH (repository files): the read is bounded -- `-n 4097`, which
|
|
160
279
|
# bash 3.2 supports -- and `read -d ''` succeeds only when it stops short of the
|
|
@@ -164,14 +283,14 @@ queue_append_both() {
|
|
|
164
283
|
# symlink and refuses a directory or FIFO, so nothing here can block; the parser
|
|
165
284
|
# itself refuses the symlink. A `\u` escape is the only JSON spelling of a
|
|
166
285
|
# letter other than the letter itself, so without one a narrowing file shows
|
|
167
|
-
# `"features"`, then `{`, then `"memory"` or `"learning"`, then
|
|
168
|
-
# literally and in that order. `.` spans newlines (POSIX ERE without
|
|
286
|
+
# `"features"`, then `{`, then `"memory"`, `"curation"` or `"learning"`, then
|
|
287
|
+
# `false`, literally and in that order. `.` spans newlines (POSIX ERE without
|
|
169
288
|
# REG_NEWLINE), so the match can only over-report -- an over-report costs one
|
|
170
289
|
# fork and the parser's answer, never a wrong one. No `"decisions"` and no
|
|
171
290
|
# top-level key before `features` is ever matched.
|
|
172
291
|
_qg_repo_file_can_narrow() {
|
|
173
292
|
local _qgf_text=""
|
|
174
|
-
local _qgf_re='"features"[[:space:]]*:[[:space:]]*[{].*"(memory|learning)"[[:space:]]*:[[:space:]]*false'
|
|
293
|
+
local _qgf_re='"features"[[:space:]]*:[[:space:]]*[{].*"(memory|curation|learning)"[[:space:]]*:[[:space:]]*false'
|
|
175
294
|
[ -f "$1" ] || return 1
|
|
176
295
|
if IFS= read -r -d '' -n 4097 _qgf_text 2>/dev/null < "$1"; then return 1; fi
|
|
177
296
|
case "$_qgf_text" in *'\u'*) return 0 ;; esac
|
|
@@ -180,22 +299,24 @@ _qg_repo_file_can_narrow() {
|
|
|
180
299
|
|
|
181
300
|
queue_read_gates() {
|
|
182
301
|
local _qg_manifest="${1:-}" _qg_root="${2:-}" _qg_text="" _qg_parse="false" _qg_repo="false" _qg_fields=""
|
|
302
|
+
local _qg_scripts="${BASH_SOURCE[0]%/*}/.."
|
|
183
303
|
_QG_MEMORY="true"
|
|
184
|
-
|
|
304
|
+
_QG_CURATION="true"
|
|
185
305
|
|
|
186
306
|
if [ -n "$_qg_manifest" ] && [ -f "$_qg_manifest" ]; then
|
|
187
307
|
# D-GATES-FAST-PATH: every capture hook and every session start pays this
|
|
188
308
|
# read, and on most machines both switches are on. Only a JSON boolean
|
|
189
|
-
# `false` under a "memory", "learning" or "decisions" key can
|
|
190
|
-
# off, and a `\u` escape is the only JSON spelling of a
|
|
191
|
-
# letter itself, so without one such a key appears
|
|
192
|
-
# no `\u` and no key-then-`false` sequence
|
|
193
|
-
# off -- valid or not, it reads both-on
|
|
194
|
-
# builtins (read, case, [[ =~ ]]) with
|
|
195
|
-
# the full parse below. `read -d ''`
|
|
196
|
-
# i.e. before the end of the file, so a
|
|
197
|
-
# too. An unreadable file reads empty
|
|
198
|
-
|
|
309
|
+
# `false` under a "memory", "curation", "learning" or "decisions" key can
|
|
310
|
+
# switch anything off, and a `\u` escape is the only JSON spelling of a
|
|
311
|
+
# letter other than the letter itself, so without one such a key appears
|
|
312
|
+
# literally. A text holding no `\u` and no key-then-`false` sequence
|
|
313
|
+
# therefore cannot switch anything off -- valid or not, it reads both-on
|
|
314
|
+
# either way -- and is settled by shell builtins (read, case, [[ =~ ]]) with
|
|
315
|
+
# no jq/node fork; everything else takes the full parse below. `read -d ''`
|
|
316
|
+
# succeeds only on reaching a NUL byte, i.e. before the end of the file, so a
|
|
317
|
+
# text it cut short is parsed in full too. An unreadable file reads empty
|
|
318
|
+
# and fails open, as the parser would.
|
|
319
|
+
local _qg_re='"(memory|curation|learning|decisions)"[[:space:]]*:[[:space:]]*false'
|
|
199
320
|
IFS= read -r -d '' _qg_text 2>/dev/null < "$_qg_manifest" && _qg_parse="true"
|
|
200
321
|
case "$_qg_text" in *'\u'*) _qg_parse="true" ;; esac
|
|
201
322
|
if [[ $_qg_text =~ $_qg_re ]]; then _qg_parse="true"; fi
|
|
@@ -216,7 +337,7 @@ queue_read_gates() {
|
|
|
216
337
|
# manifest too, read only when the fast path above flagged it (an unflagged
|
|
217
338
|
# manifest is both-on), so a false in all three layers still costs one fork.
|
|
218
339
|
# jq cannot see duplicate keys, so this path is node whatever _HAS_JQ says.
|
|
219
|
-
local
|
|
340
|
+
local _qg_manifest_arg=""
|
|
220
341
|
[ "$_qg_parse" = "true" ] && _qg_manifest_arg="$_qg_manifest"
|
|
221
342
|
_qg_fields=$(node -e "
|
|
222
343
|
const S = require(require('path').resolve(process.argv[1], 'resolve-settings.cjs'));
|
|
@@ -225,7 +346,7 @@ queue_read_gates() {
|
|
|
225
346
|
try { m = JSON.parse(require('fs').readFileSync(process.argv[2], 'utf8')); } catch (_) { m = undefined; }
|
|
226
347
|
}
|
|
227
348
|
const w = S.foldSettings(Object.assign({ manifest: m }, S.readRepoLayers(process.argv[3]))).switches;
|
|
228
|
-
process.stdout.write(String(w.memory.on) + String.fromCharCode(10) + String(w.
|
|
349
|
+
process.stdout.write(String(w.memory.on) + String.fromCharCode(10) + String(w.curation.on));
|
|
229
350
|
" -- "$_qg_scripts" "$_qg_manifest_arg" "$_qg_root" 2>/dev/null) || _qg_fields=""
|
|
230
351
|
fi
|
|
231
352
|
|
|
@@ -237,20 +358,24 @@ queue_read_gates() {
|
|
|
237
358
|
# `try` covers a manifest whose top level or `features` is not an object; a
|
|
238
359
|
# parse failure empties the output, which reads as "not switched off". The
|
|
239
360
|
# comma produces two raw-output lines (newline-separated) in one jq process.
|
|
361
|
+
# Curation (#529): off when a boolean curation or learning is false; else,
|
|
362
|
+
# when neither is a boolean, off when decisions is false (isMachineFeatureOn).
|
|
240
363
|
_qg_fields=$(jq -r '
|
|
241
364
|
(if (try .features.memory catch null) == false then "false" else "true" end),
|
|
242
|
-
(if (try (.features
|
|
365
|
+
(if (try (.features
|
|
366
|
+
| ([.curation, .learning] | map(select(type == "boolean"))) as $c
|
|
367
|
+
| if ($c | length) > 0 then any($c[]; . == false) else .decisions == false end
|
|
368
|
+
) catch false)
|
|
243
369
|
then "false" else "true" end)
|
|
244
370
|
' "$_qg_manifest" 2>/dev/null) || _qg_fields=""
|
|
245
371
|
else
|
|
372
|
+
# No jq: the fold is the resolver's own machineSwitchOn (resolve-settings.cjs),
|
|
373
|
+
# so the machine rule is not copied here. One node fork, as for jq's one process.
|
|
246
374
|
_qg_fields=$(node -e "
|
|
375
|
+
const S = require(require('path').resolve(process.argv[2], 'resolve-settings.cjs'));
|
|
247
376
|
const m = JSON.parse(require('fs').readFileSync(process.argv[1], 'utf8'));
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const on = (k) => !(isObj && f[k] === false);
|
|
251
|
-
const learnKey = isObj && typeof f.learning !== 'boolean' ? 'decisions' : 'learning';
|
|
252
|
-
process.stdout.write(String(on('memory')) + String.fromCharCode(10) + String(on(learnKey)));
|
|
253
|
-
" -- "$_qg_manifest" 2>/dev/null) || _qg_fields=""
|
|
377
|
+
process.stdout.write(String(S.machineSwitchOn(m, 'memory')) + String.fromCharCode(10) + String(S.machineSwitchOn(m, 'curation')));
|
|
378
|
+
" -- "$_qg_manifest" "$_qg_scripts" 2>/dev/null) || _qg_fields=""
|
|
254
379
|
fi
|
|
255
380
|
fi
|
|
256
381
|
|
|
@@ -258,7 +383,7 @@ queue_read_gates() {
|
|
|
258
383
|
case "$_qg_fields" in
|
|
259
384
|
true$'\n'true|true$'\n'false|false$'\n'true|false$'\n'false)
|
|
260
385
|
_QG_MEMORY="${_qg_fields%%$'\n'*}"
|
|
261
|
-
|
|
386
|
+
_QG_CURATION="${_qg_fields#*$'\n'}"
|
|
262
387
|
;;
|
|
263
388
|
esac
|
|
264
389
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
# root, mirroring the TS CLI's getGitRoot() (src/core/git.ts) so the shell
|
|
9
9
|
# side anchors identically.
|
|
10
10
|
#
|
|
11
|
-
# Sourced by: the memory/
|
|
11
|
+
# Sourced by: the memory/curation/session hooks and ensure-devflow-init.
|
|
12
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
|
#
|
|
@@ -53,7 +53,7 @@ _df_nongit_root() {
|
|
|
53
53
|
# df_resolve_roots <cwd> sets TWO roots in the caller's scope, from ONE git call:
|
|
54
54
|
# DF_ROOT — the checkout's toplevel: where per-checkout data lives
|
|
55
55
|
# (memory, the .gitignore carve-out, the knowledge bases).
|
|
56
|
-
# DF_LEDGER_ROOT — where the
|
|
56
|
+
# DF_LEDGER_ROOT — where the curation ledger and its queue live.
|
|
57
57
|
#
|
|
58
58
|
# D-LEDGER-MAIN-WORKTREE: a linked worktree (`git worktree add`, `claude
|
|
59
59
|
# --worktree`) has its own toplevel, so a ledger anchored there restarts its
|