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
|
@@ -1,19 +1,41 @@
|
|
|
1
1
|
import { promises as fs } from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { writeFileAtomicExclusive } from './fs-atomic.js';
|
|
4
|
+
import { MANIFEST_LOCK_WAIT_MS, withManifestLock } from './manifest-lock.js';
|
|
4
5
|
function isJsonObject(value) {
|
|
5
6
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
6
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* The keys a RAW manifest may hold a machine feature under, current names first.
|
|
10
|
+
*
|
|
11
|
+
* `curation` is stored under two current keys, `curation` and `learning`: the
|
|
12
|
+
* feature was renamed, and a manifest written before the rename holds only
|
|
13
|
+
* `learning`. No precedence rule exists between the two (see
|
|
14
|
+
* {@link isMachineFeatureOn}); every write sets both. `memory` and `knowledge`
|
|
15
|
+
* have one current key.
|
|
16
|
+
*/
|
|
17
|
+
const CURRENT_KEYS = {
|
|
18
|
+
memory: ['memory'],
|
|
19
|
+
curation: ['curation', 'learning'],
|
|
20
|
+
knowledge: ['knowledge'],
|
|
21
|
+
};
|
|
7
22
|
/**
|
|
8
23
|
* The pre-rename key each machine feature was stored under, where one exists:
|
|
9
|
-
* `
|
|
24
|
+
* `curation` was `decisions` and `knowledge` was `kb`. A manifest no
|
|
10
25
|
* command has rewritten since the rename can still hold only the legacy key.
|
|
11
26
|
* `memory` was never renamed.
|
|
12
27
|
*/
|
|
13
28
|
const LEGACY_KEYS = {
|
|
14
|
-
|
|
29
|
+
curation: 'decisions',
|
|
15
30
|
knowledge: 'kb',
|
|
16
31
|
};
|
|
32
|
+
/**
|
|
33
|
+
* The keys a write sets for `feature`: the feature's own key, and for `curation` its
|
|
34
|
+
* pre-rename copy too (the manifest mirror). Opt-in switches have one key.
|
|
35
|
+
*/
|
|
36
|
+
function writtenKeys(feature) {
|
|
37
|
+
return feature === 'behavior' ? [feature] : CURRENT_KEYS[feature];
|
|
38
|
+
}
|
|
17
39
|
/**
|
|
18
40
|
* Whether a RAW parsed manifest leaves `feature` switched on. Pure.
|
|
19
41
|
*
|
|
@@ -23,13 +45,23 @@ const LEGACY_KEYS = {
|
|
|
23
45
|
* shell hooks, so the CLI's status and the runtime never disagree about the
|
|
24
46
|
* same file.
|
|
25
47
|
*
|
|
48
|
+
* The machine rule (issue #529), identical in every reader —
|
|
49
|
+
* this function, resolve-settings.cjs and the shell gates:
|
|
50
|
+
* 1. off when ANY current key (`curation`, `learning`) is a boolean `false`;
|
|
51
|
+
* 2. else on when any current key is a boolean `true`;
|
|
52
|
+
* 3. else, when neither current key is a boolean, the pre-rename key decides
|
|
53
|
+
* if it is a boolean (`decisions` for curation, `kb` for knowledge);
|
|
54
|
+
* 4. else ON.
|
|
55
|
+
* A non-boolean value is ignored for that key only. A disagreeing pair
|
|
56
|
+
* therefore resolves to OFF: false wins, so a switch a user turned off never
|
|
57
|
+
* comes back on because the other spelling still says true.
|
|
58
|
+
*
|
|
26
59
|
* D-LEARNING-LEGACY-DECISIONS (a sub-decision of D-FEATURES-NARROW-ONLY):
|
|
27
|
-
* `
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* precedence.
|
|
60
|
+
* the legacy `features.decisions` is read only when neither current key is a
|
|
61
|
+
* boolean — readManifest's migration precedence exactly. The legacy key is
|
|
62
|
+
* otherwise honoured only once some command happens to run readManifest and
|
|
63
|
+
* heal it, so a `decisions: false` would keep curation running until then.
|
|
64
|
+
* queue_read_gates applies the same precedence.
|
|
33
65
|
*
|
|
34
66
|
* D-KNOWLEDGE-LEGACY-KB (the same sub-decision for the other renamed key):
|
|
35
67
|
* `knowledge` is read as `features.knowledge` when that is a boolean, else the
|
|
@@ -51,11 +83,13 @@ export function isMachineFeatureOn(rawManifest, feature) {
|
|
|
51
83
|
const features = rawManifest.features;
|
|
52
84
|
if (!isJsonObject(features))
|
|
53
85
|
return true;
|
|
86
|
+
const current = CURRENT_KEYS[feature]
|
|
87
|
+
.map((key) => features[key])
|
|
88
|
+
.filter((value) => typeof value === 'boolean');
|
|
89
|
+
if (current.length > 0)
|
|
90
|
+
return !current.includes(false);
|
|
54
91
|
const legacyKey = LEGACY_KEYS[feature];
|
|
55
|
-
|
|
56
|
-
? features[legacyKey]
|
|
57
|
-
: features[feature];
|
|
58
|
-
return value !== false;
|
|
92
|
+
return legacyKey === undefined || features[legacyKey] !== false;
|
|
59
93
|
}
|
|
60
94
|
/**
|
|
61
95
|
* Set `feature` in a RAW parsed manifest. Pure — returns a new object and never
|
|
@@ -63,7 +97,9 @@ export function isMachineFeatureOn(rawManifest, feature) {
|
|
|
63
97
|
* is no `features` record to write into).
|
|
64
98
|
*
|
|
65
99
|
* Only `features.<feature>` and `updatedAt` change; every other key is carried
|
|
66
|
-
* verbatim. Writing `
|
|
100
|
+
* verbatim. Writing `curation` sets BOTH current keys (`curation` and its
|
|
101
|
+
* pre-rename copy `learning`) to the one value, so a disagreeing pair is
|
|
102
|
+
* overwritten, never carried. It leaves a legacy `decisions` key in place, and
|
|
67
103
|
* writing `knowledge` a legacy `kb` key, inert: the boolean just written wins
|
|
68
104
|
* over it (D-LEARNING-LEGACY-DECISIONS, D-KNOWLEDGE-LEGACY-KB). Going through
|
|
69
105
|
* readManifest()/writeManifest() instead would refuse a manifest that reader
|
|
@@ -74,21 +110,28 @@ export function isMachineFeatureOn(rawManifest, feature) {
|
|
|
74
110
|
export function setMachineFeature(rawManifest, feature, enabled, now) {
|
|
75
111
|
if (!isJsonObject(rawManifest) || !isJsonObject(rawManifest.features))
|
|
76
112
|
return null;
|
|
113
|
+
const written = {};
|
|
114
|
+
for (const key of writtenKeys(feature))
|
|
115
|
+
written[key] = enabled;
|
|
77
116
|
return {
|
|
78
117
|
...rawManifest,
|
|
79
|
-
features: { ...rawManifest.features,
|
|
118
|
+
features: { ...rawManifest.features, ...written },
|
|
80
119
|
updatedAt: now,
|
|
81
120
|
};
|
|
82
121
|
}
|
|
83
122
|
/**
|
|
84
123
|
* Whether a RAW parsed manifest already records `feature` as exactly `enabled`:
|
|
85
|
-
* `
|
|
124
|
+
* every key a write sets (`writtenKeys`) is that boolean. Pure. Stricter than
|
|
86
125
|
* {@link isMachineFeatureOn} on purpose — an absent key, a non-boolean value and
|
|
87
|
-
* a legacy key all read as a state without being recorded as it,
|
|
88
|
-
*
|
|
126
|
+
* a legacy key all read as a state without being recorded as it, and for
|
|
127
|
+
* `curation` a disagreeing pair, or one key alone, is a copy that disagrees or is
|
|
128
|
+
* missing: not recorded, so the write that follows puts both keys right.
|
|
89
129
|
*/
|
|
90
130
|
function isMachineFeatureRecorded(rawManifest, feature, enabled) {
|
|
91
|
-
|
|
131
|
+
if (!isJsonObject(rawManifest) || !isJsonObject(rawManifest.features))
|
|
132
|
+
return false;
|
|
133
|
+
const features = rawManifest.features;
|
|
134
|
+
return writtenKeys(feature).every((key) => features[key] === enabled);
|
|
92
135
|
}
|
|
93
136
|
async function readRawManifest(devflowDir) {
|
|
94
137
|
try {
|
|
@@ -136,16 +179,49 @@ export async function readOptInFeature(devflowDir, feature) {
|
|
|
136
179
|
* D-NOOP-TOGGLE: a toggle to the value the manifest already records writes
|
|
137
180
|
* nothing. The only bytes such a write could change are `updatedAt`, and that
|
|
138
181
|
* field means "the manifest's content last changed", so a no-op must not move
|
|
139
|
-
* it. This is the one write point of `devflow memory|
|
|
182
|
+
* it. This is the one write point of `devflow memory|curation|knowledge|behavior
|
|
140
183
|
* --enable/--disable`, so the rule holds for all four at once.
|
|
184
|
+
*
|
|
185
|
+
* The read and the write run under the manifest lock (manifest-lock.ts), so the
|
|
186
|
+
* heal in readManifest cannot write an older snapshot over this write. The wait is
|
|
187
|
+
* bounded, and longer than the stale age, so a lock left by a dead process is broken
|
|
188
|
+
* inside it. A lock that is still not taken after that wait cannot be broken, and
|
|
189
|
+
* the lock then protects nothing: the toggle goes ahead without it rather than refuse
|
|
190
|
+
* an explicit switch. Just before its write the toggle checks that the lock directory is
|
|
191
|
+
* still its own; when another caller took it, the toggle takes the lock again, at most
|
|
192
|
+
* {@link MANIFEST_LOCK_ATTEMPTS} times and within the same wait, and then goes ahead
|
|
193
|
+
* without it.
|
|
141
194
|
*/
|
|
142
195
|
export async function writeMachineFeature(devflowDir, feature, enabled) {
|
|
196
|
+
const deadline = Date.now() + MANIFEST_LOCK_WAIT_MS;
|
|
197
|
+
for (let attempt = 0; attempt < MANIFEST_LOCK_ATTEMPTS; attempt++) {
|
|
198
|
+
const timeoutMs = Math.max(0, deadline - Date.now());
|
|
199
|
+
const locked = await withManifestLock(devflowDir, { timeoutMs }, (lock) => recordMachineFeature(devflowDir, feature, enabled, lock.stillHeld));
|
|
200
|
+
if (!locked.ok)
|
|
201
|
+
break;
|
|
202
|
+
if (locked.value !== LOCK_LOST)
|
|
203
|
+
return locked.value;
|
|
204
|
+
}
|
|
205
|
+
return recordMachineFeature(devflowDir, feature, enabled);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* How many times a toggle takes the lock when it finds, just before its write, that
|
|
209
|
+
* another caller took the lock directory from it (manifest-lock.ts). The tries share
|
|
210
|
+
* the one wait, {@link MANIFEST_LOCK_WAIT_MS}. After them the toggle goes ahead without
|
|
211
|
+
* the lock, as it does when the wait ends.
|
|
212
|
+
*/
|
|
213
|
+
const MANIFEST_LOCK_ATTEMPTS = 3;
|
|
214
|
+
/** `recordMachineFeature` did not write: the lock was taken from this toggle first. */
|
|
215
|
+
const LOCK_LOST = 'lock-lost';
|
|
216
|
+
async function recordMachineFeature(devflowDir, feature, enabled, stillHeld) {
|
|
143
217
|
const raw = await readRawManifest(devflowDir);
|
|
144
218
|
if (isMachineFeatureRecorded(raw, feature, enabled))
|
|
145
219
|
return { ok: true, value: undefined };
|
|
146
220
|
const next = setMachineFeature(raw, feature, enabled, new Date().toISOString());
|
|
147
221
|
if (next === null)
|
|
148
222
|
return { ok: false, error: 'not-installed' };
|
|
223
|
+
if (stillHeld !== undefined && !(await stillHeld()))
|
|
224
|
+
return LOCK_LOST;
|
|
149
225
|
await writeFileAtomicExclusive(path.join(devflowDir, 'manifest.json'), JSON.stringify(next, null, 2) + '\n');
|
|
150
226
|
return { ok: true, value: undefined };
|
|
151
227
|
}
|
package/dist/core/fs-atomic.js
CHANGED
|
@@ -6,7 +6,7 @@ import { promises as fs } from 'fs';
|
|
|
6
6
|
*
|
|
7
7
|
* Call sites: used by the CLI's exclusive-write call sites (migrations, init, post-install,
|
|
8
8
|
* uninstall, security, ambient, memory, HUD).
|
|
9
|
-
* The CJS counterpart (`writeExclusive` in `src/assets/scripts/hooks/lib/
|
|
9
|
+
* The CJS counterpart (`writeExclusive` in `src/assets/scripts/hooks/lib/curation-store.cjs`)
|
|
10
10
|
* intentionally remains a separate implementation — same semantics, different module
|
|
11
11
|
* system. Any change to the retry logic here MUST be mirrored there.
|
|
12
12
|
*/
|
package/dist/core/gate-level.js
CHANGED
|
@@ -17,12 +17,20 @@ import { describeEchoedValue } from './echo-value.js';
|
|
|
17
17
|
* diff.
|
|
18
18
|
*
|
|
19
19
|
* A present value that is not one of the three words is MALFORMED. A reader reads
|
|
20
|
-
* it as
|
|
21
|
-
* heals it to absent, which would lower the level
|
|
20
|
+
* it as {@link MALFORMED_GATE_LEVEL_READS_AS} and leaves it on disk as written: no
|
|
21
|
+
* read and no unrelated write heals it to absent, which would lower the level
|
|
22
|
+
* without a word.
|
|
22
23
|
*/
|
|
23
24
|
export const GATE_LEVELS = Object.freeze(['full', 'standard', 'lean']);
|
|
24
25
|
/** The level when neither the personal file nor the manifest records one. */
|
|
25
26
|
export const DEFAULT_GATE_LEVEL = 'standard';
|
|
27
|
+
/**
|
|
28
|
+
* The level every reader gives a MALFORMED value: the strictest, so a typo never
|
|
29
|
+
* lowers the checking. `devflow gates` and the init prompt show it from here. The
|
|
30
|
+
* settings resolver (`resolve-settings.cjs`) folds to the same level in its own
|
|
31
|
+
* copy, and a parity test holds the two to each other.
|
|
32
|
+
*/
|
|
33
|
+
export const MALFORMED_GATE_LEVEL_READS_AS = 'full';
|
|
26
34
|
/**
|
|
27
35
|
* Largest manifest read, in bytes. Pinned by a parity test to the resolver
|
|
28
36
|
* library's MAX_MANIFEST_BYTES, so the CLI and a prompt never disagree about a
|
package/dist/core/git.js
CHANGED
|
@@ -24,13 +24,25 @@ export function isTrunkBranch(branch) {
|
|
|
24
24
|
return TRUNK_BRANCH_PREFIXES.some(p => branch.startsWith(p));
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
27
|
+
* The repository root named by the output of `git rev-parse --show-toplevel`, or null
|
|
28
|
+
* when the output is not one clean absolute path.
|
|
29
29
|
*
|
|
30
|
-
* Security:
|
|
30
|
+
* Security: validates output to prevent command injection
|
|
31
31
|
* - Rejects paths with injection characters (newlines, semicolons, shell operators)
|
|
32
32
|
* - Ensures path is absolute
|
|
33
33
|
* - Resolves path canonically
|
|
34
|
+
*/
|
|
35
|
+
export function parseGitToplevel(stdout) {
|
|
36
|
+
const gitRootRaw = stdout.trim();
|
|
37
|
+
if (!gitRootRaw || gitRootRaw.includes('\n') || gitRootRaw.includes(';') || gitRootRaw.includes('&&')) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const gitRoot = path.resolve(gitRootRaw);
|
|
41
|
+
return path.isAbsolute(gitRoot) ? gitRoot : null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get git repository root directory (async, non-blocking)
|
|
45
|
+
* Returns null if not in a git repository
|
|
34
46
|
*
|
|
35
47
|
* @param cwd - Directory to resolve the git root from. Defaults to process.cwd().
|
|
36
48
|
* Callers that already carry an explicit working directory must pass it, otherwise
|
|
@@ -42,17 +54,7 @@ export async function getGitRoot(cwd = process.cwd()) {
|
|
|
42
54
|
cwd,
|
|
43
55
|
encoding: 'utf-8'
|
|
44
56
|
});
|
|
45
|
-
|
|
46
|
-
// Validate git root path (security: prevent injection)
|
|
47
|
-
if (!gitRootRaw || gitRootRaw.includes('\n') || gitRootRaw.includes(';') || gitRootRaw.includes('&&')) {
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
// Validate it's an absolute path
|
|
51
|
-
const gitRoot = path.resolve(gitRootRaw);
|
|
52
|
-
if (!path.isAbsolute(gitRoot)) {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
return gitRoot;
|
|
57
|
+
return parseGitToplevel(stdout);
|
|
56
58
|
}
|
|
57
59
|
catch {
|
|
58
60
|
return null;
|
package/dist/core/ledger-root.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file ledger-root.ts
|
|
3
3
|
*
|
|
4
|
-
* Where the
|
|
4
|
+
* Where the curation ledger lives for a working directory — the TypeScript twin of
|
|
5
5
|
* `df_resolve_roots`' DF_LEDGER_ROOT in src/assets/scripts/hooks/resolve-project-root.
|
|
6
6
|
*
|
|
7
7
|
* D-LEDGER-MAIN-WORKTREE: the ledger is one per REPOSITORY, not per checkout. In a
|
|
8
8
|
* linked worktree the hooks queue and render into the main worktree's
|
|
9
|
-
* `.devflow/
|
|
9
|
+
* `.devflow/curation/`, so `devflow curation` and the HUD must read and clear that
|
|
10
10
|
* same directory — resolving the worktree's own toplevel (getGitRoot) would show an
|
|
11
11
|
* empty ledger and drain a queue nothing writes to. The rule, from ONE git call:
|
|
12
12
|
*
|
|
@@ -18,8 +18,11 @@
|
|
|
18
18
|
* - two absolute lines otherwise → the toplevel;
|
|
19
19
|
* - git before 2.31 echoes `--path-format=absolute` back as its own line → the
|
|
20
20
|
* toplevel it printed after it (the hooks fall back to the toplevel too);
|
|
21
|
-
* - git failed (not a work tree) → null: the caller keeps its non-git fallback;
|
|
22
|
-
*
|
|
21
|
+
* - git failed (not a work tree) → null: the caller keeps its non-git fallback; git
|
|
22
|
+
* that did not answer within the caller's time budget → null too (`lookupLedgerRoot`
|
|
23
|
+
* tells the two apart);
|
|
24
|
+
* - any other output → `git rev-parse --show-toplevel` alone, the hooks' df_resolve_root
|
|
25
|
+
* fallback, with the same time budget as the first call.
|
|
23
26
|
*
|
|
24
27
|
* Memory is NOT resolved here: working memory stays per checkout (getGitRoot).
|
|
25
28
|
* Parity with the shell helper is pinned by tests/core/ledger-root.test.ts, which
|
|
@@ -29,9 +32,24 @@ import { execFile } from 'child_process';
|
|
|
29
32
|
import { promises as fs } from 'fs';
|
|
30
33
|
import * as os from 'os';
|
|
31
34
|
import * as path from 'path';
|
|
32
|
-
import {
|
|
35
|
+
import { parseGitToplevel } from './git.js';
|
|
36
|
+
/**
|
|
37
|
+
* The budget of one ledger-root lookup, in ms: the HUD's per-git-command budget
|
|
38
|
+
* (src/hud/git.ts GIT_TIMEOUT). The HUD and the init move pass it as `timeoutMs`, so a
|
|
39
|
+
* project on a hung mount costs a bounded wait (one budget for each of the lookup's two
|
|
40
|
+
* git calls at most) and never stalls the rest.
|
|
41
|
+
*/
|
|
42
|
+
export const LEDGER_ROOT_TIMEOUT_MS = 1000;
|
|
43
|
+
/**
|
|
44
|
+
* How long past its timeout a git call may stay unanswered before the lookup stops
|
|
45
|
+
* waiting for it: the timeout's own kill needs the process to die, and a process stuck
|
|
46
|
+
* in I/O on a hung mount may not.
|
|
47
|
+
*/
|
|
48
|
+
const KILL_GRACE_MS = 500;
|
|
33
49
|
/** The one git call, as argv (no shell). */
|
|
34
50
|
const LEDGER_ROOT_GIT_ARGS = ['rev-parse', '--path-format=absolute', '--show-toplevel', '--git-common-dir'];
|
|
51
|
+
/** The fallback git call, as argv (no shell): the toplevel alone, as the hooks' df_resolve_root asks. */
|
|
52
|
+
const TOPLEVEL_GIT_ARGS = ['rev-parse', '--show-toplevel'];
|
|
35
53
|
/** What git before 2.31 prints first: the flag it does not know, echoed back. */
|
|
36
54
|
const PATH_FORMAT_ECHO = '--path-format=absolute';
|
|
37
55
|
/** Classify the git call's stdout. Pure. */
|
|
@@ -45,12 +63,37 @@ export function parseLedgerRootOutput(stdout) {
|
|
|
45
63
|
}
|
|
46
64
|
return { kind: 'unrecognised' };
|
|
47
65
|
}
|
|
48
|
-
/**
|
|
49
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Run `git <args>` from `cwd`. With a timeout the call is bounded twice: Node kills git
|
|
68
|
+
* at `timeoutMs`, and the lookup stops waiting KILL_GRACE_MS later even if git is still
|
|
69
|
+
* alive (and kills it then), so a process stuck on a hung mount cannot hold the caller.
|
|
70
|
+
* Never rejects.
|
|
71
|
+
*/
|
|
72
|
+
function runGit(args, cwd, timeoutMs) {
|
|
50
73
|
return new Promise((resolve) => {
|
|
51
|
-
|
|
52
|
-
|
|
74
|
+
let settled = false;
|
|
75
|
+
let backstop;
|
|
76
|
+
const settle = (run) => {
|
|
77
|
+
if (settled)
|
|
78
|
+
return;
|
|
79
|
+
settled = true;
|
|
80
|
+
if (backstop !== undefined)
|
|
81
|
+
clearTimeout(backstop);
|
|
82
|
+
resolve(run);
|
|
83
|
+
};
|
|
84
|
+
const child = execFile('git', [...args], { cwd, timeout: timeoutMs, encoding: 'utf-8' }, (err, stdout) => {
|
|
85
|
+
if (!err)
|
|
86
|
+
settle({ kind: 'ok', stdout });
|
|
87
|
+
else
|
|
88
|
+
settle(timeoutMs > 0 && err.killed === true ? { kind: 'timed-out' } : { kind: 'failed' });
|
|
53
89
|
});
|
|
90
|
+
if (timeoutMs > 0) {
|
|
91
|
+
backstop = setTimeout(() => {
|
|
92
|
+
child.kill('SIGKILL');
|
|
93
|
+
settle({ kind: 'timed-out' });
|
|
94
|
+
}, timeoutMs + KILL_GRACE_MS);
|
|
95
|
+
backstop.unref();
|
|
96
|
+
}
|
|
54
97
|
});
|
|
55
98
|
}
|
|
56
99
|
/** A path's physical location, or null when it cannot be resolved. */
|
|
@@ -76,27 +119,44 @@ async function mainHoldsLedger(main, home) {
|
|
|
76
119
|
return mainReal !== null && mainReal !== homeReal;
|
|
77
120
|
}
|
|
78
121
|
/**
|
|
79
|
-
* The
|
|
80
|
-
*
|
|
122
|
+
* The curation ledger root for `cwd`, saying why when there is none: git named no work
|
|
123
|
+
* tree (`no-root`), or did not answer in time (`timed-out`). Each of the two git calls
|
|
124
|
+
* (the main one and the fallback) gets `options.timeoutMs`; 0 or none waits for ever.
|
|
81
125
|
*/
|
|
82
|
-
export async function
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
126
|
+
export async function lookupLedgerRoot(cwd = process.cwd(), options = {}) {
|
|
127
|
+
const timeoutMs = options.timeoutMs ?? 0;
|
|
128
|
+
const run = await runGit(LEDGER_ROOT_GIT_ARGS, cwd, timeoutMs);
|
|
129
|
+
if (run.kind === 'timed-out')
|
|
130
|
+
return { kind: 'timed-out' };
|
|
131
|
+
if (run.kind === 'failed')
|
|
132
|
+
return { kind: 'no-root' };
|
|
133
|
+
const parsed = parseLedgerRootOutput(run.stdout);
|
|
87
134
|
switch (parsed.kind) {
|
|
88
135
|
case 'toplevel':
|
|
89
|
-
return parsed.toplevel;
|
|
90
|
-
case 'unrecognised':
|
|
91
|
-
|
|
136
|
+
return { kind: 'root', root: parsed.toplevel };
|
|
137
|
+
case 'unrecognised': {
|
|
138
|
+
const fallback = await runGit(TOPLEVEL_GIT_ARGS, cwd, timeoutMs);
|
|
139
|
+
if (fallback.kind === 'timed-out')
|
|
140
|
+
return { kind: 'timed-out' };
|
|
141
|
+
const toplevel = fallback.kind === 'ok' ? parseGitToplevel(fallback.stdout) : null;
|
|
142
|
+
return toplevel === null ? { kind: 'no-root' } : { kind: 'root', root: toplevel };
|
|
143
|
+
}
|
|
92
144
|
case 'roots': {
|
|
93
145
|
if (!parsed.commonDir.endsWith('/.git'))
|
|
94
|
-
return parsed.toplevel;
|
|
146
|
+
return { kind: 'root', root: parsed.toplevel };
|
|
95
147
|
const main = parsed.commonDir.slice(0, -'/.git'.length);
|
|
96
148
|
if (main && await mainHoldsLedger(main, options.home ?? os.homedir()))
|
|
97
|
-
return main;
|
|
98
|
-
return parsed.toplevel;
|
|
149
|
+
return { kind: 'root', root: main };
|
|
150
|
+
return { kind: 'root', root: parsed.toplevel };
|
|
99
151
|
}
|
|
100
152
|
}
|
|
101
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* The curation ledger root for `cwd` — the main worktree or this checkout's
|
|
156
|
+
* toplevel — or null outside a git work tree (or when git did not answer in time).
|
|
157
|
+
*/
|
|
158
|
+
export async function getLedgerRoot(cwd = process.cwd(), options = {}) {
|
|
159
|
+
const lookup = await lookupLedgerRoot(cwd, options);
|
|
160
|
+
return lookup.kind === 'root' ? lookup.root : null;
|
|
161
|
+
}
|
|
102
162
|
//# sourceMappingURL=ledger-root.js.map
|
package/dist/core/linked-path.js
CHANGED
|
@@ -12,17 +12,17 @@ import { promises as fs } from 'fs';
|
|
|
12
12
|
* D-CLI-NO-SYMLINK: the CLI writes or deletes under a project's `.devflow/` only
|
|
13
13
|
* where neither `.devflow` nor the entry it acts on through it is a symbolic link:
|
|
14
14
|
* `devflow init` stamps its carve-out marker, removes the legacy markers and
|
|
15
|
-
* writes `.devflow/config.json` only then, `devflow
|
|
16
|
-
* the project's `
|
|
15
|
+
* writes `.devflow/config.json` only then, `devflow curation --configure` writes
|
|
16
|
+
* the project's `curation.json` only then, and the queue drains (`devflow curation
|
|
17
17
|
* --clear|--disable`, `devflow memory --disable|--clear` and `devflow init
|
|
18
|
-
* --no-
|
|
18
|
+
* --no-curation|--no-memory`) delete nothing when `.devflow` or the queue's folder
|
|
19
19
|
* is one. `devflow uninstall`, removing a legacy local install, deletes or rewrites
|
|
20
20
|
* nothing under the project's `.devflow` or `.claude` where either, or anything
|
|
21
21
|
* below it on the way to what it removes, is one (legacyLocalChangeGuard in
|
|
22
22
|
* uninstall.ts). Reason: a repository can commit `.devflow`, or a folder or file in
|
|
23
23
|
* it, as a link to any place on the machine, and a write or delete through one lands
|
|
24
24
|
* wherever it points. The hooks hold the same rule (D-HOOKS-NO-SYMLINK, git-marker)
|
|
25
|
-
* and so do the
|
|
25
|
+
* and so do the curation ops (D-NO-LINKED-TREE). Only paths below the project root
|
|
26
26
|
* are passed here: the root and the folders above it are the user's choice.
|
|
27
27
|
*
|
|
28
28
|
* Each path is checked with lstat, so a link is seen rather than followed. Nothing at
|