devflow-kit 3.8.0 → 3.9.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 +120 -0
- package/README.md +5 -5
- package/dist/agents/code.md +4 -4
- package/dist/agents/fix.md +1 -1
- package/dist/agents/git.md +2 -2
- package/dist/agents/skim.md +1 -1
- package/dist/agents/triage.md +7 -0
- package/dist/cli/commands/curation.js +67 -16
- package/dist/cli/commands/gate-level-prompts.js +8 -6
- package/dist/cli/commands/gates.js +5 -4
- package/dist/cli/commands/init.js +8 -7
- package/dist/cli/commands/memory.js +59 -12
- package/dist/cli/commands/uninstall.js +77 -9
- package/dist/commands/bug-analysis.md +2 -2
- package/dist/commands/code-review.md +11 -11
- package/dist/commands/debug.md +2 -2
- package/dist/commands/dynamic-build.md +2 -2
- package/dist/commands/dynamic-plan.md +2 -2
- package/dist/commands/dynamic-profile.md +2 -2
- package/dist/commands/dynamic-tickets.md +2 -2
- package/dist/commands/explore.md +2 -2
- package/dist/commands/implement.md +2 -2
- package/dist/commands/plan.md +2 -2
- package/dist/commands/release.md +2 -2
- package/dist/commands/research.md +2 -2
- package/dist/commands/resolve.md +16 -16
- package/dist/commands/self-review.md +2 -2
- package/dist/core/area-pause-notice.js +44 -0
- package/dist/core/curation-move-skip.js +4 -5
- package/dist/core/curation-queue-cleanup.js +6 -5
- package/dist/core/curation-store.js +1 -0
- package/dist/core/evidence-policy.js +10 -8
- package/dist/core/plugins.js +4 -4
- package/dist/core/tracked-areas.js +138 -14
- package/dist/curation-off/agents/code.md +4 -4
- package/dist/curation-off/agents/fix.md +1 -1
- package/dist/curation-off/agents/triage.md +7 -0
- package/dist/curation-off/commands/code-review.md +11 -11
- package/dist/curation-off/commands/debug.md +2 -2
- package/dist/curation-off/commands/dynamic-build.md +2 -2
- package/dist/curation-off/commands/explore.md +2 -2
- package/dist/curation-off/commands/implement.md +2 -2
- package/dist/curation-off/commands/plan.md +2 -2
- package/dist/curation-off/commands/resolve.md +16 -16
- package/dist/curation-off/commands/self-review.md +2 -2
- package/dist/hud/components/curation-counts.js +27 -0
- package/dist/skills/git/references/pr/post-resolution-summary.md +12 -11
- package/dist/skills/git/references/pr/post-review-summary.md +12 -11
- package/dist/skills/git/references/pr/update-pr-evidence.md +2 -2
- package/dist/skills/git/references/publication-gate.md +6 -7
- package/dist/skills/git/references/tracker/github/manage-debt.md +6 -2
- package/dist/skills/git/references/tracker/jira/manage-debt.md +6 -2
- package/dist/skills/git/references/tracker/linear/manage-debt.md +6 -2
- package/package.json +1 -1
- package/src/assets/agents/code.mds +4 -4
- package/src/assets/agents/evaluate.md +6 -12
- package/src/assets/agents/fix.mds +1 -1
- package/src/assets/agents/git.mds +2 -2
- package/src/assets/agents/skim.mds +1 -1
- package/src/assets/agents/triage.mds +7 -0
- package/src/assets/agents/validate.md +20 -27
- package/src/assets/commands/_partials/_decisions.mds +10 -8
- package/src/assets/commands/_partials/_settings.mds +2 -2
- package/src/assets/commands/code-review.mds +9 -9
- package/src/assets/commands/resolve.mds +14 -14
- package/src/assets/mds/git/_ops.mds +6 -1
- package/src/assets/mds/git/_ops_pr.mds +20 -10
- package/src/assets/mds/git/_ops_steps.mds +10 -0
- package/src/assets/mds/git/_pr.mds +14 -14
- package/src/assets/mds/git/_references.mds +6 -7
- package/src/assets/mds/tracker/_github.mds +1 -4
- package/src/assets/mds/tracker/_jira.mds +1 -4
- package/src/assets/mds/tracker/_linear.mds +1 -4
- package/src/assets/scripts/ci-wait.cjs +389 -92
- package/src/assets/scripts/hooks/background-memory-update +79 -1
- package/src/assets/scripts/hooks/json-helper.cjs +44 -2
- package/src/assets/scripts/hooks/lib/curation-store.cjs +82 -20
- package/src/assets/scripts/hooks/lib/render-decisions.cjs +9 -1
- package/src/assets/scripts/hooks/lib/tracked-areas.cjs +226 -33
- package/src/assets/scripts/hooks/memory-permission +10 -2
- package/src/assets/scripts/hooks/memory-worker +11 -0
- package/src/assets/scripts/hooks/pre-compact-memory +11 -0
- package/src/assets/scripts/hooks/queue-append +218 -1
- package/src/assets/scripts/hooks/session-start-context +89 -37
- package/src/assets/scripts/hooks/session-start-memory +39 -0
- package/src/assets/scripts/pr-evidence.cjs +5 -2
- package/src/assets/scripts/resolve-settings.cjs +244 -30
- package/src/assets/scripts/verify-evidence.cjs +307 -75
- package/src/assets/skills/agent-references/SKILL.md +7 -1
- package/src/assets/skills/agent-references/references/commands/implement-levels.md +6 -4
- package/src/assets/skills/agent-references/references/commands/resolve-levels.md +3 -2
- package/src/assets/skills/agent-references/references/synthesize/review.md +4 -4
- package/src/assets/skills/agent-references/references/triage/severity-tiers.md +43 -0
- package/src/assets/skills/agent-references/references/validate/passed-alone.md +14 -0
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Does git track any entry under one of devflow's data folders at a root? One
|
|
4
4
|
// `git ls-files` call over a fixed set of pathspecs answers `tracked`, `untracked` or
|
|
5
|
-
// `unknown
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// `unknown`, for one area at a time: `curation` (the curation folder, the old ledger
|
|
6
|
+
// folder and the backup folders), `memory` (the memory folder) or `both` (the union of the
|
|
7
|
+
// two, one call, for a caller that serves both areas at one root). The one-time ledger move
|
|
8
|
+
// and the backup sweep ask it for the curation area before they lock, read or rename
|
|
9
|
+
// anything: a folder that git tracks is the repository's content, so they leave it alone.
|
|
8
10
|
//
|
|
9
11
|
// DESIGN: a leaf. It loads node built-ins and project-paths.cjs (which loads node
|
|
10
12
|
// built-ins only) and requires neither the store nor the move module (the move requires
|
|
@@ -13,14 +15,30 @@
|
|
|
13
15
|
// every failure is the answer `unknown`, with a cause from the closed set UNKNOWN_CAUSES.
|
|
14
16
|
//
|
|
15
17
|
// The folder names come from project-paths.cjs, which owns them: AREA_FOLDERS lists the
|
|
16
|
-
// data folders
|
|
17
|
-
//
|
|
18
|
+
// curation area's data folders, the backup prefix names the folders the move leaves
|
|
19
|
+
// behind, and the memory folder is the one getMemoryDir names. To cover another folder of
|
|
20
|
+
// an area, add it to that area's pathspecs.
|
|
18
21
|
//
|
|
19
22
|
// The decision is the repository's index, never the disk. A root with no `.git` entry
|
|
20
23
|
// (a file or a folder) is not a git working tree and gets no git call. A root with one
|
|
21
24
|
// gets exactly one call, and every failure of that call (git missing, a timeout, a
|
|
22
25
|
// non-zero exit with nothing listed) is `unknown`: the caller cannot tell what git
|
|
23
26
|
// tracks and must change nothing. git's error text is never read.
|
|
27
|
+
//
|
|
28
|
+
// trackedAreaCall splits the check in two: it decides whether a call is due and, when it
|
|
29
|
+
// is, describes it (working directory, argv, environment); a caller that runs git in
|
|
30
|
+
// another way (the TypeScript seam's bounded async runner, the settings resolver's
|
|
31
|
+
// injected exec) executes that description and hands the result to judgeTrackedAreas, so
|
|
32
|
+
// every runner gives the answer this leaf gives.
|
|
33
|
+
//
|
|
34
|
+
// COPIES: the leaf is not the only place that holds some of these lists. The shell twin in
|
|
35
|
+
// the capture gate file (hooks/queue-append) keeps its own copy of the pathspecs
|
|
36
|
+
// (`_qta_specs`) and of the list of git variables it removes (the `unset` line of
|
|
37
|
+
// `_qta_probe`), because it runs git without node. Two folder-to-area maps restate which
|
|
38
|
+
// folder belongs to which area: `_qar_area_of` in that file and `projectDataAreaOf` in
|
|
39
|
+
// src/cli/commands/uninstall.ts. Each copy is held equal to the lists here by
|
|
40
|
+
// tests/decisions/tracked-area-parity.test.ts, so a change to a list here fails that test
|
|
41
|
+
// until every copy follows it.
|
|
24
42
|
|
|
25
43
|
'use strict';
|
|
26
44
|
|
|
@@ -28,11 +46,14 @@ const childProcess = require('child_process');
|
|
|
28
46
|
const fs = require('fs');
|
|
29
47
|
const path = require('path');
|
|
30
48
|
|
|
31
|
-
const { LEGACY_NAMES, CURATION_DIR_NAME } = require('./project-paths.cjs');
|
|
49
|
+
const { LEGACY_NAMES, CURATION_DIR_NAME, getMemoryDir } = require('./project-paths.cjs');
|
|
32
50
|
|
|
33
|
-
/** The data folders under `.devflow` that the check covers, by the names the path helpers hold. */
|
|
51
|
+
/** The curation data folders under `.devflow` that the check covers, by the names the path helpers hold. */
|
|
34
52
|
const AREA_FOLDERS = Object.freeze([LEGACY_NAMES.folder, CURATION_DIR_NAME]);
|
|
35
53
|
|
|
54
|
+
/** The memory folder under `.devflow`, by the name getMemoryDir gives it. */
|
|
55
|
+
const MEMORY_FOLDER = path.basename(getMemoryDir(path.sep));
|
|
56
|
+
|
|
36
57
|
/**
|
|
37
58
|
* The spellings a case-folding file system opens as `.devflow` beyond ASCII letter case.
|
|
38
59
|
* git's `icase` folds ASCII letters only, but APFS (the macOS default) compares names by
|
|
@@ -42,8 +63,9 @@ const AREA_FOLDERS = Object.freeze([LEGACY_NAMES.folder, CURATION_DIR_NAME]);
|
|
|
42
63
|
const DEVFLOW_SPELLINGS = Object.freeze(['.devflow', '.dev\u{FB02}ow']);
|
|
43
64
|
|
|
44
65
|
/**
|
|
45
|
-
* The pathspecs of the
|
|
46
|
-
* the file system or `core.ignorecase`, and each spelled every way
|
|
66
|
+
* The pathspecs of the curation area, each matched without regard to ASCII letter case,
|
|
67
|
+
* whatever the file system or `core.ignorecase`, and each spelled every way
|
|
68
|
+
* DEVFLOW_SPELLINGS lists:
|
|
47
69
|
* - each folder of AREA_FOLDERS, as a literal path: an index entry at the folder, or
|
|
48
70
|
* anywhere below it, matches
|
|
49
71
|
* - an entry named like a backup folder (`<backup prefix>*`), and every entry below one
|
|
@@ -54,6 +76,8 @@ const DEVFLOW_SPELLINGS = Object.freeze(['.devflow', '.dev\u{FB02}ow']);
|
|
|
54
76
|
* keeps `glob` from matching as a leading directory, so the many files under a
|
|
55
77
|
* normal `.devflow/` are not listed. Committed content then stands at the path where
|
|
56
78
|
* the folders are opened.
|
|
79
|
+
* The ledger move and the backup sweep ask for this set, and its order and spelling stay
|
|
80
|
+
* as they are.
|
|
57
81
|
*/
|
|
58
82
|
const TRACKED_AREA_PATHSPECS = Object.freeze([
|
|
59
83
|
...DEVFLOW_SPELLINGS.flatMap(dir => [
|
|
@@ -64,6 +88,30 @@ const TRACKED_AREA_PATHSPECS = Object.freeze([
|
|
|
64
88
|
...DEVFLOW_SPELLINGS.map(dir => ':(icase,glob)[.]' + dir.slice(1)),
|
|
65
89
|
]);
|
|
66
90
|
|
|
91
|
+
/**
|
|
92
|
+
* The pathspecs of the memory area, matched and spelled as the curation area's are: the
|
|
93
|
+
* memory folder as a literal path (an entry at the folder, or anywhere below it), and the
|
|
94
|
+
* `.devflow` entry itself.
|
|
95
|
+
*/
|
|
96
|
+
const MEMORY_AREA_PATHSPECS = Object.freeze([
|
|
97
|
+
...DEVFLOW_SPELLINGS.map(dir => ':(icase,literal)' + dir + '/' + MEMORY_FOLDER),
|
|
98
|
+
...DEVFLOW_SPELLINGS.map(dir => ':(icase,glob)[.]' + dir.slice(1)),
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The pathspecs of the two areas together: the curation set, then the memory set, each
|
|
103
|
+
* pathspec once (the `.devflow` entry forms stand in both sets). One call over this set
|
|
104
|
+
* answers `tracked` when git tracks anything either area covers.
|
|
105
|
+
*/
|
|
106
|
+
const BOTH_AREAS_PATHSPECS = Object.freeze([...new Set([...TRACKED_AREA_PATHSPECS, ...MEMORY_AREA_PATHSPECS])]);
|
|
107
|
+
|
|
108
|
+
/** The pathspecs of each area, by the area's name: the closed set of areas the check knows. */
|
|
109
|
+
const AREA_PATHSPECS = Object.freeze({
|
|
110
|
+
curation: TRACKED_AREA_PATHSPECS,
|
|
111
|
+
memory: MEMORY_AREA_PATHSPECS,
|
|
112
|
+
both: BOTH_AREAS_PATHSPECS,
|
|
113
|
+
});
|
|
114
|
+
|
|
67
115
|
/**
|
|
68
116
|
* Why a check answered `unknown`, as one fixed word. The words come from this list
|
|
69
117
|
* alone: they name a spawn error code, a time out, a signal, an exit status or the step
|
|
@@ -108,6 +156,20 @@ const GIT_REDIRECT_VARIABLES = Object.freeze([
|
|
|
108
156
|
'GIT_ICASE_PATHSPECS',
|
|
109
157
|
]);
|
|
110
158
|
|
|
159
|
+
/**
|
|
160
|
+
* A copy of `base` without the redirecting variables (GIT_REDIRECT_VARIABLES): git then
|
|
161
|
+
* reads the repository, index and pathspec rules of the directory it runs in. Nothing else
|
|
162
|
+
* changes. For a caller that runs git for its own purpose and needs only the scrub.
|
|
163
|
+
*
|
|
164
|
+
* @param {NodeJS.ProcessEnv} base
|
|
165
|
+
* @returns {NodeJS.ProcessEnv}
|
|
166
|
+
*/
|
|
167
|
+
function gitEnvWithoutRedirects(base) {
|
|
168
|
+
const env = { ...base };
|
|
169
|
+
for (const name of GIT_REDIRECT_VARIABLES) delete env[name];
|
|
170
|
+
return env;
|
|
171
|
+
}
|
|
172
|
+
|
|
111
173
|
/**
|
|
112
174
|
* The environment of the git call: the process environment without the redirecting
|
|
113
175
|
* variables, with a fixed locale, no optional lock and a ceiling directory. Answers a new
|
|
@@ -120,8 +182,7 @@ const GIT_REDIRECT_VARIABLES = Object.freeze([
|
|
|
120
182
|
* @returns {NodeJS.ProcessEnv}
|
|
121
183
|
*/
|
|
122
184
|
function trackedAreasGitEnv(base, ceiling) {
|
|
123
|
-
const env =
|
|
124
|
-
for (const name of GIT_REDIRECT_VARIABLES) delete env[name];
|
|
185
|
+
const env = gitEnvWithoutRedirects(base);
|
|
125
186
|
env.LC_ALL = 'C';
|
|
126
187
|
env.GIT_OPTIONAL_LOCKS = '0';
|
|
127
188
|
env.GIT_CEILING_DIRECTORIES = ceiling;
|
|
@@ -193,42 +254,108 @@ function entryExists(target) {
|
|
|
193
254
|
}
|
|
194
255
|
|
|
195
256
|
/**
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
* a staged entry counts. The answer carries the cause (a value of UNKNOWN_CAUSES) when
|
|
199
|
-
* it is `unknown`, and null otherwise.
|
|
257
|
+
* The pathspecs of `area`, or null when the leaf does not know the area. Only the own
|
|
258
|
+
* names of AREA_PATHSPECS count: a name from the object prototype is no area.
|
|
200
259
|
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
260
|
+
* @param {unknown} area
|
|
261
|
+
* @returns {readonly string[] | null}
|
|
262
|
+
*/
|
|
263
|
+
function pathspecsOf(area) {
|
|
264
|
+
if (typeof area !== 'string' || !Object.prototype.hasOwnProperty.call(AREA_PATHSPECS, area)) return null;
|
|
265
|
+
return AREA_PATHSPECS[/** @type {'curation' | 'memory' | 'both'} */ (area)];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Whether a git call is due for `area` at `root`, and the call when it is. Makes no
|
|
270
|
+
* call and starts no process: it reads only the `.git` entry at the root and the root's
|
|
271
|
+
* real path. Answers one of:
|
|
272
|
+
*
|
|
273
|
+
* {answer, cause} no call is due: `untracked` (no `.git` entry at the root), or
|
|
274
|
+
* `unknown` with a cause from UNKNOWN_CAUSES (an area the leaf does
|
|
275
|
+
* not know, a root that is not an absolute path string, a `.git`
|
|
276
|
+
* entry that could not be probed, a folder above the root that
|
|
277
|
+
* holds the list separator, or a failure)
|
|
278
|
+
* {cwd, argv, env} the call: run `git` with `argv` and `env` in `cwd` with no shell,
|
|
279
|
+
* bound its time (TRACKED_AREAS_GIT_TIMEOUT_MS) and its output
|
|
280
|
+
* (TRACKED_AREAS_GIT_MAX_BUFFER), and give the result to
|
|
281
|
+
* judgeTrackedAreas
|
|
206
282
|
*
|
|
207
|
-
* The call is `git --no-literal-pathspecs -c core.fsmonitor=false ls-files -z -- <paths
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
* named as one, and answers `unknown`.
|
|
283
|
+
* The call is `git --no-literal-pathspecs -c core.fsmonitor=false ls-files -z -- <paths>`.
|
|
284
|
+
* `--no-literal-pathspecs` leads the argv, so a forced literal-pathspec setting cannot
|
|
285
|
+
* turn the magic off and hide an entry. Reading the index makes git run the command a
|
|
286
|
+
* repository's config names in `core.fsmonitor`, so the call turns that off for itself and
|
|
287
|
+
* stays a pure read. The folder above the root's real path is a ceiling directory
|
|
288
|
+
* (trackedAreasGitEnv); a parent path that holds the list separator can not be named as
|
|
289
|
+
* one, and answers `unknown`. Each answer is a new object.
|
|
215
290
|
*
|
|
216
291
|
* @param {unknown} root - an absolute path
|
|
217
|
-
* @
|
|
292
|
+
* @param {unknown} area - 'curation', 'memory' or 'both'
|
|
293
|
+
* @param {NodeJS.ProcessEnv} [baseEnv] - the environment the call's own derives from (default: the process environment)
|
|
294
|
+
* @returns {{ answer: 'tracked' | 'untracked' | 'unknown', cause: string | null } | { cwd: string, argv: string[], env: NodeJS.ProcessEnv }}
|
|
218
295
|
*/
|
|
219
|
-
function
|
|
296
|
+
function trackedAreaCall(root, area, baseEnv = process.env) {
|
|
297
|
+
const pathspecs = pathspecsOf(area);
|
|
220
298
|
if (typeof root !== 'string' || !path.isAbsolute(root)) return unknownBecause(UNKNOWN_CAUSES.rootNotAbsolute);
|
|
299
|
+
if (pathspecs === null) return unknownBecause(UNKNOWN_CAUSES.checkFailed);
|
|
221
300
|
try {
|
|
222
301
|
const gitEntry = entryExists(path.join(root, '.git'));
|
|
223
302
|
if (gitEntry === null) return unknownBecause(UNKNOWN_CAUSES.gitEntryUnreadable);
|
|
224
303
|
if (gitEntry === false) return { answer: 'untracked', cause: null };
|
|
225
304
|
const ceiling = path.dirname(fs.realpathSync(root));
|
|
226
305
|
if (ceiling.includes(path.delimiter)) return unknownBecause(UNKNOWN_CAUSES.ceilingUnusable);
|
|
227
|
-
|
|
306
|
+
return {
|
|
228
307
|
cwd: root,
|
|
229
|
-
|
|
308
|
+
argv: ['--no-literal-pathspecs', '-c', 'core.fsmonitor=false', 'ls-files', '-z', '--', ...pathspecs],
|
|
309
|
+
env: trackedAreasGitEnv(baseEnv, ceiling),
|
|
310
|
+
};
|
|
311
|
+
} catch (_) {
|
|
312
|
+
return unknownBecause(UNKNOWN_CAUSES.checkFailed);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* The time bound of one check: `options.timeoutMs` when it is a positive finite number of
|
|
318
|
+
* milliseconds, otherwise TRACKED_AREAS_GIT_TIMEOUT_MS. A zero would mean no bound at all
|
|
319
|
+
* to the spawn call, so it is never passed on.
|
|
320
|
+
*
|
|
321
|
+
* @param {unknown} options
|
|
322
|
+
* @returns {number}
|
|
323
|
+
*/
|
|
324
|
+
function timeoutOf(options) {
|
|
325
|
+
const timeoutMs = options !== null && typeof options === 'object' ? /** @type {{ timeoutMs?: unknown }} */ (options).timeoutMs : undefined;
|
|
326
|
+
return typeof timeoutMs === 'number' && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : TRACKED_AREAS_GIT_TIMEOUT_MS;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Whether git tracks any entry under the folders of `area` ('curation', 'memory' or 'both',
|
|
331
|
+
* the union of the two) at `root`, or an entry named `.devflow` itself (a link, a gitlink or
|
|
332
|
+
* a file). The index
|
|
333
|
+
* decides, not the disk, and a staged entry counts. The answer carries the cause (a value
|
|
334
|
+
* of UNKNOWN_CAUSES) when it is `unknown`, and null otherwise.
|
|
335
|
+
*
|
|
336
|
+
* untracked no `.git` entry at the root (no git call is made), or git listed nothing
|
|
337
|
+
* tracked git listed an entry
|
|
338
|
+
* unknown a `.git` entry stands and git did not answer (see judgeTrackedAreas),
|
|
339
|
+
* the `.git` entry could not be probed, the root is not an absolute path
|
|
340
|
+
* string, or the area is not one the leaf knows (no git call is made)
|
|
341
|
+
*
|
|
342
|
+
* The call is the one trackedAreaCall describes, run with a time bound (`options.timeoutMs`,
|
|
343
|
+
* default TRACKED_AREAS_GIT_TIMEOUT_MS) and an output bound.
|
|
344
|
+
*
|
|
345
|
+
* @param {unknown} root - an absolute path
|
|
346
|
+
* @param {unknown} area - 'curation', 'memory' or 'both'
|
|
347
|
+
* @param {{ timeoutMs?: number }} [options]
|
|
348
|
+
* @returns {{ answer: 'tracked' | 'untracked' | 'unknown', cause: string | null }}
|
|
349
|
+
*/
|
|
350
|
+
function inspectTrackedArea(root, area, options) {
|
|
351
|
+
const call = trackedAreaCall(root, area);
|
|
352
|
+
if (!('argv' in call)) return call;
|
|
353
|
+
try {
|
|
354
|
+
const result = childProcess.spawnSync('git', call.argv, {
|
|
355
|
+
cwd: call.cwd,
|
|
356
|
+
env: call.env,
|
|
230
357
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
231
|
-
timeout:
|
|
358
|
+
timeout: timeoutOf(options),
|
|
232
359
|
maxBuffer: TRACKED_AREAS_GIT_MAX_BUFFER,
|
|
233
360
|
windowsHide: true,
|
|
234
361
|
shell: false,
|
|
@@ -239,6 +366,60 @@ function inspectTrackedAreas(root) {
|
|
|
239
366
|
}
|
|
240
367
|
}
|
|
241
368
|
|
|
369
|
+
/**
|
|
370
|
+
* The answer of {@link inspectTrackedArea}, without its cause.
|
|
371
|
+
*
|
|
372
|
+
* @param {unknown} root - an absolute path
|
|
373
|
+
* @param {unknown} area - 'curation', 'memory' or 'both'
|
|
374
|
+
* @returns {'tracked' | 'untracked' | 'unknown'}
|
|
375
|
+
*/
|
|
376
|
+
function checkTrackedArea(root, area) {
|
|
377
|
+
return inspectTrackedArea(root, area).answer;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** The most answers the per-process memo of inspectTrackedAreaOnce holds. */
|
|
381
|
+
const ANSWER_MEMO_MAX_ENTRIES = 64;
|
|
382
|
+
|
|
383
|
+
/** The answers inspectTrackedAreaOnce has given in this process, by area and root, oldest first. */
|
|
384
|
+
const answerMemo = new Map();
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* {@link inspectTrackedArea}, asked once per root and area for the life of the process: a
|
|
388
|
+
* later ask for the same root and area gets the first answer and makes no git call, even if
|
|
389
|
+
* the project changed meanwhile. For a one-shot process (json-helper, the render check)
|
|
390
|
+
* whose steps each ask before they read or write: together they cost one git call. A
|
|
391
|
+
* long-lived caller, or one that changes what git tracks, asks inspectTrackedArea. The
|
|
392
|
+
* memo holds at most ANSWER_MEMO_MAX_ENTRIES answers; past that the oldest goes. Each
|
|
393
|
+
* answer is a new object.
|
|
394
|
+
*
|
|
395
|
+
* @param {unknown} root - an absolute path
|
|
396
|
+
* @param {unknown} area - 'curation', 'memory' or 'both'
|
|
397
|
+
* @returns {{ answer: 'tracked' | 'untracked' | 'unknown', cause: string | null }}
|
|
398
|
+
*/
|
|
399
|
+
function inspectTrackedAreaOnce(root, area) {
|
|
400
|
+
const key = typeof root === 'string' && typeof area === 'string' ? area + '\0' + root : null;
|
|
401
|
+
const known = key === null ? undefined : answerMemo.get(key);
|
|
402
|
+
if (known !== undefined) return { answer: known.answer, cause: known.cause };
|
|
403
|
+
const answer = inspectTrackedArea(root, area);
|
|
404
|
+
if (key !== null) {
|
|
405
|
+
if (answerMemo.size >= ANSWER_MEMO_MAX_ENTRIES) answerMemo.delete(answerMemo.keys().next().value);
|
|
406
|
+
answerMemo.set(key, Object.freeze({ answer: answer.answer, cause: answer.cause }));
|
|
407
|
+
}
|
|
408
|
+
return answer;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Whether git tracks any entry under the curation area's folders at `root`: the check the
|
|
413
|
+
* ledger move, the backup sweep and the drains ask. {@link inspectTrackedArea} for the
|
|
414
|
+
* curation area.
|
|
415
|
+
*
|
|
416
|
+
* @param {unknown} root - an absolute path
|
|
417
|
+
* @returns {{ answer: 'tracked' | 'untracked' | 'unknown', cause: string | null }}
|
|
418
|
+
*/
|
|
419
|
+
function inspectTrackedAreas(root) {
|
|
420
|
+
return inspectTrackedArea(root, 'curation');
|
|
421
|
+
}
|
|
422
|
+
|
|
242
423
|
/**
|
|
243
424
|
* The answer of {@link inspectTrackedAreas}, without its cause.
|
|
244
425
|
*
|
|
@@ -251,9 +432,21 @@ function checkTrackedAreas(root) {
|
|
|
251
432
|
|
|
252
433
|
module.exports = {
|
|
253
434
|
TRACKED_AREA_PATHSPECS,
|
|
435
|
+
MEMORY_AREA_PATHSPECS,
|
|
436
|
+
AREA_PATHSPECS,
|
|
437
|
+
DEVFLOW_SPELLINGS,
|
|
438
|
+
GIT_REDIRECT_VARIABLES,
|
|
439
|
+
TRACKED_AREAS_GIT_TIMEOUT_MS,
|
|
440
|
+
TRACKED_AREAS_GIT_MAX_BUFFER,
|
|
441
|
+
ANSWER_MEMO_MAX_ENTRIES,
|
|
254
442
|
UNKNOWN_CAUSES,
|
|
443
|
+
gitEnvWithoutRedirects,
|
|
255
444
|
judgeTrackedAreas,
|
|
256
445
|
classifyTrackedAreas,
|
|
446
|
+
trackedAreaCall,
|
|
447
|
+
inspectTrackedArea,
|
|
448
|
+
inspectTrackedAreaOnce,
|
|
449
|
+
checkTrackedArea,
|
|
257
450
|
inspectTrackedAreas,
|
|
258
451
|
checkTrackedAreas,
|
|
259
452
|
};
|
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
# memory_notice_* the one-time notice: the worker writes it, a session start reads it
|
|
17
17
|
# memory_denied_seen the count of denied runs that raises the `denied` notice
|
|
18
18
|
#
|
|
19
|
+
# The worker writes a notice, or counts a denied run, only while the memory area is known
|
|
20
|
+
# to be free of files that git tracks: DF_TRACKED_AREA_MEMORY (queue_tracked_area, queue-append)
|
|
21
|
+
# must read `untracked` in the calling process. Any other value, an unset one included,
|
|
22
|
+
# is a paused area, and both helpers then write nothing.
|
|
23
|
+
#
|
|
19
24
|
# Source it after resolve-project-root (it needs df_no_symlink_below and df_file_below)
|
|
20
25
|
# and, for memory_result_summary, after json-parse. log() is used when the caller has one.
|
|
21
26
|
#
|
|
@@ -241,13 +246,15 @@ memory_result_summary() {
|
|
|
241
246
|
# start, once per reason: only where neither the notice nor its `.shown` marker stands,
|
|
242
247
|
# and where no link sits on the way down from <root>. The builtin tests come first, since
|
|
243
248
|
# noclobber alone opens a link to a FIFO or a device; noclobber then makes the create
|
|
244
|
-
# itself exclusive. Content: the reason. Best effort: a refusal leaves nothing.
|
|
249
|
+
# itself exclusive. Content: the reason. Best effort: a refusal leaves nothing. Nothing is
|
|
250
|
+
# written unless DF_TRACKED_AREA_MEMORY reads `untracked` in this process.
|
|
245
251
|
memory_notice_write() {
|
|
246
252
|
local _dir="${1:-}" _root="${2:-}" _reason="${3:-}" _file
|
|
247
253
|
case " $MEMORY_NOTICE_REASONS " in
|
|
248
254
|
*" $_reason "*) ;;
|
|
249
255
|
*) return 0 ;;
|
|
250
256
|
esac
|
|
257
|
+
[ "${DF_TRACKED_AREA_MEMORY:-}" = "untracked" ] || return 0
|
|
251
258
|
[ -d "$_dir" ] || return 0
|
|
252
259
|
_file="$_dir/.worker-notice-$_reason"
|
|
253
260
|
if ! path_occupied "$_file" && ! path_occupied "$_file.shown" \
|
|
@@ -285,9 +292,10 @@ memory_notice_clear() {
|
|
|
285
292
|
# worker's write in. The first leaves a marker; a second, with the marker still there,
|
|
286
293
|
# raises the `denied` notice. A run that swapped the memory in clears both
|
|
287
294
|
# (memory_notice_clear denied), so only two denied runs with no successful update between
|
|
288
|
-
# them count.
|
|
295
|
+
# them count. No marker is left unless DF_TRACKED_AREA_MEMORY reads `untracked` in this process.
|
|
289
296
|
memory_denied_seen() {
|
|
290
297
|
local _dir="${1:-}" _root="${2:-}" _mark
|
|
298
|
+
[ "${DF_TRACKED_AREA_MEMORY:-}" = "untracked" ] || return 0
|
|
291
299
|
[ -d "$_dir" ] || return 0
|
|
292
300
|
_mark="$_dir/.worker-denied-once"
|
|
293
301
|
if path_occupied "$_mark"; then
|
|
@@ -104,6 +104,17 @@ if [ ! -x "$UPDATER" ]; then
|
|
|
104
104
|
exit 0
|
|
105
105
|
fi
|
|
106
106
|
|
|
107
|
+
# Memory area check: while git tracks files in the memory folder of this checkout, or a
|
|
108
|
+
# `.git` entry stands and git does not answer, the memory area is paused, and this hook
|
|
109
|
+
# stamps no trigger and spawns no worker. It asks once, in git mode (queue_tracked_area),
|
|
110
|
+
# at the project root, after the throttle so a throttled Stop event costs nothing.
|
|
111
|
+
queue_tracked_area "$PROJECT_ROOT" memory git
|
|
112
|
+
if [ "${DF_TRACKED_AREA_MEMORY:-unknown}" != "untracked" ]; then
|
|
113
|
+
log "Memory paused: git tracks files in the memory folder, or did not answer; trigger not stamped, worker not spawned"
|
|
114
|
+
dbg "EXIT: memory area paused"
|
|
115
|
+
exit 0
|
|
116
|
+
fi
|
|
117
|
+
|
|
107
118
|
dbg "Throttle passed: TRIGGER_AGE=${TRIGGER_AGE}s >= 120s — spawning worker"
|
|
108
119
|
|
|
109
120
|
# Touch trigger BEFORE spawning (prevents concurrent Stop hooks from double-spawning)
|
|
@@ -63,6 +63,17 @@ if [ "$_QG_MEMORY" != "true" ]; then
|
|
|
63
63
|
exit 0
|
|
64
64
|
fi
|
|
65
65
|
|
|
66
|
+
# Memory area check: while git tracks files in the memory folder of this checkout, or a
|
|
67
|
+
# `.git` entry stands and git does not answer, the memory area is paused, and this hook
|
|
68
|
+
# makes no folder, writes no backup and bootstraps no working memory. The check asks once,
|
|
69
|
+
# in node mode, so a git that hangs is cut (queue_tracked_area), at the project root.
|
|
70
|
+
queue_tracked_area "$PROJECT_ROOT" memory node
|
|
71
|
+
if [ "${DF_TRACKED_AREA_MEMORY:-unknown}" != "untracked" ]; then
|
|
72
|
+
log "Memory paused: git tracks files in the memory folder, or did not answer; no backup or working memory written"
|
|
73
|
+
dbg "EXIT: memory area paused"
|
|
74
|
+
exit 0
|
|
75
|
+
fi
|
|
76
|
+
|
|
66
77
|
# Auto-create .devflow/ and ensure .gitignore entries (idempotent after first run)
|
|
67
78
|
source "$SCRIPT_DIR/ensure-devflow-init" "$CWD" || exit 0
|
|
68
79
|
|