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
|
@@ -29,6 +29,9 @@
|
|
|
29
29
|
# for the next run (D-QUEUE-NO-ORPHAN-DELETE)
|
|
30
30
|
# No parser: neither jq nor node on PATH → exits with no LLM run; the queue and any .processing batch are left in place
|
|
31
31
|
# Parser fails: jq or node exits non-zero while it reads the queue or the batch → exits with no LLM run; the turns are kept
|
|
32
|
+
# Paused area: git tracks files in the memory folder, or a `.git` entry stands and git does not answer → exits with
|
|
33
|
+
# result=skip and no LLM run, and reads, claims, writes and notices nothing; asked at the start, and again
|
|
34
|
+
# after the run, where a late answer leaves the staged file unswapped and the batch in .processing
|
|
32
35
|
|
|
33
36
|
set -e
|
|
34
37
|
|
|
@@ -128,6 +131,59 @@ source "$SCRIPT_DIR/resolve-project-root" 2>/dev/null || true
|
|
|
128
131
|
# The staged-path check, the settings JSON with the worker's Edit allow and deny rules, the
|
|
129
132
|
# result line and the one-time notice (needs df_no_symlink_below from resolve-project-root).
|
|
130
133
|
source "$SCRIPT_DIR/memory-permission" || { echo "background-memory-update: failed to source memory-permission" >&2; exit 1; }
|
|
134
|
+
source "$SCRIPT_DIR/queue-append" || { echo "background-memory-update: failed to source queue-append" >&2; exit 1; }
|
|
135
|
+
|
|
136
|
+
# --- The memory area check: git tracks files in the memory folder, or does not answer ---
|
|
137
|
+
# A folder that git tracks holds the repository's content, not devflow's data. While git
|
|
138
|
+
# tracks files in the memory folder of this checkout, or a `.git` entry stands and git does
|
|
139
|
+
# not answer, the memory area is paused, and the worker reads and writes nothing there.
|
|
140
|
+
# Sets DF_TRACKED_AREA_MEMORY to `tracked`, `untracked` or `unknown` for the project root in
|
|
141
|
+
# $1: queue_tracked_area in git mode, run in a background subshell that answers by its exit
|
|
142
|
+
# status (10 untracked, 11 tracked), so that a git that hangs can be cut and no file is made.
|
|
143
|
+
# Every call asks git afresh: the second ask of a run must see what changed during the run.
|
|
144
|
+
# The bound is DEVFLOW_BG_AREA_PROBE_SECS (default 5, at most 60; tests override it as they
|
|
145
|
+
# do DEVFLOW_BG_WATCHDOG_SECS), kept by the watchdog's background-and-kill idiom: a SIGKILL
|
|
146
|
+
# to the probe's process group (set -m) after a sleep, and a SIGKILL cancel of the watchdog,
|
|
147
|
+
# so no exit trap runs in either subshell (the one `Run:` line, above). A probe that is
|
|
148
|
+
# cut, or ends in any other status, reads `unknown`: the area stays paused. The notices of
|
|
149
|
+
# memory-permission read this variable, so the worker leaves one only after this check has
|
|
150
|
+
# asked and found the area free. Always returns 0.
|
|
151
|
+
probe_memory_area() {
|
|
152
|
+
local _root="${1:-}" _secs _pid _wd_pid _status=1
|
|
153
|
+
DF_TRACKED_AREA_MEMORY="unknown"
|
|
154
|
+
_secs="${DEVFLOW_BG_AREA_PROBE_SECS:-5}"
|
|
155
|
+
case "$_secs" in
|
|
156
|
+
''|*[!0123456789]*|????*) _secs=5 ;;
|
|
157
|
+
esac
|
|
158
|
+
if [ "$_secs" -lt 1 ] || [ "$_secs" -gt 60 ]; then _secs=5; fi
|
|
159
|
+
set -m
|
|
160
|
+
(
|
|
161
|
+
_QTA_ASKED_MEMORY=""
|
|
162
|
+
queue_tracked_area "$_root" memory git
|
|
163
|
+
case "$DF_TRACKED_AREA_MEMORY" in
|
|
164
|
+
untracked) exit 10 ;;
|
|
165
|
+
tracked) exit 11 ;;
|
|
166
|
+
esac
|
|
167
|
+
exit 12
|
|
168
|
+
) </dev/null >/dev/null 2>&1 &
|
|
169
|
+
_pid=$!
|
|
170
|
+
set +m
|
|
171
|
+
(
|
|
172
|
+
sleep "$_secs"
|
|
173
|
+
kill -KILL -"$_pid" 2>/dev/null || true
|
|
174
|
+
) </dev/null >/dev/null 2>&1 &
|
|
175
|
+
_wd_pid=$!
|
|
176
|
+
if wait "$_pid" 2>/dev/null; then _status=0; else _status=$?; fi
|
|
177
|
+
# SIGKILL, not SIGTERM, as for the other watchdogs: no trap can run in the subshell.
|
|
178
|
+
kill -KILL "$_wd_pid" 2>/dev/null || true
|
|
179
|
+
wait "$_wd_pid" 2>/dev/null || true
|
|
180
|
+
case "$_status" in
|
|
181
|
+
10) DF_TRACKED_AREA_MEMORY="untracked" ;;
|
|
182
|
+
11) DF_TRACKED_AREA_MEMORY="tracked" ;;
|
|
183
|
+
esac
|
|
184
|
+
return 0
|
|
185
|
+
}
|
|
186
|
+
|
|
131
187
|
PROJECT_ROOT="$(df_resolve_root "$CWD" 2>/dev/null || true)"
|
|
132
188
|
[ -n "$PROJECT_ROOT" ] || PROJECT_ROOT="$CWD"
|
|
133
189
|
# One canonical root feeds every path below, the staged path in the prompt and in the
|
|
@@ -136,6 +192,7 @@ PROJECT_ROOT="$(df_resolve_root "$CWD" 2>/dev/null || true)"
|
|
|
136
192
|
PROJECT_ROOT_CANONICAL=$(cd -P -- "$PROJECT_ROOT" 2>/dev/null && pwd -P) || PROJECT_ROOT_CANONICAL=""
|
|
137
193
|
if [ -z "$PROJECT_ROOT_CANONICAL" ]; then
|
|
138
194
|
log "SKIP: project root could not be resolved; no LLM run"
|
|
195
|
+
probe_memory_area "$PROJECT_ROOT"
|
|
139
196
|
memory_notice_write "$PROJECT_ROOT/.devflow/memory" "$PROJECT_ROOT" path
|
|
140
197
|
exit 0
|
|
141
198
|
fi
|
|
@@ -154,13 +211,23 @@ OK_FILE="$MEMORY_DIR/.last-refresh-ok"
|
|
|
154
211
|
# have been disabled since spawn, or narrowed by a checkout that now carries a
|
|
155
212
|
# `features.memory: false`). Same helper as every other memory/curation gate
|
|
156
213
|
# (D-FEATURES-NARROW-ONLY, see queue-append).
|
|
157
|
-
source "$SCRIPT_DIR/queue-append" || { echo "background-memory-update: failed to source queue-append" >&2; exit 1; }
|
|
158
214
|
queue_read_gates "$DEVFLOW_MANIFEST" "$PROJECT_ROOT"
|
|
159
215
|
if [ "$_QG_MEMORY" != "true" ]; then
|
|
160
216
|
log "ABORT: memory disabled (disabled after spawn)"
|
|
161
217
|
exit 0
|
|
162
218
|
fi
|
|
163
219
|
|
|
220
|
+
# --- Ask whether git tracks files in the memory folder ---
|
|
221
|
+
# The area may have become tracked since the Stop hook spawned this worker. A paused area
|
|
222
|
+
# ends the run here, before the binary is resolved, a link is checked, the lock is taken or
|
|
223
|
+
# a batch is claimed: nothing is read, moved or written, and no notice is left. The run's
|
|
224
|
+
# `Run:` line says skip.
|
|
225
|
+
probe_memory_area "$PROJECT_ROOT"
|
|
226
|
+
if [ "$DF_TRACKED_AREA_MEMORY" != "untracked" ]; then
|
|
227
|
+
log "SKIP: the memory area is paused (git tracks files in the memory folder, or did not answer); nothing read or written; no LLM run"
|
|
228
|
+
exit 0
|
|
229
|
+
fi
|
|
230
|
+
|
|
164
231
|
# --- Resolve claude binary ---
|
|
165
232
|
CLAUDE_BIN=$(command -v claude 2>/dev/null || true)
|
|
166
233
|
if [ -z "$CLAUDE_BIN" ]; then
|
|
@@ -1126,6 +1193,17 @@ case "$MEMORY_RESULT_DENIALS" in
|
|
|
1126
1193
|
*) RESULT_DENIED="yes" ;;
|
|
1127
1194
|
esac
|
|
1128
1195
|
|
|
1196
|
+
# The run took minutes, so the memory area is asked again before anything lands in the
|
|
1197
|
+
# folder. If git tracks files there now, or does not answer, nothing below runs: the staged
|
|
1198
|
+
# file is not checked, moved or removed, no stamp is touched, no denied run is counted and
|
|
1199
|
+
# no notice is left. The batch stays in .processing.
|
|
1200
|
+
probe_memory_area "$PROJECT_ROOT"
|
|
1201
|
+
if [ "$DF_TRACKED_AREA_MEMORY" != "untracked" ]; then
|
|
1202
|
+
RUN_RESULT="skip"
|
|
1203
|
+
log "SKIP: the memory area is paused (git tracks files in the memory folder, or did not answer); nothing swapped or stamped; the batch stays in .processing"
|
|
1204
|
+
exit 0
|
|
1205
|
+
fi
|
|
1206
|
+
|
|
1129
1207
|
# A folder or a link at the staged path now was put there during the run: the path was
|
|
1130
1208
|
# empty at the spawn. It is removed the way it is before a run (memory_staged_remove), and
|
|
1131
1209
|
# the run ends as failed: the checks below find no staged file, and the batch stays for
|
|
@@ -45,9 +45,16 @@
|
|
|
45
45
|
// once; prints none | busy | busy claim |
|
|
46
46
|
// skipped tracked | skipped unknown | moved <mode>
|
|
47
47
|
// and its backup, cause and notes
|
|
48
|
+
// tracked-area <curation|memory|both> <root>
|
|
49
|
+
// Does git track files in the data folders of the area
|
|
50
|
+
// at the root? Prints tracked | untracked | unknown, and
|
|
51
|
+
// for unknown a second line `cause <word>`
|
|
48
52
|
//
|
|
49
|
-
// Every curation op above except claim-queue, release-claim
|
|
50
|
-
// refreshes the mtime of an existing queue claim — the heartbeat (D-OWNED-CLAIM).
|
|
53
|
+
// Every curation op above except claim-queue, release-claim, move-ledger and tracked-area
|
|
54
|
+
// first refreshes the mtime of an existing queue claim — the heartbeat (D-OWNED-CLAIM).
|
|
55
|
+
// Where git tracks files in the curation data folders, or does not say whether it does,
|
|
56
|
+
// every curation op refuses with kind `tracked`, exit 1 and an empty stdout, and changes
|
|
57
|
+
// nothing; the answer is asked once per process.
|
|
51
58
|
|
|
52
59
|
'use strict';
|
|
53
60
|
|
|
@@ -88,6 +95,21 @@ function curationMove() {
|
|
|
88
95
|
return curationMoveModule;
|
|
89
96
|
}
|
|
90
97
|
|
|
98
|
+
/** The tracked-areas check, once loaded; see trackedAreas(). */
|
|
99
|
+
let trackedAreasModule = null;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The tracked-areas check, loaded on first use and memoized. Only the tracked-area op
|
|
103
|
+
* loads it from here: a leaf that requires node built-ins and the path helpers, never
|
|
104
|
+
* the store.
|
|
105
|
+
*
|
|
106
|
+
* @returns {object}
|
|
107
|
+
*/
|
|
108
|
+
function trackedAreas() {
|
|
109
|
+
if (trackedAreasModule === null) trackedAreasModule = require('./lib/tracked-areas.cjs');
|
|
110
|
+
return trackedAreasModule;
|
|
111
|
+
}
|
|
112
|
+
|
|
91
113
|
/** The bytes each read of stdin asks for. */
|
|
92
114
|
const STDIN_READ_BYTES = 64 * 1024;
|
|
93
115
|
|
|
@@ -624,6 +646,26 @@ try {
|
|
|
624
646
|
break;
|
|
625
647
|
}
|
|
626
648
|
|
|
649
|
+
// -------------------------------------------------------------------------
|
|
650
|
+
// tracked-area <curation|memory|both> <root>
|
|
651
|
+
// Does git track files in the data folders of the area at the root
|
|
652
|
+
// (inspectTrackedAreaOnce, tracked-areas.cjs)? Not a curation op: it sends no
|
|
653
|
+
// heartbeat and loads only the check. The root is an argument, so the shell gate
|
|
654
|
+
// names the root it decided on; it stays one argv word, never a shell word. The
|
|
655
|
+
// answer is asked once per root and area for the life of the process.
|
|
656
|
+
// stdout: tracked | untracked | unknown, and for unknown a second line
|
|
657
|
+
// `cause <word>` (one fixed word that says why git did not answer)
|
|
658
|
+
// -------------------------------------------------------------------------
|
|
659
|
+
case 'tracked-area': {
|
|
660
|
+
const { AREA_PATHSPECS, inspectTrackedAreaOnce } = trackedAreas();
|
|
661
|
+
if (args.length !== 2 || !Object.prototype.hasOwnProperty.call(AREA_PATHSPECS, args[0])) {
|
|
662
|
+
exitWithUsage('tracked-area <curation|memory|both> <root> (the root is an absolute path)');
|
|
663
|
+
}
|
|
664
|
+
const { answer, cause } = inspectTrackedAreaOnce(args[1], args[0]);
|
|
665
|
+
process.stdout.write(answer === 'unknown' ? `unknown\ncause ${cause}\n` : `${answer}\n`);
|
|
666
|
+
break;
|
|
667
|
+
}
|
|
668
|
+
|
|
627
669
|
default:
|
|
628
670
|
process.stderr.write(`json-helper: unknown operation "${op}"\n`);
|
|
629
671
|
process.exit(1);
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
// means a broken invariant or an I/O failure, never an expected outcome; a lock
|
|
11
11
|
// held by withDecisionsLock is released on every path, the throw included.
|
|
12
12
|
//
|
|
13
|
-
// Loading: node built-ins and
|
|
13
|
+
// Loading: node built-ins and four sibling libs only (the path helpers, the lock, the
|
|
14
|
+
// path guard and the tracked-areas check). This module never requires
|
|
14
15
|
// decisions-format.cjs or render-decisions.cjs at load time: both require it, for
|
|
15
16
|
// the status list and the one-line and inactive-note text they share with `list`.
|
|
16
17
|
//
|
|
@@ -58,6 +59,7 @@ const {
|
|
|
58
59
|
} = require('./project-paths.cjs');
|
|
59
60
|
const { acquireMkdirLock, releaseLock } = require('./mkdir-lock.cjs');
|
|
60
61
|
const { safePath } = require('./safe-path.cjs');
|
|
62
|
+
const { inspectTrackedAreaOnce } = require('./tracked-areas.cjs');
|
|
61
63
|
|
|
62
64
|
// ---------------------------------------------------------------------------
|
|
63
65
|
// Constants
|
|
@@ -628,6 +630,36 @@ function linkedFolder(opName, dir) {
|
|
|
628
630
|
};
|
|
629
631
|
}
|
|
630
632
|
|
|
633
|
+
/**
|
|
634
|
+
* The refusal of an op run where git tracks files in the curation data folders under
|
|
635
|
+
* `root` (the curation folder, the old ledger folder and the backup folders), or does
|
|
636
|
+
* not say whether it does, or null where git tracks none. A folder that git tracks is the
|
|
637
|
+
* repository's content, so the curation feature is paused there: the op reads and writes
|
|
638
|
+
* nothing. The messages state the fact and name no command; the root is shown so the
|
|
639
|
+
* folder can be found. The check asks git about the index, not the disk, so it holds
|
|
640
|
+
* whether or not the folder is on disk, and it makes no git call for a root with no `.git`
|
|
641
|
+
* entry. It comes first in every op, before a ledger file is read, a lock is taken or a git
|
|
642
|
+
* call of the op's own is made. The answer is asked once per root for the life of the
|
|
643
|
+
* process (inspectTrackedAreaOnce), so the heartbeat, the op and its lock share one call.
|
|
644
|
+
*
|
|
645
|
+
* The refusal carries `reason`, `tracked` where git tracks files and `unknown` where it
|
|
646
|
+
* did not answer, so a caller that picks a notice reads the field, never the message.
|
|
647
|
+
*
|
|
648
|
+
* @param {string} opName - operation name, for the message
|
|
649
|
+
* @param {string} root - project root
|
|
650
|
+
* @returns {{ ok: false, error: { kind: 'tracked', reason: 'tracked'|'unknown', message: string } } | null}
|
|
651
|
+
*/
|
|
652
|
+
function trackedCurationArea(opName, root) {
|
|
653
|
+
const { answer } = inspectTrackedAreaOnce(root, 'curation');
|
|
654
|
+
if (answer === 'untracked') return null;
|
|
655
|
+
const reason = answer === 'tracked' ? 'tracked' : 'unknown';
|
|
656
|
+
const fact = reason === 'tracked' ? 'git tracks files' : 'git did not answer whether it tracks files';
|
|
657
|
+
return {
|
|
658
|
+
ok: false,
|
|
659
|
+
error: { kind: 'tracked', reason, message: `${opName}: ${fact} in the curation data folders under ${root}; nothing was written` },
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
|
|
631
663
|
/** The Stats of `file` without following a link, or null when nothing is there. */
|
|
632
664
|
function lstatOrNull(file) {
|
|
633
665
|
try {
|
|
@@ -744,16 +776,23 @@ function isResult(value) {
|
|
|
744
776
|
* names. Refused here, before the lock is taken, so every writer refuses in one
|
|
745
777
|
* place; the claim heartbeat makes the same check, and read-only paths still read.
|
|
746
778
|
*
|
|
779
|
+
* A writer also refuses with `tracked`, changing nothing, where git tracks files in the
|
|
780
|
+
* curation data folders under its root, or does not say whether it does
|
|
781
|
+
* (trackedCurationArea). That check follows the link check and comes before the journal,
|
|
782
|
+
* the folder check and the lock, so a paused project gets no lock folder.
|
|
783
|
+
*
|
|
747
784
|
* @param {string} opName - operation name, for messages
|
|
748
785
|
* @param {string} root - project root
|
|
749
786
|
* @param {() => { ok: boolean }} fn - the locked body; it must return a Result
|
|
750
787
|
* @param {{ timeoutMs?: number, staleMs?: number }} [opts]
|
|
751
788
|
* @returns {{ ok: true, value?: unknown } | { ok: false, error: { kind: string, message: string } }}
|
|
752
|
-
* fn's Result, or an error of kind `not-a-directory`, `no-curation-dir` or `busy`.
|
|
789
|
+
* fn's Result, or an error of kind `not-a-directory`, `tracked`, `no-curation-dir` or `busy`.
|
|
753
790
|
*/
|
|
754
791
|
function withDecisionsLock(opName, root, fn, { timeoutMs = LOCK_ACQUIRE_TIMEOUT_MS, staleMs = LOCK_STALE_MS } = {}) {
|
|
755
792
|
const linked = linkedCurationFolder(root);
|
|
756
793
|
if (linked !== null) return linkedFolder(opName, linked);
|
|
794
|
+
const paused = trackedCurationArea(opName, root);
|
|
795
|
+
if (paused !== null) return paused;
|
|
757
796
|
const journal = moveJournalPathOrNull(root);
|
|
758
797
|
const oldFolder = path.join(root, '.devflow', LEGACY_NAMES.folder);
|
|
759
798
|
if (journal !== null && moveJournalHoldsWriters(journal, oldFolder)) return unfinishedMove(opName, journal);
|
|
@@ -1344,7 +1383,7 @@ function lastActivityMs(row) {
|
|
|
1344
1383
|
* @param {{ now?: number, timeoutMs?: number }} [opts] - now: epoch ms (default Date.now())
|
|
1345
1384
|
* @returns {{ ok: true, value: { rotated: number, appended: number } } | { ok: false, error: { kind: string, message: string } }}
|
|
1346
1385
|
* rotated: rows removed from the log; appended: rows added to the archive.
|
|
1347
|
-
* Errors are withDecisionsLock's not-a-directory, no-curation-dir and busy.
|
|
1386
|
+
* Errors are withDecisionsLock's tracked, not-a-directory, no-curation-dir and busy.
|
|
1348
1387
|
*/
|
|
1349
1388
|
function rotateObservations(root, { now = Date.now(), timeoutMs } = {}) {
|
|
1350
1389
|
return withDecisionsLock('rotate-observations', root, () => {
|
|
@@ -1407,7 +1446,7 @@ function rotateObservations(root, { now = Date.now(), timeoutMs } = {}) {
|
|
|
1407
1446
|
* @param {{ now?: number, timeoutMs?: number }} [opts] - now: epoch ms (default Date.now())
|
|
1408
1447
|
* @returns {{ ok: true, value: { cleared: number, kept: number } } | { ok: false, error: { kind: string, message: string } }}
|
|
1409
1448
|
* cleared: rows removed from the log; kept: rows left in it. Error kinds:
|
|
1410
|
-
* ledger-malformed, and withDecisionsLock's not-a-directory, no-curation-dir and busy.
|
|
1449
|
+
* ledger-malformed, and withDecisionsLock's tracked, not-a-directory, no-curation-dir and busy.
|
|
1411
1450
|
*/
|
|
1412
1451
|
function clearUnreferenced(root, { now = Date.now(), timeoutMs } = {}) {
|
|
1413
1452
|
return withDecisionsLock('clear', root, () => {
|
|
@@ -1475,7 +1514,7 @@ function removeEmptyDir(dir) {
|
|
|
1475
1514
|
* @param {{ timeoutMs?: number }} [opts]
|
|
1476
1515
|
* @returns {{ ok: true, value: { removed: number } } | { ok: false, error: { kind: string, message: string } }}
|
|
1477
1516
|
* removed: the entries removed from the curation directory. Errors are
|
|
1478
|
-
* withDecisionsLock's not-a-directory, no-curation-dir and busy.
|
|
1517
|
+
* withDecisionsLock's tracked, not-a-directory, no-curation-dir and busy.
|
|
1479
1518
|
*/
|
|
1480
1519
|
function resetCuration(root, { timeoutMs } = {}) {
|
|
1481
1520
|
const curationDir = getCurationDir(root);
|
|
@@ -1643,7 +1682,8 @@ function moveQueueToClaim(queuePath, claimPath) {
|
|
|
1643
1682
|
* clobber a batch, mv kept the queue's old mtime so a fresh claim could look stale
|
|
1644
1683
|
* at once, and an unconditional final unlink deleted another run's claim.
|
|
1645
1684
|
*
|
|
1646
|
-
* Without .devflow/curation/ it answers none and creates nothing
|
|
1685
|
+
* Without .devflow/curation/ it answers none and creates nothing; where git tracks files
|
|
1686
|
+
* in the curation data folders it refuses, kind `tracked`, before it looks (trackedCurationArea).
|
|
1647
1687
|
*
|
|
1648
1688
|
* @param {string} root - project root
|
|
1649
1689
|
* @param {{ now?: number, token?: string, timeoutMs?: number }} [opts]
|
|
@@ -1654,6 +1694,8 @@ function moveQueueToClaim(queuePath, claimPath) {
|
|
|
1654
1694
|
*/
|
|
1655
1695
|
function claimQueue(root, { now = Date.now(), token = newClaimToken(), timeoutMs } = {}) {
|
|
1656
1696
|
assertClaimToken('claimQueue', token);
|
|
1697
|
+
const paused = trackedCurationArea('claim-queue', root);
|
|
1698
|
+
if (paused !== null) return paused;
|
|
1657
1699
|
if (!hasCurationDir(root)) return { ok: true, value: { state: 'none' } };
|
|
1658
1700
|
return withDecisionsLock('claim-queue', root, () => {
|
|
1659
1701
|
const claimPath = getCurationPendingTurnsProcessingPath(root);
|
|
@@ -1693,7 +1735,7 @@ function claimQueue(root, { now = Date.now(), token = newClaimToken(), timeoutMs
|
|
|
1693
1735
|
* @param {string} token
|
|
1694
1736
|
* @param {{ timeoutMs?: number }} [opts]
|
|
1695
1737
|
* @returns {{ ok: true, value: { state: 'released'|'not-owner'|'gone' } } | { ok: false, error: { kind: string, message: string } }}
|
|
1696
|
-
* errors include withDecisionsLock's not-a-directory, no-curation-dir and busy
|
|
1738
|
+
* errors include withDecisionsLock's tracked, not-a-directory, no-curation-dir and busy
|
|
1697
1739
|
* @throws {TypeError} when `token` is not a claim token
|
|
1698
1740
|
*/
|
|
1699
1741
|
function releaseClaim(root, token, { timeoutMs } = {}) {
|
|
@@ -1722,14 +1764,17 @@ function releaseClaim(root, token, { timeoutMs } = {}) {
|
|
|
1722
1764
|
* takes no lock — json-helper sends it before each curation op runs. A claim in a
|
|
1723
1765
|
* curation tree reached through a symbolic link is left alone (D-NO-LINKED-TREE):
|
|
1724
1766
|
* `lutimes` follows a linked folder above the claim, and the op that follows
|
|
1725
|
-
* refuses that tree anyway.
|
|
1767
|
+
* refuses that tree anyway. A claim in a project whose curation data folders git
|
|
1768
|
+
* tracks, or where git does not answer, is left alone as well (trackedCurationArea).
|
|
1726
1769
|
*
|
|
1727
1770
|
* @param {string} root - project root
|
|
1728
1771
|
* @param {{ now?: number }} [opts] - now: epoch ms (default Date.now())
|
|
1729
1772
|
* @returns {{ ok: true, value: { touched: boolean } } | { ok: false, error: { kind: 'heartbeat-failed', message: string } }}
|
|
1730
1773
|
*/
|
|
1731
1774
|
function touchClaim(root, { now = Date.now() } = {}) {
|
|
1732
|
-
if (linkedCurationFolder(root) !== null
|
|
1775
|
+
if (linkedCurationFolder(root) !== null || trackedCurationArea('heartbeat', root) !== null) {
|
|
1776
|
+
return { ok: true, value: { touched: false } };
|
|
1777
|
+
}
|
|
1733
1778
|
const claimPath = getCurationPendingTurnsProcessingPath(root);
|
|
1734
1779
|
const at = new Date(now);
|
|
1735
1780
|
try {
|
|
@@ -2231,13 +2276,15 @@ function restoreFirst(id, carriers) {
|
|
|
2231
2276
|
* observations: the count the log row holds afterwards; reprojected: the
|
|
2232
2277
|
* anchors re-projected, in anchor order. Error kinds: invalid-input (with
|
|
2233
2278
|
* problems), duplicate-log-id, restore-first, cannot-reproject, and
|
|
2234
|
-
* withDecisionsLock's not-a-directory, no-curation-dir and busy.
|
|
2279
|
+
* withDecisionsLock's tracked, not-a-directory, no-curation-dir and busy.
|
|
2235
2280
|
* @throws {TypeError} when `mode` is not a put mode
|
|
2236
2281
|
*/
|
|
2237
2282
|
function putObservation(root, mode, input, { now = Date.now(), timeoutMs, scopeMatches } = {}) {
|
|
2238
2283
|
if (!VALIDATION_MODES.includes(mode)) {
|
|
2239
2284
|
throw new TypeError(`putObservation: mode must be one of ${VALIDATION_MODES.join(', ')}, got '${mode}'`);
|
|
2240
2285
|
}
|
|
2286
|
+
const paused = trackedCurationArea('put-observation', root);
|
|
2287
|
+
if (paused !== null) return paused;
|
|
2241
2288
|
const matches = scopeMatches || gitScopeMatcher(root);
|
|
2242
2289
|
return withDecisionsLock('put-observation', root, () => putUnderLock(root, mode, input, { now, scopeMatches: matches }), { timeoutMs });
|
|
2243
2290
|
}
|
|
@@ -2303,14 +2350,17 @@ function putUnderLock(root, mode, input, { now, scopeMatches }) {
|
|
|
2303
2350
|
* The data behind `list`, read-only: buildListing over the ledger and the log as
|
|
2304
2351
|
* they are, every glob scope checked against the files git tracks. Malformed
|
|
2305
2352
|
* lines are counted, never quarantined (D-QUARANTINE-MALFORMED). Refuses without
|
|
2306
|
-
* .devflow/curation/ (D-NO-STRAY-TREE)
|
|
2353
|
+
* .devflow/curation/ (D-NO-STRAY-TREE), and, before it reads a file, where git tracks files
|
|
2354
|
+
* in the curation data folders (trackedCurationArea).
|
|
2307
2355
|
*
|
|
2308
2356
|
* @param {string} root - project root
|
|
2309
2357
|
* @param {{ scopeMatches?: (glob: string) => boolean }} [opts] - default gitScopeMatcher(root)
|
|
2310
2358
|
* @returns {{ ok: true, value: { active: object[], inactive: object[], observations: object[], integrity: object[], malformed: { ledger: number, log: number } } }
|
|
2311
|
-
* | { ok: false, error: { kind: 'no-curation-dir', message: string } }}
|
|
2359
|
+
* | { ok: false, error: { kind: 'tracked'|'no-curation-dir', message: string } }}
|
|
2312
2360
|
*/
|
|
2313
2361
|
function readListing(root, { scopeMatches } = {}) {
|
|
2362
|
+
const paused = trackedCurationArea('list', root);
|
|
2363
|
+
if (paused !== null) return paused;
|
|
2314
2364
|
if (!hasCurationDir(root)) return noCurationDir('list', root);
|
|
2315
2365
|
const { ledgerRows, logRows, rejected } = readCurationState(root);
|
|
2316
2366
|
return {
|
|
@@ -2393,14 +2443,17 @@ function formatListing(listing) {
|
|
|
2393
2443
|
* the log and the history as they are. When lines were skipped as malformed
|
|
2394
2444
|
* (D-QUARANTINE-MALFORMED), a found entry gains `malformed: { ledger, log }` and
|
|
2395
2445
|
* a not-found message says a skipped line may hold it. Refuses without
|
|
2396
|
-
* .devflow/curation/ (D-NO-STRAY-TREE)
|
|
2446
|
+
* .devflow/curation/ (D-NO-STRAY-TREE), and, before it reads a file, where git tracks files
|
|
2447
|
+
* in the curation data folders (trackedCurationArea).
|
|
2397
2448
|
*
|
|
2398
2449
|
* @param {string} root - project root
|
|
2399
2450
|
* @param {string} key - an anchor id or an observation id
|
|
2400
2451
|
* @returns {{ ok: true, value: { key: string, ledger: object[], log: object|null, history_versions: object[], flags: object[], malformed?: { ledger: number, log: number } } }
|
|
2401
|
-
* | { ok: false, error: { kind: 'no-curation-dir'|'invalid-key'|'not-found', message: string } }}
|
|
2452
|
+
* | { ok: false, error: { kind: 'tracked'|'no-curation-dir'|'invalid-key'|'not-found', message: string } }}
|
|
2402
2453
|
*/
|
|
2403
2454
|
function showByKey(root, key) {
|
|
2455
|
+
const paused = trackedCurationArea('show', root);
|
|
2456
|
+
if (paused !== null) return paused;
|
|
2404
2457
|
if (!hasCurationDir(root)) return noCurationDir('show', root);
|
|
2405
2458
|
const { ledgerRows, logRows, rejected } = readCurationState(root);
|
|
2406
2459
|
const shown = showEntry(key, ledgerRows, logRows, { historyVersions: id => historyVersions(root, id) });
|
|
@@ -2448,10 +2501,12 @@ function warmScopeMatcher(ledgerRows, scopeMatches) {
|
|
|
2448
2501
|
* now: epoch ms (default Date.now()); scopeMatches: default gitScopeMatcher(root)
|
|
2449
2502
|
* @returns {{ ok: true, value: { ref: { ref: 'origin/HEAD'|'HEAD', commit: string } | null, due: Array<{ anchor_id: string, reason: string, bytes: number }> } }
|
|
2450
2503
|
* | { ok: false, error: { kind: string, message: string } }}
|
|
2451
|
-
* due is selectDue's answer; errors are withDecisionsLock's not-a-directory,
|
|
2504
|
+
* due is selectDue's answer; errors are withDecisionsLock's tracked, not-a-directory,
|
|
2452
2505
|
* no-curation-dir and busy
|
|
2453
2506
|
*/
|
|
2454
2507
|
function claimDue(root, { now = Date.now(), timeoutMs, scopeMatches } = {}) {
|
|
2508
|
+
const paused = trackedCurationArea('claim-due', root);
|
|
2509
|
+
if (paused !== null) return paused;
|
|
2455
2510
|
if (!hasCurationDir(root)) return noCurationDir('claim-due', root);
|
|
2456
2511
|
const matches = scopeMatches || gitScopeMatcher(root);
|
|
2457
2512
|
const ref = resolveVerifyRef(root);
|
|
@@ -2725,12 +2780,14 @@ function mintAnchor(ledgerRows, type, citedAnchors) {
|
|
|
2725
2780
|
* | { ok: false, error: { kind: string, message: string } }}
|
|
2726
2781
|
* Error kinds: not-in-log, duplicate-log-id, already-promoted, v1-observation,
|
|
2727
2782
|
* type-mismatch, cited-numbers-exhausted, and withDecisionsLock's
|
|
2728
|
-
* not-a-directory, no-curation-dir and busy.
|
|
2783
|
+
* tracked, not-a-directory, no-curation-dir and busy.
|
|
2729
2784
|
* @throws {TypeError} for a type other than decision or pitfall, or a malformed obsId
|
|
2730
2785
|
*/
|
|
2731
2786
|
function assignAnchor(root, type, obsId, { now = Date.now(), timeoutMs, citedAnchors } = {}) {
|
|
2732
2787
|
if (anchorPrefixFor(type) === null) throw new TypeError(`assignAnchor: type must be 'decision' or 'pitfall', got '${type}'`);
|
|
2733
2788
|
if (typeof obsId !== 'string' || !OBS_ID_RE.test(obsId)) throw new TypeError('assignAnchor: obsId must be an observation id');
|
|
2789
|
+
const paused = trackedCurationArea('assign-anchor', root);
|
|
2790
|
+
if (paused !== null) return paused;
|
|
2734
2791
|
if (!hasCurationDir(root)) return noCurationDir('assign-anchor', root);
|
|
2735
2792
|
const cited = citedAnchors || collectCitedAnchorIds(root);
|
|
2736
2793
|
return withDecisionsLock('assign-anchor', root, () => assignUnderLock(root, type, obsId, { now, cited }), { timeoutMs });
|
|
@@ -2884,7 +2941,7 @@ function recordRefreshHistory(root, plans, ledgerRows, { now }) {
|
|
|
2884
2941
|
* @returns {{ ok: true, value: { refreshed: Array<{ anchor_id: string, state: 'verified'|'reprojected'|'unchanged' }> } }
|
|
2885
2942
|
* | { ok: false, error: { kind: string, message: string, problems?: Array<{ anchor_id: string, message: string }> } }}
|
|
2886
2943
|
* refreshed: each anchor once, in the order given. Error kinds: refused (with
|
|
2887
|
-
* problems), and withDecisionsLock's not-a-directory, no-curation-dir and busy.
|
|
2944
|
+
* problems), and withDecisionsLock's tracked, not-a-directory, no-curation-dir and busy.
|
|
2888
2945
|
* @throws {TypeError} when anchorIds is empty or holds anything but anchor ids
|
|
2889
2946
|
*/
|
|
2890
2947
|
function refreshAnchors(root, anchorIds, { verified = false, now = Date.now(), timeoutMs } = {}) {
|
|
@@ -3067,9 +3124,11 @@ function restoreRefusal(kind, message) {
|
|
|
3067
3124
|
* @param {{ verifyRef?: { ref: 'origin/HEAD'|'HEAD', commit: string } | null }} [opts]
|
|
3068
3125
|
* verifyRef: default resolveVerifyRef(root); null when there is no commit to check
|
|
3069
3126
|
* @returns {{ ok: true, value: { path: string, quote: string, ref: string, commit: string } }
|
|
3070
|
-
* | { ok: false, error: { kind: 'no-verify-ref'|'not-at-ref'|'git-failed'|'quote-not-found', message: string } }}
|
|
3127
|
+
* | { ok: false, error: { kind: 'tracked'|'no-verify-ref'|'not-at-ref'|'git-failed'|'quote-not-found', message: string } }}
|
|
3071
3128
|
*/
|
|
3072
3129
|
function quoteAtRef(root, at, quote, { verifyRef } = {}) {
|
|
3130
|
+
const paused = trackedCurationArea('retire-anchor', root);
|
|
3131
|
+
if (paused !== null) return paused;
|
|
3073
3132
|
const checkedAt = verifyRef === undefined ? resolveVerifyRef(root) : verifyRef;
|
|
3074
3133
|
if (checkedAt === null) {
|
|
3075
3134
|
return retireRefusal('no-verify-ref', 'there is no commit to check the quote at (not a git repository, or no commit yet); nothing was written');
|
|
@@ -3121,7 +3180,7 @@ function quoteAtRef(root, at, quote, { verifyRef } = {}) {
|
|
|
3121
3180
|
* kinds: invalid-input (with problems), quoteAtRef's, not-found,
|
|
3122
3181
|
* duplicate-anchor, already-inactive, successor-not-found,
|
|
3123
3182
|
* successor-duplicate-anchor, successor-inactive, and withDecisionsLock's
|
|
3124
|
-
* not-a-directory, no-curation-dir and busy.
|
|
3183
|
+
* tracked, not-a-directory, no-curation-dir and busy.
|
|
3125
3184
|
* @throws {TypeError} for a malformed anchorId or a status that is not inactive
|
|
3126
3185
|
*/
|
|
3127
3186
|
function retireAnchor(root, anchorId, status, input, { now = Date.now(), timeoutMs, verifyRef } = {}) {
|
|
@@ -3129,6 +3188,8 @@ function retireAnchor(root, anchorId, status, input, { now = Date.now(), timeout
|
|
|
3129
3188
|
if (!INACTIVE_STATUSES.includes(status)) {
|
|
3130
3189
|
throw new TypeError(`retireAnchor: status must be one of ${INACTIVE_STATUSES.join(', ')}, got '${status}'`);
|
|
3131
3190
|
}
|
|
3191
|
+
const paused = trackedCurationArea('retire-anchor', root);
|
|
3192
|
+
if (paused !== null) return paused;
|
|
3132
3193
|
if (!hasCurationDir(root)) return noCurationDir('retire-anchor', root);
|
|
3133
3194
|
const problems = retireInputProblems(anchorId, status, input);
|
|
3134
3195
|
if (problems.length > 0) return invalidInput('retire-anchor', problems);
|
|
@@ -3200,7 +3261,7 @@ function retireUnderLock(root, anchorId, status, note, { now }) {
|
|
|
3200
3261
|
* @returns {{ ok: true, value: { anchor_id: string, status: 'Accepted'|'Active' } }
|
|
3201
3262
|
* | { ok: false, error: { kind: string, message: string } }}
|
|
3202
3263
|
* Error kinds: not-found, duplicate-anchor, already-active, type-mismatch, and
|
|
3203
|
-
* withDecisionsLock's not-a-directory, no-curation-dir and busy.
|
|
3264
|
+
* withDecisionsLock's tracked, not-a-directory, no-curation-dir and busy.
|
|
3204
3265
|
* @throws {TypeError} for a malformed anchorId
|
|
3205
3266
|
*/
|
|
3206
3267
|
function restoreAnchor(root, anchorId, { now = Date.now(), timeoutMs } = {}) {
|
|
@@ -3268,6 +3329,7 @@ module.exports = {
|
|
|
3268
3329
|
writeJsonlAtomic,
|
|
3269
3330
|
// Locking
|
|
3270
3331
|
hasCurationDir,
|
|
3332
|
+
trackedCurationArea,
|
|
3271
3333
|
withDecisionsLock,
|
|
3272
3334
|
// State and the ledger registry
|
|
3273
3335
|
readCurationState,
|
|
@@ -51,6 +51,7 @@ const {
|
|
|
51
51
|
isV2,
|
|
52
52
|
readJsonl,
|
|
53
53
|
hasCurationDir,
|
|
54
|
+
trackedCurationArea,
|
|
54
55
|
withDecisionsLock,
|
|
55
56
|
writeFileAtomic,
|
|
56
57
|
} = require('./curation-store.cjs');
|
|
@@ -397,12 +398,19 @@ function renderCommand(root) {
|
|
|
397
398
|
* `--check`: render in memory and compare with the files on disk, naming each
|
|
398
399
|
* file that differs or is missing; exit 1 on any drift. It writes nothing and
|
|
399
400
|
* takes no lock — taking it would create the lock directory — and refuses
|
|
400
|
-
* without the curation directory (D-NO-STRAY-TREE).
|
|
401
|
+
* without the curation directory (D-NO-STRAY-TREE). Where git tracks files in the
|
|
402
|
+
* curation data folders, or does not say whether it does, it refuses first, exit 1:
|
|
403
|
+
* it reads no file there (trackedCurationArea).
|
|
401
404
|
*
|
|
402
405
|
* @param {string} root
|
|
403
406
|
* @returns {number} exit code
|
|
404
407
|
*/
|
|
405
408
|
function checkCommand(root) {
|
|
409
|
+
const paused = trackedCurationArea(CLI_NAME, root);
|
|
410
|
+
if (paused !== null) {
|
|
411
|
+
process.stderr.write(`${paused.error.message}\n`);
|
|
412
|
+
return 1;
|
|
413
|
+
}
|
|
406
414
|
if (!hasCurationDir(root)) {
|
|
407
415
|
process.stderr.write(`${CLI_NAME}: no .devflow/curation/ under ${root} — run from the project root\n`);
|
|
408
416
|
return 1;
|